updater.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  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. import asyncio
  9. import atexit
  10. import contextlib
  11. import logging
  12. import os
  13. import subprocess
  14. import sys
  15. import time
  16. import typing
  17. import git
  18. from git import GitCommandError, Repo
  19. from telethon.tl.functions.messages import (
  20. GetDialogFiltersRequest,
  21. UpdateDialogFilterRequest,
  22. )
  23. from telethon.tl.types import DialogFilter, Message
  24. from telethon.extensions.html import CUSTOM_EMOJIS
  25. from .. import loader, utils, main, version
  26. from ..inline.types import InlineCall
  27. logger = logging.getLogger(__name__)
  28. @loader.tds
  29. class UpdaterMod(loader.Module):
  30. """Updates itself"""
  31. strings = {
  32. "name": "Updater",
  33. "source": (
  34. "<emoji document_id=5456255401194429832>📖</emoji> <b>Read the source code"
  35. " from</b> <a href='{}'>here</a>"
  36. ),
  37. "restarting_caption": (
  38. "<emoji document_id=6318970114548958978>🕗</emoji> <b>Your {} is"
  39. " restarting...</b>"
  40. ),
  41. "downloading": (
  42. "<emoji document_id=6318970114548958978>🕗</emoji> <b>Downloading"
  43. " updates...</b>"
  44. ),
  45. "installing": (
  46. "<emoji document_id=6318970114548958978>🕗</emoji> <b>Installing"
  47. " updates...</b>"
  48. ),
  49. "success": (
  50. "<emoji document_id=6321050180095313397>⏱</emoji> <b>Restart successful!"
  51. " {}</b>\n<i>But still loading modules...</i>\n<i>Restart took {}s</i>"
  52. ),
  53. "origin_cfg_doc": "Git origin URL, for where to update from",
  54. "btn_restart": "🔄 Restart",
  55. "btn_update": "🧭 Update",
  56. "restart_confirm": "❓ <b>Are you sure you want to restart?</b>",
  57. "secure_boot_confirm": (
  58. "❓ <b>Are you sure you want to restart in secure boot mode?</b>"
  59. ),
  60. "update_confirm": (
  61. "❓ <b>Are you sure you"
  62. " want to update?\n\n<a"
  63. ' href="https://github.com/hikariatama/Hikka/commit/{}">{}</a> ⤑ <a'
  64. ' href="https://github.com/hikariatama/Hikka/commit/{}">{}</a></b>'
  65. ),
  66. "no_update": "🚸 <b>You are on the latest version, pull updates anyway?</b>",
  67. "cancel": "🚫 Cancel",
  68. "lavhost_restart": (
  69. "<emoji document_id=5469986291380657759>✌️</emoji> <b>Your {} is"
  70. " restarting...</b>"
  71. ),
  72. "lavhost_update": (
  73. "<emoji document_id=5469986291380657759>✌️</emoji> <b>Your {} is"
  74. " updating...</b>"
  75. ),
  76. "full_success": (
  77. "<emoji document_id=6323332130579416910>👍</emoji> <b>Userbot is fully"
  78. " loaded! {}</b>\n<i>Full restart took {}s</i>"
  79. ),
  80. "secure_boot_complete": (
  81. "🔒 <b>Secure boot completed! {}</b>\n<i>Restart took {}s</i>"
  82. ),
  83. }
  84. strings_ru = {
  85. "source": (
  86. "<emoji document_id=5456255401194429832>📖</emoji> <b>Исходный код можно"
  87. " прочитать</b> <a href='{}'>здесь</a>"
  88. ),
  89. "restarting_caption": (
  90. "<emoji document_id=6318970114548958978>🕗</emoji> <b>Твоя {}"
  91. " перезагружается...</b>"
  92. ),
  93. "downloading": (
  94. "<emoji document_id=6318970114548958978>🕗</emoji> <b>Скачивание"
  95. " обновлений...</b>"
  96. ),
  97. "installing": (
  98. "<emoji document_id=6318970114548958978>🕗</emoji> <b>Установка"
  99. " обновлений...</b>"
  100. ),
  101. "success": (
  102. "<emoji document_id=6321050180095313397>⏱</emoji> <b>Перезагрузка"
  103. " успешна! {}</b>\n<i>Но модули еще загружаются...</i>\n<i>Перезагрузка"
  104. " заняла {} сек</i>"
  105. ),
  106. "full_success": (
  107. "<emoji document_id=6323332130579416910>👍</emoji> <b>Юзербот полностью"
  108. " загружен! {}</b>\n<i>Полная перезагрузка заняла {} сек</i>"
  109. ),
  110. "secure_boot_complete": (
  111. "🔒 <b>Безопасная загрузка завершена! {}</b>\n<i>Перезагрузка заняла {}"
  112. " сек</i>"
  113. ),
  114. "origin_cfg_doc": "Ссылка, из которой будут загружаться обновления",
  115. "btn_restart": "🔄 Перезагрузиться",
  116. "btn_update": "🧭 Обновиться",
  117. "restart_confirm": "❓ <b>Ты уверен, что хочешь перезагрузиться?</b>",
  118. "secure_boot_confirm": (
  119. "❓ <b>Ты уверен, что"
  120. " хочешь перезагрузиться в режиме безопасной загрузки?</b>"
  121. ),
  122. "update_confirm": (
  123. "❓ <b>Ты уверен, что"
  124. " хочешь обновиться??\n\n<a"
  125. ' href="https://github.com/hikariatama/Hikka/commit/{}">{}</a> ⤑ <a'
  126. ' href="https://github.com/hikariatama/Hikka/commit/{}">{}</a></b>'
  127. ),
  128. "no_update": "🚸 <b>У тебя последняя версия. Обновиться принудительно?</b>",
  129. "cancel": "🚫 Отмена",
  130. "_cls_doc": "Обновляет юзербот",
  131. "lavhost_restart": (
  132. "<emoji document_id=5469986291380657759>✌️</emoji> <b>Твой {}"
  133. " перезагружается...</b>"
  134. ),
  135. "lavhost_update": (
  136. "<emoji document_id=5469986291380657759>✌️</emoji> <b>Твой {}"
  137. " обновляется...</b>"
  138. ),
  139. }
  140. def __init__(self):
  141. self.config = loader.ModuleConfig(
  142. loader.ConfigValue(
  143. "GIT_ORIGIN_URL",
  144. "https://github.com/hikariatama/Hikka",
  145. lambda: self.strings("origin_cfg_doc"),
  146. validator=loader.validators.Link(),
  147. )
  148. )
  149. @loader.owner
  150. @loader.command(ru_doc="Перезагружает юзербот")
  151. async def restart(self, message: Message):
  152. """Restarts the userbot"""
  153. secure_boot = "--secure-boot" in utils.get_args_raw(message)
  154. try:
  155. if (
  156. "--force" in (utils.get_args_raw(message) or "")
  157. or "-f" in (utils.get_args_raw(message) or "")
  158. or not self.inline.init_complete
  159. or not await self.inline.form(
  160. message=message,
  161. text=self.strings(
  162. "secure_boot_confirm" if secure_boot else "restart_confirm"
  163. ),
  164. reply_markup=[
  165. {
  166. "text": self.strings("btn_restart"),
  167. "callback": self.inline_restart,
  168. "args": (secure_boot,),
  169. },
  170. {"text": self.strings("cancel"), "action": "close"},
  171. ],
  172. )
  173. ):
  174. raise
  175. except Exception:
  176. await self.restart_common(message, secure_boot)
  177. async def inline_restart(self, call: InlineCall, secure_boot: bool = False):
  178. await self.restart_common(call, secure_boot=secure_boot)
  179. async def process_restart_message(self, msg_obj: typing.Union[InlineCall, Message]):
  180. self.set(
  181. "selfupdatemsg",
  182. msg_obj.inline_message_id
  183. if hasattr(msg_obj, "inline_message_id")
  184. else f"{utils.get_chat_id(msg_obj)}:{msg_obj.id}",
  185. )
  186. async def restart_common(
  187. self,
  188. msg_obj: typing.Union[InlineCall, Message],
  189. secure_boot: bool = False,
  190. ):
  191. if (
  192. hasattr(msg_obj, "form")
  193. and isinstance(msg_obj.form, dict)
  194. and "uid" in msg_obj.form
  195. and msg_obj.form["uid"] in self.inline._units
  196. and "message" in self.inline._units[msg_obj.form["uid"]]
  197. ):
  198. message = self.inline._units[msg_obj.form["uid"]]["message"]
  199. else:
  200. message = msg_obj
  201. if secure_boot:
  202. self._db.set(loader.__name__, "secure_boot", True)
  203. msg_obj = await utils.answer(
  204. msg_obj,
  205. self.strings("restarting_caption").format(
  206. utils.get_platform_emoji()
  207. if self._client.hikka_me.premium
  208. and CUSTOM_EMOJIS
  209. and isinstance(msg_obj, Message)
  210. else "Hikka"
  211. )
  212. if "LAVHOST" not in os.environ
  213. else self.strings("lavhost_restart").format(
  214. '</b><emoji document_id="5192756799647785066">✌️</emoji><emoji'
  215. ' document_id="5193117564015747203">✌️</emoji><emoji'
  216. ' document_id="5195050806105087456">✌️</emoji><emoji'
  217. ' document_id="5195457642587233944">✌️</emoji><b>'
  218. if self._client.hikka_me.premium
  219. and CUSTOM_EMOJIS
  220. and isinstance(msg_obj, Message)
  221. else "lavHost"
  222. ),
  223. )
  224. await self.process_restart_message(msg_obj)
  225. self.set("restart_ts", time.time())
  226. await self._db.remote_force_save()
  227. if "LAVHOST" in os.environ:
  228. os.system("lavhost restart")
  229. return
  230. with contextlib.suppress(Exception):
  231. await main.hikka.web.stop()
  232. atexit.register(restart, *sys.argv[1:])
  233. handler = logging.getLogger().handlers[0]
  234. handler.setLevel(logging.CRITICAL)
  235. for client in self.allclients:
  236. # Terminate main loop of all running clients
  237. # Won't work if not all clients are ready
  238. if client is not message.client:
  239. await client.disconnect()
  240. await message.client.disconnect()
  241. sys.exit(0)
  242. async def download_common(self):
  243. try:
  244. repo = Repo(os.path.dirname(utils.get_base_dir()))
  245. origin = repo.remote("origin")
  246. r = origin.pull()
  247. new_commit = repo.head.commit
  248. for info in r:
  249. if info.old_commit:
  250. for d in new_commit.diff(info.old_commit):
  251. if d.b_path == "requirements.txt":
  252. return True
  253. return False
  254. except git.exc.InvalidGitRepositoryError:
  255. repo = Repo.init(os.path.dirname(utils.get_base_dir()))
  256. origin = repo.create_remote("origin", self.config["GIT_ORIGIN_URL"])
  257. origin.fetch()
  258. repo.create_head("master", origin.refs.master)
  259. repo.heads.master.set_tracking_branch(origin.refs.master)
  260. repo.heads.master.checkout(True)
  261. return False
  262. @staticmethod
  263. def req_common():
  264. # Now we have downloaded new code, install requirements
  265. logger.debug("Installing new requirements...")
  266. try:
  267. subprocess.run(
  268. [
  269. sys.executable,
  270. "-m",
  271. "pip",
  272. "install",
  273. "-r",
  274. os.path.join(
  275. os.path.dirname(utils.get_base_dir()),
  276. "requirements.txt",
  277. ),
  278. "--user",
  279. ],
  280. check=True,
  281. )
  282. except subprocess.CalledProcessError:
  283. logger.exception("Req install failed")
  284. @loader.owner
  285. @loader.command(ru_doc="Скачивает обновления юзербота")
  286. async def update(self, message: Message):
  287. """Downloads userbot updates"""
  288. try:
  289. current = utils.get_git_hash()
  290. upcoming = next(
  291. git.Repo().iter_commits(f"origin/{version.branch}", max_count=1)
  292. ).hexsha
  293. if (
  294. "--force" in (utils.get_args_raw(message) or "")
  295. or "-f" in (utils.get_args_raw(message) or "")
  296. or not self.inline.init_complete
  297. or not await self.inline.form(
  298. message=message,
  299. text=self.strings("update_confirm").format(
  300. current, current[:8], upcoming, upcoming[:8]
  301. )
  302. if upcoming != current
  303. else self.strings("no_update"),
  304. reply_markup=[
  305. {
  306. "text": self.strings("btn_update"),
  307. "callback": self.inline_update,
  308. },
  309. {"text": self.strings("cancel"), "action": "close"},
  310. ],
  311. )
  312. ):
  313. raise
  314. except Exception:
  315. await self.inline_update(message)
  316. async def inline_update(
  317. self,
  318. msg_obj: typing.Union[InlineCall, Message],
  319. hard: bool = False,
  320. ):
  321. # We don't really care about asyncio at this point, as we are shutting down
  322. if hard:
  323. os.system(f"cd {utils.get_base_dir()} && cd .. && git reset --hard HEAD")
  324. try:
  325. if "LAVHOST" in os.environ:
  326. msg_obj = await utils.answer(
  327. msg_obj,
  328. self.strings("lavhost_update").format(
  329. "</b><emoji document_id=5192756799647785066>✌️</emoji><emoji"
  330. " document_id=5193117564015747203>✌️</emoji><emoji"
  331. " document_id=5195050806105087456>✌️</emoji><emoji"
  332. " document_id=5195457642587233944>✌️</emoji><b>"
  333. if self._client.hikka_me.premium
  334. and CUSTOM_EMOJIS
  335. and isinstance(msg_obj, Message)
  336. else "lavHost"
  337. ),
  338. )
  339. await self.process_restart_message(msg_obj)
  340. os.system("lavhost update")
  341. return
  342. with contextlib.suppress(Exception):
  343. msg_obj = await utils.answer(msg_obj, self.strings("downloading"))
  344. req_update = await self.download_common()
  345. with contextlib.suppress(Exception):
  346. msg_obj = await utils.answer(msg_obj, self.strings("installing"))
  347. if req_update:
  348. self.req_common()
  349. await self.restart_common(msg_obj)
  350. except GitCommandError:
  351. if not hard:
  352. await self.inline_update(msg_obj, True)
  353. return
  354. logger.critical("Got update loop. Update manually via .terminal")
  355. return
  356. @loader.unrestricted
  357. @loader.command(ru_doc="Показать ссылку на исходный код проекта")
  358. async def source(self, message: Message):
  359. """Links the source code of this project"""
  360. await utils.answer(
  361. message,
  362. self.strings("source").format(self.config["GIT_ORIGIN_URL"]),
  363. )
  364. async def client_ready(self):
  365. if self.get("selfupdatemsg") is not None:
  366. try:
  367. await self.update_complete()
  368. except Exception:
  369. logger.exception("Failed to complete update!")
  370. if self.get("do_not_create", False):
  371. return
  372. try:
  373. await self._add_folder()
  374. except Exception:
  375. logger.exception("Failed to add folder!")
  376. finally:
  377. self.set("do_not_create", True)
  378. async def _add_folder(self):
  379. folders = await self._client(GetDialogFiltersRequest())
  380. if any(getattr(folder, "title", None) == "hikka" for folder in folders):
  381. return
  382. try:
  383. folder_id = (
  384. max(
  385. folders,
  386. key=lambda x: x.id,
  387. ).id
  388. + 1
  389. )
  390. except ValueError:
  391. folder_id = 2
  392. try:
  393. await self._client(
  394. UpdateDialogFilterRequest(
  395. folder_id,
  396. DialogFilter(
  397. folder_id,
  398. title="hikka",
  399. pinned_peers=(
  400. [
  401. await self._client.get_input_entity(
  402. self._client.loader.inline.bot_id
  403. )
  404. ]
  405. if self._client.loader.inline.init_complete
  406. else []
  407. ),
  408. include_peers=[
  409. await self._client.get_input_entity(dialog.entity)
  410. async for dialog in self._client.iter_dialogs(
  411. None,
  412. ignore_migrated=True,
  413. )
  414. if dialog.name
  415. in {
  416. "hikka-logs",
  417. "hikka-onload",
  418. "hikka-assets",
  419. "hikka-backups",
  420. "hikka-acc-switcher",
  421. "silent-tags",
  422. }
  423. and dialog.is_channel
  424. and (
  425. dialog.entity.participants_count == 1
  426. or dialog.entity.participants_count == 2
  427. and dialog.name in {"hikka-logs", "silent-tags"}
  428. )
  429. or (
  430. self._client.loader.inline.init_complete
  431. and dialog.entity.id
  432. == self._client.loader.inline.bot_id
  433. )
  434. or dialog.entity.id
  435. in [
  436. 1554874075,
  437. 1697279580,
  438. 1679998924,
  439. ] # official hikka chats
  440. ],
  441. emoticon="🐱",
  442. exclude_peers=[],
  443. contacts=False,
  444. non_contacts=False,
  445. groups=False,
  446. broadcasts=False,
  447. bots=False,
  448. exclude_muted=False,
  449. exclude_read=False,
  450. exclude_archived=False,
  451. ),
  452. )
  453. )
  454. except Exception:
  455. logger.critical(
  456. "Can't create Hikka folder. Possible reasons are:\n"
  457. "- User reached the limit of folders in Telegram\n"
  458. "- User got floodwait\n"
  459. "Ignoring error and adding folder addition to ignore list"
  460. )
  461. async def update_complete(self):
  462. logger.debug("Self update successful! Edit message")
  463. start = self.get("restart_ts")
  464. try:
  465. took = round(time.time() - start)
  466. except Exception:
  467. took = "n/a"
  468. msg = self.strings("success").format(utils.ascii_face(), took)
  469. ms = self.get("selfupdatemsg")
  470. if ":" in str(ms):
  471. chat_id, message_id = ms.split(":")
  472. chat_id, message_id = int(chat_id), int(message_id)
  473. await self._client.edit_message(chat_id, message_id, msg)
  474. return
  475. await self.inline.bot.edit_message_text(
  476. inline_message_id=ms,
  477. text=self.inline.sanitise_text(msg),
  478. )
  479. async def full_restart_complete(self, secure_boot: bool = False):
  480. start = self.get("restart_ts")
  481. try:
  482. took = round(time.time() - start)
  483. except Exception:
  484. took = "n/a"
  485. self.set("restart_ts", None)
  486. ms = self.get("selfupdatemsg")
  487. msg = self.strings(
  488. "secure_boot_complete" if secure_boot else "full_success"
  489. ).format(utils.ascii_face(), took)
  490. if ms is None:
  491. return
  492. self.set("selfupdatemsg", None)
  493. if ":" in str(ms):
  494. chat_id, message_id = ms.split(":")
  495. chat_id, message_id = int(chat_id), int(message_id)
  496. await self._client.edit_message(chat_id, message_id, msg)
  497. await asyncio.sleep(60)
  498. await self._client.delete_messages(chat_id, message_id)
  499. return
  500. await self.inline.bot.edit_message_text(
  501. inline_message_id=ms,
  502. text=self.inline.sanitise_text(msg),
  503. )
  504. def restart(*argv):
  505. os.execl(
  506. sys.executable,
  507. sys.executable,
  508. "-m",
  509. os.path.relpath(utils.get_base_dir()),
  510. *argv,
  511. )