Heroku/hikka/modules/updater.py

413 lines
15 KiB
Python
Executable File
Raw Blame History

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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 GPLv3
# 🌐 https://www.gnu.org/licenses/agpl-3.0.html
import atexit
import functools
import logging
import os
import subprocess
import sys
import asyncio
from typing import Union
import git
from git import Repo, GitCommandError
from telethon.tl.types import Message, DialogFilter
from telethon.tl.functions.messages import (
UpdateDialogFilterRequest,
GetDialogFiltersRequest,
)
from .. import loader, utils
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>",
"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>",
"update_confirm": "🧭 <b>Are you sure you want to update?</b>",
"cancel": "🚫 Cancel",
"lavhost_restart": "✌️ <b>Your lavHost is restarting...\n&gt;///&lt;</b>",
"lavhost_update": "✌️ <b>Your lavHost is updating...\n&gt;///&lt;</b>",
}
strings_ru = {
"source": " <b>Исходный код можно прочитать</b> <a href='{}'>здесь</a>",
"restarting_caption": "🔄 <b>Перезагрузка...</b>",
"downloading": "🕐 <b>Скачивание обновлений...</b>",
"installing": "🕐 <b>Установка обновлений...</b>",
"success": "✅ <b>Перезагрузка успешна! {}</b>",
"origin_cfg_doc": "Ссылка, из которой будут загружаться обновления",
"btn_restart": "🔄 Перезагрузиться",
"btn_update": "🧭 Обновиться",
"restart_confirm": "🔄 <b>Ты уверен, что хочешь перезагрузиться?</b>",
"update_confirm": "🧭 <b>Ты уверен, что хочешь обновиться?</b>",
"cancel": "🚫 Отмена",
"_cmd_doc_restart": "Перезагружает юзербот",
"_cmd_doc_download": "Скачивает обновления",
"_cmd_doc_update": "Обновляет юзербот",
"_cmd_doc_source": "Ссылка на исходный код проекта",
"_cls_doc": "Обновляет юзербот",
"lavhost_restart": "✌️ <b>Твой lavHost перезагружается...\n&gt;///&lt;</b>",
"lavhost_update": "✌️ <b>Твой lavHost обновляется...\n&gt;///&lt;</b>",
}
def __init__(self):
self.config = loader.ModuleConfig(
"GIT_ORIGIN_URL",
"https://github.com/hikariatama/Hikka",
lambda: self.strings("origin_cfg_doc"),
)
@loader.owner
async def restartcmd(self, message: Message):
"""Restarts the userbot"""
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("restart_confirm"),
reply_markup=[
{
"text": self.strings("btn_restart"),
"callback": self.inline_restart,
},
{"text": self.strings("cancel"), "callback": self.inline_close},
],
)
):
raise
except Exception:
await self.restart_common(message)
async def inline_restart(self, call: InlineCall):
await self.restart_common(call)
async def inline_close(self, call: InlineCall):
await call.delete()
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]):
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._forms
and "message" in self.inline._forms[msg_obj.form["uid"]]
):
message = self.inline._forms[msg_obj.form["uid"]]["message"]
else:
message = msg_obj
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)
if "LAVHOST" in os.environ:
os.system("lavhost restart")
return
atexit.register(functools.partial(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()
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( # skipcq: PYL-W1510
[
sys.executable,
"-m",
"pip",
"install",
"-r",
os.path.join(
os.path.dirname(utils.get_base_dir()),
"requirements.txt",
),
"--user",
]
)
except subprocess.CalledProcessError:
logger.exception("Req install failed")
@loader.owner
async def updatecmd(self, message: Message):
"""Downloads userbot updates"""
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("update_confirm"),
reply_markup=[
{
"text": self.strings("btn_update"),
"callback": self.inline_update,
},
{"text": self.strings("cancel"), "callback": self.inline_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") # fmt: skip
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
try:
msg_obj = await utils.answer(msg_obj, self.strings("downloading"))
except Exception:
pass
req_update = await self.download_common()
try:
msg_obj = await utils.answer(msg_obj, self.strings("installing"))
except Exception:
pass
if req_update:
self.req_common()
try:
msg_obj = await utils.answer(
msg_obj,
self.strings("restarting_caption"),
)
except Exception:
pass
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, db):
self._db = db
self._client = client
if self.get("selfupdatemsg") is not None:
try:
await self.update_complete(client)
except Exception:
logger.exception("Failed to complete update!")
self.set("selfupdatemsg", None)
if self.get("do_not_create", False):
return
folders = await self._client(GetDialogFiltersRequest())
if any(folder.title == "hikka" for folder in folders):
return
try:
folder_id = (
max(
folders,
key=lambda x: x.id,
).id
+ 1
)
except ValueError:
folder_id = 2
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",
}
and dialog.is_channel
and (
dialog.entity.participants_count == 1
or dialog.entity.participants_count == 2
and dialog.name == "hikka-logs"
)
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,
),
)
)
self.set("do_not_create", True)
async def update_complete(self, client):
logger.debug("Self update successful! Edit message")
msg = self.strings("success").format(utils.ascii_face())
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)
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,
)