diff --git a/classes/commandset.py b/classes/commandset.py deleted file mode 100644 index e50620d..0000000 --- a/classes/commandset.py +++ /dev/null @@ -1,29 +0,0 @@ -from dataclasses import dataclass -from typing import List, Union -from pyrogram.types import ( - BotCommandScopeAllChatAdministrators, - BotCommandScopeAllGroupChats, - BotCommandScopeAllPrivateChats, - BotCommandScopeChat, - BotCommandScopeChatAdministrators, - BotCommandScopeChatMember, - BotCommandScopeDefault, - BotCommand, -) - - -@dataclass -class CommandSet: - """Command stored in PyroClient's 'commands' attribute""" - - commands: List[BotCommand] - scope: Union[ - BotCommandScopeDefault, - BotCommandScopeAllPrivateChats, - BotCommandScopeAllGroupChats, - BotCommandScopeAllChatAdministrators, - BotCommandScopeChat, - BotCommandScopeChatAdministrators, - BotCommandScopeChatMember, - ] = BotCommandScopeDefault - language_code: str = "" diff --git a/classes/pyroclient.py b/classes/pyroclient.py index 38b900c..4377a8b 100644 --- a/classes/pyroclient.py +++ b/classes/pyroclient.py @@ -1,49 +1,31 @@ import contextlib import logging -from datetime import datetime, timedelta +from datetime import datetime from io import BytesIO -from os import getpid, makedirs, remove, sep +from os import makedirs, remove, sep from pathlib import Path from shutil import rmtree from time import time from traceback import format_exc -from typing import List, Tuple, Union -import aiofiles +from typing import Dict, List, Tuple, Union -import pyrogram +import aiofiles from aiohttp import ClientSession from bson import ObjectId -from dateutil.relativedelta import relativedelta -from classes.enums.submission_types import SubmissionType -from libbot import json_read, json_write -from libbot.i18n import BotLocale +from libbot import json_write from libbot.i18n.sync import _ from photosapi_client.errors import UnexpectedStatus -from pyrogram.client import Client -from pyrogram.errors import BadRequest, bad_request_400 -from pyrogram.handlers.message_handler import MessageHandler -from pyrogram.raw.all import layer -from pyrogram.types import ( - BotCommand, - BotCommandScopeAllChatAdministrators, - BotCommandScopeAllGroupChats, - BotCommandScopeAllPrivateChats, - BotCommandScopeChat, - BotCommandScopeChatAdministrators, - BotCommandScopeChatMember, - BotCommandScopeDefault, - Message, -) +from pyrogram.errors import bad_request_400 +from pyrogram.types import Message from pytimeparse.timeparse import timeparse from ujson import dumps, loads -from classes.commandset import CommandSet +from classes.enums.submission_types import SubmissionType from classes.exceptions import ( SubmissionDuplicatesError, SubmissionUnavailableError, SubmissionUnsupportedError, ) -from classes.pyrocommand import PyroCommand from modules.api_client import ( BodyPhotoUpload, BodyVideoUpload, @@ -56,28 +38,21 @@ from modules.api_client import ( ) from modules.database import col_submitted from modules.http_client import http_session -from modules.scheduler import scheduler from modules.sender import send_content logger = logging.getLogger(__name__) -class PyroClient(Client): - def __init__(self): - with open("config.json", "r", encoding="utf-8") as f: - self.config: dict = loads(f.read()) +from datetime import datetime +from typing import List, Union - super().__init__( - name="bot_client", - api_id=self.config["bot"]["api_id"], - api_hash=self.config["bot"]["api_hash"], - bot_token=self.config["bot"]["bot_token"], - plugins=dict(root="plugins", exclude=self.config["disabled_plugins"]), - sleep_threshold=120, - max_concurrent_transmissions=self.config["bot"][ - "max_concurrent_transmissions" - ], - ) +from apscheduler.schedulers.asyncio import AsyncIOScheduler +from libbot.pyrogram.classes import PyroClient + + +class PyroClient(PyroClient): + def __init__(self, scheduler: AsyncIOScheduler): + super().__init__(scheduler=scheduler) self.version: float = 0.2 @@ -85,142 +60,75 @@ class PyroClient(Client): self.admins: List[int] = self.config["bot"]["admins"] + [ self.config["bot"]["owner"] ] - self.commands: List[PyroCommand] = [] - self.scoped_commands: bool = self.config["bot"]["scoped_commands"] - self.start_time: float = 0 - - self.bot_locale: BotLocale = BotLocale(Path(self.config["locations"]["locale"])) - self.default_locale: str = self.bot_locale.default - self.locales: dict = self.bot_locale.locales - - self._ = self.bot_locale._ - self.in_all_locales = self.bot_locale.in_all_locales - self.in_every_locale = self.bot_locale.in_every_locale self.sender_session = ClientSession() + self.scopes_placeholders: Dict[str, int] = { + "owner": self.owner, + "comments": self.config["posting"]["comments"], + } + async def start(self): await super().start() - self.start_time = time() + if self.config["reports"]["update"]: + try: + async with ClientSession( + json_serialize=dumps, + ) as http_session: + check_update = await http_session.get( + "https://git.end-play.xyz/api/v1/repos/profitroll/TelegramPoster/releases?page=1&limit=1" + ) - logger.info( - "Bot is running with Pyrogram v%s (Layer %s) and has started as @%s on PID %s.", - pyrogram.__version__, - layer, - self.me.username, - getpid(), - ) + response = await check_update.json() - try: - if Path(f"{self.config['locations']['cache']}/shutdown_time").exists(): - downtime = relativedelta( - datetime.now(), - datetime.fromtimestamp( - ( - await json_read( - Path( - f"{self.config['locations']['cache']}/shutdown_time" - ) - ) - )["timestamp"] - ), - ) + if len(response) == 0: + raise ValueError("No bot releases on git found.") - if downtime.days >= 1: - startup_message = self._( - "startup_downtime_days", - "message", - ).format(getpid(), downtime.days) - elif downtime.hours >= 1: - startup_message = self._( - "startup_downtime_hours", - "message", - ).format(getpid(), downtime.hours) + if float(response[0]["tag_name"].replace("v", "")) > self.version: + logger.info( + "New version %s found (current %s)", + response[0]["tag_name"].replace("v", ""), + self.version, + ) + await self.send_message( + self.owner, + self._( + "update_available", + "message", + ).format( + response[0]["tag_name"], + response[0]["html_url"], + response[0]["body"], + ), + ) else: - startup_message = self._( - "startup_downtime_minutes", - "message", - ).format(getpid(), downtime.minutes) + logger.info("No updates found, bot is up to date.") + except bad_request_400.PeerIdInvalid: + logger.warning( + "Could not send startup message to bot owner. Perhaps user has not started the bot yet." + ) + except Exception as exp: + logger.exception("Update check failed due to %s: %s", exp, format_exc()) + + if self.config["mode"]["post"]: + if self.config["posting"]["use_interval"]: + self.scheduler.add_job( + send_content, + "interval", + seconds=timeparse(self.config["posting"]["interval"]), + args=[self, self.sender_session], + ) else: - startup_message = (self._("startup", "message").format(getpid()),) - - await self.send_message( - chat_id=self.config["reports"]["chat_id"], - text=startup_message, - ) - - if self.config["reports"]["update"]: - try: - async with ClientSession( - json_serialize=dumps, - ) as http_session: - check_update = await http_session.get( - "https://git.end-play.xyz/api/v1/repos/profitroll/TelegramPoster/releases?page=1&limit=1" - ) - - response = await check_update.json() - - if len(response) == 0: - raise ValueError("No bot releases on git found.") - - if float(response[0]["tag_name"].replace("v", "")) > self.version: - logger.info( - "New version %s found (current %s)", - response[0]["tag_name"].replace("v", ""), - self.version, - ) - await self.send_message( - self.owner, - self._( - "update_available", - "message", - ).format( - response[0]["tag_name"], - response[0]["html_url"], - response[0]["body"], - ), - ) - else: - logger.info("No updates found, bot is up to date.") - except bad_request_400.PeerIdInvalid: - logger.warning( - "Could not send startup message to bot owner. Perhaps user has not started the bot yet." - ) - except Exception as exp: - logger.exception( - "Update check failed due to %s: %s", exp, format_exc() - ) - - scheduler.add_job( - self.register_commands, - trigger="date", - run_date=datetime.now() + timedelta(seconds=5), - kwargs={"command_sets": await self.collect_commands()}, - ) - - if self.config["mode"]["post"]: - if self.config["posting"]["use_interval"]: - scheduler.add_job( + for entry in self.config["posting"]["time"]: + dt_obj = datetime.strptime(entry, "%H:%M") + self.scheduler.add_job( send_content, - "interval", - seconds=timeparse(self.config["posting"]["interval"]), + "cron", + hour=dt_obj.hour, + minute=dt_obj.minute, args=[self, self.sender_session], ) - else: - for entry in self.config["posting"]["time"]: - dt_obj = datetime.strptime(entry, "%H:%M") - scheduler.add_job( - send_content, - "cron", - hour=dt_obj.hour, - minute=dt_obj.minute, - args=[self, self.sender_session], - ) - - scheduler.start() - except BadRequest: - logger.warning("Unable to send message to report chat.") async def stop(self): makedirs(self.config["locations"]["cache"], exist_ok=True) @@ -229,180 +137,10 @@ class PyroClient(Client): Path(f"{self.config['locations']['cache']}/shutdown_time"), ) - try: - await self.send_message( - chat_id=self.config["reports"]["chat_id"], - text=f"Bot stopped with PID `{getpid()}`", - ) - except BadRequest: - logger.warning("Unable to send message to report chat.") await http_session.close() await self.sender_session.close() + await super().stop() - logger.warning("Bot stopped with PID %s.", getpid()) - - async def collect_commands(self) -> Union[List[CommandSet], None]: - """Gather list of the bot's commands - - ### Returns: - * `List[CommandSet]`: List of the commands' sets - """ - command_sets = None - - # If config get bot.scoped_commands is true - more complicated - # scopes system will be used instead of simple global commands - if self.scoped_commands: - scopes = {} - command_sets = [] - - # Iterate through all commands in config - for command, contents in self.config["commands"].items(): - # Iterate through all scopes of a command - for scope in contents["scopes"]: - if dumps(scope) not in scopes: - scopes[dumps(scope)] = {"_": []} - - # Add command to the scope's flattened key in scopes dict - scopes[dumps(scope)]["_"].append( - BotCommand(command, _(command, "commands")) - ) - - for locale, string in ( - self.in_every_locale(command, "commands") - ).items(): - if locale not in scopes[dumps(scope)]: - scopes[dumps(scope)][locale] = [] - - scopes[dumps(scope)][locale].append(BotCommand(command, string)) - - # Iterate through all scopes and its commands - for scope, locales in scopes.items(): - # Make flat key a dict again - scope_dict = loads(scope) - - # Replace "owner" in the bot scope with owner's id - if "chat_id" in scope_dict and scope_dict["chat_id"] == "owner": - scope_dict["chat_id"] = self.owner - - # Create object with the same name and args from the dict - try: - scope_obj = globals()[scope_dict["name"]]( - **{ - key: value - for key, value in scope_dict.items() - if key != "name" - } - ) - except NameError: - logger.error( - "Could not register commands of the scope '%s' due to an invalid scope class provided!", - scope_dict["name"], - ) - continue - except TypeError: - logger.error( - "Could not register commands of the scope '%s' due to an invalid class arguments provided!", - scope_dict["name"], - ) - continue - - # Add set of commands to the list of the command sets - for locale, commands in locales.items(): - if locale == "_": - command_sets.append( - CommandSet(commands, scope=scope_obj, language_code="") - ) - continue - command_sets.append( - CommandSet(commands, scope=scope_obj, language_code=locale) - ) - - logger.info("Registering the following command sets: %s", command_sets) - - else: - # This part here looks into the handlers and looks for commands - # in it, if there are any. Then adds them to self.commands - for handler in self.dispatcher.groups[0]: - if isinstance(handler, MessageHandler): - for entry in [handler.filters.base, handler.filters.other]: - if hasattr(entry, "commands"): - for command in entry.commands: - logger.info("I see a command %s in my filters", command) - self.add_command(command) - - return command_sets - - def add_command( - self, - command: str, - ): - """Add command to the bot's internal commands list - - ### Args: - * command (`str`) - """ - self.commands.append( - PyroCommand( - command, - _(command, "commands"), - ) - ) - logger.info( - "Added command '%s' to the bot's internal commands list", - command, - ) - - async def register_commands( - self, command_sets: Union[List[CommandSet], None] = None - ): - """Register commands stored in bot's 'commands' attribute""" - - if command_sets is None: - commands = [ - BotCommand(command=command.command, description=command.description) - for command in self.commands - ] - - logger.info( - "Registering commands %s with a default scope 'BotCommandScopeDefault'" - ) - - await self.set_bot_commands(commands) - return - - for command_set in command_sets: - logger.info( - "Registering command set with commands %s and scope '%s' (%s)", - command_set.commands, - command_set.scope, - command_set.language_code, - ) - await self.set_bot_commands( - command_set.commands, - command_set.scope, - language_code=command_set.language_code, - ) - - async def remove_commands(self, command_sets: Union[List[CommandSet], None] = None): - """Remove commands stored in bot's 'commands' attribute""" - - if command_sets is None: - logger.info( - "Removing commands with a default scope 'BotCommandScopeDefault'" - ) - await self.delete_bot_commands(BotCommandScopeDefault()) - return - - for command_set in command_sets: - logger.info( - "Removing command set with scope '%s' (%s)", - command_set.scope, - command_set.language_code, - ) - await self.delete_bot_commands( - command_set.scope, - language_code=command_set.language_code, - ) async def submit_media( self, id: str @@ -422,7 +160,7 @@ class PyroClient(Client): submission, file_name=self.config["locations"]["tmp"] + sep ) except Exception as exp: - raise SubmissionUnavailableError() + raise SubmissionUnavailableError() from exp elif not Path( f"{self.config['locations']['data']}/submissions/{db_entry['temp']['uuid']}/{db_entry['temp']['file']}", @@ -470,8 +208,8 @@ class PyroClient(Client): ), caption="queue", ) - except UnexpectedStatus: - raise SubmissionUnsupportedError(str(filepath)) + except UnexpectedStatus as exp: + raise SubmissionUnsupportedError(str(filepath)) from exp response_dict = ( {} diff --git a/classes/pyrocommand.py b/classes/pyrocommand.py deleted file mode 100644 index 7b01180..0000000 --- a/classes/pyrocommand.py +++ /dev/null @@ -1,9 +0,0 @@ -from dataclasses import dataclass - - -@dataclass -class PyroCommand: - """Command stored in PyroClient's 'commands' attribute""" - - command: str - description: str diff --git a/locale/en.json b/locale/en.json index 8dfda65..cd12fb2 100644 --- a/locale/en.json +++ b/locale/en.json @@ -2,6 +2,7 @@ "commands": { "start": "Start using the bot", "rules": "Photos submission rules", + "report": "Report this post", "forwards": "Check post forwards", "import": "Submit .zip archive with photos", "export": "Get .zip archive with all photos", diff --git a/locale/uk.json b/locale/uk.json index 6d461a5..39ce5fc 100644 --- a/locale/uk.json +++ b/locale/uk.json @@ -2,6 +2,7 @@ "commands": { "start": "Почати користуватись ботом", "rules": "Правила пропонування фото", + "report": "Поскаржитись на цей пост", "forwards": "Переглянути репости", "import": "Надати боту .zip архів з фотографіями", "export": "Отримати .zip архів з усіма фотографіями", diff --git a/main.py b/main.py index 2c25b5e..c590bca 100644 --- a/main.py +++ b/main.py @@ -22,7 +22,7 @@ with contextlib.suppress(ImportError): def main(): - client = PyroClient() + client = PyroClient(scheduler=scheduler) Conversation(client) try: @@ -30,7 +30,8 @@ def main(): except KeyboardInterrupt: logger.warning("Forcefully shutting down with PID %s...", getpid()) finally: - scheduler.shutdown() + if client.scheduler is not None: + client.scheduler.shutdown() exit() diff --git a/requirements.txt b/requirements.txt index ad7eab8..e0f01e7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,17 +1,13 @@ -aiofiles~=23.1.0 aiohttp~=3.8.4 -apscheduler~=3.10.1 black~=23.3.0 convopyro==0.5 pillow~=9.4.0 psutil~=5.9.4 -pymongo~=4.3.3 -pyrogram==2.0.106 +pymongo~=4.4.0 python_dateutil==2.8.2 pytimeparse~=1.1.8 tgcrypto==1.2.5 -ujson==5.8.0 uvloop==0.17.0 --extra-index-url https://git.end-play.xyz/api/packages/profitroll/pypi/simple -libbot[speed,pyrogram]==1.0 +libbot[speed,pyrogram]==1.5 photosapi_client==0.4.0 \ No newline at end of file