# TG-UserBot - A modular Telegram UserBot script for Python. # Copyright (C) 2019 Kandarp <https://github.com/kandnub> # # TG-UserBot is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # TG-UserBot 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with TG-UserBot. If not, see <https://www.gnu.org/licenses/>. import asyncio import datetime import logging import os import os.path import sys import time from typing import Tuple, Union from heroku3 import from_key from telethon import errors from telethon.tl import types from telethon.utils import get_display_name from .client import UserBotClient from .events import NewMessage from userbot.plugins import plugins_data LOGGER = logging.getLogger('userbot') def printUser(entity: types.User) -> None: """Print the user's first name + last name upon start""" user = get_display_name(entity) print() LOGGER.warning("Successfully logged in as {0}".format(user)) def printVersion(version: int, prefix: str) -> None: """Print the version of the bot with the default prefix""" if not prefix: prefix = '.' LOGGER.warning( "UserBot v{0} is running, test it by sending {1}ping in" " any chat.".format(version, prefix) ) print() async def isRestart(client: UserBotClient) -> None: """Check if the script restarted itself and edit the last message""" userbot_restarted = os.environ.get('userbot_restarted', False) heroku = client.config['api_keys'].get('api_key_heroku', False) updated = os.environ.pop('userbot_update', False) disabled_commands = False async def success_edit(text): entity = int(userbot_restarted.split('/')[0]) message = int(userbot_restarted.split('/')[1]) try: await client.edit_message(entity, message, text) except ( ValueError, errors.MessageAuthorRequiredError, errors.MessageNotModifiedError, errors.MessageIdInvalidError ): LOGGER.debug(f"Failed to edit message ({message}) in {entity}.") if updated: text = "`Successfully updated and restarted the userbot!`" else: text = '`Successfully restarted the userbot!`' if userbot_restarted: del os.environ['userbot_restarted'] LOGGER.debug('Userbot was restarted! Editing the message.') if os.environ.get('DYNO', False) and heroku: heroku_conn = from_key(heroku) HEROKU_APP = os.environ.get('HEROKU_APP_NAME', False) if HEROKU_APP: app = heroku_conn.apps()[HEROKU_APP] for build in app.builds(): if build.status == "pending": return if app.config()['userbot_update']: del app.config()['userbot_update'] await success_edit( "`Successfully deployed a new image to heroku " "and restarted the userbot.`" ) else: await success_edit(text) del app.config()['userbot_restarted'] disabled_commands = app.config()['userbot_disabled_commands'] del app.config()['userbot_disabled_commands'] else: await success_edit(text) disabled_commands = os.environ.get( 'userbot_disabled_commands', False ) if "userbot_disabled_commands" in os.environ: del os.environ['userbot_disabled_commands'] if disabled_commands: await disable_commands(client, disabled_commands) def restarter(client: UserBotClient) -> None: executable = sys.executable.replace(' ', '\\ ') args = [executable, '-m', 'userbot'] if client.disabled_commands: disabled_list = ", ".join(client.disabled_commands.keys()) os.environ['userbot_disabled_commands'] = disabled_list if os.environ.get('userbot_afk', False): plugins_data.dump_AFK() client._kill_running_processes() if sys.platform.startswith('win'): os.spawnle(os.P_NOWAIT, executable, *args, os.environ) else: os.execle(executable, *args, os.environ) async def restart(event: NewMessage.Event) -> None: event.client.reconnect = False restart_message = f"{event.chat_id}/{event.message.id}" os.environ['userbot_restarted'] = restart_message restarter(event.client) if event.client.is_connected(): await event.client.disconnect() async def _humanfriendly_seconds(seconds: int or float) -> str: elapsed = datetime.timedelta(seconds=round(seconds)).__str__() splat = elapsed.split(', ') if len(splat) == 1: return await _human_friendly_timedelta(splat[0]) friendly_units = await _human_friendly_timedelta(splat[1]) return ', '.join([splat[0], friendly_units]) async def _human_friendly_timedelta(timedelta: str) -> str: splat = timedelta.split(':') nulls = ['0', "00"] h = splat[0] m = splat[1] s = splat[2] text = '' if h not in nulls: unit = "hour" if h == 1 else "hours" text += f"{h} {unit}" if m not in nulls: unit = "minute" if m == 1 else "minutes" delimiter = ", " if len(text) > 1 else '' text += f"{delimiter}{m} {unit}" if s not in nulls: unit = "second" if s == 1 else "seconds" delimiter = " and " if len(text) > 1 else '' text += f"{delimiter}{s} {unit}" if len(text) == 0: text = "\u221E" return text async def get_chat_link( arg: Union[types.User, types.Chat, types.Channel, NewMessage.Event], reply=None ) -> str: if isinstance(arg, (types.User, types.Chat, types.Channel)): entity = arg else: entity = await arg.get_chat() if isinstance(entity, types.User): if entity.is_self: name = "yourself" else: name = get_display_name(entity) or "Deleted Account?" extra = f"[{name}](tg://user?id={entity.id})" else: if hasattr(entity, 'username') and entity.username is not None: username = '@' + entity.username else: username = entity.id if reply is not None: if isinstance(username, str) and username.startswith('@'): username = username[1:] else: username = f"c/{username}" extra = f"[{entity.title}](https://t.me/{username}/{reply})" else: if isinstance(username, int): username = f"`{username}`" extra = f"{entity.title} ( {username} )" else: extra = f"[{entity.title}](tg://resolve?domain={username})" return extra async def disable_commands(client: UserBotClient, commands: str) -> None: commands = commands.split(", ") for command in commands: target = client.commands.get(command, False) if target: client.remove_event_handler(target.func) client.disabled_commands.update({command: target}) del client.commands[command] LOGGER.debug("Disabled command: %s", command) async def is_ffmpeg_there(): cmd = await asyncio.create_subprocess_shell( 'ffmpeg -version', stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) await cmd.communicate() return True if cmd.returncode == 0 else False async def format_speed(speed_per_second, unit): unit0 = unit[0].lower() base, unit0 = (1024, "Byte") if unit[0] == 'byte' else (1000, "bit") seq = ['', 'K', 'M', 'G'] speed = speed_per_second / unit[1] for i in seq: if speed/base < 1: return speed, i, unit0 speed /= base class ProgressCallback(): """Custom class to handle upload and download progress.""" def __init__(self, event, start=None, filen='unamed', update=5): self.event = event self.start = start or time.time() self.last_upload_edit = None self.last_download_edit = None self.filen = filen self.upload_finished = False self.download_finished = False self._uploaded = 0 self._downloaded = 0 self.update = update async def resolve_prog(self, current, total): """Calculate the necessary info and make a dict from it.""" now = time.time() elp = now - self.start speed = int(float(current) / elp) eta = await calc_eta(elp, speed, current, total) s0, s1, s2 = await format_speed(speed, ("byte", 1)) c0, c1, c2 = await format_speed(current, ("byte", 1)) t0, t1, t2 = await format_speed(total, ("byte", 1)) percentage = round(current / total * 100, 2) return { 'filen': self.filen, 'percentage': percentage, 'eta': await _humanfriendly_seconds(eta), 'elp': await _humanfriendly_seconds(elp), 'current': f'{c0:.2f}{c1}{c2[0]}', 'total': f'{t0:.2f}{t1}{t2[0]}', 'speed': f'{s0:.2f}{s1}{s2[0]}/s' } async def up_progress(self, current, total): """Handle the upload progress only.""" d = await self.resolve_prog(current, total) edit, finished = ul_prog(d, self) if finished: if not self.upload_finished: self.event = await self.event.answer(edit) self.upload_finished = True elif edit: last_edit = self.last_upload_edit if last_edit and time.time() - last_edit < 10: return self.event = await self.event.answer(edit) self.last_upload_edit = time.time() async def dl_progress(self, current, total): """Handle the download progress only.""" d = await self.resolve_prog(current, total) edit, finished = dl_prog(d, self) if finished: if not self.download_finished: self.event = await self.event.answer(edit) self.download_finished = True elif edit: last_edit = self.last_download_edit if last_edit and time.time() - last_edit < 10: return self.event = await self.event.answer(edit) self.last_download_edit = time.time() async def calc_eta(elp: float, speed: int, current: int, total: int) -> int: if total is None or total == 0: return 0 if current == 0 or elp < 0.001: return 0 speed = speed if speed else 1 return int((float(total) - float(current)) / speed) def ul_prog(d: dict, cb: ProgressCallback) -> Tuple[Union[str, bool], bool]: """ Logs the upload progress """ uploaded = cb._uploaded current = d.get('percentage', 0) # now = datetime.datetime.now(datetime.timezone.utc) log_text = ( "Uploaded %(current)s of %(total)s. " "Progress: %(percentage)s%% speed: %(speed)s" "Time elapsed: %(elp)s" ) LOGGER.debug(log_text % d) text = ( "`Uploading %(filen)s at %(speed)s.`\n" "__Progress: %(percentage)s%% of %(total)s__\n" "__ETA: %(eta)s, Elapsed: %(elp)s__" ) if current == 100: return "__Successfully uploaded %(filen)s in %(elp)s!__" % d, True elif current - uploaded >= cb.update: cb._uploaded = current return text % d, False return False, False def dl_prog(d: dict, cb: ProgressCallback) -> Tuple[Union[str, bool], bool]: """ Logs the download progress """ downloaded = cb._downloaded current = d.get('percentage', 0) # now = datetime.datetime.now(datetime.timezone.utc) log_text = ( "Downloaded %(current)s of %(total)s. " "Progress: %(percentage)s%%" "Time elapsed: %(elp)s" ) LOGGER.debug(log_text % d) text = ( "`Downloading %(filen)s at %(speed)s.`\n" "__Progress: %(percentage)s%% of %(total)s__\n" "__ETA: %(eta)s, Elapsed: %(elp)s__" ) if current == 100: return "__Successfully downloaded %(filen)s in %(elp)s!__" % d, True elif current - downloaded >= cb.update: cb._downloaded = current return text % d, False return False, False