123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570 |
- # Friendly Telegram (telegram userbot)
- # Copyright (C) 2018-2021 The Authors
- # This program is free software: you can redistribute it and/or modify
- # it under the terms of the GNU Affero General Public License as published by
- # the Free Software Foundation, either version 3 of the License, or
- # (at your option) any later version.
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU Affero General Public License for more details.
- # You should have received a copy of the GNU Affero General Public License
- # along with this program. If not, see <https://www.gnu.org/licenses/>.
- # █ █ ▀ █▄▀ ▄▀█ █▀█ ▀
- # █▀█ █ █ █ █▀█ █▀▄ █
- # © Copyright 2022
- # https://t.me/hikariatama
- #
- # 🔒 Licensed under the GNU AGPLv3
- # 🌐 https://www.gnu.org/licenses/agpl-3.0.html
- import asyncio
- import atexit
- import contextlib
- import logging
- import os
- import subprocess
- import sys
- from typing import Union
- import time
- import git
- from git import GitCommandError, Repo
- from telethon.tl.functions.messages import (
- GetDialogFiltersRequest,
- UpdateDialogFilterRequest,
- )
- from telethon.tl.types import DialogFilter, Message
- from .. import loader, utils, heroku, main
- from ..inline.types import InlineCall
- logger = logging.getLogger(__name__)
- @loader.tds
- class UpdaterMod(loader.Module):
- """Updates itself"""
- strings = {
- "name": "Updater",
- "source": "ℹ️ <b>Read the source code from</b> <a href='{}'>here</a>",
- "restarting_caption": "🔄 <b>Restarting...</b>",
- "downloading": "🕐 <b>Downloading updates...</b>",
- "installing": "🕐 <b>Installing updates...</b>",
- "success": (
- "⏳ <b>Restart successful! {}</b>\n<i>But still loading"
- " modules...</i>\n<i>Restart took {}s</i>"
- ),
- "origin_cfg_doc": "Git origin URL, for where to update from",
- "btn_restart": "🔄 Restart",
- "btn_update": "🧭 Update",
- "restart_confirm": "🔄 <b>Are you sure you want to restart?</b>",
- "secure_boot_confirm": (
- "🔄 <b>Are you sure you want to restart in secure boot mode?</b>"
- ),
- "update_confirm": (
- "🧭 <b>Are you sure you want to update?\n\n"
- '<a href="https://github.com/hikariatama/Hikka/commit/{}">{}</a> ⤑ '
- '<a href="https://github.com/hikariatama/Hikka/commit/{}">{}</a></b>'
- ),
- "no_update": "🚸 <b>You are on the latest version, pull updates anyway?</b>",
- "cancel": "🚫 Cancel",
- "lavhost_restart": "✌️ <b>Your lavHost is restarting...\n>///<</b>",
- "lavhost_update": "✌️ <b>Your lavHost is updating...\n>///<</b>",
- "heroku_update": (
- "♓️ <b>Deploying new version to Heroku...\nThis might take some time</b>"
- ),
- "heroku_update_done_nothing_to_push": (
- "😔 <b>Update complete. Nothing to push...</b>"
- ),
- "full_success": (
- "✅ <b>Userbot is fully loaded! {}</b>\n<i>Full restart took {}s</i>"
- ),
- "secure_boot_complete": (
- "🔒 <b>Secure boot completed! {}</b>\n<i>Restart took {}s</i>"
- ),
- }
- strings_ru = {
- "source": "ℹ️ <b>Исходный код можно прочитать</b> <a href='{}'>здесь</a>",
- "restarting_caption": "🔄 <b>Перезагрузка...</b>",
- "downloading": "🕐 <b>Скачивание обновлений...</b>",
- "installing": "🕐 <b>Установка обновлений...</b>",
- "success": (
- "⏳ <b>Перезагрузка успешна! {}</b>\n<i>Но модули еще"
- " загружаются...</i>\n<i>Перезагрузка заняла {} сек</i>"
- ),
- "full_success": (
- "✅ <b>Юзербот полностью загружен! {}</b>\n<i>Полная перезагрузка заняла {}"
- " сек</i>"
- ),
- "secure_boot_complete": (
- "🔒 <b>Безопасная загрузка завершена! {}</b>\n<i>Перезагрузка заняла {}"
- " сек</i>"
- ),
- "origin_cfg_doc": "Ссылка, из которой будут загружаться обновления",
- "btn_restart": "🔄 Перезагрузиться",
- "btn_update": "🧭 Обновиться",
- "restart_confirm": "🔄 <b>Ты уверен, что хочешь перезагрузиться?</b>",
- "secure_boot_confirm": (
- "🔄 <b>Ты уверен, что хочешь перезагрузиться в режиме безопасной"
- " загрузки?</b>"
- ),
- "update_confirm": (
- "🧭 <b>Ты уверен, что хочешь обновиться??\n\n"
- '<a href="https://github.com/hikariatama/Hikka/commit/{}">{}</a> ⤑ '
- '<a href="https://github.com/hikariatama/Hikka/commit/{}">{}</a></b>'
- ),
- "no_update": "🚸 <b>У тебя последняя версия. Обновиться принудительно?</b>",
- "cancel": "🚫 Отмена",
- "_cmd_doc_restart": "Перезагружает юзербот",
- "_cmd_doc_download": "Скачивает обновления",
- "_cmd_doc_update": "Обновляет юзербот",
- "_cmd_doc_source": "Ссылка на исходный код проекта",
- "_cls_doc": "Обновляет юзербот",
- "lavhost_restart": "✌️ <b>Твой lavHost перезагружается...\n>///<</b>",
- "lavhost_update": "✌️ <b>Твой lavHost обновляется...\n>///<</b>",
- "heroku_update": (
- "♓️ <b>Обновляю Heroku...\nЭто может занять некоторое время</b>"
- ),
- "heroku_update_done_nothing_to_push": (
- "😔 <b>Обновление завершено. Ничего не изменилось, нечего обновлять...</b>"
- ),
- }
- def __init__(self):
- self.config = loader.ModuleConfig(
- loader.ConfigValue(
- "GIT_ORIGIN_URL",
- "https://github.com/hikariatama/Hikka",
- lambda: self.strings("origin_cfg_doc"),
- validator=loader.validators.Link(),
- )
- )
- @loader.owner
- async def restartcmd(self, message: Message):
- """Restarts the userbot"""
- secure_boot = "--secure-boot" in utils.get_args_raw(message)
- try:
- if (
- "--force" in (utils.get_args_raw(message) or "")
- or not self.inline.init_complete
- or not await self.inline.form(
- message=message,
- text=self.strings(
- "secure_boot_confirm" if secure_boot else "restart_confirm"
- ),
- reply_markup=[
- {
- "text": self.strings("btn_restart"),
- "callback": self.inline_restart,
- "args": (secure_boot,),
- },
- {"text": self.strings("cancel"), "action": "close"},
- ],
- )
- ):
- raise
- except Exception:
- await self.restart_common(message, secure_boot)
- async def inline_restart(self, call: InlineCall, secure_boot: bool = False):
- await self.restart_common(call, secure_boot=secure_boot)
- async def process_restart_message(self, msg_obj: Union[InlineCall, Message]):
- self.set(
- "selfupdatemsg",
- msg_obj.inline_message_id
- if hasattr(msg_obj, "inline_message_id")
- else f"{utils.get_chat_id(msg_obj)}:{msg_obj.id}",
- )
- async def restart_common(
- self,
- msg_obj: Union[InlineCall, Message],
- secure_boot: bool = False,
- ):
- if (
- hasattr(msg_obj, "form")
- and isinstance(msg_obj.form, dict)
- and "uid" in msg_obj.form
- and msg_obj.form["uid"] in self.inline._units
- and "message" in self.inline._units[msg_obj.form["uid"]]
- ):
- message = self.inline._units[msg_obj.form["uid"]]["message"]
- else:
- message = msg_obj
- if secure_boot:
- self._db.set(loader.__name__, "secure_boot", True)
- msg_obj = await utils.answer(
- msg_obj,
- self.strings(
- "restarting_caption"
- if "LAVHOST" not in os.environ
- else "lavhost_restart"
- ),
- )
- await self.process_restart_message(msg_obj)
- self.set("restart_ts", time.time())
- await self._db.remote_force_save()
- if "LAVHOST" in os.environ:
- os.system("lavhost restart")
- return
- if "DYNO" in os.environ:
- app = heroku.get_app(api_token=main.hikka.api_token)[0]
- app.restart()
- return
- with contextlib.suppress(Exception):
- await main.hikka.web.stop()
- atexit.register(restart, *sys.argv[1:])
- handler = logging.getLogger().handlers[0]
- handler.setLevel(logging.CRITICAL)
- for client in self.allclients:
- # Terminate main loop of all running clients
- # Won't work if not all clients are ready
- if client is not message.client:
- await client.disconnect()
- await message.client.disconnect()
- sys.exit(0)
- async def download_common(self):
- try:
- repo = Repo(os.path.dirname(utils.get_base_dir()))
- origin = repo.remote("origin")
- r = origin.pull()
- new_commit = repo.head.commit
- for info in r:
- if info.old_commit:
- for d in new_commit.diff(info.old_commit):
- if d.b_path == "requirements.txt":
- return True
- return False
- except git.exc.InvalidGitRepositoryError:
- repo = Repo.init(os.path.dirname(utils.get_base_dir()))
- origin = repo.create_remote("origin", self.config["GIT_ORIGIN_URL"])
- origin.fetch()
- repo.create_head("master", origin.refs.master)
- repo.heads.master.set_tracking_branch(origin.refs.master)
- repo.heads.master.checkout(True)
- return False
- @staticmethod
- def req_common():
- # Now we have downloaded new code, install requirements
- logger.debug("Installing new requirements...")
- try:
- subprocess.run(
- [
- sys.executable,
- "-m",
- "pip",
- "install",
- "-r",
- os.path.join(
- os.path.dirname(utils.get_base_dir()),
- "requirements.txt",
- ),
- "--user",
- ],
- check=True,
- )
- except subprocess.CalledProcessError:
- logger.exception("Req install failed")
- @loader.owner
- async def updatecmd(self, message: Message):
- """Downloads userbot updates"""
- try:
- current = utils.get_git_hash()
- upcoming = next(
- git.Repo().iter_commits("origin/master", max_count=1)
- ).hexsha
- if (
- "--force" in (utils.get_args_raw(message) or "")
- or not self.inline.init_complete
- or not await self.inline.form(
- message=message,
- text=self.strings("update_confirm").format(
- *(
- [current, current[:8], upcoming, upcoming[:8]]
- if "DYNO" not in os.environ
- else ["", "", "", ""]
- )
- )
- if upcoming != current or "DYNO" in os.environ
- else self.strings("no_update"),
- reply_markup=[
- {
- "text": self.strings("btn_update"),
- "callback": self.inline_update,
- },
- {"text": self.strings("cancel"), "action": "close"},
- ],
- )
- ):
- raise
- except Exception:
- await self.inline_update(message)
- async def inline_update(
- self,
- msg_obj: Union[InlineCall, Message],
- hard: bool = False,
- ):
- # We don't really care about asyncio at this point, as we are shutting down
- if hard:
- os.system(f"cd {utils.get_base_dir()} && cd .. && git reset --hard HEAD")
- try:
- if "LAVHOST" in os.environ:
- msg_obj = await utils.answer(msg_obj, self.strings("lavhost_update"))
- await self.process_restart_message(msg_obj)
- os.system("lavhost update")
- return
- if "DYNO" in os.environ:
- msg_obj = await utils.answer(msg_obj, self.strings("heroku_update"))
- await self.process_restart_message(msg_obj)
- try:
- nosave = "--no-save" in utils.get_args_raw(msg_obj)
- except Exception:
- nosave = False
- if not nosave:
- await self._db.remote_force_save()
- app, _ = heroku.get_app(
- api_token=main.hikka.api_token,
- create_new=False,
- )
- repo = heroku.get_repo()
- url = app.git_url.replace(
- "https://",
- f"https://api:{os.environ.get('heroku_api_token')}@",
- )
- if "heroku" in repo.remotes:
- remote = repo.remote("heroku")
- remote.set_url(url)
- else:
- remote = repo.create_remote("heroku", url)
- await utils.run_sync(remote.push, refspec="HEAD:refs/heads/master")
- await utils.answer(
- msg_obj,
- self.strings("heroku_update_done_nothing_to_push"),
- )
- return
- with contextlib.suppress(Exception):
- msg_obj = await utils.answer(msg_obj, self.strings("downloading"))
- req_update = await self.download_common()
- with contextlib.suppress(Exception):
- msg_obj = await utils.answer(msg_obj, self.strings("installing"))
- if req_update:
- self.req_common()
- await self.restart_common(msg_obj)
- except GitCommandError:
- if not hard:
- await self.inline_update(msg_obj, True)
- return
- logger.critical("Got update loop. Update manually via .terminal")
- return
- @loader.unrestricted
- async def sourcecmd(self, message: Message):
- """Links the source code of this project"""
- await utils.answer(
- message,
- self.strings("source").format(self.config["GIT_ORIGIN_URL"]),
- )
- async def client_ready(self, client, _):
- if self.get("selfupdatemsg") is not None:
- try:
- await self.update_complete(client)
- except Exception:
- logger.exception("Failed to complete update!")
- if self.get("do_not_create", False):
- return
- try:
- await self._add_folder()
- except Exception:
- logger.exception("Failed to add folder!")
- finally:
- self.set("do_not_create", True)
- async def _add_folder(self):
- folders = await self._client(GetDialogFiltersRequest())
- if any(getattr(folder, "title", None) == "hikka" for folder in folders):
- return
- try:
- folder_id = (
- max(
- folders,
- key=lambda x: x.id,
- ).id
- + 1
- )
- except ValueError:
- folder_id = 2
- try:
- await self._client(
- UpdateDialogFilterRequest(
- folder_id,
- DialogFilter(
- folder_id,
- title="hikka",
- pinned_peers=(
- [
- await self._client.get_input_entity(
- self._client.loader.inline.bot_id
- )
- ]
- if self._client.loader.inline.init_complete
- else []
- ),
- include_peers=[
- await self._client.get_input_entity(dialog.entity)
- async for dialog in self._client.iter_dialogs(
- None,
- ignore_migrated=True,
- )
- if dialog.name
- in {
- "hikka-logs",
- "hikka-onload",
- "hikka-assets",
- "hikka-backups",
- "hikka-acc-switcher",
- "silent-tags",
- }
- and dialog.is_channel
- and (
- dialog.entity.participants_count == 1
- or dialog.entity.participants_count == 2
- and dialog.name in {"hikka-logs", "silent-tags"}
- )
- or (
- self._client.loader.inline.init_complete
- and dialog.entity.id
- == self._client.loader.inline.bot_id
- )
- or dialog.entity.id
- in [
- 1554874075,
- 1697279580,
- 1679998924,
- ] # official hikka chats
- ],
- emoticon="🐱",
- exclude_peers=[],
- contacts=False,
- non_contacts=False,
- groups=False,
- broadcasts=False,
- bots=False,
- exclude_muted=False,
- exclude_read=False,
- exclude_archived=False,
- ),
- )
- )
- except Exception:
- logger.critical(
- "Can't create Hikka folder. Possible reasons are:\n"
- "- User reached the limit of folders in Telegram\n"
- "- User got floodwait\n"
- "Ignoring error and adding folder addition to ignore list"
- )
- async def update_complete(self, client: "TelegramClient"): # type: ignore
- logger.debug("Self update successful! Edit message")
- start = self.get("restart_ts")
- try:
- took = round(time.time() - start)
- except Exception:
- took = "n/a"
- msg = self.strings("success").format(utils.ascii_face(), took)
- ms = self.get("selfupdatemsg")
- if ":" in str(ms):
- chat_id, message_id = ms.split(":")
- chat_id, message_id = int(chat_id), int(message_id)
- await self._client.edit_message(chat_id, message_id, msg)
- return
- await self.inline.bot.edit_message_text(
- inline_message_id=ms,
- text=msg,
- )
- async def full_restart_complete(self, secure_boot: bool = False):
- start = self.get("restart_ts")
- try:
- took = round(time.time() - start)
- except Exception:
- took = "n/a"
- self.set("restart_ts", None)
- ms = self.get("selfupdatemsg")
- msg = self.strings(
- "secure_boot_complete" if secure_boot else "full_success"
- ).format(utils.ascii_face(), took)
- if ms is None:
- return
- self.set("selfupdatemsg", None)
- if ":" in str(ms):
- chat_id, message_id = ms.split(":")
- chat_id, message_id = int(chat_id), int(message_id)
- await self._client.edit_message(chat_id, message_id, msg)
- await asyncio.sleep(60)
- await self._client.delete_messages(chat_id, message_id)
- return
- await self.inline.bot.edit_message_text(
- inline_message_id=ms,
- text=msg,
- )
- def restart(*argv):
- os.execl(
- sys.executable,
- sys.executable,
- "-m",
- os.path.relpath(utils.get_base_dir()),
- *argv,
- )
|