# 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 .
# █ █ ▀ █▄▀ ▄▀█ █▀█ ▀ ▄▀█ ▀█▀ ▄▀█ █▀▄▀█ ▄▀█
# █▀█ █ █ █ █▀█ █▀▄ █ ▄ █▀█ █ █▀█ █ ▀ █ █▀█
#
# © Copyright 2022
#
# https://t.me/hikariatama
#
# 🔒 Licensed under the GNU GPLv3
# 🌐 https://www.gnu.org/licenses/agpl-3.0.html
import asyncio
import atexit
import functools
import logging
import os
import subprocess
import sys
from typing import Union
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
from ..inline.types import InlineCall
logger = logging.getLogger(__name__)
@loader.tds
class UpdaterMod(loader.Module):
"""Updates itself"""
strings = {
"name": "Updater",
"source": "ℹ️ Read the source code from here",
"restarting_caption": "🔄 Restarting...",
"downloading": "🕐 Downloading updates...",
"installing": "🕐 Installing updates...",
"success": "✅ Restart successful! {}",
"origin_cfg_doc": "Git origin URL, for where to update from",
"btn_restart": "🔄 Restart",
"btn_update": "🧭 Update",
"restart_confirm": "🔄 Are you sure you want to restart?",
"update_confirm": "🧭 Are you sure you want to update?",
"cancel": "🚫 Cancel",
"lavhost_restart": "✌️ Your lavHost is restarting...\n>///<",
"lavhost_update": "✌️ Your lavHost is updating...\n>///<",
}
strings_ru = {
"source": "ℹ️ Исходный код можно прочитать здесь",
"restarting_caption": "🔄 Перезагрузка...",
"downloading": "🕐 Скачивание обновлений...",
"installing": "🕐 Установка обновлений...",
"success": "✅ Перезагрузка успешна! {}",
"origin_cfg_doc": "Ссылка, из которой будут загружаться обновления",
"btn_restart": "🔄 Перезагрузиться",
"btn_update": "🧭 Обновиться",
"restart_confirm": "🔄 Ты уверен, что хочешь перезагрузиться?",
"update_confirm": "🧭 Ты уверен, что хочешь обновиться?",
"cancel": "🚫 Отмена",
"_cmd_doc_restart": "Перезагружает юзербот",
"_cmd_doc_download": "Скачивает обновления",
"_cmd_doc_update": "Обновляет юзербот",
"_cmd_doc_source": "Ссылка на исходный код проекта",
"_cls_doc": "Обновляет юзербот",
"lavhost_restart": "✌️ Твой lavHost перезагружается...\n>///<",
"lavhost_update": "✌️ Твой lavHost обновляется...\n>///<",
}
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()
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
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",
}
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,
),
)
)
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"
)
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,
)