loader.py 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239
  1. """Loads and registers modules"""
  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. # scope: inline
  22. import asyncio
  23. import contextlib
  24. import copy
  25. import functools
  26. import importlib
  27. import inspect
  28. import logging
  29. import os
  30. import re
  31. import ast
  32. import sys
  33. import time
  34. import uuid
  35. from collections import ChainMap
  36. from importlib.machinery import ModuleSpec
  37. from typing import Optional, Union
  38. from urllib.parse import urlparse
  39. import requests
  40. import telethon
  41. from telethon.tl.types import Message, Channel
  42. from telethon.tl.functions.channels import JoinChannelRequest
  43. from .. import loader, main, utils
  44. from ..compat import geek
  45. from ..inline.types import InlineCall
  46. from .._types import CoreOverwriteError
  47. logger = logging.getLogger(__name__)
  48. VALID_PIP_PACKAGES = re.compile(
  49. r"^\s*# ?requires:(?: ?)((?:{url} )*(?:{url}))\s*$".format(
  50. url=r"[-[\]_.~:/?#@!$&'()*+,;%<=>a-zA-Z0-9]+"
  51. ),
  52. re.MULTILINE,
  53. )
  54. USER_INSTALL = "PIP_TARGET" not in os.environ and "VIRTUAL_ENV" not in os.environ
  55. @loader.tds
  56. class LoaderMod(loader.Module):
  57. """Loads modules"""
  58. strings = {
  59. "name": "Loader",
  60. "repo_config_doc": "Fully qualified URL to a module repo",
  61. "avail_header": "<b>📲 Official modules from repo</b>",
  62. "select_preset": "<b>⚠️ Please select a preset</b>",
  63. "no_preset": "<b>🚫 Preset not found</b>",
  64. "preset_loaded": "<b>✅ Preset loaded</b>",
  65. "no_module": "<b>🚫 Module not available in repo.</b>",
  66. "no_file": "<b>🚫 File not found</b>",
  67. "provide_module": "<b>⚠️ Provide a module to load</b>",
  68. "bad_unicode": "<b>🚫 Invalid Unicode formatting in module</b>",
  69. "load_failed": "<b>🚫 Loading failed. See logs for details</b>",
  70. "loaded": "<b>🔭 Module </b><code>{}</code>{}<b> loaded {}</b>{}{}{}{}{}{}",
  71. "no_class": "<b>What class needs to be unloaded?</b>",
  72. "unloaded": "<b>🧹 Module {} unloaded.</b>",
  73. "not_unloaded": "<b>🚫 Module not unloaded.</b>",
  74. "requirements_failed": "<b>🚫 Requirements installation failed</b>",
  75. "requirements_failed_termux": (
  76. "🕶🚫 <b>Requirements installation failed</b>\n<b>The most common reason is"
  77. " that Termux doesn't support many libraries. Don't report it as bug, this"
  78. " can't be solved.</b>"
  79. ),
  80. "heroku_install_failed": (
  81. "♓️⚠️ <b>This module requires additional libraries to be installed, which"
  82. " can't be done on Heroku. Don't report it as bug, this can't be"
  83. " solved.</b>"
  84. ),
  85. "requirements_installing": "<b>🔄 Installing requirements:\n\n{}</b>",
  86. "requirements_restart": (
  87. "<b>🔄 Requirements installed, but a restart is required for"
  88. " </b><code>{}</code><b> to apply</b>"
  89. ),
  90. "all_modules_deleted": "<b>✅ All modules deleted</b>",
  91. "single_cmd": "\n▫️ <code>{}{}</code> {}",
  92. "undoc_cmd": "🦥 No docs",
  93. "ihandler": "\n🎹 <code>{}</code> {}",
  94. "undoc_ihandler": "🦥 No docs",
  95. "inline_init_failed": (
  96. "🚫 <b>This module requires Hikka inline feature and "
  97. "initialization of InlineManager failed</b>\n"
  98. "<i>Please, remove one of your old bots from @BotFather and "
  99. "restart userbot to load this module</i>"
  100. ),
  101. "version_incompatible": (
  102. "🚫 <b>This module requires Hikka {}+\nPlease, update with"
  103. " </b><code>.update</code>"
  104. ),
  105. "ffmpeg_required": (
  106. "🚫 <b>This module requires FFMPEG, which is not installed</b>"
  107. ),
  108. "developer": "\n\n💻 <b>Developer: </b>{}",
  109. "depends_from": "\n\n📦 <b>Dependencies: </b>\n{}",
  110. "by": "by",
  111. "module_fs": (
  112. "💿 <b>Would you like to save this module to filesystem, so it won't get"
  113. " unloaded after restart?</b>"
  114. ),
  115. "save": "💿 Save",
  116. "no_save": "🚫 Don't save",
  117. "save_for_all": "💽 Always save to fs",
  118. "never_save": "🚫 Never save to fs",
  119. "will_save_fs": (
  120. "💽 Now all modules, loaded with .loadmod will be saved to filesystem"
  121. ),
  122. "add_repo_config_doc": "Additional repos to load from",
  123. "share_link_doc": "Share module link in result message of .dlmod",
  124. "modlink": "\n\n🌍 <b>Link: </b><code>{}</code>",
  125. "blob_link": (
  126. "\n🚸 <b>Do not use `blob` links to download modules. Consider switching to"
  127. " `raw` instead</b>"
  128. ),
  129. "suggest_subscribe": (
  130. "\n\n💬 <b>This module is made by {}. Do you want to join this channel to"
  131. " support developer?</b>"
  132. ),
  133. "subscribe": "💬 Subscribe",
  134. "no_subscribe": "🚫 Don't subscribe",
  135. "subscribed": "💬 Subscribed",
  136. "not_subscribed": "🚫 I will no longer suggest subscribing to this channel",
  137. "confirm_clearmodules": "⚠️ <b>Are you sure you want to clear all modules?</b>",
  138. "clearmodules": "🗑 Clear modules",
  139. "cancel": "🚫 Cancel",
  140. "overwrite_module": (
  141. "🚫 <b>This module attempted to override the core one"
  142. " (</b><code>{}</code><b>)</b>\n\n<i>💡 Don't report it as bug. It's a"
  143. " security measure to prevent replacing core modules with some junk</i>"
  144. ),
  145. "overwrite_command": (
  146. "🚫 <b>This module attempted to override the core command"
  147. " (</b><code>{}{}</code><b>)</b>\n\n<i>💡 Don't report it as bug. It's a"
  148. " security measure to prevent replacing core modules' commands with some"
  149. " junk</i>"
  150. ),
  151. "cannot_unload_lib": "🚫 <b>You can't unload library</b>",
  152. }
  153. strings_ru = {
  154. "repo_config_doc": "Ссылка для загрузки модулей",
  155. "add_repo_config_doc": "Дополнительные репозитории",
  156. "avail_header": "<b>📲 Официальные модули из репозитория</b>",
  157. "select_preset": "<b>⚠️ Выбери пресет</b>",
  158. "no_preset": "<b>🚫 Пресет не найден</b>",
  159. "preset_loaded": "<b>✅ Пресет загружен</b>",
  160. "no_module": "<b>🚫 Модуль недоступен в репозитории.</b>",
  161. "no_file": "<b>🚫 Файл не найден</b>",
  162. "provide_module": "<b>⚠️ Укажи модуль для загрузки</b>",
  163. "bad_unicode": "<b>🚫 Неверная кодировка модуля</b>",
  164. "load_failed": "<b>🚫 Загрузка не увенчалась успехом. Смотри логи.</b>",
  165. "loaded": "<b>🔭 Модуль </b><code>{}</code>{}<b> загружен {}</b>{}{}{}{}{}{}",
  166. "no_class": "<b>А что выгружать то?</b>",
  167. "unloaded": "<b>🧹 Модуль {} выгружен.</b>",
  168. "not_unloaded": "<b>🚫 Модуль не выгружен.</b>",
  169. "requirements_failed": "<b>🚫 Ошибка установки зависимостей</b>",
  170. "requirements_failed_termux": (
  171. "🕶🚫 <b>Ошибка установки зависимостей</b>\n<b>Наиболее часто возникает из-за"
  172. " того, что Termux не поддерживает многие библиотека. Не сообщайте об этом"
  173. " как об ошибке, это не может быть исправлено.</b>"
  174. ),
  175. "heroku_install_failed": (
  176. "♓️⚠️ <b>Этому модулю требуются дополнительные библиотека, которые нельзя"
  177. " установить на Heroku. Не сообщайте об этом как об ошибке, это не может"
  178. " быть исправлено</b>"
  179. ),
  180. "requirements_installing": "<b>🔄 Устанавливаю зависимости:\n\n{}</b>",
  181. "requirements_restart": (
  182. "<b>🔄 Зависимости установлены, но нужна перезагрузка для применения"
  183. " </b><code>{}</code>"
  184. ),
  185. "all_modules_deleted": "<b>✅ Модули удалены</b>",
  186. "single_cmd": "\n▫️ <code>{}{}</code> {}",
  187. "undoc_cmd": "🦥 Нет описания",
  188. "ihandler": "\n🎹 <code>{}</code> {}",
  189. "undoc_ihandler": "🦥 Нет описания",
  190. "version_incompatible": (
  191. "🚫 <b>Этому модулю требуется Hikka версии {}+\nОбновись с помощью"
  192. " </b><code>.update</code>"
  193. ),
  194. "ffmpeg_required": (
  195. "🚫 <b>Этому модулю требуется FFMPEG, который не установлен</b>"
  196. ),
  197. "developer": "\n\n💻 <b>Разработчик: </b>{}",
  198. "depends_from": "\n\n📦 <b>Зависимости: </b>\n{}",
  199. "by": "от",
  200. "module_fs": (
  201. "💿 <b>Ты хочешь сохранить модуль на жесткий диск, чтобы он не выгружался"
  202. " при перезагрузке?</b>"
  203. ),
  204. "save": "💿 Сохранить",
  205. "no_save": "🚫 Не сохранять",
  206. "save_for_all": "💽 Всегда сохранять",
  207. "never_save": "🚫 Никогда не сохранять",
  208. "will_save_fs": (
  209. "💽 Теперь все модули, загруженные из файла, будут сохраняться на жесткий"
  210. " диск"
  211. ),
  212. "inline_init_failed": (
  213. "🚫 <b>Этому модулю нужен HikkaInline, а инициализация менеджера инлайна"
  214. " неудачна</b>\n<i>Попробуй удалить одного из старых ботов в @BotFather и"
  215. " перезагрузить юзербота</i>"
  216. ),
  217. "_cmd_doc_dlmod": "Скачивает и устаналвивает модуль из репозитория",
  218. "_cmd_doc_dlpreset": "Скачивает и устанавливает определенный набор модулей",
  219. "_cmd_doc_loadmod": "Скачивает и устанавливает модуль из файла",
  220. "_cmd_doc_unloadmod": "Выгружает (удаляет) модуль",
  221. "_cmd_doc_clearmodules": "Выгружает все установленные модули",
  222. "_cls_doc": "Загружает модули",
  223. "share_link_doc": "Указывать ссылку на модуль после загрузки через .dlmod",
  224. "modlink": "\n\n🌍 <b>Ссылка: </b><code>{}</code>",
  225. "blob_link": (
  226. "\n🚸 <b>Не используй `blob` ссылки для загрузки модулей. Лучше загружать из"
  227. " `raw`</b>"
  228. ),
  229. "raw_link": "\n🌍 <b>Ссылка: </b><code>{}</code>",
  230. "suggest_subscribe": (
  231. "\n\n💬 <b>Этот модуль сделан {}. Подписаться на него, чтобы поддержать"
  232. " разработчика?</b>"
  233. ),
  234. "subscribe": "💬 Подписаться",
  235. "no_subscribe": "🚫 Не подписываться",
  236. "subscribed": "💬 Подписался!",
  237. "unsubscribed": "🚫 Я больше не буду предлагать подписаться на этот канал",
  238. "confirm_clearmodules": (
  239. "⚠️ <b>Вы уверены, что хотите выгрузить все модули?</b>"
  240. ),
  241. "clearmodules": "🗑 Выгрузить модули",
  242. "cancel": "🚫 Отмена",
  243. "overwrite_module": (
  244. "🚫 <b>Этот модуль попытался перезаписать встроенный"
  245. " (</b><code>{}</code><b>)</b>\n\n<i>💡 Это не ошибка, а мера безопасности,"
  246. " требуемая для предотвращения замены встроенных модулей всяким хламом. Не"
  247. " сообщайте о ней в support чате</i>"
  248. ),
  249. "overwrite_command": (
  250. "🚫 <b>Этот модуль попытался перезаписать встроенную команду"
  251. " (</b><code>{}</code><b>)</b>\n\n<i>💡 Это не ошибка, а мера безопасности,"
  252. " требуемая для предотвращения замены команд встроенных модулей всяким"
  253. " хламом. Не сообщайте о ней в support чате</i>"
  254. ),
  255. "cannot_unload_lib": "🚫 <b>Ты не можешь выгрузить библиотеку</b>",
  256. }
  257. _fully_loaded = False
  258. _links_cache = {}
  259. def __init__(self):
  260. self.config = loader.ModuleConfig(
  261. loader.ConfigValue(
  262. "MODULES_REPO",
  263. "https://mods.hikariatama.ru",
  264. lambda: self.strings("repo_config_doc"),
  265. validator=loader.validators.Link(),
  266. ),
  267. loader.ConfigValue(
  268. "ADDITIONAL_REPOS",
  269. # Currenly the trusted developers are specified
  270. [
  271. "https://github.com/hikariatama/host/raw/master",
  272. "https://github.com/MoriSummerz/ftg-mods/raw/main",
  273. "https://gitlab.com/CakesTwix/friendly-userbot-modules/-/raw/master",
  274. ],
  275. lambda: self.strings("add_repo_config_doc"),
  276. validator=loader.validators.Series(validator=loader.validators.Link()),
  277. ),
  278. loader.ConfigValue(
  279. "share_link",
  280. doc=lambda: self.strings("share_link_doc"),
  281. validator=loader.validators.Boolean(),
  282. ),
  283. )
  284. async def client_ready(self, *_):
  285. self.allmodules.add_aliases(self.lookup("settings").get("aliases", {}))
  286. main.hikka.ready.set()
  287. asyncio.ensure_future(self._update_modules())
  288. asyncio.ensure_future(self.get_repo_list("full"))
  289. @loader.loop(interval=3, wait_before=True, autostart=True)
  290. async def _config_autosaver(self):
  291. for mod in self.allmodules.modules:
  292. if not hasattr(mod, "config") or not mod.config:
  293. continue
  294. for option, config in mod.config._config.items():
  295. if not hasattr(config, "_save_marker"):
  296. continue
  297. delattr(mod.config._config[option], "_save_marker")
  298. self._db.setdefault(mod.__class__.__name__, {}).setdefault(
  299. "__config__", {}
  300. )[option] = config.value
  301. for lib in self.allmodules.libraries:
  302. if not hasattr(lib, "config") or not lib.config:
  303. continue
  304. for option, config in lib.config._config.items():
  305. if not hasattr(config, "_save_marker"):
  306. continue
  307. delattr(lib.config._config[option], "_save_marker")
  308. self._db.setdefault(lib.__class__.__name__, {}).setdefault(
  309. "__config__", {}
  310. )[option] = config.value
  311. self._db.save()
  312. def _update_modules_in_db(self):
  313. self.set(
  314. "loaded_modules",
  315. {
  316. module.__class__.__name__: module.__origin__
  317. for module in self.allmodules.modules
  318. if module.__origin__.startswith("http")
  319. },
  320. )
  321. @loader.owner
  322. async def dlmodcmd(self, message: Message):
  323. """Downloads and installs a module from the official module repo"""
  324. if args := utils.get_args(message):
  325. args = args[0]
  326. await self.download_and_install(args, message)
  327. if self._fully_loaded:
  328. self._update_modules_in_db()
  329. else:
  330. await self.inline.list(
  331. message,
  332. [
  333. self.strings("avail_header")
  334. + f"\n☁️ {repo.strip('/')}\n\n"
  335. + "\n".join(
  336. [
  337. " | ".join(chunk)
  338. for chunk in utils.chunks(
  339. [
  340. f"<code>{i}</code>"
  341. for i in sorted(
  342. [
  343. utils.escape_html(
  344. i.split("/")[-1].split(".")[0]
  345. )
  346. for i in mods.values()
  347. ]
  348. )
  349. ],
  350. 5,
  351. )
  352. ]
  353. )
  354. for repo, mods in (await self.get_repo_list("full")).items()
  355. ],
  356. )
  357. @loader.owner
  358. async def dlpresetcmd(self, message: Message):
  359. """Set modules preset"""
  360. args = utils.get_args(message)
  361. if not args:
  362. await utils.answer(message, self.strings("select_preset"))
  363. return
  364. await self.get_repo_list(args[0])
  365. self.set("chosen_preset", args[0])
  366. await utils.answer(message, self.strings("preset_loaded"))
  367. await self.allmodules.commands["restart"](
  368. await message.reply(f"{self.get_prefix()}restart --force")
  369. )
  370. async def _get_modules_to_load(self):
  371. preset = self.get("chosen_preset")
  372. if preset != "disable":
  373. possible_mods = (
  374. await self.get_repo_list(preset, only_primary=True)
  375. ).values()
  376. todo = dict(ChainMap(*possible_mods))
  377. else:
  378. todo = {}
  379. todo.update(**self.get("loaded_modules", {}))
  380. logger.debug(f"Loading modules: {todo}")
  381. return todo
  382. async def _get_repo(self, repo: str, preset: str) -> str:
  383. repo = repo.strip("/")
  384. preset_id = f"{repo}/{preset}"
  385. if self._links_cache.get(preset_id, {}).get("exp", 0) >= time.time():
  386. return self._links_cache[preset_id]["data"]
  387. res = await utils.run_sync(
  388. requests.get,
  389. f"{repo}/{preset}.txt",
  390. )
  391. if not str(res.status_code).startswith("2"):
  392. logger.debug(f"Can't load {repo=}, {preset=}, {res.status_code=}")
  393. return []
  394. self._links_cache[preset_id] = {
  395. "exp": time.time() + 5 * 60,
  396. "data": [link for link in res.text.strip().splitlines() if link],
  397. }
  398. return self._links_cache[preset_id]["data"]
  399. async def get_repo_list(
  400. self,
  401. preset: Optional[str] = None,
  402. only_primary: Optional[bool] = False,
  403. ) -> dict:
  404. if preset is None or preset == "none":
  405. preset = "minimal"
  406. return {
  407. repo: {
  408. f"Mod/{repo_id}/{i}": f'{repo.strip("/")}/{link}.py'
  409. for i, link in enumerate(set(await self._get_repo(repo, preset)))
  410. }
  411. for repo_id, repo in enumerate(
  412. [self.config["MODULES_REPO"]]
  413. + ([] if only_primary else self.config["ADDITIONAL_REPOS"])
  414. )
  415. if repo.startswith("http")
  416. }
  417. async def get_links_list(self):
  418. def converter(repo_dict: dict) -> list:
  419. return list(dict(ChainMap(*list(repo_dict.values()))).values())
  420. links = await self.get_repo_list("full")
  421. # Make `MODULES_REPO` primary one
  422. main_repo = list(links[self.config["MODULES_REPO"]].values())
  423. del links[self.config["MODULES_REPO"]]
  424. return main_repo + converter(links)
  425. async def _find_link(self, module_name: str) -> Union[str, bool]:
  426. links = await self.get_links_list()
  427. return next(
  428. (
  429. link
  430. for link in links
  431. if link.lower().endswith(f"/{module_name.lower()}.py")
  432. ),
  433. False,
  434. )
  435. async def download_and_install(
  436. self,
  437. module_name: str,
  438. message: Optional[Message] = None,
  439. ):
  440. try:
  441. blob_link = False
  442. module_name = module_name.strip()
  443. if urlparse(module_name).netloc:
  444. url = module_name
  445. if re.match(
  446. r"^(https:\/\/github\.com\/.*?\/.*?\/blob\/.*\.py)|"
  447. r"(https:\/\/gitlab\.com\/.*?\/.*?\/-\/blob\/.*\.py)$",
  448. url,
  449. ):
  450. url = url.replace("/blob/", "/raw/")
  451. blob_link = True
  452. else:
  453. url = await self._find_link(module_name)
  454. if not url:
  455. if message is not None:
  456. await utils.answer(message, self.strings("no_module"))
  457. return False
  458. r = await utils.run_sync(requests.get, url)
  459. if r.status_code == 404:
  460. if message is not None:
  461. await utils.answer(message, self.strings("no_module"))
  462. return False
  463. r.raise_for_status()
  464. return await self.load_module(
  465. r.content.decode("utf-8"),
  466. message,
  467. module_name,
  468. url,
  469. blob_link=blob_link,
  470. )
  471. except Exception:
  472. logger.exception(f"Failed to load {module_name}")
  473. async def _inline__load(
  474. self,
  475. call: InlineCall,
  476. doc: str,
  477. path_: Optional[str],
  478. mode: str,
  479. ):
  480. save = False
  481. if mode == "all_yes":
  482. self._db.set(main.__name__, "permanent_modules_fs", True)
  483. self._db.set(main.__name__, "disable_modules_fs", False)
  484. await call.answer(self.strings("will_save_fs"))
  485. save = True
  486. elif mode == "all_no":
  487. self._db.set(main.__name__, "disable_modules_fs", True)
  488. self._db.set(main.__name__, "permanent_modules_fs", False)
  489. elif mode == "once":
  490. save = True
  491. await self.load_module(doc, call, origin=path_ or "<string>", save_fs=save)
  492. @loader.owner
  493. async def loadmodcmd(self, message: Message):
  494. """Loads the module file"""
  495. msg = message if message.file else (await message.get_reply_message())
  496. if msg is None or msg.media is None:
  497. if args := utils.get_args(message):
  498. try:
  499. path_ = args[0]
  500. with open(path_, "rb") as f:
  501. doc = f.read()
  502. except FileNotFoundError:
  503. await utils.answer(message, self.strings("no_file"))
  504. return
  505. else:
  506. await utils.answer(message, self.strings("provide_module"))
  507. return
  508. else:
  509. path_ = None
  510. doc = await msg.download_media(bytes)
  511. logger.debug("Loading external module...")
  512. try:
  513. doc = doc.decode("utf-8")
  514. except UnicodeDecodeError:
  515. await utils.answer(message, self.strings("bad_unicode"))
  516. return
  517. if (
  518. not self._db.get(
  519. main.__name__,
  520. "disable_modules_fs",
  521. False,
  522. )
  523. and not self._db.get(main.__name__, "permanent_modules_fs", False)
  524. and "DYNO" not in os.environ
  525. ):
  526. if message.file:
  527. await message.edit("")
  528. message = await message.respond("🌘")
  529. if await self.inline.form(
  530. self.strings("module_fs"),
  531. message=message,
  532. reply_markup=[
  533. [
  534. {
  535. "text": self.strings("save"),
  536. "callback": self._inline__load,
  537. "args": (doc, path_, "once"),
  538. },
  539. {
  540. "text": self.strings("no_save"),
  541. "callback": self._inline__load,
  542. "args": (doc, path_, "no"),
  543. },
  544. ],
  545. [
  546. {
  547. "text": self.strings("save_for_all"),
  548. "callback": self._inline__load,
  549. "args": (doc, path_, "all_yes"),
  550. }
  551. ],
  552. [
  553. {
  554. "text": self.strings("never_save"),
  555. "callback": self._inline__load,
  556. "args": (doc, path_, "all_no"),
  557. }
  558. ],
  559. ],
  560. ):
  561. return
  562. if path_ is not None:
  563. await self.load_module(
  564. doc,
  565. message,
  566. origin=path_,
  567. save_fs=self._db.get(main.__name__, "permanent_modules_fs", False)
  568. and not self._db.get(main.__name__, "disable_modules_fs", False),
  569. )
  570. else:
  571. await self.load_module(
  572. doc,
  573. message,
  574. save_fs=self._db.get(main.__name__, "permanent_modules_fs", False)
  575. and not self._db.get(main.__name__, "disable_modules_fs", False),
  576. )
  577. async def _send_stats(self, url: str, retry: bool = False):
  578. """Send anonymous stats to Hikka"""
  579. try:
  580. if not self.get("token"):
  581. self.set(
  582. "token",
  583. (
  584. await self._client.inline_query(
  585. "@hikkamods_bot", "#get_hikka_token"
  586. )
  587. )[0].title,
  588. )
  589. res = await utils.run_sync(
  590. requests.post,
  591. "https://heta.hikariatama.ru/stats",
  592. data={"url": url},
  593. headers={"X-Hikka-Token": self.get("token")},
  594. )
  595. if res.status_code == 403:
  596. if retry:
  597. return
  598. self.set("token", None)
  599. return await self._send_stats(url, retry=True)
  600. except Exception:
  601. logger.debug("Failed to send stats", exc_info=True)
  602. async def load_module(
  603. self,
  604. doc: str,
  605. message: Message,
  606. name: Optional[Union[str, None]] = None,
  607. origin: Optional[str] = "<string>",
  608. did_requirements: Optional[bool] = False,
  609. save_fs: Optional[bool] = False,
  610. blob_link: Optional[bool] = False,
  611. ):
  612. if any(
  613. line.replace(" ", "") == "#scope:ffmpeg" for line in doc.splitlines()
  614. ) and os.system("ffmpeg -version 1>/dev/null 2>/dev/null"):
  615. if isinstance(message, Message):
  616. await utils.answer(message, self.strings("ffmpeg_required"))
  617. return
  618. if (
  619. any(line.replace(" ", "") == "#scope:inline" for line in doc.splitlines())
  620. and not self.inline.init_complete
  621. ):
  622. if isinstance(message, Message):
  623. await utils.answer(message, self.strings("inline_init_failed"))
  624. return
  625. if re.search(r"# ?scope: ?hikka_min", doc):
  626. ver = re.search(r"# ?scope: ?hikka_min ((\d+\.){2}\d+)", doc).group(1)
  627. ver_ = tuple(map(int, ver.split(".")))
  628. if main.__version__ < ver_:
  629. if isinstance(message, Message):
  630. if getattr(message, "file", None):
  631. m = utils.get_chat_id(message)
  632. await message.edit("")
  633. else:
  634. m = message
  635. await self.inline.form(
  636. self.strings("version_incompatible").format(ver),
  637. m,
  638. reply_markup=[
  639. {
  640. "text": self.lookup("updater").strings("btn_update"),
  641. "callback": self.lookup("updater").inline_update,
  642. },
  643. {
  644. "text": self.lookup("updater").strings("cancel"),
  645. "action": "close",
  646. },
  647. ],
  648. )
  649. return
  650. developer = re.search(r"# ?meta developer: ?(.+)", doc)
  651. developer = developer.group(1) if developer else False
  652. blob_link = self.strings("blob_link") if blob_link else ""
  653. url = copy.deepcopy(name)
  654. if name is None:
  655. try:
  656. node = ast.parse(doc)
  657. uid = next(n.name for n in node.body if isinstance(n, ast.ClassDef))
  658. except Exception:
  659. logger.debug(
  660. "Can't parse classname from code, using legacy uid instead",
  661. exc_info=True,
  662. )
  663. uid = "__extmod_" + str(uuid.uuid4())
  664. else:
  665. if name.startswith(self.config["MODULES_REPO"]):
  666. name = name.split("/")[-1].split(".py")[0]
  667. uid = name.replace("%", "%%").replace(".", "%d")
  668. module_name = f"hikka.modules.{uid}"
  669. doc = geek.compat(doc)
  670. async def core_overwrite(e: CoreOverwriteError):
  671. nonlocal message
  672. with contextlib.suppress(Exception):
  673. self.allmodules.modules.remove(instance)
  674. if not message:
  675. return
  676. await utils.answer(
  677. message,
  678. self.strings(f"overwrite_{e.type}").format(
  679. *(e.target,)
  680. if e.type == "module"
  681. else (self.get_prefix(), e.target)
  682. ),
  683. )
  684. try:
  685. try:
  686. spec = ModuleSpec(
  687. module_name,
  688. loader.StringLoader(
  689. doc, f"<string {uid}>" if origin == "<string>" else origin
  690. ),
  691. origin=f"<string {uid}>" if origin == "<string>" else origin,
  692. )
  693. instance = self.allmodules.register_module(
  694. spec,
  695. module_name,
  696. origin,
  697. save_fs=save_fs,
  698. )
  699. except ImportError as e:
  700. logger.info(
  701. "Module loading failed, attemping dependency installation",
  702. exc_info=True,
  703. )
  704. # Let's try to reinstall dependencies
  705. try:
  706. requirements = list(
  707. filter(
  708. lambda x: not x.startswith(("-", "_", ".")),
  709. map(
  710. str.strip,
  711. VALID_PIP_PACKAGES.search(doc)[1].split(),
  712. ),
  713. )
  714. )
  715. except TypeError:
  716. logger.warning(
  717. "No valid pip packages specified in code, attemping"
  718. " installation from error"
  719. )
  720. requirements = [e.name]
  721. logger.debug(f"Installing requirements: {requirements}")
  722. if not requirements:
  723. raise Exception("Nothing to install") from e
  724. if did_requirements:
  725. if message is not None:
  726. if "DYNO" in os.environ:
  727. await utils.answer(
  728. message,
  729. self.strings("heroku_install_failed"),
  730. )
  731. else:
  732. await utils.answer(
  733. message,
  734. self.strings("requirements_restart").format(e.name),
  735. )
  736. return
  737. if message is not None:
  738. await utils.answer(
  739. message,
  740. self.strings("requirements_installing").format(
  741. "\n".join(f"▫️ {req}" for req in requirements)
  742. ),
  743. )
  744. pip = await asyncio.create_subprocess_exec(
  745. sys.executable,
  746. "-m",
  747. "pip",
  748. "install",
  749. "--upgrade",
  750. "-q",
  751. "--disable-pip-version-check",
  752. "--no-warn-script-location",
  753. *["--user"] if USER_INSTALL else [],
  754. *requirements,
  755. )
  756. rc = await pip.wait()
  757. if rc != 0:
  758. if message is not None:
  759. if "com.termux" in os.environ.get("PREFIX", ""):
  760. await utils.answer(
  761. message,
  762. self.strings("requirements_failed_termux"),
  763. )
  764. else:
  765. await utils.answer(
  766. message,
  767. self.strings("requirements_failed"),
  768. )
  769. return
  770. importlib.invalidate_caches()
  771. kwargs = utils.get_kwargs()
  772. kwargs["did_requirements"] = True
  773. return await self.load_module(**kwargs) # Try again
  774. except loader.LoadError as e:
  775. with contextlib.suppress(ValueError):
  776. self.allmodules.modules.remove(instance) # skipcq: PYL-E0601
  777. if message:
  778. await utils.answer(message, f"🚫 <b>{utils.escape_html(str(e))}</b>")
  779. return
  780. except CoreOverwriteError as e:
  781. await core_overwrite(e)
  782. return
  783. except BaseException as e:
  784. logger.exception(f"Loading external module failed due to {e}")
  785. if message is not None:
  786. await utils.answer(message, self.strings("load_failed"))
  787. return
  788. instance.inline = self.inline
  789. if hasattr(instance, "__version__") and isinstance(instance.__version__, tuple):
  790. version = (
  791. "<b><i>"
  792. f" (v{'.'.join(list(map(str, list(instance.__version__))))})</i></b>"
  793. )
  794. else:
  795. version = ""
  796. try:
  797. try:
  798. self.allmodules.send_config_one(instance)
  799. await self.allmodules.send_ready_one(
  800. instance,
  801. no_self_unload=True,
  802. from_dlmod=bool(message),
  803. )
  804. except loader.LoadError as e:
  805. with contextlib.suppress(ValueError):
  806. self.allmodules.modules.remove(instance)
  807. if message:
  808. await utils.answer(message, f"🚫 <b>{utils.escape_html(str(e))}</b>")
  809. return
  810. except loader.SelfUnload as e:
  811. logging.debug(f"Unloading {instance}, because it raised `SelfUnload`")
  812. with contextlib.suppress(ValueError):
  813. self.allmodules.modules.remove(instance)
  814. if message:
  815. await utils.answer(message, f"🚫 <b>{utils.escape_html(str(e))}</b>")
  816. return
  817. except loader.SelfSuspend as e:
  818. logging.debug(f"Suspending {instance}, because it raised `SelfSuspend`")
  819. if message:
  820. await utils.answer(
  821. message,
  822. "🥶 <b>Module suspended itself\nReason:"
  823. f" {utils.escape_html(str(e))}</b>",
  824. )
  825. return
  826. except CoreOverwriteError as e:
  827. await core_overwrite(e)
  828. return
  829. except Exception as e:
  830. logger.exception(f"Module threw because {e}")
  831. if message is not None:
  832. await utils.answer(message, self.strings("load_failed"))
  833. return
  834. with contextlib.suppress(Exception):
  835. if (
  836. not any(
  837. line.replace(" ", "") == "#scope:no_stats"
  838. for line in doc.splitlines()
  839. )
  840. and self._db.get(main.__name__, "stats", True)
  841. and url is not None
  842. and utils.check_url(url)
  843. ):
  844. asyncio.ensure_future(self._send_stats(url))
  845. for alias, cmd in self.lookup("settings").get("aliases", {}).items():
  846. if cmd in instance.commands:
  847. self.allmodules.add_alias(alias, cmd)
  848. if message is None:
  849. return
  850. try:
  851. modname = instance.strings("name")
  852. except KeyError:
  853. modname = getattr(instance, "name", "ERROR")
  854. modhelp = ""
  855. if instance.__doc__:
  856. modhelp += f"<i>\nℹ️ {utils.escape_html(inspect.getdoc(instance))}</i>\n"
  857. subscribe = ""
  858. subscribe_markup = None
  859. depends_from = []
  860. for key in dir(instance):
  861. value = getattr(instance, key)
  862. if isinstance(value, loader.Library):
  863. depends_from.append(
  864. f"▫️ <code>{value.__class__.__name__}</code><b>"
  865. f" {self.strings('by')} </b><code>{value.developer if isinstance(getattr(value, 'developer', None), str) else 'Unknown'}</code>"
  866. )
  867. depends_from = (
  868. self.strings("depends_from").format("\n".join(depends_from))
  869. if depends_from
  870. else ""
  871. )
  872. def loaded_msg(use_subscribe: bool = True):
  873. nonlocal modname, version, modhelp, developer, origin, subscribe, blob_link, depends_from
  874. return self.strings("loaded").format(
  875. modname.strip(),
  876. version,
  877. utils.ascii_face(),
  878. modhelp,
  879. developer if not subscribe or not use_subscribe else "",
  880. depends_from,
  881. self.strings("modlink").format(origin)
  882. if origin != "<string>" and self.config["share_link"]
  883. else "",
  884. blob_link,
  885. subscribe if use_subscribe else "",
  886. )
  887. if developer:
  888. if developer.startswith("@") and developer not in self.get(
  889. "do_not_subscribe", []
  890. ):
  891. try:
  892. if developer in self._client._hikka_cache and getattr(
  893. await self._client.get_entity(developer), "left", True
  894. ):
  895. developer_entity = await self._client.force_get_entity(
  896. developer
  897. )
  898. else:
  899. developer_entity = await self._client.get_entity(developer)
  900. except Exception:
  901. developer_entity = None
  902. if (
  903. isinstance(developer_entity, Channel)
  904. and getattr(developer_entity, "left", True)
  905. and self._db.get(main.__name__, "suggest_subscribe", True)
  906. ):
  907. subscribe = self.strings("suggest_subscribe").format(
  908. f"@{utils.escape_html(developer_entity.username)}"
  909. )
  910. subscribe_markup = [
  911. {
  912. "text": self.strings("subscribe"),
  913. "callback": self._inline__subscribe,
  914. "args": (
  915. developer_entity.id,
  916. functools.partial(loaded_msg, use_subscribe=False),
  917. True,
  918. ),
  919. },
  920. {
  921. "text": self.strings("no_subscribe"),
  922. "callback": self._inline__subscribe,
  923. "args": (
  924. developer,
  925. functools.partial(loaded_msg, use_subscribe=False),
  926. False,
  927. ),
  928. },
  929. ]
  930. try:
  931. is_channel = isinstance(
  932. await self._client.get_entity(developer),
  933. Channel,
  934. )
  935. except Exception:
  936. is_channel = False
  937. developer = self.strings("developer").format(
  938. utils.escape_html(developer)
  939. if is_channel
  940. else f"<code>{utils.escape_html(developer)}</code>"
  941. )
  942. else:
  943. developer = ""
  944. if any(
  945. line.replace(" ", "") == "#scope:disable_onload_docs"
  946. for line in doc.splitlines()
  947. ):
  948. await utils.answer(message, loaded_msg(), reply_markup=subscribe_markup)
  949. return
  950. for _name, fun in sorted(
  951. instance.commands.items(),
  952. key=lambda x: x[0],
  953. ):
  954. modhelp += self.strings("single_cmd").format(
  955. self.get_prefix(),
  956. _name,
  957. (
  958. utils.escape_html(inspect.getdoc(fun))
  959. if fun.__doc__
  960. else self.strings("undoc_cmd")
  961. ),
  962. )
  963. if self.inline.init_complete:
  964. if hasattr(instance, "inline_handlers"):
  965. for _name, fun in sorted(
  966. instance.inline_handlers.items(),
  967. key=lambda x: x[0],
  968. ):
  969. modhelp += self.strings("ihandler").format(
  970. f"@{self.inline.bot_username} {_name}",
  971. (
  972. utils.escape_html(inspect.getdoc(fun))
  973. if fun.__doc__
  974. else self.strings("undoc_ihandler")
  975. ),
  976. )
  977. try:
  978. await utils.answer(message, loaded_msg(), reply_markup=subscribe_markup)
  979. except telethon.errors.rpcerrorlist.MediaCaptionTooLongError:
  980. await message.reply(loaded_msg(False))
  981. async def _inline__subscribe(
  982. self,
  983. call: InlineCall,
  984. entity: int,
  985. msg: callable,
  986. subscribe: bool,
  987. ):
  988. if not subscribe:
  989. self.set("do_not_subscribe", self.get("do_not_subscribe", []) + [entity])
  990. await utils.answer(call, msg())
  991. await call.answer(self.strings("not_subscribed"))
  992. return
  993. await self._client(JoinChannelRequest(entity))
  994. await utils.answer(call, msg())
  995. await call.answer(self.strings("subscribed"))
  996. @loader.owner
  997. async def unloadmodcmd(self, message: Message):
  998. """Unload module by class name"""
  999. args = utils.get_args_raw(message)
  1000. if not args:
  1001. await utils.answer(message, self.strings("no_class"))
  1002. return
  1003. instance = self.lookup(args)
  1004. if issubclass(instance.__class__, loader.Library):
  1005. await utils.answer(message, self.strings("cannot_unload_lib"))
  1006. return
  1007. worked = self.allmodules.unload_module(args)
  1008. self.set(
  1009. "loaded_modules",
  1010. {
  1011. mod: link
  1012. for mod, link in self.get("loaded_modules", {}).items()
  1013. if mod not in worked
  1014. },
  1015. )
  1016. msg = (
  1017. self.strings("unloaded").format(
  1018. ", ".join(
  1019. [(mod[:-3] if mod.endswith("Mod") else mod) for mod in worked]
  1020. )
  1021. )
  1022. if worked
  1023. else self.strings("not_unloaded")
  1024. )
  1025. await utils.answer(message, msg)
  1026. @loader.owner
  1027. async def clearmodulescmd(self, message: Message):
  1028. """Delete all installed modules"""
  1029. await self.inline.form(
  1030. self.strings("confirm_clearmodules"),
  1031. message,
  1032. reply_markup=[
  1033. {
  1034. "text": self.strings("clearmodules"),
  1035. "callback": self._inline__clearmodules,
  1036. },
  1037. {
  1038. "text": self.strings("cancel"),
  1039. "action": "close",
  1040. },
  1041. ],
  1042. )
  1043. async def _inline__clearmodules(self, call: InlineCall):
  1044. self.set("loaded_modules", {})
  1045. if "DYNO" not in os.environ:
  1046. for file in os.scandir(loader.LOADED_MODULES_DIR):
  1047. os.remove(file)
  1048. self.set("chosen_preset", "none")
  1049. await utils.answer(call, self.strings("all_modules_deleted"))
  1050. await self.lookup("Updater").restart_common(call)
  1051. async def _update_modules(self):
  1052. todo = await self._get_modules_to_load()
  1053. # ⚠️⚠️ WARNING! ⚠️⚠️
  1054. # If you are a module developer, and you'll try to bypass this protection to
  1055. # force user join your channel, you will be added to SCAM modules
  1056. # list and you will be banned from Hikka federation.
  1057. # Let USER decide, which channel he will follow. Do not be so petty
  1058. # I hope, you understood me.
  1059. # Thank you
  1060. if any(
  1061. arg in todo.values()
  1062. for arg in {
  1063. "https://mods.hikariatama.ru/forbid_joins.py",
  1064. "https://heta.hikariatama.ru/hikariatama/ftg/forbid_joins.py",
  1065. "https://github.com/hikariatama/ftg/raw/master/forbid_joins.py",
  1066. "https://raw.githubusercontent.com/hikariatama/ftg/master/forbid_joins.py",
  1067. }
  1068. ):
  1069. from ..forbid_joins import install_join_forbidder
  1070. install_join_forbidder(self._client)
  1071. secure_boot = False
  1072. if self._db.get(loader.__name__, "secure_boot", False):
  1073. self._db.set(loader.__name__, "secure_boot", False)
  1074. secure_boot = True
  1075. else:
  1076. for mod in todo.values():
  1077. await self.download_and_install(mod)
  1078. self._update_modules_in_db()
  1079. aliases = {
  1080. alias: cmd
  1081. for alias, cmd in self.lookup("settings").get("aliases", {}).items()
  1082. if self.allmodules.add_alias(alias, cmd)
  1083. }
  1084. self.lookup("settings").set("aliases", aliases)
  1085. self._fully_loaded = True
  1086. with contextlib.suppress(AttributeError):
  1087. await self.lookup("Updater").full_restart_complete(secure_boot)