From f5b3335af0b2f40645f2ef85af87191734cd3d3a Mon Sep 17 00:00:00 2001 From: Kostiantyn Kuleshov Date: Mon, 24 Apr 2023 14:38:03 +0300 Subject: [PATCH 01/28] master (#21) Co-authored-by: Renovate Co-authored-by: profitroll Reviewed-on: https://git.end-play.xyz/profitroll/TelegramPoster/pulls/21 --- modules/api_client.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/api_client.py b/modules/api_client.py index d7cf50b..af0507a 100644 --- a/modules/api_client.py +++ b/modules/api_client.py @@ -82,12 +82,12 @@ async def random_pic(token: Union[str, None] = None) -> Tuple[str, str]: f'{configGet("address", "posting", "api")}/albums/{configGet("album", "posting", "api")}/photos?q=&page_size={configGet("page_size", "posting")}&caption=queue', headers={"Authorization": f"Bearer {token}"}, ) - logWrite( - locale("random_pic_response", "console", locale=configGet("locale_log")).format( - await resp.json() - ), - debug=True, - ) + # logWrite( + # locale("random_pic_response", "console", locale=configGet("locale_log")).format( + # await resp.json() + # ), + # debug=True, + # ) if resp.status != 200: logWrite( locale( From 93804345df3916d6c3ebae79f4ef072dbd835788 Mon Sep 17 00:00:00 2001 From: Renovate Date: Mon, 8 May 2023 11:51:15 +0300 Subject: [PATCH 02/28] Update dependency pyrogram to v2.0.106 (#22) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Update | Change | |---|---|---| | [pyrogram](https://github.com/pyrogram) ([source](https://github.com/pyrogram/pyrogram)) | patch | `==2.0.104` -> `==2.0.106` | --- ### Release Notes
pyrogram/pyrogram ### [`v2.0.106`](https://github.com/pyrogram/pyrogram/compare/v2.0.105...v2.0.106) [Compare Source](https://github.com/pyrogram/pyrogram/compare/v2.0.105...v2.0.106) ### [`v2.0.105`](https://github.com/pyrogram/pyrogram/compare/v2.0.104...v2.0.105) [Compare Source](https://github.com/pyrogram/pyrogram/compare/v2.0.104...v2.0.105)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). Reviewed-on: https://git.end-play.xyz/profitroll/TelegramPoster/pulls/22 Co-authored-by: Renovate Co-committed-by: Renovate --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index bba88b7..c1d08d0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ python_dateutil==2.8.2 apscheduler==3.10.1 pytimeparse==1.1.8 convopyro==0.5 -pyrogram==2.0.104 +pyrogram==2.0.106 aiofiles~=23.1.0 tgcrypto==1.2.5 aiohttp~=3.8.4 From 337a7b28aac65a3bb5ee7f0eb5875bdc842e6110 Mon Sep 17 00:00:00 2001 From: profitroll Date: Tue, 16 May 2023 12:30:56 +0200 Subject: [PATCH 03/28] Fixed coroutines not being awaited --- modules/sender.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/sender.py b/modules/sender.py index ed4de5a..52a0df0 100644 --- a/modules/sender.py +++ b/modules/sender.py @@ -53,14 +53,14 @@ async def send_content(app: PosterClient) -> None: logWrite( locale( "post_invalid_pic", "console", locale=configGet("locale") - ).format(response.status, str(response.json())) + ).format(response.status, str(await response.json())) ) if configGet("error", "reports"): await app.send_message( app.owner, locale( "post_invalid_pic", "message", locale=configGet("locale") - ).format(response.status, response.json()), + ).format(response.status, await response.json()), ) tmp_dir = str(uuid4()) From bd62149a2c6a2f415e9bc9c57e28ff73ed3bc251 Mon Sep 17 00:00:00 2001 From: Renovate Date: Sun, 11 Jun 2023 12:29:02 +0300 Subject: [PATCH 04/28] Update dependency ujson to v5.8.0 (#24) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ujson](https://github.com/ultrajson/ultrajson) | minor | `==5.7.0` -> `==5.8.0` | --- ### Release Notes
ultrajson/ultrajson ### [`v5.8.0`](https://github.com/ultrajson/ultrajson/releases/tag/5.8.0) [Compare Source](https://github.com/ultrajson/ultrajson/compare/5.7.0...5.8.0) #### Added - Build wheel for Python 3.12 beta (built against 3.12.0b2) ([#​594](https://github.com/ultrajson/ultrajson/issues/594)) [@​hugovk](https://github.com/hugovk) #### Changed - Drop support for Python 3.7 ([#​595](https://github.com/ultrajson/ultrajson/issues/595)) [@​hugovk](https://github.com/hugovk) #### Fixed - Include BSD-3-Clause and TCL license text ([#​584](https://github.com/ultrajson/ultrajson/issues/584)) [@​musicinmybrain](https://github.com/musicinmybrain)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). Co-authored-by: Renovate Reviewed-on: https://git.end-play.xyz/profitroll/TelegramPoster/pulls/24 Co-authored-by: Renovate Co-committed-by: Renovate --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c1d08d0..20ce50a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,4 +9,4 @@ aiohttp~=3.8.4 psutil==5.9.5 pymongo==4.3.3 pillow~=9.5.0 -ujson==5.7.0 \ No newline at end of file +ujson==5.8.0 \ No newline at end of file From e9e68cb6b3bc1d0d301992769dcc40876855db88 Mon Sep 17 00:00:00 2001 From: Renovate Date: Wed, 21 Jun 2023 22:45:23 +0300 Subject: [PATCH 05/28] Update dependency pymongo to v4.4.0 (#25) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Update | Change | |---|---|---| | [pymongo](https://github.com/mongodb/mongo-python-driver) | minor | `==4.3.3` -> `==4.4.0` | --- ### Release Notes
mongodb/mongo-python-driver ### [`v4.4.0`](https://github.com/mongodb/mongo-python-driver/releases/tag/4.4.0): PyMongo 4.4.0 [Compare Source](https://github.com/mongodb/mongo-python-driver/compare/4.3.3...4.4.0) Release notes: https://www.mongodb.com/community/forums/t/pymongo-4-4-released/232211
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). Co-authored-by: Renovate Reviewed-on: https://git.end-play.xyz/profitroll/TelegramPoster/pulls/25 Co-authored-by: Renovate Co-committed-by: Renovate --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 20ce50a..f622884 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,6 @@ aiofiles~=23.1.0 tgcrypto==1.2.5 aiohttp~=3.8.4 psutil==5.9.5 -pymongo==4.3.3 +pymongo==4.4.0 pillow~=9.5.0 ujson==5.8.0 \ No newline at end of file From f003638128587d4c2a9d9550867181bf50b40df8 Mon Sep 17 00:00:00 2001 From: Profitroll Date: Wed, 28 Jun 2023 00:57:13 +0300 Subject: [PATCH 06/28] Update 'requirements.txt' --- requirements.txt | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/requirements.txt b/requirements.txt index f622884..19b25b1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,14 @@ -python_dateutil==2.8.2 -apscheduler==3.10.1 -pytimeparse==1.1.8 -convopyro==0.5 -pyrogram==2.0.106 -aiofiles~=23.1.0 -tgcrypto==1.2.5 aiohttp~=3.8.4 -psutil==5.9.5 -pymongo==4.4.0 -pillow~=9.5.0 -ujson==5.8.0 \ No newline at end of file +black~=23.3.0 +convopyro==0.5 +pillow~=9.4.0 +psutil~=5.9.4 +pymongo~=4.4.0 +pyrogram==2.0.106 +python_dateutil==2.8.2 +pytimeparse~=1.1.8 +tgcrypto==1.2.5 +uvloop==0.17.0 +--extra-index-url https://git.end-play.xyz/api/packages/profitroll/pypi/simple +libbot[speed,pyrogram]==1.5 +photosapi_client==0.5.0 \ No newline at end of file From 5adb004a2a552394385544989778446188b8d0ac Mon Sep 17 00:00:00 2001 From: Profitroll Date: Wed, 28 Jun 2023 00:57:30 +0300 Subject: [PATCH 07/28] API usage overhaul (#27) * `/report` command added * Updated to libbot 1.5 * Moved to [PhotosAPI_Client](https://git.end-play.xyz/profitroll/PhotosAPI_Client) v0.5.0 from using self-made API client * Video support (almost stable) * Bug fixes and improvements Co-authored-by: profitroll Reviewed-on: https://git.end-play.xyz/profitroll/TelegramPoster/pulls/27 --- .gitignore | 24 +- classes/enums/submission_types.py | 2 +- classes/exceptions.py | 5 + classes/poster_client.py | 98 ------- classes/pyroclient.py | 259 +++++++++++++++++ classes/user.py | 29 +- config_example.json | 116 ++++++-- locale/en.json | 13 +- locale/uk.json | 13 +- main.py | 39 +++ modules/api_client.py | 328 +++++++-------------- modules/app.py | 14 - modules/cli.py | 78 ----- modules/commands_register.py | 57 ---- modules/logger.py | 67 ----- modules/scheduler.py | 28 -- modules/sender.py | 242 ++++++++++------ modules/utils.py | 245 +--------------- plugins/callbacks/nothing.py | 12 +- plugins/callbacks/shutdown.py | 44 ++- plugins/callbacks/submission.py | 199 ++++++------- plugins/commands/general.py | 72 ++--- plugins/commands/mode_submit.py | 31 +- plugins/commands/photos.py | 463 ++++++++++++++++++------------ plugins/commands/report.py | 42 +++ plugins/handlers/submission.py | 202 +++++++------ plugins/remove_commands.py | 13 + poster.py | 266 ----------------- requirements-optional.txt | 0 29 files changed, 1332 insertions(+), 1669 deletions(-) delete mode 100644 classes/poster_client.py create mode 100644 classes/pyroclient.py create mode 100644 main.py delete mode 100644 modules/app.py delete mode 100644 modules/cli.py delete mode 100644 modules/commands_register.py delete mode 100644 modules/logger.py create mode 100644 plugins/commands/report.py create mode 100644 plugins/remove_commands.py delete mode 100644 poster.py delete mode 100644 requirements-optional.txt diff --git a/.gitignore b/.gitignore index 9ac0792..b4a0707 100644 --- a/.gitignore +++ b/.gitignore @@ -152,21 +152,13 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ -# ---> VisualStudioCode -.vscode/* -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -!.vscode/*.code-snippets +# Custom +config.json +*.session +*.session-journal -# Local History for Visual Studio Code -.history/ +venv +venv_linux +venv_windows -# Built Visual Studio Code Extensions -*.vsix - -# Project -cache -data -logs -config.json \ No newline at end of file +.vscode \ No newline at end of file diff --git a/classes/enums/submission_types.py b/classes/enums/submission_types.py index 265ce7a..2bd043f 100644 --- a/classes/enums/submission_types.py +++ b/classes/enums/submission_types.py @@ -4,5 +4,5 @@ from enum import Enum class SubmissionType(Enum): DOCUMENT = "document" VIDEO = "video" - ANIMATION = "animation" + # ANIMATION = "animation" PHOTO = "photo" diff --git a/classes/exceptions.py b/classes/exceptions.py index 4ca8e58..b1a6d06 100644 --- a/classes/exceptions.py +++ b/classes/exceptions.py @@ -22,6 +22,11 @@ class SubmissionDuplicatesError(Exception): ) +class SubmissionUnsupportedError(Exception): + def __init__(self, file_path: str) -> None: + super().__init__(f"Type of file does not seem to be supported: '{file_path}'") + + class UserCreationError(Exception): def __init__(self, code: int, data: str) -> None: self.code = code diff --git a/classes/poster_client.py b/classes/poster_client.py deleted file mode 100644 index ba0aace..0000000 --- a/classes/poster_client.py +++ /dev/null @@ -1,98 +0,0 @@ -from os import path, remove, sep -from shutil import rmtree -from typing import Tuple, Union -from pyrogram.client import Client -from pyrogram.types import Message -from classes.exceptions import SubmissionDuplicatesError, SubmissionUnavailableError -from modules.api_client import upload_pic -from modules.database import col_submitted -from bson import ObjectId -from modules.logger import logWrite - -from modules.utils import configGet - - -class PosterClient(Client): - def __init__(self, name: str, **kwargs): # type: ignore - super().__init__(name, **kwargs) - self.owner = configGet("owner") - self.admins = configGet("admins") + [configGet("owner")] - - async def submit_photo( - self, id: str - ) -> Tuple[Union[Message, None], Union[str, None]]: - db_entry = col_submitted.find_one({"_id": ObjectId(id)}) - submission = None - - if db_entry is None: - raise SubmissionUnavailableError() - else: - if db_entry["temp"]["uuid"] is not None: - if not path.exists( - path.join( - configGet("data", "locations"), - "submissions", - db_entry["temp"]["uuid"], - db_entry["temp"]["file"], - ) - ): - raise SubmissionUnavailableError() - else: - filepath = path.join( - configGet("data", "locations"), - "submissions", - db_entry["temp"]["uuid"], - db_entry["temp"]["file"], - ) - try: - submission = await self.get_messages( - db_entry["user"], db_entry["telegram"]["msg_id"] - ) - except: - pass - else: - try: - submission = await self.get_messages( - db_entry["user"], db_entry["telegram"]["msg_id"] - ) - filepath = await self.download_media( - submission, file_name=configGet("tmp", "locations") + sep - ) - except: - raise SubmissionUnavailableError() - - response = await upload_pic( - str(filepath), allow_duplicates=configGet("allow_duplicates", "submission") - ) - - if len(response[1]) > 0: - raise SubmissionDuplicatesError(str(filepath), response[1]) - - col_submitted.find_one_and_update( - {"_id": ObjectId(id)}, {"$set": {"done": True}} - ) - - try: - if db_entry["temp"]["uuid"] is not None: - rmtree( - path.join( - configGet("data", "locations"), - "submissions", - db_entry["temp"]["uuid"], - ), - ignore_errors=True, - ) - else: - remove(str(filepath)) - except (FileNotFoundError, NotADirectoryError): - logWrite( - f"Could not delete '{filepath}' on submission accepted", debug=True - ) - - return submission, response[2] - - async def ban_user(self, id: int) -> None: - pass - - async def unban_user(self, id: int) -> None: - pass diff --git a/classes/pyroclient.py b/classes/pyroclient.py new file mode 100644 index 0000000..54f2edc --- /dev/null +++ b/classes/pyroclient.py @@ -0,0 +1,259 @@ +import contextlib +import logging +from datetime import datetime +from io import BytesIO +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 Dict, List, Tuple, Union + +import aiofiles +from aiohttp import ClientSession +from bson import ObjectId +from libbot import json_write +from libbot.i18n.sync import _ +from photosapi_client.errors import UnexpectedStatus +from pyrogram.errors import bad_request_400 +from pyrogram.types import Message +from pytimeparse.timeparse import timeparse +from ujson import dumps, loads + +from classes.enums.submission_types import SubmissionType +from classes.exceptions import ( + SubmissionDuplicatesError, + SubmissionUnavailableError, + SubmissionUnsupportedError, +) +from modules.api_client import ( + BodyPhotoUpload, + BodyVideoUpload, + File, + Photo, + Video, + client, + photo_upload, + video_upload, +) +from modules.database import col_submitted +from modules.http_client import http_session +from modules.sender import send_content + +logger = logging.getLogger(__name__) + + +from datetime import datetime +from typing import List, Union + +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 + + self.owner: int = self.config["bot"]["owner"] + self.admins: List[int] = self.config["bot"]["admins"] + [ + self.config["bot"]["owner"] + ] + + 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() + + 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()) + + 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: + for entry in self.config["posting"]["time"]: + dt_obj = datetime.strptime(entry, "%H:%M") + self.scheduler.add_job( + send_content, + "cron", + hour=dt_obj.hour, + minute=dt_obj.minute, + args=[self, self.sender_session], + ) + + async def stop(self): + makedirs(self.config["locations"]["cache"], exist_ok=True) + await json_write( + {"timestamp": time()}, + Path(f"{self.config['locations']['cache']}/shutdown_time"), + ) + + await http_session.close() + await self.sender_session.close() + + await super().stop() + + async def submit_media( + self, id: str + ) -> Tuple[Union[Message, None], Union[str, None]]: + db_entry = col_submitted.find_one({"_id": ObjectId(id)}) + submission = None + + if db_entry is None: + raise SubmissionUnavailableError() + + if db_entry["temp"]["uuid"] is None: + try: + submission = await self.get_messages( + db_entry["user"], db_entry["telegram"]["msg_id"] + ) + filepath = await self.download_media( + submission, file_name=self.config["locations"]["tmp"] + sep + ) + except Exception as exp: + raise SubmissionUnavailableError() from exp + + elif not Path( + f"{self.config['locations']['data']}/submissions/{db_entry['temp']['uuid']}/{db_entry['temp']['file']}", + ).exists(): + raise SubmissionUnavailableError() + else: + filepath = Path( + f"{self.config['locations']['data']}/submissions/{db_entry['temp']['uuid']}/{db_entry['temp']['file']}", + ) + with contextlib.suppress(Exception): + submission = await self.get_messages( + db_entry["user"], db_entry["telegram"]["msg_id"] + ) + + async with aiofiles.open(str(filepath), "rb") as fh: + media_bytes = BytesIO(await fh.read()) + + try: + if db_entry["type"] == SubmissionType.PHOTO.value: + response = await photo_upload( + self.config["posting"]["api"]["album"], + client=client, + multipart_data=BodyPhotoUpload( + File(media_bytes, filepath.name, "image/jpeg") + ), + ignore_duplicates=self.config["submission"]["allow_duplicates"], + compress=False, + caption="queue", + ) + elif db_entry["type"] == SubmissionType.VIDEO.value: + response = await video_upload( + self.config["posting"]["api"]["album"], + client=client, + multipart_data=BodyVideoUpload( + File(media_bytes, filepath.name, "video/*") + ), + caption="queue", + ) + # elif db_entry["type"] == SubmissionType.ANIMATION.value: + # response = await video_upload( + # self.config["posting"]["api"]["album"], + # client=client, + # multipart_data=BodyVideoUpload( + # File(media_bytes, filepath.name, "video/*") + # ), + # caption="queue", + # ) + except UnexpectedStatus as exp: + raise SubmissionUnsupportedError(str(filepath)) from exp + + response_dict = ( + {} + if not hasattr(response, "content") + else loads(response.content.decode("utf-8")) + ) + + if "duplicates" in response_dict and len(response_dict["duplicates"]) > 0: + duplicates = [] + for index, duplicate in enumerate(response_dict["duplicates"]): # type: ignore + if response_dict["access_token"] is None: + duplicates.append( + f"`{duplicate['id']}`:\n{self.config['posting']['api']['address_external']}/photos/{duplicate['id']}" + ) + else: + duplicates.append( + f"`{duplicate['id']}`:\n{self.config['posting']['api']['address_external']}/token/photo/{response_dict['access_token']}?id={index}" + ) + raise SubmissionDuplicatesError(str(filepath), duplicates) + + col_submitted.find_one_and_update( + {"_id": ObjectId(id)}, {"$set": {"done": True}} + ) + + try: + if db_entry["temp"]["uuid"] is not None: + rmtree( + Path( + f"{self.config['locations']['data']}/submissions/{db_entry['temp']['uuid']}", + ), + ignore_errors=True, + ) + else: + remove(str(filepath)) + except (FileNotFoundError, NotADirectoryError): + logger.error("Could not delete '%s' on submission accepted", filepath) + + return ( + submission, + response.id if not hasattr(response, "parsed") else response.parsed.id, + ) + + async def ban_user(self, id: int) -> None: + pass + + async def unban_user(self, id: int) -> None: + pass diff --git a/classes/user.py b/classes/user.py index 8042908..7e6d368 100644 --- a/classes/user.py +++ b/classes/user.py @@ -1,7 +1,8 @@ -from modules.app import app from datetime import datetime + +from libbot import sync + from modules.database import col_banned, col_users -from modules.utils import configGet class PosterUser: @@ -31,18 +32,20 @@ class PosterUser: ### Returns: `bool`: Must be `True` if on the cooldown and `False` if not """ - if self.id in app.admins: + if self.id in sync.config_get("admins", "bot"): return False - else: - db_record = col_users.find_one({"user": self.id}) - if db_record is None: - return False - return ( - True - if (datetime.now() - db_record["cooldown"]).total_seconds() - < configGet("timeout", "submission") - else False - ) + + db_record = col_users.find_one({"user": self.id}) + + if db_record is None: + return False + + return ( + True + if (datetime.now() - db_record["cooldown"]).total_seconds() + < sync.config_get("timeout", "submission") + else False + ) def limit(self) -> None: """Restart user's cooldown. Used after post has been submitted.""" diff --git a/config_example.json b/config_example.json index 473f439..481317d 100644 --- a/config_example.json +++ b/config_example.json @@ -2,12 +2,14 @@ "locale": "en", "locale_log": "en", "locale_fallback": "en", - "owner": 0, - "admins": [], "bot": { + "owner": 0, + "admins": [], "api_id": 0, "api_hash": "", - "bot_token": "" + "bot_token": "", + "max_concurrent_transmissions": 5, + "scoped_commands": true }, "database": { "user": null, @@ -16,17 +18,18 @@ "port": 27017, "name": "tgposter" }, - "mode": { - "post": true, - "submit": true - }, "reports": { + "chat_id": 0, "sent": false, "error": true, "update": true, "startup": true, "shutdown": true }, + "mode": { + "post": true, + "submit": true + }, "logging": { "size": 512, "location": "logs" @@ -40,23 +43,27 @@ "index": "data/index.json", "locale": "locale" }, + "disabled_plugins": [], "posting": { "channel": 0, + "comments": 0, "silent": false, "move_sent": false, "use_interval": false, "interval": "1h30m", - "page_size": 300, "submitted_caption": { "enabled": true, "ignore_admins": true, "text": "#submitted" }, + "types": { + "photo": true, + "video": false + }, "extensions": { "photo": [ "jpg", "png", - "gif", "jpeg" ], "video": [ @@ -104,20 +111,89 @@ }, "mime_types": [ "image/png", - "image/gif", "image/jpeg", "video/mp4", "video/quicktime" ] }, - "commands": [ - "start", - "rules" - ], - "commands_admin": [ - "import", - "export", - "remove", - "shutdown" - ] + "commands": { + "start": { + "scopes": [ + { + "name": "BotCommandScopeDefault" + }, + { + "name": "BotCommandScopeChat", + "chat_id": "owner" + } + ] + }, + "rules": { + "scopes": [ + { + "name": "BotCommandScopeDefault" + }, + { + "name": "BotCommandScopeChat", + "chat_id": "owner" + } + ] + }, + "report": { + "scopes": [ + { + "name": "BotCommandScopeChat", + "chat_id": "comments" + } + ] + }, + "forwards": { + "scopes": [ + { + "name": "BotCommandScopeChat", + "chat_id": "owner" + } + ] + }, + "import": { + "scopes": [ + { + "name": "BotCommandScopeChat", + "chat_id": "owner" + } + ] + }, + "export": { + "scopes": [ + { + "name": "BotCommandScopeChat", + "chat_id": "owner" + } + ] + }, + "remove": { + "scopes": [ + { + "name": "BotCommandScopeChat", + "chat_id": "owner" + } + ] + }, + "purge": { + "scopes": [ + { + "name": "BotCommandScopeChat", + "chat_id": "owner" + } + ] + }, + "shutdown": { + "scopes": [ + { + "name": "BotCommandScopeChat", + "chat_id": "owner" + } + ] + } + } } \ No newline at end of file diff --git a/locale/en.json b/locale/en.json index 3a23761..f07791b 100644 --- a/locale/en.json +++ b/locale/en.json @@ -1,9 +1,8 @@ { "commands": { "start": "Start using the bot", - "rules": "Photos submission rules" - }, - "commands_admin": { + "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", @@ -60,7 +59,10 @@ "remove_abort": "Removal aborted.", "remove_success": "Removed media with ID `{0}`.", "remove_failure": "Could not remove media with ID `{0}`. Check if provided ID is correct and if it is - you can also check bot's log for details.", - "update_available": "**New version found**\nThere's a newer version of a bot found. You can update your bot to [{0}]({1}) using command line of your host.\n\n**Release notes**\n{2}\n\nRead more about updating you bot on the [wiki page](https://git.end-play.xyz/profitroll/TelegramPoster/wiki/Updating-Instance).\n\nPlease not that you can also disable this notification by editing `reports.update` key of the config." + "update_available": "**New version found**\nThere's a newer version of a bot found. You can update your bot to [{0}]({1}) using command line of your host.\n\n**Release notes**\n{2}\n\nRead more about updating you bot on the [wiki page](https://git.end-play.xyz/profitroll/TelegramPoster/wiki/Updating-Instance).\n\nPlease not that you can also disable this notification by editing `reports.update` key of the config.", + "shutdown_confirm": "There are {0} unfinished users' contexts. If you turn off the bot, those will be lost. Please confirm shutdown using a button below.", + "report_sent": "We've notified admins about presumable violation. Thank you for cooperation.", + "report_received": "This message has been reported by **{0}** (@{1}, `{2}`)" }, "button": { "sub_yes": "✅ Accept", @@ -70,7 +72,8 @@ "sub_unblock": "🏳️ Unblock sender", "post_view": "View in channel", "accepted": "✅ Accepted", - "declined": "❌ Declined" + "declined": "❌ Declined", + "shutdown": "Confirm shutdown" }, "callback": { "sub_yes": "✅ Submission approved", diff --git a/locale/uk.json b/locale/uk.json index 0696eb3..128e39a 100644 --- a/locale/uk.json +++ b/locale/uk.json @@ -1,9 +1,8 @@ { "commands": { "start": "Почати користуватись ботом", - "rules": "Правила пропонування фото" - }, - "commands_admin": { + "rules": "Правила пропонування фото", + "report": "Поскаржитись на цей пост", "forwards": "Переглянути репости", "import": "Надати боту .zip архів з фотографіями", "export": "Отримати .zip архів з усіма фотографіями", @@ -60,7 +59,10 @@ "remove_abort": "Видалення перервано.", "remove_success": "Видалено медіа з ID `{0}`.", "remove_failure": "Не вдалося видалити медіа з ID `{0}`. Перевірте, чи вказано правильний ID, і якщо він правильний, ви також можете переглянути логи бота для отримання більш детальної інформації.", - "update_available": "**Знайдено нову версію**\nЗнайдено нову версію бота. Ви можете оновити бота до [{0}]({1}) за допомогою командного рядка вашого хосту.\n\n**Примітки до релізу**\n{2}\n\nДетальніше про оновлення бота можна знайти на [вікі-сторінці](https://git.end-play.xyz/profitroll/TelegramPoster/wiki/Updating-Instance).\n\nЗверніть увагу, що ви також можете вимкнути це сповіщення, відредагувавши ключ `reports.update` у конфігурації." + "update_available": "**Знайдено нову версію**\nЗнайдено нову версію бота. Ви можете оновити бота до [{0}]({1}) за допомогою командного рядка вашого хосту.\n\n**Примітки до релізу**\n{2}\n\nДетальніше про оновлення бота можна знайти на [вікі-сторінці](https://git.end-play.xyz/profitroll/TelegramPoster/wiki/Updating-Instance).\n\nЗверніть увагу, що ви також можете вимкнути це сповіщення, відредагувавши ключ `reports.update` у конфігурації.", + "shutdown_confirm": "Існує {0} незавершених контекстів користувачів. Якщо ви вимкнете бота, вони будуть втрачені. Будь ласка, підтвердіть вимкнення за допомогою кнопки нижче.", + "report_sent": "Ми повідомили адміністрацію про потенційне порушення. Дякую за співпрацю.", + "report_received": "На це повідомлення було отримано скаргу від **{0}** (@{1}, `{2}`)" }, "button": { "sub_yes": "✅ Прийняти", @@ -70,7 +72,8 @@ "sub_unblock": "🏳️ Розблокувати відправника", "post_view": "Переглянути на каналі", "accepted": "✅ Прийнято", - "declined": "❌ Відхилено" + "declined": "❌ Відхилено", + "shutdown": "Підтвердити вимкнення" }, "callback": { "sub_yes": "✅ Подання схвалено", diff --git a/main.py b/main.py new file mode 100644 index 0000000..c590bca --- /dev/null +++ b/main.py @@ -0,0 +1,39 @@ +import contextlib +import logging +from os import getpid + +from convopyro import Conversation + +from classes.pyroclient import PyroClient +from modules.scheduler import scheduler + +logging.basicConfig( + level=logging.INFO, + format="%(name)s.%(funcName)s | %(levelname)s | %(message)s", + datefmt="[%X]", +) + +logger = logging.getLogger(__name__) + +with contextlib.suppress(ImportError): + import uvloop + + uvloop.install() + + +def main(): + client = PyroClient(scheduler=scheduler) + Conversation(client) + + try: + client.run() + except KeyboardInterrupt: + logger.warning("Forcefully shutting down with PID %s...", getpid()) + finally: + if client.scheduler is not None: + client.scheduler.shutdown() + exit() + + +if __name__ == "__main__": + main() diff --git a/modules/api_client.py b/modules/api_client.py index af0507a..7404320 100644 --- a/modules/api_client.py +++ b/modules/api_client.py @@ -1,38 +1,87 @@ -"""This is only a temporary solution. Complete Photos API client is yet to be developed.""" - import asyncio +import logging from base64 import b64decode, b64encode from os import makedirs, path, sep -from random import choice -from traceback import print_exc -from typing import Tuple, Union +from pathlib import Path +from typing import Union import aiofiles -from aiohttp import FormData - -from classes.exceptions import ( - AlbumCreationDuplicateError, - AlbumCreationError, - AlbumCreationNameError, - SubmissionUploadError, - UserCreationDuplicateError, - UserCreationError, +from aiohttp import ClientSession +from libbot import config_get, i18n, sync +from photosapi_client import AuthenticatedClient, Client +from photosapi_client.api.default.album_create_albums_post import ( + asyncio as album_create, ) -from modules.logger import logWrite -from modules.utils import configGet, locale +from photosapi_client.api.default.album_delete_album_id_delete import ( + asyncio as album_delete, +) +from photosapi_client.api.default.album_find_albums_get import asyncio as album_find +from photosapi_client.api.default.login_for_access_token_token_post import sync as login +from photosapi_client.api.default.photo_delete_photos_id_delete import ( + asyncio as photo_delete, +) +from photosapi_client.api.default.photo_find_albums_album_photos_get import ( + asyncio as photo_find, +) +from photosapi_client.api.default.photo_get_photos_id_get import asyncio as photo_get +from photosapi_client.api.default.photo_patch_photos_id_patch import ( + asyncio as photo_patch, +) +from photosapi_client.api.default.photo_random_albums_album_photos_random_get import ( + asyncio as photo_random, +) +from photosapi_client.api.default.photo_upload_albums_album_photos_post import ( + asyncio_detailed as photo_upload, +) +from photosapi_client.api.default.user_create_users_post import asyncio as user_create +from photosapi_client.api.default.user_me_users_me_get import sync as user_me +from photosapi_client.api.default.video_find_albums_album_videos_get import ( + asyncio as video_find, +) +from photosapi_client.api.default.video_get_videos_id_get import asyncio as video_get +from photosapi_client.api.default.video_patch_videos_id_patch import ( + asyncio as video_patch, +) +from photosapi_client.api.default.video_random_albums_album_videos_random_get import ( + asyncio as video_random, +) +from photosapi_client.api.default.video_upload_albums_album_videos_post import ( + asyncio as video_upload, +) +from photosapi_client.models.body_login_for_access_token_token_post import ( + BodyLoginForAccessTokenTokenPost, +) +from photosapi_client.models.body_photo_upload_albums_album_photos_post import ( + BodyPhotoUploadAlbumsAlbumPhotosPost as BodyPhotoUpload, +) +from photosapi_client.models.body_video_upload_albums_album_videos_post import ( + BodyVideoUploadAlbumsAlbumVideosPost as BodyVideoUpload, +) +from photosapi_client.models.http_validation_error import HTTPValidationError +from photosapi_client.models.photo import Photo +from photosapi_client.models.photo_search import PhotoSearch +from photosapi_client.models.token import Token +from photosapi_client.models.video import Video +from photosapi_client.models.video_search import VideoSearch +from photosapi_client.types import File + from modules.http_client import http_session +logger = logging.getLogger(__name__) -async def authorize() -> str: - makedirs(configGet("cache", "locations"), exist_ok=True) - if path.exists(configGet("cache", "locations") + sep + "api_access") is True: + +async def authorize(custom_session: Union[ClientSession, None] = None) -> str: + makedirs(await config_get("cache", "locations"), exist_ok=True) + session = http_session if custom_session is None else custom_session + + if path.exists(await config_get("cache", "locations") + sep + "api_access") is True: async with aiofiles.open( - configGet("cache", "locations") + sep + "api_access", "rb" + await config_get("cache", "locations") + sep + "api_access", "rb" ) as file: token = b64decode(await file.read()).decode("utf-8") if ( - await http_session.get( - configGet("address", "posting", "api") + "/users/me/", + await session.get( + await config_get("address", "posting", "api") + "/users/me/", headers={"Authorization": f"Bearer {token}"}, ) ).status == 200: @@ -40,27 +89,28 @@ async def authorize() -> str: payload = { "grant_type": "password", "scope": "me albums.list albums.read albums.write photos.list photos.read photos.write videos.list videos.read videos.write", - "username": configGet("username", "posting", "api"), - "password": configGet("password", "posting", "api"), + "username": await config_get("username", "posting", "api"), + "password": await config_get("password", "posting", "api"), } - response = await http_session.post( - configGet("address", "posting", "api") + "/token", data=payload + response = await session.post( + await config_get("address", "posting", "api") + "/token", data=payload ) if not response.ok: - logWrite( - locale( + logger.warning( + i18n._( "api_creds_invalid", "console", - locale=configGet("locale_log").format( - configGet("address", "posting", "api"), - configGet("username", "posting", "api"), + locale=(await config_get("locale_log")).format( + await config_get("address", "posting", "api"), + await config_get("username", "posting", "api"), response.status, ), ) ) raise ValueError async with aiofiles.open( - configGet("cache", "locations") + sep + "api_access", "wb" + str(Path(f"{await config_get('cache', 'locations')}/api_access")), + "wb", ) as file: await file.write( b64encode((await response.json())["access_token"].encode("utf-8")) @@ -68,196 +118,36 @@ async def authorize() -> str: return (await response.json())["access_token"] -async def random_pic(token: Union[str, None] = None) -> Tuple[str, str]: - """Returns random image id and filename from the queue. +unauthorized_client = Client( + base_url=sync.config_get("address", "posting", "api"), + timeout=5.0, + verify_ssl=True, + raise_on_unexpected_status=True, +) - ### Returns: - * `Tuple[str, str]`: First value is an ID and the filename in the filesystem to be indexed. - """ - token = await authorize() if token is None else token - logWrite( - f'{configGet("address", "posting", "api")}/albums/{configGet("album", "posting", "api")}/photos?q=&page_size={configGet("page_size", "posting")}&caption=queue' +login_token = login( + client=unauthorized_client, + form_data=BodyLoginForAccessTokenTokenPost( + grant_type="password", + scope="me albums.list albums.read albums.write photos.list photos.read photos.write videos.list videos.read videos.write", + username=sync.config_get("username", "posting", "api"), + password=sync.config_get("password", "posting", "api"), + ), +) + +if not isinstance(login_token, Token): + logger.warning( + "Could not initialize connection due to invalid token: %s", login_token ) - resp = await http_session.get( - f'{configGet("address", "posting", "api")}/albums/{configGet("album", "posting", "api")}/photos?q=&page_size={configGet("page_size", "posting")}&caption=queue', - headers={"Authorization": f"Bearer {token}"}, - ) - # logWrite( - # locale("random_pic_response", "console", locale=configGet("locale_log")).format( - # await resp.json() - # ), - # debug=True, - # ) - if resp.status != 200: - logWrite( - locale( - "random_pic_error_code", - "console", - locale=configGet("locale_log").format( - configGet("album", "posting", "api"), resp.status - ), - ), - ) - logWrite( - locale( - "random_pic_error_debug", - "console", - locale=configGet("locale_log").format( - configGet("address", "posting", "api"), - configGet("album", "posting", "api"), - configGet("page_size", "posting"), - token, - resp.status, - ), - ), - debug=True, - ) - raise ValueError - if len((await resp.json())["results"]) == 0: - raise KeyError - pic = choice((await resp.json())["results"]) - return pic["id"], pic["filename"] - - -async def upload_pic( - filepath: str, allow_duplicates: bool = False, token: Union[str, None] = None -) -> Tuple[bool, list, Union[str, None]]: - token = await authorize() if token is None else token - try: - pic_name = path.basename(filepath) - logWrite(f"Uploading {pic_name} to the API...", debug=True) - async with aiofiles.open(filepath, "rb") as f: - file_bytes = await f.read() - formdata = FormData() - formdata.add_field( - "file", file_bytes, filename=pic_name, content_type="image/jpeg" - ) - response = await http_session.post( - f'{configGet("address", "posting", "api")}/albums/{configGet("album", "posting", "api")}/photos', - params={ - "caption": "queue", - "compress": "false", - "ignore_duplicates": str(allow_duplicates).lower(), - }, - headers={"Authorization": f"Bearer {token}"}, - data=formdata, - ) - response_json = await response.json() - if response.status != 200 and response.status != 409: - logWrite( - locale( - "pic_upload_error", - "console", - locale=configGet("locale_log").format( - filepath, response.status, response.content - ), - ), - ) - raise SubmissionUploadError( - str(filepath), response.status, response.content - ) - id = response_json["id"] if "id" in await response.json() else None - duplicates = [] - if "duplicates" in response_json: - for index, duplicate in enumerate(response_json["duplicates"]): # type: ignore - if response_json["access_token"] is None: - duplicates.append( - f'`{duplicate["id"]}`:\n{configGet("address_external", "posting", "api")}/photos/{duplicate["id"]}' - ) - else: - duplicates.append( - f'`{duplicate["id"]}`:\n{configGet("address_external", "posting", "api")}/token/photo/{response_json["access_token"]}?id={index}' - ) - return True, duplicates, id - except Exception as exp: - print_exc() - return False, [], None - - -async def find_pic( - name: str, caption: Union[str, None] = None, token: Union[str, None] = None -) -> Union[dict, None]: - token = await authorize() if token is None else token - try: - response = await http_session.get( - f'{configGet("address", "posting", "api")}/albums/{configGet("album", "posting", "api")}/photos', - params={"q": name, "caption": caption}, - headers={"Authorization": f"Bearer {token}"}, - ) - # logWrite(response.json()) - if response.status != 200: - return None - if len((await response.json())["results"]) == 0: - return None - return (await response.json())["results"] - except Exception as exp: - logWrite( - locale( - "find_pic_error", - "console", - locale=configGet("locale_log").format(name, caption, exp), - ), - ) - return None - - -async def move_pic(id: str, token: Union[str, None] = None) -> bool: - token = await authorize() if token is None else token - try: - response = await http_session.patch( - f'{configGet("address", "posting", "api")}/photos/{id}?caption=sent', - headers={"Authorization": f"Bearer {token}"}, - ) - if response.status != 200: - logWrite(f"Media moving failed with HTTP {response.status}", debug=True) - return False - return True - except: - return False - - -async def remove_pic(id: str, token: Union[str, None] = None) -> bool: - token = await authorize() if token is None else token - try: - response = await http_session.delete( - f'{configGet("address", "posting", "api")}/photos/{id}', - headers={"Authorization": f"Bearer {token}"}, - ) - if response.status != 204: - logWrite(f"Media removal failed with HTTP {response.status}", debug=True) - return False - return True - except: - return False - - -async def create_user(username: str, email: str, password: str) -> None: - response = await http_session.post( - f'{configGet("address", "posting", "api")}/users', - data={"user": username, "email": email, "password": password}, - ) - if response.status == 409: - raise UserCreationDuplicateError(username) - elif response.status != 204: - raise UserCreationError(response.status, await response.text(encoding="utf-8")) - return None - - -async def create_album(name: str, title: str) -> None: - token = await authorize() - response = await http_session.post( - f'{configGet("address", "posting", "api")}/albums', - params={"name": name, "title": title}, - headers={"Authorization": f"Bearer {token}"}, - ) - if response.status == 409: - raise AlbumCreationDuplicateError(name) - elif response.status == 406: - raise AlbumCreationNameError(await response.json()) - elif response.status != 200: - raise AlbumCreationError(response.status, await response.text(encoding="utf-8")) - return None + exit() +client = AuthenticatedClient( + base_url=sync.config_get("address", "posting", "api"), + timeout=5.0, + verify_ssl=True, + raise_on_unexpected_status=True, + token=login_token.access_token, +) if __name__ == "__main__": print(asyncio.run(authorize())) diff --git a/modules/app.py b/modules/app.py deleted file mode 100644 index 2724747..0000000 --- a/modules/app.py +++ /dev/null @@ -1,14 +0,0 @@ -from modules.utils import configGet -from classes.poster_client import PosterClient -from convopyro import Conversation - -app = PosterClient( - "duptsiaposter", - bot_token=configGet("bot_token", "bot"), - api_id=configGet("api_id", "bot"), - api_hash=configGet("api_hash", "bot"), -) - -Conversation(app) - -users_with_context = [] diff --git a/modules/cli.py b/modules/cli.py deleted file mode 100644 index 0e4768e..0000000 --- a/modules/cli.py +++ /dev/null @@ -1,78 +0,0 @@ -import asyncio -from sys import exit -from traceback import print_exc -from modules.api_client import create_album, create_user, http_session -from argparse import ArgumentParser - -from modules.utils import configSet - -parser = ArgumentParser( - prog="Telegram Poster", - description="Bot for posting some of your stuff and also receiving submissions.", -) - -parser.add_argument("--create-user", action="store_true") -parser.add_argument("--create-album", action="store_true") - -args = parser.parse_args() - - -async def cli_create_user() -> None: - print( - "To set up Photos API connection you need to create a new user.\nIf you have email confirmation enabled in your Photos API config - you need to use a real email that will get a confirmation code afterwards.", - flush=True, - ) - username = input("Choose username for new Photos API user: ").strip() - email = input(f"Choose email for user '{username}': ").strip() - password = input(f"Choose password for user '{username}': ").strip() - try: - result_1 = await create_user(username, email, password) - # asyncio.run(create_user(username, email, password)) - configSet("username", username, "posting", "api") - configSet("password", password, "posting", "api") - none = input( - "Alright. If you have email confirmation enabled - please confirm registration by using the link in your email. After that press Enter. Otherwise just press Enter." - ) - except Exception as exp: - print(f"Could not create a user due to {exp}", flush=True) - print_exc() - exit() - if not args.create_album: - print("You're done!", flush=True) - await http_session.close() - exit() - return None - - -async def cli_create_album() -> None: - print( - "To use Photos API your user needs to have an album to store its data.\nThis wizard will help you to create a new album with its name and title.", - flush=True, - ) - name = input("Choose a name for your album: ").strip() - title = input(f"Choose a title for album '{name}': ").strip() - try: - result_2 = await create_album(name, title) - # asyncio.run(create_album(name, title)) - configSet("album", name, "posting", "api") - except Exception as exp: - print(f"Could not create an album due to {exp}", flush=True) - print_exc() - exit() - print("You're done!", flush=True) - await http_session.close() - exit() - return None - - -if args.create_user or args.create_album: - loop = asyncio.get_event_loop() - tasks = [] - - if args.create_user: - loop.run_until_complete(asyncio.wait([loop.create_task(cli_create_user())])) - - if args.create_album: - loop.run_until_complete(asyncio.wait([loop.create_task(cli_create_album())])) - - loop.close() diff --git a/modules/commands_register.py b/modules/commands_register.py deleted file mode 100644 index de1bcbe..0000000 --- a/modules/commands_register.py +++ /dev/null @@ -1,57 +0,0 @@ -from os import listdir -from classes.poster_client import PosterClient -from pyrogram.types import BotCommand, BotCommandScopeChat -from modules.utils import configGet, locale - - -async def register_commands(app: PosterClient) -> None: - if configGet("submit", "mode"): - # Registering user commands - for entry in listdir(configGet("locale", "locations")): - if entry.endswith(".json"): - commands_list = [] - for command in configGet("commands"): - commands_list.append( - BotCommand( - command, - locale( - command, "commands", locale=entry.replace(".json", "") - ), - ) - ) - await app.set_bot_commands( - commands_list, language_code=entry.replace(".json", "") - ) - - # Registering user commands for fallback locale - commands_list = [] - for command in configGet("commands"): - commands_list.append( - BotCommand( - command, - locale(command, "commands", locale=configGet("locale_fallback")), - ) - ) - await app.set_bot_commands(commands_list) - - # Registering admin commands - commands_admin_list = [] - - if configGet("submit", "mode"): - for command in configGet("commands"): - commands_admin_list.append( - BotCommand( - command, locale(command, "commands", locale=configGet("locale")) - ) - ) - for command in configGet("commands_admin"): - commands_admin_list.append( - BotCommand( - command, locale(command, "commands_admin", locale=configGet("locale")) - ) - ) - - for admin in app.admins: - await app.set_bot_commands( - commands_admin_list, scope=BotCommandScopeChat(chat_id=admin) - ) diff --git a/modules/logger.py b/modules/logger.py deleted file mode 100644 index 0f32b14..0000000 --- a/modules/logger.py +++ /dev/null @@ -1,67 +0,0 @@ -from datetime import datetime -from gzip import open as gzipopen -from os import getcwd, makedirs, path, stat -from shutil import copyfileobj - -from ujson import loads - -with open(getcwd() + path.sep + "config.json", "r", encoding="utf8") as file: - json_contents = loads(file.read()) - log_size = json_contents["logging"]["size"] - log_folder = json_contents["logging"]["location"] - file.close() - - -# Check latest log size -def checkSize(debug=False) -> None: - global log_folder - - if debug: - log_file = "debug.log" - else: - log_file = "latest.log" - - try: - makedirs(log_folder, exist_ok=True) - log = stat(path.join(log_folder, log_file)) - if (log.st_size / 1024) > log_size: - with open(path.join(log_folder, log_file), "rb") as f_in: - with gzipopen( - path.join( - log_folder, - f'{datetime.now().strftime("%d.%m.%Y_%H:%M:%S")}.log.gz', - ), - "wb", - ) as f_out: - copyfileobj(f_in, f_out) - print( - f'Copied {path.join(log_folder, datetime.now().strftime("%d.%m.%Y_%H:%M:%S"))}.log.gz' - ) - open(path.join(log_folder, log_file), "w").close() - except FileNotFoundError: - print(f"Log file {path.join(log_folder, log_file)} does not exist") - pass - - -# Append string to log -def logAppend(message, debug=False) -> None: - global log_folder - - message_formatted = f'[{datetime.now().strftime("%d.%m.%Y")}] [{datetime.now().strftime("%H:%M:%S")}] {message}' - checkSize(debug=debug) - - if debug: - log_file = "debug.log" - else: - log_file = "latest.log" - - log = open(path.join(log_folder, log_file), "a") - log.write(f"{message_formatted}\n") - log.close() - - -# Print to stdout and then to log -def logWrite(message, debug=False) -> None: - # save to log file and rotation is to be done - logAppend(f"{message}", debug=debug) - print(f"{message}", flush=True) diff --git a/modules/scheduler.py b/modules/scheduler.py index ba8fe06..a5eb79d 100644 --- a/modules/scheduler.py +++ b/modules/scheduler.py @@ -1,31 +1,3 @@ -from datetime import datetime, timedelta from apscheduler.schedulers.asyncio import AsyncIOScheduler -from pytimeparse.timeparse import timeparse -from modules.utils import configGet -from modules.sender import send_content -from modules.commands_register import register_commands -from modules.app import app scheduler = AsyncIOScheduler() - -if configGet("post", "mode"): - if configGet("use_interval", "posting"): - scheduler.add_job( - send_content, - "interval", - seconds=timeparse(configGet("interval", "posting")), - args=[app], - ) - else: - for entry in configGet("time", "posting"): - dt_obj = datetime.strptime(entry, "%H:%M") - scheduler.add_job( - send_content, "cron", hour=dt_obj.hour, minute=dt_obj.minute, args=[app] - ) - -scheduler.add_job( - register_commands, - "date", - run_date=datetime.now() + timedelta(seconds=10), - args=[app], -) diff --git a/modules/sender.py b/modules/sender.py index 52a0df0..9455ca9 100644 --- a/modules/sender.py +++ b/modules/sender.py @@ -1,113 +1,183 @@ +import logging from datetime import datetime from os import makedirs, path -from random import choice +from random import choice, sample from shutil import rmtree -from traceback import format_exc +from traceback import format_exc, print_exc +from typing import Union from uuid import uuid4 -from PIL import Image + import aiofiles +from aiohttp import ClientSession +from libbot.pyrogram.classes import PyroClient +from photosapi_client.errors import UnexpectedStatus +from PIL import Image -from classes.poster_client import PosterClient - -from modules.api_client import authorize, move_pic, random_pic, http_session +from modules.api_client import ( + File, + PhotoSearch, + VideoSearch, + authorize, + client, + photo_get, + photo_patch, + photo_random, + video_get, + video_patch, + video_random, +) from modules.database import col_sent, col_submitted -from modules.logger import logWrite -from modules.utils import configGet, locale + +logger = logging.getLogger(__name__) -async def send_content(app: PosterClient) -> None: +async def send_content(app: PyroClient, http_session: ClientSession) -> None: try: try: - token = await authorize() + token = await authorize(http_session) except ValueError: await app.send_message( app.owner, - locale("api_creds_invalid", "message", locale=configGet("locale")), + app._("api_creds_invalid", "message"), ) return try: - pic = await random_pic() - except KeyError: - logWrite(locale("post_empty", "console", locale=configGet("locale"))) - if configGet("error", "reports"): - await app.send_message( - app.owner, - locale("api_queue_empty", "message", locale=configGet("locale")), + funcs = [] + + if app.config["posting"]["types"]["photo"]: + funcs.append((photo_random, photo_get, app.send_photo, photo_patch)) + + if app.config["posting"]["types"]["video"]: + funcs.append((video_random, video_get, app.send_video, video_patch)) + + if not funcs: + raise KeyError( + "No media source provided: all seem to be disabled in config" ) - return - except ValueError: - if configGet("error", "reports"): + + if len(funcs) > 1: + found = False + for func_iter in sample(funcs, len(funcs)): + func = func_iter + + random_results = ( + await func_iter[0]( + album=app.config["posting"]["api"]["album"], + caption="queue", + client=client, + limit=1, + ) + ).results + + if not random_results: + continue + + media: Union[PhotoSearch, VideoSearch] = random_results[0] + + try: + response: File = await func_iter[1](id=media.id, client=client) + except Exception as exp: + print_exc() + logger.error("Media is invalid: %s", exp) + if app.config["reports"]["error"]: + await app.send_message( + app.owner, f"Media is invalid: {exp}" + ) + return + + found = True + break + + if not found: + raise KeyError("No media found") + else: + func = funcs[0] + media: Union[PhotoSearch, VideoSearch] = ( + await func[0]( + album=app.config["posting"]["api"]["album"], + caption="queue", + client=client, + limit=1, + ) + ).results[0] + try: + response: File = await func[1](id=media.id, client=client) + except Exception as exp: + print_exc() + logger.error("Media is invalid: %s", exp) + if app.config["reports"]["error"]: + await app.send_message(app.owner, f"Media is invalid: {exp}") + return + + except (KeyError, AttributeError, TypeError, IndexError): + logger.info(app._("post_empty", "console")) + if app.config["reports"]["error"]: await app.send_message( app.owner, - locale("api_queue_error", "message", locale=configGet("locale")), + app._("api_queue_empty", "message"), ) return - response = await http_session.get( - f'{configGet("address", "posting", "api")}/photos/{pic[0]}', - headers={"Authorization": f"Bearer {token}"}, - ) - - if response.status != 200: - logWrite( - locale( - "post_invalid_pic", "console", locale=configGet("locale") - ).format(response.status, str(await response.json())) - ) - if configGet("error", "reports"): + except (ValueError, UnexpectedStatus): + if app.config["reports"]["error"]: await app.send_message( app.owner, - locale( - "post_invalid_pic", "message", locale=configGet("locale") - ).format(response.status, await response.json()), + app._("api_queue_error", "message"), ) + return tmp_dir = str(uuid4()) - makedirs(path.join(configGet("tmp", "locations"), tmp_dir), exist_ok=True) + makedirs(path.join(app.config["locations"]["tmp"], tmp_dir), exist_ok=True) - tmp_path = path.join(tmp_dir, pic[1]) + tmp_path = path.join(tmp_dir, media.filename) async with aiofiles.open( - path.join(configGet("tmp", "locations"), tmp_path), "wb" + path.join(app.config["locations"]["tmp"], tmp_path), "wb" ) as out_file: - await out_file.write(await response.read()) + await out_file.write(response.payload.read()) - logWrite( - f'Candidate {pic[1]} ({pic[0]}) is {path.getsize(path.join(configGet("tmp", "locations"), tmp_path))} bytes big', - debug=True, + logger.info( + "Candidate %s (%s) is %s bytes big", + media.filename, + media.id, + path.getsize(path.join(app.config["locations"]["tmp"], tmp_path)), ) - if path.getsize(path.join(configGet("tmp", "locations"), tmp_path)) > 5242880: - image = Image.open(path.join(configGet("tmp", "locations"), tmp_path)) + if ( + path.getsize(path.join(app.config["locations"]["tmp"], tmp_path)) > 5242880 + ) and func[0] is photo_random: + image = Image.open(path.join(app.config["locations"]["tmp"], tmp_path)) width, height = image.size image = image.resize((int(width / 2), int(height / 2)), Image.ANTIALIAS) if tmp_path.lower().endswith(".jpeg") or tmp_path.lower().endswith(".jpg"): image.save( - path.join(configGet("tmp", "locations"), tmp_path), + path.join(app.config["locations"]["tmp"], tmp_path), "JPEG", optimize=True, quality=50, ) elif tmp_path.lower().endswith(".png"): image.save( - path.join(configGet("tmp", "locations"), tmp_path), + path.join(app.config["locations"]["tmp"], tmp_path), "PNG", optimize=True, compress_level=8, ) image.close() - if path.getsize(path.join(configGet("tmp", "locations"), tmp_path)) > 5242880: + if ( + path.getsize(path.join(app.config["locations"]["tmp"], tmp_path)) > 5242880 + ) and func[0] is photo_random: rmtree( - path.join(configGet("tmp", "locations"), tmp_dir), ignore_errors=True + path.join(app.config["locations"]["tmp"], tmp_dir), ignore_errors=True ) raise BytesWarning del response - submitted = col_submitted.find_one({"temp.file": pic[1]}) + submitted = col_submitted.find_one({"temp.file": media.filename}) if submitted is not None and submitted["caption"] is not None: caption = submitted["caption"].strip() @@ -116,86 +186,82 @@ async def send_content(app: PosterClient) -> None: if ( submitted is not None - and configGet("enabled", "posting", "submitted_caption") + and app.config["posting"]["submitted_caption"]["enabled"] and ( (submitted["user"] not in app.admins) - or (configGet("ignore_admins", "posting", "submitted_caption") is False) + or ( + app.config["posting"]["submitted_caption"]["ignore_admins"] is False + ) ) ): caption = ( - f"{caption}\n\n{configGet('text', 'posting', 'submitted_caption')}\n" + f"{caption}\n\n{app.config['posting']['submitted_caption']['text']}\n" ) else: caption = f"{caption}\n\n" - if configGet("enabled", "caption"): - if configGet("link", "caption") != None: - caption = f"{caption}[{choice(configGet('text', 'caption'))}]({configGet('link', 'caption')})" + if app.config["caption"]["enabled"]: + if app.config["caption"]["link"] is not None: + caption = f"{caption}[{choice(app.config['caption']['text'])}]({app.config['caption']['link']})" else: - caption = f"{caption}{choice(configGet('text', 'caption'))}" + caption = f"{caption}{choice(app.config['caption']['text'])}" else: caption = caption try: - sent = await app.send_photo( - configGet("channel", "posting"), - path.join(configGet("tmp", "locations"), tmp_path), + sent = await func[2]( + app.config["posting"]["channel"], + path.join(app.config["locations"]["tmp"], tmp_path), caption=caption, - disable_notification=configGet("silent", "posting"), + disable_notification=app.config["posting"]["silent"], ) except Exception as exp: - logWrite(f"Could not send image {pic[1]} ({pic[0]}) due to {exp}") - if configGet("error", "reports"): + logger.error( + "Could not send media %s (%s) due to %s", media.filename, media.id, exp + ) + if app.config["reports"]["error"]: await app.send_message( app.owner, - locale( - "post_exception", "message", locale=configGet("locale") - ).format(exp, format_exc()), + app._("post_exception", "message").format(exp, format_exc()), ) - # rmtree(path.join(configGet("tmp", "locations"), tmp_dir), ignore_errors=True) + # rmtree(path.join(app.config['locations']['tmp'], tmp_dir), ignore_errors=True) return col_sent.insert_one( { "date": datetime.now(), - "image": pic[0], - "filename": pic[1], - "channel": configGet("channel", "posting"), + "image": media.id, + "filename": media.filename, + "channel": app.config["posting"]["channel"], "caption": None if (submitted is None or submitted["caption"] is None) else submitted["caption"].strip(), } ) - await move_pic(pic[0]) + await func[3](id=media.id, client=client, caption="sent") - rmtree(path.join(configGet("tmp", "locations"), tmp_dir), ignore_errors=True) + rmtree(path.join(app.config["locations"]["tmp"], tmp_dir), ignore_errors=True) - logWrite( - locale("post_sent", "console", locale=configGet("locale")).format( - pic[0], - str(configGet("channel", "posting")), + logger.info( + app._("post_sent", "console").format( + media.id, + str(app.config["posting"]["channel"]), caption.replace("\n", "%n"), - str(configGet("silent", "posting")), + str(app.config["posting"]["silent"]), ) ) except Exception as exp: - logWrite( - locale("post_exception", "console", locale=configGet("locale")).format( - str(exp), format_exc() - ) - ) - if configGet("error", "reports"): + logger.error(app._("post_exception", "console").format(str(exp), format_exc())) + if app.config["reports"]["error"]: await app.send_message( app.owner, - locale("post_exception", "message", locale=configGet("locale")).format( - exp, format_exc() - ), + app._("post_exception", "message").format(exp, format_exc()), ) try: rmtree( - path.join(configGet("tmp", "locations"), tmp_dir), ignore_errors=True + path.join(app.config["locations"]["tmp"], tmp_dir), ignore_errors=True ) except: pass diff --git a/modules/utils.py b/modules/utils.py index 0256377..df017b9 100644 --- a/modules/utils.py +++ b/modules/utils.py @@ -1,252 +1,33 @@ -from os import kill, makedirs -from os import name as osname -from os import path, sep -from sys import exit -from traceback import print_exc -from typing import Any +import logging +from os import makedirs, path +from pathlib import Path +from typing import List, Union from zipfile import ZipFile import aiofiles -from ujson import JSONDecodeError, dumps, loads -from modules.logger import logWrite +logger = logging.getLogger(__name__) -default_config = { - "locale": "en", - "locale_log": "en", - "locale_fallback": "en", - "owner": 0, - "admins": [], - "bot": {"api_id": 0, "api_hash": "", "bot_token": ""}, - "database": { - "user": None, - "password": None, - "host": "127.0.0.1", - "port": 27017, - "name": "tgposter", - }, - "mode": {"post": True, "submit": True}, - "reports": {"sent": False, "error": True, "startup": True, "shutdown": True}, - "logging": {"size": 512, "location": "logs"}, - "locations": { - "tmp": "tmp", - "data": "data", - "cache": "cache", - "sent": "data/sent", - "queue": "data/queue", - "index": "data/index.json", - "locale": "locale", - }, - "posting": { - "channel": 0, - "silent": False, - "move_sent": False, - "use_interval": False, - "interval": "1h30m", - "page_size": 300, - "submitted_caption": { - "enabled": True, - "ignore_admins": True, - "text": "#submitted", - }, - "extensions": { - "photo": ["jpg", "png", "gif", "jpeg"], - "video": ["mp4", "avi", "mkv", "webm", "mov"], - }, - "time": [ - "08:00", - "10:00", - "12:00", - "14:00", - "16:00", - "18:00", - "20:00", - "22:00", - ], - "api": { - "address": "http://localhost:8054", - "address_external": "https://photos.domain.com", - "username": "", - "password": "", - "album": "", - }, - }, - "caption": {"enabled": False, "link": None, "text": ["sample text"]}, - "submission": { - "timeout": 30, - "file_size": 15728640, - "tmp_size": 15728640, - "allow_duplicates": False, - "send_uploaded_id": False, - "require_confirmation": {"users": True, "admins": True}, - "mime_types": [ - "image/png", - "image/gif", - "image/jpeg", - "video/mp4", - "video/quicktime", - ], - }, - "commands": ["start", "rules"], - "commands_admin": ["import", "export", "shutdown"], -} +USERS_WITH_CONTEXT: List[int] = [] -def jsonLoad(filename: str) -> Any: - """Loads arg1 as json and returns its contents""" - with open(filename, "r", encoding="utf8") as file: - try: - output = loads(file.read()) - except JSONDecodeError: - logWrite( - f"Could not load json file {filename}: file seems to be incorrect!\n{print_exc()}" - ) - raise - except FileNotFoundError: - logWrite( - f"Could not load json file {filename}: file does not seem to exist!\n{print_exc()}" - ) - raise - file.close() - return output - - -def jsonSave(contents: Any, filename: str) -> None: - """Dumps dict/list arg1 to file arg2""" - try: - with open(filename, "w", encoding="utf8") as file: - file.write( - dumps( - contents, ensure_ascii=False, indent=4, escape_forward_slashes=False - ) - ) - file.close() - except Exception as exp: - logWrite(f"Could not save json file {filename}: {exp}\n{print_exc()}") - return - - -def configSet(key: str, value, *args: str): - """Set key to a value - Args: - * key (str): The last key of the keys path. - * value (str/int/float/list/dict/None): Some needed value. - * *args (str): Path to key like: dict[args][key]. - """ - this_dict = jsonLoad("config.json") - string = "this_dict" - for arg in args: - string += f'["{arg}"]' - if type(value) in [str]: - string += f'["{key}"] = "{value}"' - else: - string += f'["{key}"] = {value}' - exec(string) - jsonSave(this_dict, "config.json") - return - - -def configGet(key: str, *args: str): - """Get value of the config key - Args: - * key (str): The last key of the keys path. - * *args (str): Path to key like: dict[args][key]. - Returns: - * any: Value of provided key - """ - this_dict = jsonLoad("config.json") - try: - this_key = this_dict - for dict_key in args: - this_key = this_key[dict_key] - this_key[key] - except KeyError: - print( - f"Could not find config key '{key}' under path {args}: falling back to default config", - flush=True, - ) - this_key = default_config - for dict_key in args: - this_key = this_key[dict_key] - configSet(key, this_key[key], *args) - return this_key[key] - - -def locale(key: str, *args: str, locale=configGet("locale")): - """Get value of locale string - Args: - * key (str): The last key of the locale's keys path. - * *args (list): Path to key like: dict[args][key]. - * locale (str): Locale to looked up in. Defaults to config's locale value. - Returns: - * any: Value of provided locale key - """ - if locale == None: - locale = configGet("locale") - - try: - this_dict = jsonLoad(f'{configGet("locale", "locations")}{sep}{locale}.json') - except FileNotFoundError: - try: - this_dict = jsonLoad( - f'{configGet("locale", "locations")}{sep}{configGet("locale")}.json' - ) - except FileNotFoundError: - try: - this_dict = jsonLoad( - f'{configGet("locale_fallback", "locations")}{sep}{configGet("locale")}.json' - ) - except: - return f'⚠️ Locale in config is invalid: could not get "{key}" in {str(args)} from locale "{locale}"' - - this_key = this_dict - for dict_key in args: - this_key = this_key[dict_key] - - try: - return this_key[key] - except KeyError: - return f'⚠️ Locale in config is invalid: could not get "{key}" in {str(args)} from locale "{locale}"' - - -async def extract_and_save(handle: ZipFile, filename: str, destpath: str): +async def extract_and_save(handle: ZipFile, filename: str, destpath: Union[str, Path]): """Extract and save file from archive - Args: - * handle (ZipFile): ZipFile handler - * filename (str): File base name - * path (str): Path where to store + ### Args: + * handle (`ZipFile`): ZipFile handler + * filename (`str`): File base name + * path (`Union[str, Path]`): Path where to store """ data = handle.read(filename) - filepath = path.join(destpath, filename) + filepath = path.join(str(destpath), filename) try: makedirs(path.dirname(filepath), exist_ok=True) async with aiofiles.open(filepath, "wb") as fd: await fd.write(data) - logWrite(f"Unzipped {filename}", debug=True) + logger.debug("Unzipped %s", filename) except IsADirectoryError: makedirs(filepath, exist_ok=True) except FileNotFoundError: pass return - - -try: - from psutil import Process -except ModuleNotFoundError: - print(locale("deps_missing", "console", locale=configGet("locale")), flush=True) - exit() - - -def killProc(pid: int) -> None: - """Kill process by its PID. Meant to be used to kill the main process of bot itself. - - ### Args: - * pid (`int`): PID of the target - """ - if osname == "posix": - from signal import SIGKILL - - kill(pid, SIGKILL) - else: - Process(pid).kill() diff --git a/plugins/callbacks/nothing.py b/plugins/callbacks/nothing.py index a2ccaa0..d0a3086 100644 --- a/plugins/callbacks/nothing.py +++ b/plugins/callbacks/nothing.py @@ -1,12 +1,12 @@ -from modules.app import app from pyrogram import filters +from pyrogram.client import Client from pyrogram.types import CallbackQuery -from classes.poster_client import PosterClient -from modules.utils import locale + +from classes.pyroclient import PyroClient -@app.on_callback_query(filters.regex("nothing")) -async def callback_query_nothing(app: PosterClient, clb: CallbackQuery): +@Client.on_callback_query(filters.regex("nothing")) +async def callback_query_nothing(app: PyroClient, clb: CallbackQuery): await clb.answer( - text=locale("nothing", "callback", locale=clb.from_user.language_code) + text=app._("nothing", "callback", locale=clb.from_user.language_code) ) diff --git a/plugins/callbacks/shutdown.py b/plugins/callbacks/shutdown.py index 595471a..197a0d8 100644 --- a/plugins/callbacks/shutdown.py +++ b/plugins/callbacks/shutdown.py @@ -1,29 +1,25 @@ -from os import getpid, makedirs, path +from os import makedirs, path from time import time -from modules.app import app + +from libbot import config_get, json_write from pyrogram import filters +from pyrogram.client import Client from pyrogram.types import CallbackQuery -from classes.poster_client import PosterClient -from modules.scheduler import scheduler -from modules.logger import logWrite -from modules.utils import configGet, jsonSave, locale + +from classes.pyroclient import PyroClient -@app.on_callback_query(filters.regex("shutdown")) -async def callback_query_nothing(app: PosterClient, clb: CallbackQuery): - if clb.from_user.id in app.admins: - pid = getpid() - logWrite(f"Shutting down bot with pid {pid}") - await clb.answer() - await clb.message.reply_text( - locale("shutdown", "message", locale=clb.from_user.language_code).format( - pid - ), - ) - scheduler.shutdown() - makedirs(configGet("cache", "locations"), exist_ok=True) - jsonSave( - {"timestamp": time()}, - path.join(configGet("cache", "locations"), "shutdown_time"), - ) - exit() +@Client.on_callback_query(filters.regex("shutdown")) +async def callback_query_nothing(app: PyroClient, clb: CallbackQuery): + if clb.from_user.id not in app.admins: + return + + await clb.answer() + + makedirs(await config_get("cache", "locations"), exist_ok=True) + await json_write( + {"timestamp": time()}, + path.join(await config_get("cache", "locations"), "shutdown_time"), + ) + + exit() diff --git a/plugins/callbacks/submission.py b/plugins/callbacks/submission.py index 33178b1..f6a68a9 100644 --- a/plugins/callbacks/submission.py +++ b/plugins/callbacks/submission.py @@ -1,20 +1,28 @@ +import logging from os import path +from pathlib import Path from shutil import rmtree -from pyrogram import filters -from pyrogram.types import CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton -from classes.exceptions import SubmissionDuplicatesError, SubmissionUnavailableError -from classes.poster_client import PosterClient -from classes.user import PosterUser -from modules.app import app -from modules.logger import logWrite -from modules.utils import configGet, locale -from modules.database import col_submitted from bson import ObjectId +from libbot import config_get +from pyrogram import filters +from pyrogram.client import Client +from pyrogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup + +from classes.exceptions import ( + SubmissionDuplicatesError, + SubmissionUnavailableError, + SubmissionUnsupportedError, +) +from classes.pyroclient import PyroClient +from classes.user import PosterUser +from modules.database import col_submitted + +logger = logging.getLogger(__name__) -@app.on_callback_query(filters.regex("sub_yes_[\s\S]*")) -async def callback_query_yes(app: PosterClient, clb: CallbackQuery): +@Client.on_callback_query(filters.regex("sub_yes_[\s\S]*")) +async def callback_query_yes(app: PyroClient, clb: CallbackQuery): fullclb = str(clb.data).split("_") user_locale = clb.from_user.language_code @@ -24,44 +32,51 @@ async def callback_query_yes(app: PosterClient, clb: CallbackQuery): submission = await app.submit_photo(fullclb[2]) except SubmissionUnavailableError: await clb.answer( - text=locale("sub_msg_unavail", "callback", locale=user_locale), + text=app._("sub_msg_unavail", "callback", locale=user_locale), + show_alert=True, + ) + return + except SubmissionUnsupportedError: + await clb.answer( + text=app._("mime_not_allowed", "message", locale=user_locale).format( + ", ".join(app.config["submission"]["mime_types"]), quote=True + ), show_alert=True, ) return except SubmissionDuplicatesError as exp: await clb.answer( - text=locale("sub_duplicates_found", "callback", locale=user_locale), + text=app._("sub_duplicates_found", "callback", locale=user_locale), show_alert=True, ) await clb.message.reply_text( - locale("sub_media_duplicates_list", "message", locale=user_locale).format( + app._("sub_media_duplicates_list", "message", locale=user_locale).format( "\n • ".join(exp.duplicates) ), quote=True, ) - logWrite( - locale( + logger.info( + app._( "submission_duplicate", "console", - locale=configGet("locale_log").format( - fullclb[2], - str(exp.duplicates), - ), + locale=app.config["locale_log"], + ).format( + fullclb[2], + str(exp.duplicates), ), - debug=True, ) return if submission[0] is not None: await submission[0].reply_text( - locale("sub_yes", "message", locale=submission[0].from_user.language_code), + app._("sub_yes", "message", locale=submission[0].from_user.language_code), quote=True, ) elif db_entry is not None: - await app.send_message(db_entry["user"], locale("sub_yes", "message")) + await app.send_message(db_entry["user"], app._("sub_yes", "message")) await clb.answer( - text=locale("sub_yes", "callback", locale=user_locale).format(fullclb[2]), + text=app._("sub_yes", "callback", locale=user_locale).format(fullclb[2]), show_alert=True, ) @@ -69,7 +84,7 @@ async def callback_query_yes(app: PosterClient, clb: CallbackQuery): [ [ InlineKeyboardButton( - text=str(locale("accepted", "button", locale=user_locale)), + text=str(app._("accepted", "button", locale=user_locale)), callback_data="nothing", ) ], @@ -79,14 +94,14 @@ async def callback_query_yes(app: PosterClient, clb: CallbackQuery): else [ [ InlineKeyboardButton( - text=str(locale("accepted", "button", locale=user_locale)), + text=str(app._("accepted", "button", locale=user_locale)), callback_data="nothing", ) ] ] ) - if configGet("send_uploaded_id", "submission"): + if await config_get("send_uploaded_id", "submission"): await clb.message.edit_caption( clb.message.caption + f"\n\nID: `{submission[1]}`" ) @@ -95,79 +110,52 @@ async def callback_query_yes(app: PosterClient, clb: CallbackQuery): reply_markup=InlineKeyboardMarkup(edited_markup) ) - logWrite( - locale( + logger.info( + app._( "submission_accepted", "console", - locale=configGet("locale_log").format(fullclb[2], submission[1]), - ), - debug=True, + locale=app.config["locale_log"], + ).format(fullclb[2], submission[1]), ) - # try: - # if configGet("api_based", "mode") is True: - # media = await app.download_media(submission, file_name=configGet("tmp", "locations")+sep) - # upload = upload_pic(media) - # if upload[0] is False: - # await clb.answer(text=locale("sub_media_failed", "message", locale=user_locale), show_alert=True) - # elif len(upload[1]) > 0: - # await clb.answer(text=locale("sub_media_duplicates", "message", locale=user_locale)) - # await clb.message.reply_text(locale("sub_media_duplicates_list", "message", locale=user_locale).format("\n • ".join(upload[1]))) - # else: - # if clb.data.endswith("_caption"): - # index = jsonLoad(configGet("index", "locations")) - # index["captions"][Path(media).name] = submission.caption - # jsonSave(index, configGet("index", "locations")) - # else: - # media = await app.download_media(submission, file_name=configGet("queue", "locations")+sep) - # if clb.data.endswith("_caption"): - # index = jsonLoad(configGet("index", "locations")) - # index["captions"][Path(media).name] = submission.caption - # jsonSave(index, configGet("index", "locations")) - # except: - # await clb.answer(text=locale("sub_media_unavail", "message", locale=user_locale), show_alert=True) - # return - -@app.on_callback_query(filters.regex("sub_no_[\s\S]*")) -async def callback_query_no(app: PosterClient, clb: CallbackQuery): +@Client.on_callback_query(filters.regex("sub_no_[\s\S]*")) +async def callback_query_no(app: PyroClient, clb: CallbackQuery): fullclb = str(clb.data).split("_") user_locale = clb.from_user.language_code db_entry = col_submitted.find_one_and_delete({"_id": ObjectId(fullclb[2])}) - if db_entry["temp"]["uuid"] is not None: - if path.exists( - path.join( - configGet("data", "locations"), "submissions", db_entry["temp"]["uuid"] - ) - ): - rmtree( - path.join( - configGet("data", "locations"), - "submissions", - db_entry["temp"]["uuid"], - ), - ignore_errors=True, - ) + if ( + db_entry["temp"]["uuid"] is not None + and Path( + f"{app.config['locations']['data']}/submissions/{db_entry['temp']['uuid']}" + ).exists() + ): + rmtree( + Path( + f"{app.config['locations']['data']}/submissions/{db_entry['temp']['uuid']}" + ), + ignore_errors=True, + ) try: submission = await app.get_messages( db_entry["user"], db_entry["telegram"]["msg_id"] ) - except: + except Exception as exp: await clb.answer( - text=locale("sub_msg_unavail", "message", locale=user_locale), + text=app._("sub_msg_unavail", "message", locale=user_locale), show_alert=True, ) return await submission.reply_text( - locale("sub_no", "message", locale=submission.from_user.language_code), + app._("sub_no", "message", locale=submission.from_user.language_code), quote=True, ) await clb.answer( - text=locale("sub_no", "callback", locale=user_locale).format(fullclb[2]), + text=app._("sub_no", "callback", locale=user_locale).format(fullclb[2]), show_alert=True, ) @@ -175,7 +163,7 @@ async def callback_query_no(app: PosterClient, clb: CallbackQuery): [ [ InlineKeyboardButton( - text=str(locale("declined", "button", locale=user_locale)), + text=str(app._("declined", "button", locale=user_locale)), callback_data="nothing", ) ], @@ -185,7 +173,7 @@ async def callback_query_no(app: PosterClient, clb: CallbackQuery): else [ [ InlineKeyboardButton( - text=str(locale("declined", "button", locale=user_locale)), + text=str(app._("declined", "button", locale=user_locale)), callback_data="nothing", ) ] @@ -194,26 +182,28 @@ async def callback_query_no(app: PosterClient, clb: CallbackQuery): await clb.message.edit_reply_markup( reply_markup=InlineKeyboardMarkup(edited_markup) ) - logWrite( - locale( + logger.info( + app._( "submission_rejected", "console", - locale=configGet("locale_log").format(fullclb[2]), - ), - debug=True, + locale=app.config["locale_log"], + ).format(fullclb[2]), ) -@app.on_callback_query(filters.regex("sub_block_[\s\S]*")) -async def callback_query_block(app: PosterClient, clb: CallbackQuery): +@Client.on_callback_query(filters.regex("sub_block_[\s\S]*")) +async def callback_query_block(app: PyroClient, clb: CallbackQuery): fullclb = str(clb.data).split("_") user_locale = clb.from_user.language_code + await app.send_message( - int(fullclb[2]), locale("sub_blocked", "message", locale=configGet("locale")) + int(fullclb[2]), + app._("sub_blocked", "message"), ) PosterUser(int(fullclb[2])).block() + await clb.answer( - text=locale("sub_block", "callback", locale=user_locale).format(fullclb[2]), + text=app._("sub_block", "callback", locale=user_locale).format(fullclb[2]), show_alert=True, ) @@ -221,7 +211,7 @@ async def callback_query_block(app: PosterClient, clb: CallbackQuery): clb.message.reply_markup.inline_keyboard[0], [ InlineKeyboardButton( - text=str(locale("sub_unblock", "button", locale=user_locale)), + text=str(app._("sub_unblock", "button", locale=user_locale)), callback_data=f"sub_unblock_{fullclb[2]}", ) ], @@ -229,26 +219,26 @@ async def callback_query_block(app: PosterClient, clb: CallbackQuery): await clb.message.edit_reply_markup( reply_markup=InlineKeyboardMarkup(edited_markup) ) - logWrite( - locale( + logger.info( + app._( "user_blocked", "console", - locale=configGet("locale_log").format(fullclb[2]), - ), - debug=True, + locale=app.config["locale_log"], + ).format(fullclb[2]), ) -@app.on_callback_query(filters.regex("sub_unblock_[\s\S]*")) -async def callback_query_unblock(app: PosterClient, clb: CallbackQuery): +@Client.on_callback_query(filters.regex("sub_unblock_[\s\S]*")) +async def callback_query_unblock(app: PyroClient, clb: CallbackQuery): fullclb = str(clb.data).split("_") user_locale = clb.from_user.language_code - await app.send_message( - int(fullclb[2]), locale("sub_unblocked", "message", locale=configGet("locale")) - ) + + await app.send_message(int(fullclb[2]), app._("sub_unblocked", "message")) + PosterUser(int(fullclb[2])).unblock() + await clb.answer( - text=locale("sub_unblock", "callback", locale=user_locale).format(fullclb[2]), + text=app._("sub_unblock", "callback", locale=user_locale).format(fullclb[2]), show_alert=True, ) @@ -256,7 +246,7 @@ async def callback_query_unblock(app: PosterClient, clb: CallbackQuery): clb.message.reply_markup.inline_keyboard[0], [ InlineKeyboardButton( - text=str(locale("sub_block", "button", locale=user_locale)), + text=str(app._("sub_block", "button", locale=user_locale)), callback_data=f"sub_block_{fullclb[2]}", ) ], @@ -264,11 +254,10 @@ async def callback_query_unblock(app: PosterClient, clb: CallbackQuery): await clb.message.edit_reply_markup( reply_markup=InlineKeyboardMarkup(edited_markup) ) - logWrite( - locale( + logger.info( + app._( "user_unblocked", "console", - locale=configGet("locale_log").format(fullclb[2]), - ), - debug=True, + locale=app.config["locale_log"], + ).format(fullclb[2]), ) diff --git a/plugins/commands/general.py b/plugins/commands/general.py index e839482..8aaf34a 100644 --- a/plugins/commands/general.py +++ b/plugins/commands/general.py @@ -1,45 +1,45 @@ -from os import getpid, makedirs, path +from os import makedirs +from pathlib import Path from time import time +from libbot import json_write from pyrogram import filters -from pyrogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton +from pyrogram.client import Client +from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message -from classes.poster_client import PosterClient -from modules.app import app, users_with_context -from modules.logger import logWrite -from modules.scheduler import scheduler -from modules.utils import configGet, jsonSave, locale +from classes.pyroclient import PyroClient +from modules.utils import USERS_WITH_CONTEXT -@app.on_message(~filters.scheduled & filters.command(["shutdown"], prefixes=["", "/"])) -async def cmd_kill(app: PosterClient, msg: Message): - if msg.from_user.id in app.admins: - global users_with_context - if len(users_with_context) > 0: - await msg.reply_text( - f"There're {len(users_with_context)} unfinished users' contexts. If you turn off the bot, those will be lost. Please confirm shutdown using a button below.", - reply_markup=InlineKeyboardMarkup( - [ - [ - InlineKeyboardButton( - "Confirm shutdown", callback_data="shutdown" - ) - ] - ] - ), - ) - return - pid = getpid() - logWrite(f"Shutting down bot with pid {pid}") +@Client.on_message( + ~filters.scheduled & filters.command(["shutdown"], prefixes=["", "/"]) +) +async def cmd_kill(app: PyroClient, msg: Message): + if msg.from_user.id not in app.admins: + return + + if len(USERS_WITH_CONTEXT) > 0: await msg.reply_text( - locale("shutdown", "message", locale=msg.from_user.language_code).format( - pid + app._("shutdown_confirm", "message").format(len(USERS_WITH_CONTEXT)), + reply_markup=InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton( + app._( + "shutdown", "button", locale=msg.from_user.language_code + ), + callback_data="shutdown", + ) + ] + ] ), ) - scheduler.shutdown() - makedirs(configGet("cache", "locations"), exist_ok=True) - jsonSave( - {"timestamp": time()}, - path.join(configGet("cache", "locations"), "shutdown_time"), - ) - exit() + return + + makedirs(app.config["locations"]["cache"], exist_ok=True) + await json_write( + {"timestamp": time()}, + Path(f"{app.config['locations']['cache']}/shutdown_time"), + ) + + exit() diff --git a/plugins/commands/mode_submit.py b/plugins/commands/mode_submit.py index 016801d..f759b33 100644 --- a/plugins/commands/mode_submit.py +++ b/plugins/commands/mode_submit.py @@ -1,23 +1,24 @@ from pyrogram import filters +from pyrogram.client import Client from pyrogram.types import Message -from modules.app import app -from modules.utils import locale +from classes.pyroclient import PyroClient from classes.user import PosterUser -from classes.poster_client import PosterClient -@app.on_message(~filters.scheduled & filters.command(["start"], prefixes="/")) -async def cmd_start(app: PosterClient, msg: Message): - if PosterUser(msg.from_user.id).is_blocked() is False: - await msg.reply_text( - locale("start", "message", locale=msg.from_user.language_code) - ) +@Client.on_message(~filters.scheduled & filters.command(["start"], prefixes="/")) +async def cmd_start(app: PyroClient, msg: Message): + if PosterUser(msg.from_user.id).is_blocked(): + return + + await msg.reply_text(app._("start", "message", locale=msg.from_user.language_code)) -@app.on_message(~filters.scheduled & filters.command(["rules", "help"], prefixes="/")) -async def cmd_rules(app: PosterClient, msg: Message): - if PosterUser(msg.from_user.id).is_blocked() is False: - await msg.reply_text( - locale("rules", "message", locale=msg.from_user.language_code) - ) +@Client.on_message( + ~filters.scheduled & filters.command(["rules", "help"], prefixes="/") +) +async def cmd_rules(app: PyroClient, msg: Message): + if PosterUser(msg.from_user.id).is_blocked(): + return + + await msg.reply_text(app._("rules", "message", locale=msg.from_user.language_code)) diff --git a/plugins/commands/photos.py b/plugins/commands/photos.py index e56a798..9a17fad 100644 --- a/plugins/commands/photos.py +++ b/plugins/commands/photos.py @@ -1,214 +1,303 @@ import asyncio +import logging from glob import iglob +from io import BytesIO from os import getcwd, makedirs, path, remove +from pathlib import Path from shutil import disk_usage, rmtree from traceback import format_exc from uuid import uuid4 from zipfile import ZipFile from convopyro import listen_message +from photosapi_client.errors import UnexpectedStatus from pyrogram import filters +from pyrogram.client import Client from pyrogram.types import Message +from ujson import loads -from classes.poster_client import PosterClient -from modules.api_client import remove_pic, upload_pic -from modules.app import app, users_with_context -from modules.logger import logWrite -from modules.utils import configGet, extract_and_save, locale +from classes.pyroclient import PyroClient +from modules.api_client import ( + BodyPhotoUpload, + File, + client, + photo_delete, + photo_upload, +) +from modules.utils import USERS_WITH_CONTEXT, extract_and_save + +logger = logging.getLogger(__name__) -@app.on_message(~filters.scheduled & filters.command(["import"], prefixes=["", "/"])) -async def cmd_import(app: PosterClient, msg: Message): - if msg.from_user.id in app.admins: - global users_with_context - if msg.from_user.id not in users_with_context: - users_with_context.append(msg.from_user.id) - else: - return +@Client.on_message(~filters.scheduled & filters.command(["import"], prefixes=["", "/"])) +async def cmd_import(app: PyroClient, msg: Message): + if msg.from_user.id not in app.admins: + return + + global USERS_WITH_CONTEXT + + if msg.from_user.id not in USERS_WITH_CONTEXT: + USERS_WITH_CONTEXT.append(msg.from_user.id) + else: + return + + await msg.reply_text( + app._("import_request", "message", locale=msg.from_user.language_code) + ) + + answer = await listen_message(app, msg.chat.id, timeout=600) + + USERS_WITH_CONTEXT.remove(msg.from_user.id) + + if answer is None: await msg.reply_text( - locale("import_request", "message", locale=msg.from_user.language_code) - ) - answer = await listen_message(app, msg.chat.id, timeout=600) - users_with_context.remove(msg.from_user.id) - if answer is None: - await msg.reply_text( - locale("import_ignored", "message", locale=msg.from_user.language_code), - quote=True, - ) - return - if answer.text == "/cancel": - await answer.reply_text( - locale("import_abort", "message", locale=msg.from_user.language_code) - ) - return - if answer.document is None: - await answer.reply_text( - locale( - "import_invalid_media", - "message", - locale=msg.from_user.language_code, - ), - quote=True, - ) - return - if answer.document.mime_type != "application/zip": - await answer.reply_text( - locale( - "import_invalid_mime", "message", locale=msg.from_user.language_code - ), - quote=True, - ) - return - if disk_usage(getcwd())[2] < (answer.document.file_size) * 3: - await msg.reply_text( - locale( - "import_too_big", "message", locale=msg.from_user.language_code - ).format( - answer.document.file_size // (2**30), - disk_usage(getcwd())[2] // (2**30), - ) - ) - return - tmp_dir = str(uuid4()) - logWrite( - f"Importing '{answer.document.file_name}' file {answer.document.file_size} bytes big (TMP ID {tmp_dir})" - ) - makedirs(path.join(configGet("tmp", "locations"), tmp_dir), exist_ok=True) - tmp_path = path.join(configGet("tmp", "locations"), answer.document.file_id) - downloading = await answer.reply_text( - locale("import_downloading", "message", locale=msg.from_user.language_code), - quote=True, - ) - await app.download_media(answer, file_name=tmp_path) - await downloading.edit( - locale("import_unpacking", "message", locale=msg.from_user.language_code) - ) - try: - with ZipFile(tmp_path, "r") as handle: - tasks = [ - extract_and_save( - handle, name, path.join(configGet("tmp", "locations"), tmp_dir) - ) - for name in handle.namelist() - ] - _ = await asyncio.gather(*tasks) - except Exception as exp: - logWrite( - f"Could not import '{answer.document.file_name}' due to {exp}: {format_exc}" - ) - await answer.reply_text( - locale( - "import_unpack_error", "message", locale=msg.from_user.language_code - ).format(exp, format_exc()) - ) - return - logWrite(f"Downloaded '{answer.document.file_name}' - awaiting upload") - await downloading.edit( - locale("import_uploading", "message", locale=msg.from_user.language_code) - ) - remove(tmp_path) - - for filename in iglob( - path.join(configGet("tmp", "locations"), tmp_dir) + "**/**", recursive=True - ): - if not path.isfile(filename): - continue - # upload filename - uploaded = await upload_pic(filename) - if uploaded[0] is False: - logWrite( - f"Could not upload '{filename}' from '{path.join(configGet('tmp', 'locations'), tmp_dir)}'. Duplicates: {str(uploaded[1])}", - debug=True, - ) - if len(uploaded[1]) > 0: - await msg.reply_text( - locale( - "import_upload_error_duplicate", - "message", - locale=msg.from_user.language_code, - ).format(path.basename(filename)), - disable_notification=True, - ) - else: - await msg.reply_text( - locale( - "import_upload_error_other", - "message", - locale=msg.from_user.language_code, - ).format(path.basename(filename)), - disable_notification=True, - ) - else: - logWrite( - f"Uploaded '{filename}' from '{path.join(configGet('tmp', 'locations'), tmp_dir)}' and got ID {uploaded[2]}", - debug=True, - ) - - await downloading.delete() - logWrite( - f"Removing '{path.join(configGet('tmp', 'locations'), tmp_dir)}' after uploading", - debug=True, - ) - rmtree(path.join(configGet("tmp", "locations"), tmp_dir), ignore_errors=True) - await answer.reply_text( - locale("import_finished", "message", locale=msg.from_user.language_code), + app._("import_ignored", "message", locale=msg.from_user.language_code), quote=True, ) return - -@app.on_message(~filters.scheduled & filters.command(["export"], prefixes=["", "/"])) -async def cmd_export(app: PosterClient, msg: Message): - if msg.from_user.id in app.admins: - pass - - -@app.on_message(~filters.scheduled & filters.command(["remove"], prefixes=["", "/"])) -async def cmd_remove(app: PosterClient, msg: Message): - if msg.from_user.id in app.admins: - global users_with_context - if msg.from_user.id not in users_with_context: - users_with_context.append(msg.from_user.id) - else: - return - await msg.reply_text( - locale("remove_request", "message", locale=msg.from_user.language_code) + if answer.text == "/cancel": + await answer.reply_text( + app._("import_abort", "message", locale=msg.from_user.language_code) ) - answer = await listen_message(app, msg.chat.id, timeout=600) - users_with_context.remove(msg.from_user.id) - if answer is None: + return + + if answer.document is None: + await answer.reply_text( + app._( + "import_invalid_media", + "message", + locale=msg.from_user.language_code, + ), + quote=True, + ) + return + + if answer.document.mime_type != "application/zip": + await answer.reply_text( + app._("import_invalid_mime", "message", locale=msg.from_user.language_code), + quote=True, + ) + return + + if disk_usage(getcwd())[2] < (answer.document.file_size) * 3: + await msg.reply_text( + app._( + "import_too_big", "message", locale=msg.from_user.language_code + ).format( + answer.document.file_size // (2**30), + disk_usage(getcwd())[2] // (2**30), + ) + ) + return + + tmp_dir = str(uuid4()) + + logging.info( + "Importing '%s' file %s bytes big (TMP ID %s)", + answer.document.file_name, + answer.document.file_size, + tmp_dir, + ) + + makedirs(Path(f"{app.config['locations']['tmp']}/{tmp_dir}"), exist_ok=True) + tmp_path = Path(f"{app.config['locations']['tmp']}/{answer.document.file_id}") + + downloading = await answer.reply_text( + app._("import_downloading", "message", locale=msg.from_user.language_code), + quote=True, + ) + + await app.download_media(answer, file_name=str(tmp_path)) + await downloading.edit( + app._("import_unpacking", "message", locale=msg.from_user.language_code) + ) + + try: + with ZipFile(tmp_path, "r") as handle: + tasks = [ + extract_and_save( + handle, name, Path(f"{app.config['locations']['tmp']}/{tmp_dir}") + ) + for name in handle.namelist() + ] + _ = await asyncio.gather(*tasks) + except Exception as exp: + logger.error( + "Could not import '%s' due to %s: %s", + answer.document.file_name, + exp, + format_exc(), + ) + await answer.reply_text( + app._( + "import_unpack_error", "message", locale=msg.from_user.language_code + ).format(exp, format_exc()) + ) + return + + logger.info("Downloaded '%s' - awaiting upload", answer.document.file_name) + + await downloading.edit( + app._("import_uploading", "message", locale=msg.from_user.language_code) + ) + + remove(tmp_path) + + for filename in iglob( + str(Path(f"{app.config['locations']['tmp']}/{tmp_dir}")) + "**/**", + recursive=True, + ): + if not path.isfile(filename): + continue + + with open(str(filename), "rb") as fh: + photo_bytes = BytesIO(fh.read()) + + try: + uploaded = await photo_upload( + app.config["posting"]["api"]["album"], + client=client, + multipart_data=BodyPhotoUpload( + File(photo_bytes, Path(filename).name, "image/jpeg") + ), + ignore_duplicates=app.config["submission"]["allow_duplicates"], + compress=False, + caption="queue", + ) + except UnexpectedStatus as exp: + logger.error( + "Could not upload '%s' from '%s': %s", + filename, + Path(f"{app.config['locations']['tmp']}/{tmp_dir}"), + exp, + ) await msg.reply_text( - locale("remove_ignored", "message", locale=msg.from_user.language_code), - quote=True, + app._( + "import_upload_error_other", + "message", + locale=msg.from_user.language_code, + ).format(path.basename(filename)), + disable_notification=True, ) - return - if answer.text == "/cancel": - await answer.reply_text( - locale("remove_abort", "message", locale=msg.from_user.language_code) - ) - return - response = await remove_pic(answer.text) - if response: - logWrite( - f"Removed '{answer.text}' by request of user {answer.from_user.id}" - ) - await answer.reply_text( - locale( - "remove_success", "message", locale=msg.from_user.language_code - ).format(answer.text) + continue + + uploaded_dict = loads(uploaded.content.decode("utf-8")) + + if "duplicates" in uploaded_dict: + logger.warning( + "Could not upload '%s' from '%s'. Duplicates: %s", + filename, + Path(f"{app.config['locations']['tmp']}/{tmp_dir}"), + str(uploaded_dict["duplicates"]), ) + + if len(uploaded_dict["duplicates"]) > 0: + await msg.reply_text( + app._( + "import_upload_error_duplicate", + "message", + locale=msg.from_user.language_code, + ).format(path.basename(filename)), + disable_notification=True, + ) + else: + await msg.reply_text( + app._( + "import_upload_error_other", + "message", + locale=msg.from_user.language_code, + ).format(path.basename(filename)), + disable_notification=True, + ) else: - logWrite( - f"Could not remove '{answer.text}' by request of user {answer.from_user.id}" - ) - await answer.reply_text( - locale( - "remove_failure", "message", locale=msg.from_user.language_code - ).format(answer.text) + logger.info( + "Uploaded '%s' from '%s' and got ID %s", + filename, + Path(f"{app.config['locations']['tmp']}/{tmp_dir}"), + uploaded.parsed.id, ) + await downloading.delete() -@app.on_message(~filters.scheduled & filters.command(["purge"], prefixes=["", "/"])) -async def cmd_purge(app: PosterClient, msg: Message): - if msg.from_user.id in app.admins: - pass + logger.info( + "Removing '%s' after uploading", + Path(f"{app.config['locations']['tmp']}/{tmp_dir}"), + ) + rmtree(Path(f"{app.config['locations']['tmp']}/{tmp_dir}"), ignore_errors=True) + + await answer.reply_text( + app._("import_finished", "message", locale=msg.from_user.language_code), + quote=True, + ) + + return + + +@Client.on_message(~filters.scheduled & filters.command(["export"], prefixes=["", "/"])) +async def cmd_export(app: PyroClient, msg: Message): + if msg.from_user.id not in app.admins: + return + + +@Client.on_message(~filters.scheduled & filters.command(["remove"], prefixes=["", "/"])) +async def cmd_remove(app: PyroClient, msg: Message): + if msg.from_user.id not in app.admins: + return + + global USERS_WITH_CONTEXT + + if msg.from_user.id not in USERS_WITH_CONTEXT: + USERS_WITH_CONTEXT.append(msg.from_user.id) + else: + return + + await msg.reply_text( + app._("remove_request", "message", locale=msg.from_user.language_code) + ) + + answer = await listen_message(app, msg.chat.id, timeout=600) + + USERS_WITH_CONTEXT.remove(msg.from_user.id) + + if answer is None: + await msg.reply_text( + app._("remove_ignored", "message", locale=msg.from_user.language_code), + quote=True, + ) + return + + if answer.text == "/cancel": + await answer.reply_text( + app._("remove_abort", "message", locale=msg.from_user.language_code) + ) + return + + response = await photo_delete(id=answer.text, client=client) + + if response: + logger.info( + "Removed '%s' by request of user %s", answer.text, answer.from_user.id + ) + await answer.reply_text( + app._( + "remove_success", "message", locale=msg.from_user.language_code + ).format(answer.text) + ) + else: + logger.warning( + "Could not remove '%s' by request of user %s", + answer.text, + answer.from_user.id, + ) + await answer.reply_text( + app._( + "remove_failure", "message", locale=msg.from_user.language_code + ).format(answer.text) + ) + + +@Client.on_message(~filters.scheduled & filters.command(["purge"], prefixes=["", "/"])) +async def cmd_purge(app: PyroClient, msg: Message): + if msg.from_user.id not in app.admins: + return diff --git a/plugins/commands/report.py b/plugins/commands/report.py new file mode 100644 index 0000000..063fe4b --- /dev/null +++ b/plugins/commands/report.py @@ -0,0 +1,42 @@ +from pyrogram.client import Client +from pyrogram import filters +from pyrogram.types import Message, User +from libbot import sync +from classes.pyroclient import PyroClient + + +@Client.on_message( + ~filters.scheduled + & filters.chat(sync.config_get("comments", "posting")) + & filters.reply + & filters.command(["report"], prefixes=["", "/"]) +) +async def command_report(app: PyroClient, msg: Message): + if msg.reply_to_message.forward_from_chat.id == app.config["posting"]["channel"]: + await msg.reply_text( + app._( + "report_sent", + "message", + locale=msg.from_user.language_code + if msg.from_user is not None + else None, + ) + ) + + print(msg) + + report_sent = await msg.reply_to_message.forward(app.owner) + sender = msg.from_user if msg.from_user is not None else msg.sender_chat + + sender_name = ( + sender.first_name if isinstance(sender, User) else sender.title + ) + + # ACTION NEEDED + # Name and username are somehow None + await report_sent.reply_text( + app._("report_received", "message").format( + sender_name, sender.username, sender.id + ), + quote=True, + ) diff --git a/plugins/handlers/submission.py b/plugins/handlers/submission.py index c28c67d..43e8113 100644 --- a/plugins/handlers/submission.py +++ b/plugins/handlers/submission.py @@ -1,32 +1,40 @@ +import logging from datetime import datetime from os import makedirs, path, sep +from pathlib import Path from traceback import format_exc from uuid import uuid4 from pyrogram import filters +from pyrogram.client import Client from pyrogram.enums.chat_action import ChatAction from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message from classes.enums.submission_types import SubmissionType -from classes.exceptions import SubmissionDuplicatesError -from classes.poster_client import PosterClient +from classes.exceptions import SubmissionDuplicatesError, SubmissionUnsupportedError +from classes.pyroclient import PyroClient from classes.user import PosterUser -from modules.app import app, users_with_context from modules.database import col_banned, col_submitted -from modules.logger import logWrite -from modules.utils import configGet, locale +from modules.utils import USERS_WITH_CONTEXT + +logger = logging.getLogger(__name__) -@app.on_message( +@Client.on_message( ~filters.scheduled & filters.private & filters.photo | filters.video - | filters.animation + # | filters.animation | filters.document ) -async def get_submission(app: PosterClient, msg: Message): - global users_with_context - if msg.from_user.id in users_with_context: +async def get_submission(app: PyroClient, msg: Message): + global USERS_WITH_CONTEXT + + if not hasattr(msg.from_user, "id"): return + + if msg.from_user.id in USERS_WITH_CONTEXT: + return + try: if col_banned.find_one({"user": msg.from_user.id}) is not None: return @@ -39,34 +47,37 @@ async def get_submission(app: PosterClient, msg: Message): if PosterUser(msg.from_user.id).is_limited(): await msg.reply_text( - locale("sub_cooldown", "message", locale=user_locale).format( - str(configGet("timeout", "submission")) + app._("sub_cooldown", "message", locale=user_locale).format( + str(app.config["submission"]["timeout"]) ) ) return if msg.document is not None: - logWrite( - f"User {msg.from_user.id} is trying to submit a file of type '{msg.document.mime_type}' with name '{msg.document.file_name}' and size of {msg.document.file_size / 1024 / 1024} MB", - debug=True, + logger.info( + "User %s is trying to submit a file of type '%s' with name '%s' and size of %s MB", + msg.from_user.id, + msg.document.mime_type, + msg.document.file_name, + msg.document.file_size / 1024 / 1024, ) - if msg.document.mime_type not in configGet("mime_types", "submission"): + if msg.document.mime_type not in app.config["submission"]["mime_types"]: await msg.reply_text( - locale("mime_not_allowed", "message", locale=user_locale).format( - ", ".join(configGet("mime_types", "submission")) + app._("mime_not_allowed", "message", locale=user_locale).format( + ", ".join(app.config["submission"]["mime_types"]) ), quote=True, ) return - if msg.document.file_size > configGet("file_size", "submission"): + if msg.document.file_size > app.config["submission"]["file_size"]: await msg.reply_text( - locale("document_too_large", "message", locale=user_locale).format( - str(configGet("file_size", "submission") / 1024 / 1024) + app._("document_too_large", "message", locale=user_locale).format( + str(app.config["submission"]["file_size"] / 1024 / 1024) ), quote=True, ) return - if msg.document.file_size > configGet("tmp_size", "submission"): + if msg.document.file_size > app.config["submission"]["tmp_size"]: save_tmp = False contents = ( msg.document.file_id, @@ -74,63 +85,72 @@ async def get_submission(app: PosterClient, msg: Message): ) # , msg.document.file_name if msg.video is not None: - logWrite( - f"User {msg.from_user.id} is trying to submit a video with name '{msg.video.file_name}' and size of {msg.video.file_size / 1024 / 1024} MB", - debug=True, + logger.info( + "User %s is trying to submit a video with name '%s' and size of %s MB", + msg.from_user.id, + msg.video.file_name, + msg.video.file_size / 1024 / 1024, ) - if msg.video.file_size > configGet("file_size", "submission"): + if msg.video.file_size > app.config["submission"]["file_size"]: await msg.reply_text( - locale("document_too_large", "message", locale=user_locale).format( - str(configGet("file_size", "submission") / 1024 / 1024) + app._("document_too_large", "message", locale=user_locale).format( + str(app.config["submission"]["file_size"] / 1024 / 1024) ), quote=True, ) return - if msg.video.file_size > configGet("tmp_size", "submission"): + if msg.video.file_size > app.config["submission"]["tmp_size"]: save_tmp = False contents = msg.video.file_id, SubmissionType.VIDEO # , msg.video.file_name - if msg.animation is not None: - logWrite( - f"User {msg.from_user.id} is trying to submit an animation with name '{msg.animation.file_name}' and size of {msg.animation.file_size / 1024 / 1024} MB", - debug=True, - ) - if msg.animation.file_size > configGet("file_size", "submission"): - await msg.reply_text( - locale("document_too_large", "message", locale=user_locale).format( - str(configGet("file_size", "submission") / 1024 / 1024) - ), - quote=True, - ) - return - if msg.animation.file_size > configGet("tmp_size", "submission"): - save_tmp = False - contents = ( - msg.animation.file_id, - SubmissionType.ANIMATION, - ) # , msg.animation.file_name + # if msg.animation is not None: + # logger.info( + # "User %s is trying to submit an animation with name '%s' and size of %s MB", + # msg.from_user.id, + # msg.animation.file_name, + # msg.animation.file_size / 1024 / 1024, + # ) + # if msg.animation.file_size > app.config["submission"]["file_size"]: + # await msg.reply_text( + # app._("document_too_large", "message", locale=user_locale).format( + # str(app.config["submission"]["file_size"] / 1024 / 1024) + # ), + # quote=True, + # ) + # return + # if msg.animation.file_size > app.config["submission"]["tmp_size"]: + # save_tmp = False + # contents = ( + # msg.animation.file_id, + # SubmissionType.ANIMATION, + # ) # , msg.animation.file_name if msg.photo is not None: - logWrite( - f"User {msg.from_user.id} is trying to submit a photo with ID '{msg.photo.file_id}' and size of {msg.photo.file_size / 1024 / 1024} MB", - debug=True, + logger.info( + "User %s is trying to submit a photo with ID '%s' and size of %s MB", + msg.from_user.id, + msg.photo.file_id, + msg.photo.file_size / 1024 / 1024, ) contents = msg.photo.file_id, SubmissionType.PHOTO # , "please_generate" - if save_tmp is not None: - if contents is None: - return + if contents is None: + return + if save_tmp is not None: tmp_id = str(uuid4()) + # filename = tmp_id if contents[1] == "please_generate" else contents[1] makedirs( - path.join(configGet("data", "locations"), "submissions", tmp_id), + Path(f"{app.config['locations']['data']}/submissions/{tmp_id}"), exist_ok=True, ) downloaded = await app.download_media( msg, - path.join(configGet("data", "locations"), "submissions", tmp_id) + sep, + str(Path(f"{app.config['locations']['data']}/submissions/{tmp_id}")) + + sep, ) + inserted = col_submitted.insert_one( { "user": msg.from_user.id, @@ -144,9 +164,6 @@ async def get_submission(app: PosterClient, msg: Message): ) else: - if contents is None: - return - inserted = col_submitted.insert_one( { "user": msg.from_user.id, @@ -162,7 +179,7 @@ async def get_submission(app: PosterClient, msg: Message): buttons = [ [ InlineKeyboardButton( - text=locale("sub_yes", "button", locale=configGet("locale")), + text=app._("sub_yes", "button"), callback_data=f"sub_yes_{str(inserted.inserted_id)}", ) ] @@ -172,28 +189,20 @@ async def get_submission(app: PosterClient, msg: Message): caption = str(msg.caption) buttons[0].append( InlineKeyboardButton( - text=locale( - "sub_yes_caption", "button", locale=configGet("locale") - ), + text=app._("sub_yes_caption", "button"), callback_data=f"sub_yes_{str(inserted.inserted_id)}_caption", ) ) - buttons[0].append( - InlineKeyboardButton( - text=locale("sub_no", "button", locale=configGet("locale")), - callback_data=f"sub_no_{str(inserted.inserted_id)}", - ) - ) else: caption = "" - buttons[0].append( - InlineKeyboardButton( - text=locale("sub_no", "button", locale=configGet("locale")), - callback_data=f"sub_no_{str(inserted.inserted_id)}", - ) - ) - caption += locale("sub_by", "message", locale=locale(configGet("locale"))) + buttons[0].append( + InlineKeyboardButton( + text=app._("sub_no", "button"), + callback_data=f"sub_no_{str(inserted.inserted_id)}", + ) + ) + caption += app._("sub_by", "message") if msg.from_user.first_name is not None: caption += f" {msg.from_user.first_name}" @@ -206,22 +215,30 @@ async def get_submission(app: PosterClient, msg: Message): if ( msg.from_user.id in app.admins - and configGet("admins", "submission", "require_confirmation") is False + and app.config["submission"]["require_confirmation"]["admins"] is False ): try: - submitted = await app.submit_photo(str(inserted.inserted_id)) + submitted = await app.submit_media(str(inserted.inserted_id)) await msg.reply_text( - locale("sub_yes_auto", "message", locale=user_locale), + app._("sub_yes_auto", "message", locale=user_locale), disable_notification=True, quote=True, ) - if configGet("send_uploaded_id", "submission"): + if app.config["submission"]["send_uploaded_id"]: caption += f"\n\nID: `{submitted[1]}`" await msg.copy(app.owner, caption=caption, disable_notification=True) return + except SubmissionUnsupportedError: + await msg.reply_text( + app._("mime_not_allowed", "message", locale=user_locale).format( + ", ".join(app.config["submission"]["mime_types"]), quote=True + ), + quote=True, + ) + return except SubmissionDuplicatesError as exp: await msg.reply_text( - locale( + app._( "sub_media_duplicates_list", "message", locale=user_locale ).format("\n • ".join(exp.duplicates)), quote=True, @@ -232,28 +249,35 @@ async def get_submission(app: PosterClient, msg: Message): return elif ( msg.from_user.id not in app.admins - and configGet("users", "submission", "require_confirmation") is False + and app.config["submission"]["require_confirmation"]["users"] is False ): try: submitted = await app.submit_photo(str(inserted.inserted_id)) await msg.reply_text( - locale("sub_yes_auto", "message", locale=user_locale), + app._("sub_yes_auto", "message", locale=user_locale), disable_notification=True, quote=True, ) - if configGet("send_uploaded_id", "submission"): + if app.config["submission"]["send_uploaded_id"]: caption += f"\n\nID: `{submitted[1]}`" await msg.copy(app.owner, caption=caption) return + except SubmissionUnsupportedError: + await msg.reply_text( + app._("mime_not_allowed", "message", locale=user_locale).format( + ", ".join(app.config["submission"]["mime_types"]), quote=True + ) + ) + return except SubmissionDuplicatesError as exp: await msg.reply_text( - locale("sub_dup", "message", locale=user_locale), quote=True + app._("sub_dup", "message", locale=user_locale), quote=True ) return except Exception as exp: await app.send_message( app.owner, - locale("sub_error_admin", "message").format( + app._("sub_error_admin", "message").format( msg.from_user.id, format_exc() ), ) @@ -264,7 +288,7 @@ async def get_submission(app: PosterClient, msg: Message): buttons += [ [ InlineKeyboardButton( - text=locale("sub_block", "button", locale=configGet("locale")), + text=app._("sub_block", "button"), callback_data=f"sub_block_{msg.from_user.id}", ) ] @@ -274,7 +298,7 @@ async def get_submission(app: PosterClient, msg: Message): if msg.from_user.id != app.owner: await msg.reply_text( - locale("sub_sent", "message", locale=user_locale), + app._("sub_sent", "message", locale=user_locale), disable_notification=True, quote=True, ) @@ -284,4 +308,4 @@ async def get_submission(app: PosterClient, msg: Message): ) except AttributeError: - logWrite(f"from_user in function get_submission does not seem to contain id") + logger.error("'from_user' does not seem to contain 'id'") diff --git a/plugins/remove_commands.py b/plugins/remove_commands.py new file mode 100644 index 0000000..939dab5 --- /dev/null +++ b/plugins/remove_commands.py @@ -0,0 +1,13 @@ +from pyrogram import filters +from pyrogram.client import Client +from pyrogram.types import Message + +from classes.pyroclient import PyroClient + + +@Client.on_message( + ~filters.scheduled & filters.private & filters.command(["remove_commands"], prefixes=["/"]) # type: ignore +) +async def command_remove_commands(app: PyroClient, msg: Message): + await msg.reply_text("Okay.") + await app.remove_commands(command_sets=await app.collect_commands()) diff --git a/poster.py b/poster.py deleted file mode 100644 index b570fba..0000000 --- a/poster.py +++ /dev/null @@ -1,266 +0,0 @@ -from datetime import datetime -from os import getpid, path -from sys import exit -from time import time -from traceback import format_exc -from modules.api_client import authorize - -from modules.cli import * -from modules.http_client import http_session -from modules.logger import logWrite -from modules.scheduler import scheduler -from modules.utils import configGet, jsonLoad, jsonSave, locale - -# Import =================================================================================================================================== -try: - from dateutil.relativedelta import relativedelta - from pyrogram.errors import bad_request_400 - from pyrogram.sync import idle - - from modules.app import app -except ModuleNotFoundError: - print(locale("deps_missing", "console", locale=configGet("locale")), flush=True) - exit() -# =========================================================================================================================================== - - -pid = getpid() -version = 0.1 - -# Work in progress -# def check_forwards(app): - -# try: - -# index = jsonLoad(configGet("index", "locations")) -# channel = app.get_chat(configGet("channel", "posting")) - -# peer = app.resolve_peer(configGet("channel", "posting")) -# print(peer, flush=True) - -# posts_list = [i for i in range(index["last_id"]-100,index["last_id"])] -# last_posts = app.get_messages(configGet("channel", "posting"), message_ids=posts_list) - -# for post in last_posts: -# post_forwards = GetMessagePublicForwards(channel=peer, msg_id=post.id, offset_peer=peer, offset_rate=0, offset_id=0, limit=100) -# print(post_forwards, flush=True) -# for forward in post_forwards: -# print(forward, flush=True) - -# except Exception as exp: - -# logWrite("Could not get last posts forwards due to {0} with traceback {1}".format(str(exp), traceback.format_exc()), debug=True) - -# if configGet("error", "reports"): -# app.send_message(configGet("admin"), traceback.format_exc()) - -# pass - - -# Work in progress -# @app.on_message(~ filters.scheduled & filters.command(["forwards"], prefixes="/")) -# def cmd_forwards(app, msg): -# check_forwards(app) - - -from plugins.callbacks.shutdown import * - -# Imports ================================================================================================================================== -from plugins.commands.general import * - -if configGet("submit", "mode"): - from plugins.callbacks.nothing import * - from plugins.callbacks.submission import * - from plugins.commands.mode_submit import * - from plugins.handlers.submission import * - -if configGet("post", "mode"): - from plugins.commands.photos import * - -# if configGet("api_based", "mode"): -# from modules.api_client import authorize -# =========================================================================================================================================== - -# Work in progress -# Handle new forwards -# @app.on_raw_update() -# def fwd_got(app, update, users, chats): -# if isinstance(update, UpdateChannelMessageForwards): -# logWrite(f'Forward count increased to {update["forwards"]} on post {update["id"]} in channel {update["channel_id"]}') -# logWrite(str(users), debug=True) -# logWrite(str(chats), debug=True) -# # else: -# # logWrite(f"Got raw update of type {type(update)} with contents {update}", debug=True) - -# async def main(): - -# await app.start() - -# logWrite(locale("startup", "console", locale=configGet("locale")).format(str(pid))) - -# if configGet("startup", "reports"): -# await app.send_message(configGet("admin"), locale("startup", "message", locale=configGet("locale")).format(str(pid))) - -# if configGet("post", "mode"): -# scheduler.start() - -# if configGet("api_based", "mode"): -# token = authorize() -# if len(get(f'{configGet("address", "posting", "api")}/albums?q={configGet("queue", "posting", "api", "albums")}', headers={"Authorization": f"Bearer {token}"}).json()["results"]) == 0: -# post(f'{configGet("address", "posting", "api")}/albums?name={configGet("queue", "posting", "api", "albums")}&title={configGet("queue", "posting", "api", "albums")}', headers={"Authorization": f"Bearer {token}"}) - -# await idle() - -# await app.send_message(configGet("admin"), locale("shutdown", "message", locale=configGet("locale")).format(str(pid))) -# logWrite(locale("shutdown", "console", locale=configGet("locale")).format(str(pid))) - -# killProc(pid) - -# if __name__ == "__main__": -# if find_spec("uvloop") is not None: -# uvloop.install() -# asyncio.run(main()) - - -async def main(): - logWrite(locale("startup", "console").format(str(pid))) - - await app.start() - - if configGet("startup", "reports"): - try: - if path.exists(path.join(configGet("cache", "locations"), "shutdown_time")): - downtime = relativedelta( - datetime.now(), - datetime.fromtimestamp( - jsonLoad( - path.join(configGet("cache", "locations"), "shutdown_time") - )["timestamp"] - ), - ) - if downtime.days >= 1: - await app.send_message( - configGet("owner"), - locale( - "startup_downtime_days", - "message", - ).format(pid, downtime.days), - ) - elif downtime.hours >= 1: - await app.send_message( - configGet("owner"), - locale( - "startup_downtime_hours", - "message", - ).format(pid, downtime.hours), - ) - else: - await app.send_message( - configGet("owner"), - locale( - "startup_downtime_minutes", - "message", - locale=configGet("locale"), - ).format(pid, downtime.minutes), - ) - else: - await app.send_message( - configGet("owner"), - locale("startup", "message").format(pid), - ) - except bad_request_400.PeerIdInvalid: - logWrite( - f"Could not send startup message to bot owner. Perhaps user has not started the bot yet." - ) - - if configGet("update", "reports"): - try: - 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", "")) > version: - logWrite( - f'New version {response[0]["tag_name"].replace("v", "")} found (current {version})' - ) - await app.send_message( - configGet("owner"), - locale( - "update_available", - "message", - ).format( - response[0]["tag_name"], - response[0]["html_url"], - response[0]["body"], - ), - ) - else: - logWrite(f"No updates found, bot is up to date.") - except bad_request_400.PeerIdInvalid: - logWrite( - f"Could not send startup message to bot owner. Perhaps user has not started the bot yet." - ) - except Exception as exp: - logWrite(f"Update check failed due to {exp}: {format_exc()}") - - if configGet("post", "mode"): - scheduler.start() - - try: - token = await authorize() - - if ( - len( - ( - await ( - await http_session.get( - f'{configGet("address", "posting", "api")}/albums?q={configGet("album", "posting", "api")}', - headers={"Authorization": f"Bearer {token}"}, - ) - ).json() - )["results"] - ) - == 0 - ): - logWrite("Media album does not exist on API server. Trying to create it...") - try: - await http_session.post( - f'{configGet("address", "posting", "api")}/albums?name={configGet("album", "posting", "api")}&title={configGet("album", "posting", "api")}', - headers={"Authorization": f"Bearer {token}"}, - ) - logWrite("Created media album on API server.") - except Exception as exp: - logWrite( - f"Could not create media album on API server due to {exp}: {format_exc()}" - ) - except Exception as exp: - logWrite(f"Could not check if API album exists due to {exp}: {format_exc()}") - - await idle() - - try: - await app.send_message( - configGet("owner"), - locale("shutdown", "message").format(pid), - ) - except bad_request_400.PeerIdInvalid: - logWrite( - f"Could not send shutdown message to bot owner. Perhaps user has not started the bot yet." - ) - - makedirs(configGet("cache", "locations"), exist_ok=True) - jsonSave( - {"timestamp": time()}, - path.join(configGet("cache", "locations"), "shutdown_time"), - ) - - logWrite(locale("shutdown", "console").format(str(pid))) - - scheduler.shutdown() - - -if __name__ == "__main__": - loop = asyncio.get_event_loop() - loop.run_until_complete(main()) diff --git a/requirements-optional.txt b/requirements-optional.txt deleted file mode 100644 index e69de29..0000000 From 97b3aa1505e4471f9dd2d686e68785d9a0c8f840 Mon Sep 17 00:00:00 2001 From: profitroll Date: Wed, 28 Jun 2023 08:22:18 +0200 Subject: [PATCH 08/28] Starting scripts and README were updated --- README.md | 14 +++++++------- loop.bat | 2 +- loop.sh | 2 +- start.bat | 2 +- start.sh | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 2a4b80d..4a0058f 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ This bot is used for one and only task - post pictures from my personal archive. ## Dependencies -* [Python 3.7+](https://www.python.org) (3.9+ recommended) +* [Python 3.8+](https://www.python.org) (3.9+ recommended) * [MongoDB](https://www.mongodb.com) * [PhotosAPI](https://git.end-play.xyz/profitroll/PhotosAPI) @@ -54,7 +54,7 @@ To make this bot run at first you need to have a Python interpreter, Photos API, 1. Copy file `config_example.json` to `config.json` 2. Open `config.json` using your favorite text editor. For example `nano config.json`, but you can edit with vim, nano, on Windows it's Notepad or Notepad++. Whatever - 3. Change `"owner"`, `"bot.api_id"`, `"bot.api_hash"` and `"bot.bot_token"` keys' values. + 3. Change `"bot.owner"`, `"reports.chat_id"`, `"bot.api_id"`, `"bot.api_hash"` and `"bot.bot_token"` keys' values. If you don't know where to find bot_token and your id - here you can find some hints: [get bot token](https://www.siteguarding.com/en/how-to-get-telegram-bot-api-token), [get your id](https://www.alphr.com/telegram-find-user-id), [get api_hash and api_id](https://core.telegram.org/api/obtaining_api_id). @@ -67,11 +67,11 @@ To make this bot run at first you need to have a Python interpreter, Photos API, 2. Configure Photos API: 1. Change `"posting.api.address"` to the one your API servers uses - 2. Run your bot using `python poster.py --create-user --create-album` to configure its new user and album. You can also use manual user and album creation described [in the wiki](https://git.end-play.xyz/profitroll/TelegramPoster/wiki/Configuring-API). You can also change username, password and album in`"posting.api"` to the user and album you have if you already have Photos API album and user set up. In that case you don't need to create a new one. + 2. Run your bot using `python main.py --create-user --create-album` to configure its new user and album. You can also use manual user and album creation described [in the wiki](https://git.end-play.xyz/profitroll/TelegramPoster/wiki/Configuring-API). You can also change username, password and album in`"posting.api"` to the user and album you have if you already have Photos API album and user set up. In that case you don't need to create a new one. 7. Add bot to the channel: - To use your bot of course you need to have a channel or group otherwise makes no sense to have such a bot. [Here](https://stackoverflow.com/a/33497769) you can find a quick guide how to add your bot to a channel. After that simply set `"posting.channel"` to your channel's ID. + To use your bot of course you need to have a channel or group otherwise makes no sense to have such a bot. [Here](https://stackoverflow.com/a/33497769) you can find a quick guide how to add your bot to a channel. After that simply set `"posting.channel"` to your channel's ID and `"posting.comments"` to comments group's ID. 8. Configure posting time: @@ -79,7 +79,7 @@ To make this bot run at first you need to have a Python interpreter, Photos API, 9. Good to go, run it! - Make sure MongoDB and Photos API are running and use `python poster.py` to start it. + Make sure MongoDB and Photos API are running and use `python main.py` to start it. Or you can also use `.\start.bat` on Windows and `bash ./start.sh` on Linux. Additionally there are `loop.sh` and `loop.bat` available if you want your bot to start again after being stopped or after using `/shutdown` command. @@ -94,8 +94,8 @@ Of course bot also has them. You can perform some actions with them. Examples: -* `python poster.py --create-user` -* `python poster.py --create-user --create-album` +* `python main.py --create-user` +* `python main.py --create-user --create-album` ## Tips and improvements diff --git a/loop.bat b/loop.bat index 20b4b75..260cdf5 100644 --- a/loop.bat +++ b/loop.bat @@ -4,7 +4,7 @@ REM You can cd to your directory here, if you want REM cd C:\Users\user\TelegramPoster :start -python poster.py +python main.py echo To completely stop TelegramPoster now, please press Ctrl+C during the countdown! echo Restarting in 5 seconds... Timeout /t 5 diff --git a/loop.sh b/loop.sh index 8ec2365..44a2f41 100644 --- a/loop.sh +++ b/loop.sh @@ -5,7 +5,7 @@ while true do - python poster.py + python main.py echo "To completely stop TelegramPoster now, please press Ctrl+C during the countdown!" echo "Restarting in:" for i in 5 4 3 2 1 diff --git a/start.bat b/start.bat index 00f036e..e9a42b4 100644 --- a/start.bat +++ b/start.bat @@ -3,4 +3,4 @@ REM You can cd to your directory here, if you want REM cd C:\Users\user\TelegramPoster -python poster.py \ No newline at end of file +python main.py \ No newline at end of file diff --git a/start.sh b/start.sh index b5f9ff9..a3494da 100644 --- a/start.sh +++ b/start.sh @@ -3,4 +3,4 @@ # You can cd to your directory here, if you want # cd /home/user/TelegramPoster -python poster.py \ No newline at end of file +python main.py \ No newline at end of file From e06cb4b37776eddd553bacb82fcd36844c76a46e Mon Sep 17 00:00:00 2001 From: profitroll Date: Wed, 28 Jun 2023 08:53:15 +0200 Subject: [PATCH 09/28] /remove command fixed --- locale/en.json | 6 +++- locale/uk.json | 6 +++- modules/api_client.py | 3 ++ plugins/commands/photos.py | 67 ++++++++++++++++++++++++++++++++++++-- 4 files changed, 78 insertions(+), 4 deletions(-) diff --git a/locale/en.json b/locale/en.json index f07791b..49b9319 100644 --- a/locale/en.json +++ b/locale/en.json @@ -59,6 +59,8 @@ "remove_abort": "Removal aborted.", "remove_success": "Removed media with ID `{0}`.", "remove_failure": "Could not remove media with ID `{0}`. Check if provided ID is correct and if it is - you can also check bot's log for details.", + "remove_kind": "Please choose the type of media to delete. Use /cancel if you want to abort this operation.", + "remove_unknown": "Unknown media type. It can only be \"{0}\" or \"{1}\".", "update_available": "**New version found**\nThere's a newer version of a bot found. You can update your bot to [{0}]({1}) using command line of your host.\n\n**Release notes**\n{2}\n\nRead more about updating you bot on the [wiki page](https://git.end-play.xyz/profitroll/TelegramPoster/wiki/Updating-Instance).\n\nPlease not that you can also disable this notification by editing `reports.update` key of the config.", "shutdown_confirm": "There are {0} unfinished users' contexts. If you turn off the bot, those will be lost. Please confirm shutdown using a button below.", "report_sent": "We've notified admins about presumable violation. Thank you for cooperation.", @@ -73,7 +75,9 @@ "post_view": "View in channel", "accepted": "✅ Accepted", "declined": "❌ Declined", - "shutdown": "Confirm shutdown" + "shutdown": "Confirm shutdown", + "photo": "Photo", + "video": "Video" }, "callback": { "sub_yes": "✅ Submission approved", diff --git a/locale/uk.json b/locale/uk.json index 128e39a..fc03e39 100644 --- a/locale/uk.json +++ b/locale/uk.json @@ -59,6 +59,8 @@ "remove_abort": "Видалення перервано.", "remove_success": "Видалено медіа з ID `{0}`.", "remove_failure": "Не вдалося видалити медіа з ID `{0}`. Перевірте, чи вказано правильний ID, і якщо він правильний, ви також можете переглянути логи бота для отримання більш детальної інформації.", + "remove_kind": "Будь ласка, оберіть тип контенту для видалення. Використовуйте /cancel, якщо ви хочете перервати цю операцію.", + "remove_unknown": "Невідомий тип контенту. Може бути тільки \"{0}\" або \"{1}\".", "update_available": "**Знайдено нову версію**\nЗнайдено нову версію бота. Ви можете оновити бота до [{0}]({1}) за допомогою командного рядка вашого хосту.\n\n**Примітки до релізу**\n{2}\n\nДетальніше про оновлення бота можна знайти на [вікі-сторінці](https://git.end-play.xyz/profitroll/TelegramPoster/wiki/Updating-Instance).\n\nЗверніть увагу, що ви також можете вимкнути це сповіщення, відредагувавши ключ `reports.update` у конфігурації.", "shutdown_confirm": "Існує {0} незавершених контекстів користувачів. Якщо ви вимкнете бота, вони будуть втрачені. Будь ласка, підтвердіть вимкнення за допомогою кнопки нижче.", "report_sent": "Ми повідомили адміністрацію про потенційне порушення. Дякую за співпрацю.", @@ -73,7 +75,9 @@ "post_view": "Переглянути на каналі", "accepted": "✅ Прийнято", "declined": "❌ Відхилено", - "shutdown": "Підтвердити вимкнення" + "shutdown": "Підтвердити вимкнення", + "photo": "Фото", + "video": "Відео" }, "callback": { "sub_yes": "✅ Подання схвалено", diff --git a/modules/api_client.py b/modules/api_client.py index 7404320..211e290 100644 --- a/modules/api_client.py +++ b/modules/api_client.py @@ -35,6 +35,9 @@ from photosapi_client.api.default.photo_upload_albums_album_photos_post import ( ) from photosapi_client.api.default.user_create_users_post import asyncio as user_create from photosapi_client.api.default.user_me_users_me_get import sync as user_me +from photosapi_client.api.default.video_delete_videos_id_delete import ( + asyncio as video_delete, +) from photosapi_client.api.default.video_find_albums_album_videos_get import ( asyncio as video_find, ) diff --git a/plugins/commands/photos.py b/plugins/commands/photos.py index 9a17fad..a678795 100644 --- a/plugins/commands/photos.py +++ b/plugins/commands/photos.py @@ -13,7 +13,12 @@ from convopyro import listen_message from photosapi_client.errors import UnexpectedStatus from pyrogram import filters from pyrogram.client import Client -from pyrogram.types import Message +from pyrogram.types import ( + KeyboardButton, + Message, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, +) from ujson import loads from classes.pyroclient import PyroClient @@ -23,6 +28,8 @@ from modules.api_client import ( client, photo_delete, photo_upload, + video_delete, + video_upload, ) from modules.utils import USERS_WITH_CONTEXT, extract_and_save @@ -155,6 +162,7 @@ async def cmd_import(app: PyroClient, msg: Message): photo_bytes = BytesIO(fh.read()) try: + # VIDEO SUPPORT IS PLANNED HERE TOO uploaded = await photo_upload( app.config["posting"]["api"]["album"], client=client, @@ -273,7 +281,62 @@ async def cmd_remove(app: PyroClient, msg: Message): ) return - response = await photo_delete(id=answer.text, client=client) + await msg.reply_text( + app._("remove_kind", "message", locale=msg.from_user.language_code), + reply_markup=ReplyKeyboardMarkup( + [ + [ + KeyboardButton( + app._("photo", "button", locale=msg.from_user.language_code) + ), + KeyboardButton( + app._("video", "button", locale=msg.from_user.language_code) + ), + ] + ], + resize_keyboard=True, + one_time_keyboard=True, + ), + ) + + USERS_WITH_CONTEXT.append(msg.from_user.id) + + answer = await listen_message(app, msg.chat.id, timeout=600) + + USERS_WITH_CONTEXT.remove(msg.from_user.id) + + if answer is None: + await msg.reply_text( + app._("remove_ignored", "message", locale=msg.from_user.language_code), + quote=True, + reply_markup=ReplyKeyboardRemove(), + ) + return + + if answer.text == "/cancel": + await answer.reply_text( + app._("remove_abort", "message", locale=msg.from_user.language_code), + reply_markup=ReplyKeyboardRemove(), + ) + return + + if answer.text in app.in_all_locales("photo", "button"): + func = photo_delete + elif answer.text in app.in_all_locales("video", "button"): + func = video_delete + else: + await answer.reply_text( + app._( + "remove_unknown", "message", locale=msg.from_user.language_code + ).format( + app._("photo", "button", locale=msg.from_user.language_code), + app._("video", "button", locale=msg.from_user.language_code), + ), + reply_markup=ReplyKeyboardRemove(), + ) + return + + response = await func(id=answer.text, client=client) if response: logger.info( From 51da210817e3679692b84cbbda5586ea8f2c088e Mon Sep 17 00:00:00 2001 From: profitroll Date: Wed, 28 Jun 2023 08:56:21 +0200 Subject: [PATCH 10/28] Small fix --- plugins/commands/photos.py | 44 +++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/plugins/commands/photos.py b/plugins/commands/photos.py index a678795..82e0959 100644 --- a/plugins/commands/photos.py +++ b/plugins/commands/photos.py @@ -264,19 +264,19 @@ async def cmd_remove(app: PyroClient, msg: Message): app._("remove_request", "message", locale=msg.from_user.language_code) ) - answer = await listen_message(app, msg.chat.id, timeout=600) + answer_id = await listen_message(app, msg.chat.id, timeout=600) USERS_WITH_CONTEXT.remove(msg.from_user.id) - if answer is None: + if answer_id is None: await msg.reply_text( app._("remove_ignored", "message", locale=msg.from_user.language_code), quote=True, ) return - if answer.text == "/cancel": - await answer.reply_text( + if answer_id.text == "/cancel": + await answer_id.reply_text( app._("remove_abort", "message", locale=msg.from_user.language_code) ) return @@ -301,11 +301,11 @@ async def cmd_remove(app: PyroClient, msg: Message): USERS_WITH_CONTEXT.append(msg.from_user.id) - answer = await listen_message(app, msg.chat.id, timeout=600) + answer_kind = await listen_message(app, msg.chat.id, timeout=600) USERS_WITH_CONTEXT.remove(msg.from_user.id) - if answer is None: + if answer_kind is None: await msg.reply_text( app._("remove_ignored", "message", locale=msg.from_user.language_code), quote=True, @@ -313,19 +313,19 @@ async def cmd_remove(app: PyroClient, msg: Message): ) return - if answer.text == "/cancel": - await answer.reply_text( + if answer_kind.text == "/cancel": + await answer_kind.reply_text( app._("remove_abort", "message", locale=msg.from_user.language_code), reply_markup=ReplyKeyboardRemove(), ) return - if answer.text in app.in_all_locales("photo", "button"): + if answer_kind.text in app.in_all_locales("photo", "button"): func = photo_delete - elif answer.text in app.in_all_locales("video", "button"): + elif answer_kind.text in app.in_all_locales("video", "button"): func = video_delete else: - await answer.reply_text( + await answer_kind.reply_text( app._( "remove_unknown", "message", locale=msg.from_user.language_code ).format( @@ -336,27 +336,31 @@ async def cmd_remove(app: PyroClient, msg: Message): ) return - response = await func(id=answer.text, client=client) + response = await func(id=answer_id.text, client=client) if response: logger.info( - "Removed '%s' by request of user %s", answer.text, answer.from_user.id + "Removed %s '%s' by request of user %s", + answer_kind.text, + answer_id.text, + answer_id.from_user.id, ) - await answer.reply_text( + await answer_kind.reply_text( app._( "remove_success", "message", locale=msg.from_user.language_code - ).format(answer.text) + ).format(answer_id.text) ) else: logger.warning( - "Could not remove '%s' by request of user %s", - answer.text, - answer.from_user.id, + "Could not remove %s '%s' by request of user %s", + answer_kind.text, + answer_id.text, + answer_id.from_user.id, ) - await answer.reply_text( + await answer_kind.reply_text( app._( "remove_failure", "message", locale=msg.from_user.language_code - ).format(answer.text) + ).format(answer_id.text) ) From 9e0a8150621d20ff8b6fcf6d4aac39aaa093c5cc Mon Sep 17 00:00:00 2001 From: profitroll Date: Wed, 28 Jun 2023 08:57:09 +0200 Subject: [PATCH 11/28] Fixed markup --- plugins/commands/photos.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/commands/photos.py b/plugins/commands/photos.py index 82e0959..d52e9f3 100644 --- a/plugins/commands/photos.py +++ b/plugins/commands/photos.py @@ -348,7 +348,8 @@ async def cmd_remove(app: PyroClient, msg: Message): await answer_kind.reply_text( app._( "remove_success", "message", locale=msg.from_user.language_code - ).format(answer_id.text) + ).format(answer_id.text), + reply_markup=ReplyKeyboardRemove(), ) else: logger.warning( @@ -360,7 +361,8 @@ async def cmd_remove(app: PyroClient, msg: Message): await answer_kind.reply_text( app._( "remove_failure", "message", locale=msg.from_user.language_code - ).format(answer_id.text) + ).format(answer_id.text), + reply_markup=ReplyKeyboardRemove(), ) From 93f3439a11401e8f480d4a2658141ad03aa34cb2 Mon Sep 17 00:00:00 2001 From: profitroll Date: Wed, 28 Jun 2023 08:59:40 +0200 Subject: [PATCH 12/28] Fixed response type check --- plugins/commands/photos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/commands/photos.py b/plugins/commands/photos.py index d52e9f3..cdad7d8 100644 --- a/plugins/commands/photos.py +++ b/plugins/commands/photos.py @@ -338,7 +338,7 @@ async def cmd_remove(app: PyroClient, msg: Message): response = await func(id=answer_id.text, client=client) - if response: + if response is None: logger.info( "Removed %s '%s' by request of user %s", answer_kind.text, From 6f8b560acc770ff7c433a3b83e8a559186e96442 Mon Sep 17 00:00:00 2001 From: profitroll Date: Wed, 28 Jun 2023 10:15:45 +0200 Subject: [PATCH 13/28] WIP: New User system --- classes/pyroclient.py | 36 +++++++++++++++++++++++----- classes/pyrouser.py | 55 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 6 deletions(-) create mode 100644 classes/pyrouser.py diff --git a/classes/pyroclient.py b/classes/pyroclient.py index 54f2edc..bb966bf 100644 --- a/classes/pyroclient.py +++ b/classes/pyroclient.py @@ -16,7 +16,7 @@ from libbot import json_write from libbot.i18n.sync import _ from photosapi_client.errors import UnexpectedStatus from pyrogram.errors import bad_request_400 -from pyrogram.types import Message +from pyrogram.types import Message, User from pytimeparse.timeparse import timeparse from ujson import dumps, loads @@ -26,6 +26,7 @@ from classes.exceptions import ( SubmissionUnavailableError, SubmissionUnsupportedError, ) +from classes.pyrouser import PyroUser from modules.api_client import ( BodyPhotoUpload, BodyVideoUpload, @@ -36,7 +37,7 @@ from modules.api_client import ( photo_upload, video_upload, ) -from modules.database import col_submitted +from modules.database import col_submitted, col_users from modules.http_client import http_session from modules.sender import send_content @@ -252,8 +253,31 @@ class PyroClient(PyroClient): response.id if not hasattr(response, "parsed") else response.parsed.id, ) - async def ban_user(self, id: int) -> None: - pass + async def find_user(self, user: Union[int, User]) -> PyroUser: + """Find User by it's ID or User object - async def unban_user(self, id: int) -> None: - pass + ### Args: + * user (`Union[int, User]`): ID or User object to extract ID from + + ### Returns: + * `PyroUser`: PyroUser object + """ + if ( + col_users.find_one( + {"id": user.id if isinstance(user, User) else user} + ) # type: ignore + is None + ): + col_users.insert_one( + { + "id": user.id if isinstance(user, User) else user, + "locale": user.language_code if isinstance(user, User) else None, + "subscription": {"expires": datetime(1970, 1, 1, 0, 0)}, + } + ) # type: ignore + + db_record = col_users.find_one( + {"id": user.id if isinstance(user, User) else user} + ) # type: ignore + + return PyroUser(**db_record) diff --git a/classes/pyrouser.py b/classes/pyrouser.py new file mode 100644 index 0000000..6f77db6 --- /dev/null +++ b/classes/pyrouser.py @@ -0,0 +1,55 @@ +from dataclasses import dataclass +from datetime import datetime +from typing import Union + +from bson import ObjectId +from libbot import config_get +from libbot.pyrogram.classes import PyroClient + +from modules.database import col_users + + +@dataclass +class PyroUser: + """Dataclass of DB entry of a user""" + + _id: ObjectId + id: int + locale: Union[str, None] + banned: bool + cooldown: datetime + subscription: dict + + async def update_locale(self, locale: str): + col_users.update_one({"_id": self._id}, {"$set": {"locale": locale}}) + + async def update_cooldown(self, time: datetime = datetime.now()): + col_users.update_one({"_id": self._id}, {"$set": {"cooldown": time}}) + + async def block(self) -> None: + """Ban user from using command and submitting content.""" + col_users.update_one({"_id": self._id}, {"$set": {"banned": True}}) + + async def unblock(self) -> None: + """Allow user to use command and submit posts again.""" + col_users.update_one({"_id": self._id}, {"$set": {"banned": False}}) + + async def is_limited(self, app: Union[PyroClient, None] = None) -> bool: + """Check if user is on a cooldown after submitting something. + + ### Returns: + `bool`: Must be `True` if on the cooldown and `False` if not + """ + admins = ( + app.admins + if app is not None + else ( + await config_get("admins", "bot") + [await config_get("owner", "bot")] + ) + ) + + return (datetime.now() - self.cooldown).total_seconds() < ( + app.config["submission"]["timeout"] + if app is not None + else await config_get("timeout", "submission") + ) From 10c60ae9321414e8a4d8a57409401a1e9f4d4406 Mon Sep 17 00:00:00 2001 From: profitroll Date: Wed, 28 Jun 2023 10:37:18 +0200 Subject: [PATCH 14/28] WIP: /language system --- classes/user.py | 58 --------------------- plugins/callbacks/nothing.py | 6 +-- plugins/callbacks/submission.py | 83 +++++++++++++++++++----------- plugins/commands/general.py | 10 ++-- plugins/commands/mode_submit.py | 13 +++-- plugins/commands/photos.py | 90 ++++++++++++++------------------- plugins/commands/report.py | 51 +++++++++---------- plugins/handlers/submission.py | 55 ++++++++++---------- plugins/language.py | 42 +++++++++++++++ 9 files changed, 203 insertions(+), 205 deletions(-) delete mode 100644 classes/user.py create mode 100644 plugins/language.py diff --git a/classes/user.py b/classes/user.py deleted file mode 100644 index 7e6d368..0000000 --- a/classes/user.py +++ /dev/null @@ -1,58 +0,0 @@ -from datetime import datetime - -from libbot import sync - -from modules.database import col_banned, col_users - - -class PosterUser: - def __init__(self, id: int): - self.id = id - - def is_blocked(self) -> bool: - """Check if user is banned from submitting content. - - ### Returns: - `bool`: Must be `True` if banned and `False` if not - """ - return False if col_banned.find_one({"user": self.id}) is None else True - - def block(self) -> None: - """Ban user from using command and submitting content.""" - if col_banned.find_one({"user": self.id}) is None: - col_banned.insert_one({"user": self.id, "date": datetime.now()}) - - def unblock(self) -> None: - """Allow user to use command and submit posts again.""" - col_banned.find_one_and_delete({"user": self.id}) - - def is_limited(self) -> bool: - """Check if user is on a cooldown after submitting something. - - ### Returns: - `bool`: Must be `True` if on the cooldown and `False` if not - """ - if self.id in sync.config_get("admins", "bot"): - return False - - db_record = col_users.find_one({"user": self.id}) - - if db_record is None: - return False - - return ( - True - if (datetime.now() - db_record["cooldown"]).total_seconds() - < sync.config_get("timeout", "submission") - else False - ) - - def limit(self) -> None: - """Restart user's cooldown. Used after post has been submitted.""" - if ( - col_users.find_one_and_update( - {"user": self.id}, {"$set": {"cooldown": datetime.now()}} - ) - is None - ): - col_users.insert_one({"user": self.id, "cooldown": datetime.now()}) diff --git a/plugins/callbacks/nothing.py b/plugins/callbacks/nothing.py index d0a3086..b8baca7 100644 --- a/plugins/callbacks/nothing.py +++ b/plugins/callbacks/nothing.py @@ -7,6 +7,6 @@ from classes.pyroclient import PyroClient @Client.on_callback_query(filters.regex("nothing")) async def callback_query_nothing(app: PyroClient, clb: CallbackQuery): - await clb.answer( - text=app._("nothing", "callback", locale=clb.from_user.language_code) - ) + user = await app.find_user(clb.from_user) + + await clb.answer(text=app._("nothing", "callback", locale=user.locale)) diff --git a/plugins/callbacks/submission.py b/plugins/callbacks/submission.py index f6a68a9..a113805 100644 --- a/plugins/callbacks/submission.py +++ b/plugins/callbacks/submission.py @@ -15,7 +15,6 @@ from classes.exceptions import ( SubmissionUnsupportedError, ) from classes.pyroclient import PyroClient -from classes.user import PosterUser from modules.database import col_submitted logger = logging.getLogger(__name__) @@ -23,22 +22,22 @@ logger = logging.getLogger(__name__) @Client.on_callback_query(filters.regex("sub_yes_[\s\S]*")) async def callback_query_yes(app: PyroClient, clb: CallbackQuery): + user = await app.find_user(clb.from_user) fullclb = str(clb.data).split("_") - user_locale = clb.from_user.language_code db_entry = col_submitted.find_one({"_id": ObjectId(fullclb[2])}) try: - submission = await app.submit_photo(fullclb[2]) + submission = await app.submit_media(fullclb[2]) except SubmissionUnavailableError: await clb.answer( - text=app._("sub_msg_unavail", "callback", locale=user_locale), + text=app._("sub_msg_unavail", "callback", locale=user.locale), show_alert=True, ) return except SubmissionUnsupportedError: await clb.answer( - text=app._("mime_not_allowed", "message", locale=user_locale).format( + text=app._("mime_not_allowed", "message", locale=user.locale).format( ", ".join(app.config["submission"]["mime_types"]), quote=True ), show_alert=True, @@ -46,11 +45,11 @@ async def callback_query_yes(app: PyroClient, clb: CallbackQuery): return except SubmissionDuplicatesError as exp: await clb.answer( - text=app._("sub_duplicates_found", "callback", locale=user_locale), + text=app._("sub_duplicates_found", "callback", locale=user.locale), show_alert=True, ) await clb.message.reply_text( - app._("sub_media_duplicates_list", "message", locale=user_locale).format( + app._("sub_media_duplicates_list", "message", locale=user.locale).format( "\n • ".join(exp.duplicates) ), quote=True, @@ -69,14 +68,25 @@ async def callback_query_yes(app: PyroClient, clb: CallbackQuery): if submission[0] is not None: await submission[0].reply_text( - app._("sub_yes", "message", locale=submission[0].from_user.language_code), + app._( + "sub_yes", + "message", + locale=(await app.find_user(submission[0].from_user)).locale, + ), quote=True, ) elif db_entry is not None: - await app.send_message(db_entry["user"], app._("sub_yes", "message")) + await app.send_message( + db_entry["user"], + app._( + "sub_yes", + "message", + locale=(await app.find_user(db_entry["user"])).locale, + ), + ) await clb.answer( - text=app._("sub_yes", "callback", locale=user_locale).format(fullclb[2]), + text=app._("sub_yes", "callback", locale=user.locale).format(fullclb[2]), show_alert=True, ) @@ -84,7 +94,7 @@ async def callback_query_yes(app: PyroClient, clb: CallbackQuery): [ [ InlineKeyboardButton( - text=str(app._("accepted", "button", locale=user_locale)), + text=str(app._("accepted", "button", locale=user.locale)), callback_data="nothing", ) ], @@ -94,7 +104,7 @@ async def callback_query_yes(app: PyroClient, clb: CallbackQuery): else [ [ InlineKeyboardButton( - text=str(app._("accepted", "button", locale=user_locale)), + text=str(app._("accepted", "button", locale=user.locale)), callback_data="nothing", ) ] @@ -103,7 +113,7 @@ async def callback_query_yes(app: PyroClient, clb: CallbackQuery): if await config_get("send_uploaded_id", "submission"): await clb.message.edit_caption( - clb.message.caption + f"\n\nID: `{submission[1]}`" + f"{clb.message.caption}\n\nID: `{submission[1]}`" ) await clb.message.edit_reply_markup( @@ -121,8 +131,8 @@ async def callback_query_yes(app: PyroClient, clb: CallbackQuery): @Client.on_callback_query(filters.regex("sub_no_[\s\S]*")) async def callback_query_no(app: PyroClient, clb: CallbackQuery): + user = await app.find_user(clb.from_user) fullclb = str(clb.data).split("_") - user_locale = clb.from_user.language_code db_entry = col_submitted.find_one_and_delete({"_id": ObjectId(fullclb[2])}) @@ -145,17 +155,21 @@ async def callback_query_no(app: PyroClient, clb: CallbackQuery): ) except Exception as exp: await clb.answer( - text=app._("sub_msg_unavail", "message", locale=user_locale), + text=app._("sub_msg_unavail", "message", locale=user.locale), show_alert=True, ) return await submission.reply_text( - app._("sub_no", "message", locale=submission.from_user.language_code), + app._( + "sub_no", + "message", + locale=(await app.find_user(submission.from_user)).locale, + ), quote=True, ) await clb.answer( - text=app._("sub_no", "callback", locale=user_locale).format(fullclb[2]), + text=app._("sub_no", "callback", locale=user.locale).format(fullclb[2]), show_alert=True, ) @@ -163,7 +177,7 @@ async def callback_query_no(app: PyroClient, clb: CallbackQuery): [ [ InlineKeyboardButton( - text=str(app._("declined", "button", locale=user_locale)), + text=str(app._("declined", "button", locale=user.locale)), callback_data="nothing", ) ], @@ -173,7 +187,7 @@ async def callback_query_no(app: PyroClient, clb: CallbackQuery): else [ [ InlineKeyboardButton( - text=str(app._("declined", "button", locale=user_locale)), + text=str(app._("declined", "button", locale=user.locale)), callback_data="nothing", ) ] @@ -193,17 +207,21 @@ async def callback_query_no(app: PyroClient, clb: CallbackQuery): @Client.on_callback_query(filters.regex("sub_block_[\s\S]*")) async def callback_query_block(app: PyroClient, clb: CallbackQuery): + user = await app.find_user(clb.from_user) fullclb = str(clb.data).split("_") - user_locale = clb.from_user.language_code await app.send_message( int(fullclb[2]), - app._("sub_blocked", "message"), + app._( + "sub_blocked", + "message", + locale=(await app.find_user(int(fullclb[2]))).locale, + ), ) - PosterUser(int(fullclb[2])).block() + await user.block() await clb.answer( - text=app._("sub_block", "callback", locale=user_locale).format(fullclb[2]), + text=app._("sub_block", "callback", locale=user.locale).format(fullclb[2]), show_alert=True, ) @@ -211,7 +229,7 @@ async def callback_query_block(app: PyroClient, clb: CallbackQuery): clb.message.reply_markup.inline_keyboard[0], [ InlineKeyboardButton( - text=str(app._("sub_unblock", "button", locale=user_locale)), + text=str(app._("sub_unblock", "button", locale=user.locale)), callback_data=f"sub_unblock_{fullclb[2]}", ) ], @@ -230,15 +248,22 @@ async def callback_query_block(app: PyroClient, clb: CallbackQuery): @Client.on_callback_query(filters.regex("sub_unblock_[\s\S]*")) async def callback_query_unblock(app: PyroClient, clb: CallbackQuery): + user = await app.find_user(clb.from_user) fullclb = str(clb.data).split("_") - user_locale = clb.from_user.language_code - await app.send_message(int(fullclb[2]), app._("sub_unblocked", "message")) + await app.send_message( + int(fullclb[2]), + app._( + "sub_unblocked", + "message", + locale=(await app.find_user(int(fullclb[2]))).locale, + ), + ) - PosterUser(int(fullclb[2])).unblock() + await user.unblock() await clb.answer( - text=app._("sub_unblock", "callback", locale=user_locale).format(fullclb[2]), + text=app._("sub_unblock", "callback", locale=user.locale).format(fullclb[2]), show_alert=True, ) @@ -246,7 +271,7 @@ async def callback_query_unblock(app: PyroClient, clb: CallbackQuery): clb.message.reply_markup.inline_keyboard[0], [ InlineKeyboardButton( - text=str(app._("sub_block", "button", locale=user_locale)), + text=str(app._("sub_block", "button", locale=user.locale)), callback_data=f"sub_block_{fullclb[2]}", ) ], diff --git a/plugins/commands/general.py b/plugins/commands/general.py index 8aaf34a..18a0357 100644 --- a/plugins/commands/general.py +++ b/plugins/commands/general.py @@ -18,16 +18,18 @@ async def cmd_kill(app: PyroClient, msg: Message): if msg.from_user.id not in app.admins: return + user = await app.find_user(msg.from_user) + if len(USERS_WITH_CONTEXT) > 0: await msg.reply_text( - app._("shutdown_confirm", "message").format(len(USERS_WITH_CONTEXT)), + app._("shutdown_confirm", "message", locale=user.locale).format( + len(USERS_WITH_CONTEXT) + ), reply_markup=InlineKeyboardMarkup( [ [ InlineKeyboardButton( - app._( - "shutdown", "button", locale=msg.from_user.language_code - ), + app._("shutdown", "button", locale=user.locale), callback_data="shutdown", ) ] diff --git a/plugins/commands/mode_submit.py b/plugins/commands/mode_submit.py index f759b33..649e4ac 100644 --- a/plugins/commands/mode_submit.py +++ b/plugins/commands/mode_submit.py @@ -3,22 +3,25 @@ from pyrogram.client import Client from pyrogram.types import Message from classes.pyroclient import PyroClient -from classes.user import PosterUser @Client.on_message(~filters.scheduled & filters.command(["start"], prefixes="/")) async def cmd_start(app: PyroClient, msg: Message): - if PosterUser(msg.from_user.id).is_blocked(): + user = await app.find_user(msg.from_user) + + if user.banned: return - await msg.reply_text(app._("start", "message", locale=msg.from_user.language_code)) + await msg.reply_text(app._("start", "message", locale=user.locale)) @Client.on_message( ~filters.scheduled & filters.command(["rules", "help"], prefixes="/") ) async def cmd_rules(app: PyroClient, msg: Message): - if PosterUser(msg.from_user.id).is_blocked(): + user = await app.find_user(msg.from_user) + + if user.banned: return - await msg.reply_text(app._("rules", "message", locale=msg.from_user.language_code)) + await msg.reply_text(app._("rules", "message", locale=user.locale)) diff --git a/plugins/commands/photos.py b/plugins/commands/photos.py index cdad7d8..77d4433 100644 --- a/plugins/commands/photos.py +++ b/plugins/commands/photos.py @@ -48,9 +48,9 @@ async def cmd_import(app: PyroClient, msg: Message): else: return - await msg.reply_text( - app._("import_request", "message", locale=msg.from_user.language_code) - ) + user = await app.find_user(msg.from_user) + + await msg.reply_text(app._("import_request", "message", locale=user.locale)) answer = await listen_message(app, msg.chat.id, timeout=600) @@ -58,15 +58,13 @@ async def cmd_import(app: PyroClient, msg: Message): if answer is None: await msg.reply_text( - app._("import_ignored", "message", locale=msg.from_user.language_code), + app._("import_ignored", "message", locale=user.locale), quote=True, ) return if answer.text == "/cancel": - await answer.reply_text( - app._("import_abort", "message", locale=msg.from_user.language_code) - ) + await answer.reply_text(app._("import_abort", "message", locale=user.locale)) return if answer.document is None: @@ -74,7 +72,7 @@ async def cmd_import(app: PyroClient, msg: Message): app._( "import_invalid_media", "message", - locale=msg.from_user.language_code, + locale=user.locale, ), quote=True, ) @@ -82,16 +80,14 @@ async def cmd_import(app: PyroClient, msg: Message): if answer.document.mime_type != "application/zip": await answer.reply_text( - app._("import_invalid_mime", "message", locale=msg.from_user.language_code), + app._("import_invalid_mime", "message", locale=user.locale), quote=True, ) return if disk_usage(getcwd())[2] < (answer.document.file_size) * 3: await msg.reply_text( - app._( - "import_too_big", "message", locale=msg.from_user.language_code - ).format( + app._("import_too_big", "message", locale=user.locale).format( answer.document.file_size // (2**30), disk_usage(getcwd())[2] // (2**30), ) @@ -111,14 +107,12 @@ async def cmd_import(app: PyroClient, msg: Message): tmp_path = Path(f"{app.config['locations']['tmp']}/{answer.document.file_id}") downloading = await answer.reply_text( - app._("import_downloading", "message", locale=msg.from_user.language_code), + app._("import_downloading", "message", locale=user.locale), quote=True, ) await app.download_media(answer, file_name=str(tmp_path)) - await downloading.edit( - app._("import_unpacking", "message", locale=msg.from_user.language_code) - ) + await downloading.edit(app._("import_unpacking", "message", locale=user.locale)) try: with ZipFile(tmp_path, "r") as handle: @@ -137,17 +131,15 @@ async def cmd_import(app: PyroClient, msg: Message): format_exc(), ) await answer.reply_text( - app._( - "import_unpack_error", "message", locale=msg.from_user.language_code - ).format(exp, format_exc()) + app._("import_unpack_error", "message", locale=user.locale).format( + exp, format_exc() + ) ) return logger.info("Downloaded '%s' - awaiting upload", answer.document.file_name) - await downloading.edit( - app._("import_uploading", "message", locale=msg.from_user.language_code) - ) + await downloading.edit(app._("import_uploading", "message", locale=user.locale)) remove(tmp_path) @@ -184,7 +176,7 @@ async def cmd_import(app: PyroClient, msg: Message): app._( "import_upload_error_other", "message", - locale=msg.from_user.language_code, + locale=user.locale, ).format(path.basename(filename)), disable_notification=True, ) @@ -205,7 +197,7 @@ async def cmd_import(app: PyroClient, msg: Message): app._( "import_upload_error_duplicate", "message", - locale=msg.from_user.language_code, + locale=user.locale, ).format(path.basename(filename)), disable_notification=True, ) @@ -214,7 +206,7 @@ async def cmd_import(app: PyroClient, msg: Message): app._( "import_upload_error_other", "message", - locale=msg.from_user.language_code, + locale=user.locale, ).format(path.basename(filename)), disable_notification=True, ) @@ -235,7 +227,7 @@ async def cmd_import(app: PyroClient, msg: Message): rmtree(Path(f"{app.config['locations']['tmp']}/{tmp_dir}"), ignore_errors=True) await answer.reply_text( - app._("import_finished", "message", locale=msg.from_user.language_code), + app._("import_finished", "message", locale=user.locale), quote=True, ) @@ -260,9 +252,9 @@ async def cmd_remove(app: PyroClient, msg: Message): else: return - await msg.reply_text( - app._("remove_request", "message", locale=msg.from_user.language_code) - ) + user = await app.find_user(msg.from_user) + + await msg.reply_text(app._("remove_request", "message", locale=user.locale)) answer_id = await listen_message(app, msg.chat.id, timeout=600) @@ -270,28 +262,22 @@ async def cmd_remove(app: PyroClient, msg: Message): if answer_id is None: await msg.reply_text( - app._("remove_ignored", "message", locale=msg.from_user.language_code), + app._("remove_ignored", "message", locale=user.locale), quote=True, ) return if answer_id.text == "/cancel": - await answer_id.reply_text( - app._("remove_abort", "message", locale=msg.from_user.language_code) - ) + await answer_id.reply_text(app._("remove_abort", "message", locale=user.locale)) return await msg.reply_text( - app._("remove_kind", "message", locale=msg.from_user.language_code), + app._("remove_kind", "message", locale=user.locale), reply_markup=ReplyKeyboardMarkup( [ [ - KeyboardButton( - app._("photo", "button", locale=msg.from_user.language_code) - ), - KeyboardButton( - app._("video", "button", locale=msg.from_user.language_code) - ), + KeyboardButton(app._("photo", "button", locale=user.locale)), + KeyboardButton(app._("video", "button", locale=user.locale)), ] ], resize_keyboard=True, @@ -307,7 +293,7 @@ async def cmd_remove(app: PyroClient, msg: Message): if answer_kind is None: await msg.reply_text( - app._("remove_ignored", "message", locale=msg.from_user.language_code), + app._("remove_ignored", "message", locale=user.locale), quote=True, reply_markup=ReplyKeyboardRemove(), ) @@ -315,7 +301,7 @@ async def cmd_remove(app: PyroClient, msg: Message): if answer_kind.text == "/cancel": await answer_kind.reply_text( - app._("remove_abort", "message", locale=msg.from_user.language_code), + app._("remove_abort", "message", locale=user.locale), reply_markup=ReplyKeyboardRemove(), ) return @@ -326,11 +312,9 @@ async def cmd_remove(app: PyroClient, msg: Message): func = video_delete else: await answer_kind.reply_text( - app._( - "remove_unknown", "message", locale=msg.from_user.language_code - ).format( - app._("photo", "button", locale=msg.from_user.language_code), - app._("video", "button", locale=msg.from_user.language_code), + app._("remove_unknown", "message", locale=user.locale).format( + app._("photo", "button", locale=user.locale), + app._("video", "button", locale=user.locale), ), reply_markup=ReplyKeyboardRemove(), ) @@ -346,9 +330,9 @@ async def cmd_remove(app: PyroClient, msg: Message): answer_id.from_user.id, ) await answer_kind.reply_text( - app._( - "remove_success", "message", locale=msg.from_user.language_code - ).format(answer_id.text), + app._("remove_success", "message", locale=user.locale).format( + answer_id.text + ), reply_markup=ReplyKeyboardRemove(), ) else: @@ -359,9 +343,9 @@ async def cmd_remove(app: PyroClient, msg: Message): answer_id.from_user.id, ) await answer_kind.reply_text( - app._( - "remove_failure", "message", locale=msg.from_user.language_code - ).format(answer_id.text), + app._("remove_failure", "message", locale=user.locale).format( + answer_id.text + ), reply_markup=ReplyKeyboardRemove(), ) diff --git a/plugins/commands/report.py b/plugins/commands/report.py index 063fe4b..09f0513 100644 --- a/plugins/commands/report.py +++ b/plugins/commands/report.py @@ -1,7 +1,8 @@ -from pyrogram.client import Client -from pyrogram import filters -from pyrogram.types import Message, User from libbot import sync +from pyrogram import filters +from pyrogram.client import Client +from pyrogram.types import Message, User + from classes.pyroclient import PyroClient @@ -12,31 +13,29 @@ from classes.pyroclient import PyroClient & filters.command(["report"], prefixes=["", "/"]) ) async def command_report(app: PyroClient, msg: Message): - if msg.reply_to_message.forward_from_chat.id == app.config["posting"]["channel"]: - await msg.reply_text( - app._( - "report_sent", - "message", - locale=msg.from_user.language_code - if msg.from_user is not None - else None, - ) + if msg.reply_to_message.forward_from_chat.id != app.config["posting"]["channel"]: + return + + user = await app.find_user(msg.from_user) + + await msg.reply_text( + app._( + "report_sent", + "message", + locale=user.locale if msg.from_user is not None else None, ) + ) - print(msg) + print(msg) - report_sent = await msg.reply_to_message.forward(app.owner) - sender = msg.from_user if msg.from_user is not None else msg.sender_chat + report_sent = await msg.reply_to_message.forward(app.owner) + sender = msg.from_user if msg.from_user is not None else msg.sender_chat - sender_name = ( - sender.first_name if isinstance(sender, User) else sender.title - ) + sender_name = sender.first_name if isinstance(sender, User) else sender.title - # ACTION NEEDED - # Name and username are somehow None - await report_sent.reply_text( - app._("report_received", "message").format( - sender_name, sender.username, sender.id - ), - quote=True, - ) + await report_sent.reply_text( + app._("report_received", "message", locale=user.locale).format( + sender_name, sender.username, sender.id + ), + quote=True, + ) diff --git a/plugins/handlers/submission.py b/plugins/handlers/submission.py index 43e8113..74159f9 100644 --- a/plugins/handlers/submission.py +++ b/plugins/handlers/submission.py @@ -13,7 +13,6 @@ from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message from classes.enums.submission_types import SubmissionType from classes.exceptions import SubmissionDuplicatesError, SubmissionUnsupportedError from classes.pyroclient import PyroClient -from classes.user import PosterUser from modules.database import col_banned, col_submitted from modules.utils import USERS_WITH_CONTEXT @@ -35,20 +34,22 @@ async def get_submission(app: PyroClient, msg: Message): if msg.from_user.id in USERS_WITH_CONTEXT: return + user = await app.find_user(msg.from_user) + user_owner = await app.find_user(app.owner) + try: if col_banned.find_one({"user": msg.from_user.id}) is not None: return await app.send_chat_action(msg.chat.id, ChatAction.TYPING) - user_locale = msg.from_user.language_code save_tmp = True contents = None - if PosterUser(msg.from_user.id).is_limited(): + if await user.is_limited(): await msg.reply_text( - app._("sub_cooldown", "message", locale=user_locale).format( - str(app.config["submission"]["timeout"]) + app._("sub_cooldown", "message", locale=user.locale).format( + app.config["submission"]["timeout"] ) ) return @@ -63,7 +64,7 @@ async def get_submission(app: PyroClient, msg: Message): ) if msg.document.mime_type not in app.config["submission"]["mime_types"]: await msg.reply_text( - app._("mime_not_allowed", "message", locale=user_locale).format( + app._("mime_not_allowed", "message", locale=user.locale).format( ", ".join(app.config["submission"]["mime_types"]) ), quote=True, @@ -71,8 +72,8 @@ async def get_submission(app: PyroClient, msg: Message): return if msg.document.file_size > app.config["submission"]["file_size"]: await msg.reply_text( - app._("document_too_large", "message", locale=user_locale).format( - str(app.config["submission"]["file_size"] / 1024 / 1024) + app._("document_too_large", "message", locale=user.locale).format( + app.config["submission"]["file_size"] / 1024 / 1024 ), quote=True, ) @@ -93,8 +94,8 @@ async def get_submission(app: PyroClient, msg: Message): ) if msg.video.file_size > app.config["submission"]["file_size"]: await msg.reply_text( - app._("document_too_large", "message", locale=user_locale).format( - str(app.config["submission"]["file_size"] / 1024 / 1024) + app._("document_too_large", "message", locale=user.locale).format( + app.config["submission"]["file_size"] / 1024 / 1024 ), quote=True, ) @@ -112,7 +113,7 @@ async def get_submission(app: PyroClient, msg: Message): # ) # if msg.animation.file_size > app.config["submission"]["file_size"]: # await msg.reply_text( - # app._("document_too_large", "message", locale=user_locale).format( + # app._("document_too_large", "message", locale=user.locale).format( # str(app.config["submission"]["file_size"] / 1024 / 1024) # ), # quote=True, @@ -179,7 +180,7 @@ async def get_submission(app: PyroClient, msg: Message): buttons = [ [ InlineKeyboardButton( - text=app._("sub_yes", "button"), + text=app._("sub_yes", "button", locale=user_owner.locale), callback_data=f"sub_yes_{str(inserted.inserted_id)}", ) ] @@ -189,7 +190,7 @@ async def get_submission(app: PyroClient, msg: Message): caption = str(msg.caption) buttons[0].append( InlineKeyboardButton( - text=app._("sub_yes_caption", "button"), + text=app._("sub_yes_caption", "button", locale=user_owner.locale), callback_data=f"sub_yes_{str(inserted.inserted_id)}_caption", ) ) @@ -198,11 +199,11 @@ async def get_submission(app: PyroClient, msg: Message): buttons[0].append( InlineKeyboardButton( - text=app._("sub_no", "button"), + text=app._("sub_no", "button", locale=user_owner.locale), callback_data=f"sub_no_{str(inserted.inserted_id)}", ) ) - caption += app._("sub_by", "message") + caption += app._("sub_by", "message", locale=user_owner.locale) if msg.from_user.first_name is not None: caption += f" {msg.from_user.first_name}" @@ -220,7 +221,7 @@ async def get_submission(app: PyroClient, msg: Message): try: submitted = await app.submit_media(str(inserted.inserted_id)) await msg.reply_text( - app._("sub_yes_auto", "message", locale=user_locale), + app._("sub_yes_auto", "message", locale=user.locale), disable_notification=True, quote=True, ) @@ -230,7 +231,7 @@ async def get_submission(app: PyroClient, msg: Message): return except SubmissionUnsupportedError: await msg.reply_text( - app._("mime_not_allowed", "message", locale=user_locale).format( + app._("mime_not_allowed", "message", locale=user.locale).format( ", ".join(app.config["submission"]["mime_types"]), quote=True ), quote=True, @@ -239,7 +240,7 @@ async def get_submission(app: PyroClient, msg: Message): except SubmissionDuplicatesError as exp: await msg.reply_text( app._( - "sub_media_duplicates_list", "message", locale=user_locale + "sub_media_duplicates_list", "message", locale=user.locale ).format("\n • ".join(exp.duplicates)), quote=True, ) @@ -254,7 +255,7 @@ async def get_submission(app: PyroClient, msg: Message): try: submitted = await app.submit_photo(str(inserted.inserted_id)) await msg.reply_text( - app._("sub_yes_auto", "message", locale=user_locale), + app._("sub_yes_auto", "message", locale=user.locale), disable_notification=True, quote=True, ) @@ -264,22 +265,22 @@ async def get_submission(app: PyroClient, msg: Message): return except SubmissionUnsupportedError: await msg.reply_text( - app._("mime_not_allowed", "message", locale=user_locale).format( + app._("mime_not_allowed", "message", locale=user.locale).format( ", ".join(app.config["submission"]["mime_types"]), quote=True ) ) return except SubmissionDuplicatesError as exp: await msg.reply_text( - app._("sub_dup", "message", locale=user_locale), quote=True + app._("sub_dup", "message", locale=user.locale), quote=True ) return except Exception as exp: await app.send_message( app.owner, - app._("sub_error_admin", "message").format( - msg.from_user.id, format_exc() - ), + app._( + "sub_error_admin", "message", locale=user_owner.locale + ).format(msg.from_user.id, format_exc()), ) await msg.reply_text("sub_error", quote=True) return @@ -288,17 +289,17 @@ async def get_submission(app: PyroClient, msg: Message): buttons += [ [ InlineKeyboardButton( - text=app._("sub_block", "button"), + text=app._("sub_block", "button", locale=user_owner.locale), callback_data=f"sub_block_{msg.from_user.id}", ) ] ] - PosterUser(msg.from_user.id).limit() + await user.update_cooldown() if msg.from_user.id != app.owner: await msg.reply_text( - app._("sub_sent", "message", locale=user_locale), + app._("sub_sent", "message", locale=user.locale), disable_notification=True, quote=True, ) diff --git a/plugins/language.py b/plugins/language.py new file mode 100644 index 0000000..5c0fbd5 --- /dev/null +++ b/plugins/language.py @@ -0,0 +1,42 @@ +from pykeyboard import InlineButton, InlineKeyboard +from pyrogram import filters +from pyrogram.client import Client +from pyrogram.types import CallbackQuery, Message + +from classes.pyroclient import PyroClient + + +@Client.on_message( + ~filters.scheduled & filters.private & filters.command(["language"], prefixes=["/"]) # type: ignore +) +async def command_language(app: PyroClient, message: Message): + user = await app.find_user(message.from_user) + keyboard = InlineKeyboard(row_width=2) + buttons = [] + + for locale, data in app.in_every_locale("metadata").items(): + buttons.append( + InlineButton(f"{data['flag']} {data['name']}", f"language:{locale}") + ) + + keyboard.add(*buttons) + + await message.reply_text( + app._("locale_choice", "messages", locale=user.locale), + reply_markup=keyboard, + ) + + +@Client.on_callback_query(filters.regex(r"language:[\s\S]*")) # type: ignore +async def callback_language(app: PyroClient, callback: CallbackQuery): + user = await app.find_user(callback.from_user) + language = str(callback.data).split(":")[1] + + await user.update_locale(language) + + await callback.answer( + app._("locale_set", "callbacks", locale=language).format( + locale=app._("name", "metadata", locale=language) + ), + show_alert=True, + ) From b747dde66492a2864c2fb717d8122f7a944b4fe9 Mon Sep 17 00:00:00 2001 From: profitroll Date: Wed, 28 Jun 2023 10:39:39 +0200 Subject: [PATCH 15/28] Added missing requirement --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 19b25b1..9b5c13f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ black~=23.3.0 convopyro==0.5 pillow~=9.4.0 psutil~=5.9.4 +pykeyboard==0.1.5 pymongo~=4.4.0 pyrogram==2.0.106 python_dateutil==2.8.2 From 420a4cb7eb7e844b1a86c3eaca531d53d5d768eb Mon Sep 17 00:00:00 2001 From: profitroll Date: Wed, 28 Jun 2023 10:43:13 +0200 Subject: [PATCH 16/28] Fixed locale strings and commands --- config_example.json | 7 +++++++ locale/en.json | 12 ++++++++++++ locale/uk.json | 11 +++++++++++ plugins/language.py | 4 ++-- 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/config_example.json b/config_example.json index 481317d..799b4ac 100644 --- a/config_example.json +++ b/config_example.json @@ -139,6 +139,13 @@ } ] }, + "language": { + "scopes": [ + { + "name": "BotCommandScopeDefault" + } + ] + }, "report": { "scopes": [ { diff --git a/locale/en.json b/locale/en.json index 49b9319..50ae6fb 100644 --- a/locale/en.json +++ b/locale/en.json @@ -1,7 +1,17 @@ { + "metadata": { + "flag": "🇬🇧", + "name": "English", + "codes": [ + "en", + "en-US", + "en-GB" + ] + }, "commands": { "start": "Start using the bot", "rules": "Photos submission rules", + "language": "Change bot's language", "report": "Report this post", "forwards": "Check post forwards", "import": "Submit .zip archive with photos", @@ -54,6 +64,7 @@ "import_upload_error_duplicate": "Could not upload `{0}` because there're duplicates on server.", "import_upload_error_other": "Could not upload `{0}`. Probably disallowed filetype.", "import_finished": "Import finished.", + "locale_choice": "Alright. Please choose the language using keyboard below.", "remove_request": "Please send me an ID to delete. You might have it from upload dialog. Use /cancel if you want to abort this operation.", "remove_ignored": "No response, aborting removal.", "remove_abort": "Removal aborted.", @@ -88,6 +99,7 @@ "sub_media_unavail": "Could not download submission", "sub_done": "You've already decided what to do with submission", "sub_duplicates_found": "There're duplicates in bot's database", + "locale_set": "Your language now is: {locale}", "nothing": "🏁 This action is already finished" }, "console": { diff --git a/locale/uk.json b/locale/uk.json index fc03e39..6e562c6 100644 --- a/locale/uk.json +++ b/locale/uk.json @@ -1,7 +1,16 @@ { + "metadata": { + "flag": "🇺🇦", + "name": "Українська", + "codes": [ + "uk", + "uk-UA" + ] + }, "commands": { "start": "Почати користуватись ботом", "rules": "Правила пропонування фото", + "language": "Змінити мову бота", "report": "Поскаржитись на цей пост", "forwards": "Переглянути репости", "import": "Надати боту .zip архів з фотографіями", @@ -54,6 +63,7 @@ "import_upload_error_duplicate": "Не вдалося завантажити `{0}`, оскільки на сервері є дублікати.", "import_upload_error_other": "Не вдалося завантажити `{0}`. Ймовірно, заборонений тип файлу.", "import_finished": "Імпорт завершено.", + "locale_choice": "Гаразд. Будь ласка, оберіть мову за допомогою клавіатури нижче.", "remove_request": "Будь ласка, надішліть мені ID для видалення. Ви могли отримати його з діалогу завантаження. Використовуйте /cancel, якщо ви хочете перервати цю операцію.", "remove_ignored": "Немає відповіді, перериваємо видалення.", "remove_abort": "Видалення перервано.", @@ -88,6 +98,7 @@ "sub_media_unavail": "Не вдалося завантажити подання", "sub_done": "Ви вже обрали що зробити з цим поданням", "sub_duplicates_found": "Знайдено дублікати в базі даних бота", + "locale_set": "Встановлено мову: {locale}", "nothing": "🏁 Цю дію вже було завершено" }, "console": { diff --git a/plugins/language.py b/plugins/language.py index 5c0fbd5..b9d6658 100644 --- a/plugins/language.py +++ b/plugins/language.py @@ -22,7 +22,7 @@ async def command_language(app: PyroClient, message: Message): keyboard.add(*buttons) await message.reply_text( - app._("locale_choice", "messages", locale=user.locale), + app._("locale_choice", "message", locale=user.locale), reply_markup=keyboard, ) @@ -35,7 +35,7 @@ async def callback_language(app: PyroClient, callback: CallbackQuery): await user.update_locale(language) await callback.answer( - app._("locale_set", "callbacks", locale=language).format( + app._("locale_set", "callback", locale=language).format( locale=app._("name", "metadata", locale=language) ), show_alert=True, From d8245934e23682efbf6190b209ba08a9ead57c6b Mon Sep 17 00:00:00 2001 From: profitroll Date: Wed, 28 Jun 2023 10:45:23 +0200 Subject: [PATCH 17/28] Fixed wrong db record --- classes/pyroclient.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/classes/pyroclient.py b/classes/pyroclient.py index bb966bf..205df61 100644 --- a/classes/pyroclient.py +++ b/classes/pyroclient.py @@ -250,7 +250,7 @@ class PyroClient(PyroClient): return ( submission, - response.id if not hasattr(response, "parsed") else response.parsed.id, + response.parsed.id if hasattr(response, "parsed") else response.id, ) async def find_user(self, user: Union[int, User]) -> PyroUser: @@ -272,6 +272,8 @@ class PyroClient(PyroClient): { "id": user.id if isinstance(user, User) else user, "locale": user.language_code if isinstance(user, User) else None, + "banned": False, + "cooldown": datetime(1970, 1, 1, 0, 0), "subscription": {"expires": datetime(1970, 1, 1, 0, 0)}, } ) # type: ignore From 3d87f035e723843486e728f3e55a67894ba41669 Mon Sep 17 00:00:00 2001 From: profitroll Date: Wed, 28 Jun 2023 10:48:14 +0200 Subject: [PATCH 18/28] Added /language for owner --- config_example.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config_example.json b/config_example.json index 799b4ac..dea8d3a 100644 --- a/config_example.json +++ b/config_example.json @@ -143,6 +143,10 @@ "scopes": [ { "name": "BotCommandScopeDefault" + }, + { + "name": "BotCommandScopeChat", + "chat_id": "owner" } ] }, From 11dbf3239d83c8d1939a1ff1902f20a7591840b1 Mon Sep 17 00:00:00 2001 From: profitroll Date: Wed, 28 Jun 2023 10:52:00 +0200 Subject: [PATCH 19/28] Removed deprecated collection --- modules/database.py | 5 ++--- plugins/handlers/submission.py | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/modules/database.py b/modules/database.py index dbfa7df..acdb504 100644 --- a/modules/database.py +++ b/modules/database.py @@ -25,11 +25,10 @@ db = db_client.get_database(name=db_config["name"]) collections = db.list_collection_names() -for collection in ["sent", "users", "banned", "submitted"]: - if not collection in collections: +for collection in ["sent", "users", "submitted"]: + if collection not in collections: db.create_collection(collection) col_sent = db.get_collection("sent") col_users = db.get_collection("users") -col_banned = db.get_collection("banned") col_submitted = db.get_collection("submitted") diff --git a/plugins/handlers/submission.py b/plugins/handlers/submission.py index 74159f9..3206c85 100644 --- a/plugins/handlers/submission.py +++ b/plugins/handlers/submission.py @@ -13,7 +13,7 @@ from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message from classes.enums.submission_types import SubmissionType from classes.exceptions import SubmissionDuplicatesError, SubmissionUnsupportedError from classes.pyroclient import PyroClient -from modules.database import col_banned, col_submitted +from modules.database import col_submitted from modules.utils import USERS_WITH_CONTEXT logger = logging.getLogger(__name__) @@ -38,7 +38,7 @@ async def get_submission(app: PyroClient, msg: Message): user_owner = await app.find_user(app.owner) try: - if col_banned.find_one({"user": msg.from_user.id}) is not None: + if user.banned: return await app.send_chat_action(msg.chat.id, ChatAction.TYPING) From fd0c4c05452db7f9af1e62de907c462747e12fed Mon Sep 17 00:00:00 2001 From: profitroll Date: Fri, 30 Jun 2023 11:33:34 +0200 Subject: [PATCH 20/28] Bump libbot to 1.7 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9b5c13f..bee2675 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,5 +11,5 @@ pytimeparse~=1.1.8 tgcrypto==1.2.5 uvloop==0.17.0 --extra-index-url https://git.end-play.xyz/api/packages/profitroll/pypi/simple -libbot[speed,pyrogram]==1.5 +libbot[speed,pyrogram]==1.7 photosapi_client==0.5.0 \ No newline at end of file From bfec702bef0d54c20759dcc6f7f1bf3450bbab38 Mon Sep 17 00:00:00 2001 From: profitroll Date: Fri, 30 Jun 2023 11:34:06 +0200 Subject: [PATCH 21/28] Config cleanup --- classes/pyroclient.py | 11 +++-------- config_example.json | 7 +------ 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/classes/pyroclient.py b/classes/pyroclient.py index 205df61..52dd38a 100644 --- a/classes/pyroclient.py +++ b/classes/pyroclient.py @@ -11,9 +11,11 @@ from typing import Dict, List, Tuple, Union import aiofiles from aiohttp import ClientSession +from apscheduler.schedulers.asyncio import AsyncIOScheduler from bson import ObjectId from libbot import json_write from libbot.i18n.sync import _ +from libbot.pyrogram.classes import PyroClient from photosapi_client.errors import UnexpectedStatus from pyrogram.errors import bad_request_400 from pyrogram.types import Message, User @@ -44,16 +46,9 @@ from modules.sender import send_content logger = logging.getLogger(__name__) -from datetime import datetime -from typing import List, Union - -from apscheduler.schedulers.asyncio import AsyncIOScheduler -from libbot.pyrogram.classes import PyroClient - - class PyroClient(PyroClient): def __init__(self, scheduler: AsyncIOScheduler): - super().__init__(scheduler=scheduler) + super().__init__(locales_root=Path("locale"), scheduler=scheduler) self.version: float = 0.2 diff --git a/config_example.json b/config_example.json index dea8d3a..186f3c9 100644 --- a/config_example.json +++ b/config_example.json @@ -1,7 +1,6 @@ { "locale": "en", "locale_log": "en", - "locale_fallback": "en", "bot": { "owner": 0, "admins": [], @@ -37,11 +36,7 @@ "locations": { "tmp": "tmp", "data": "data", - "cache": "cache", - "sent": "data/sent", - "queue": "data/queue", - "index": "data/index.json", - "locale": "locale" + "cache": "cache" }, "disabled_plugins": [], "posting": { From 28b5449f2ac7ffe45e6d7fcda090c573def280ee Mon Sep 17 00:00:00 2001 From: profitroll Date: Fri, 30 Jun 2023 11:34:15 +0200 Subject: [PATCH 22/28] Improved /shutdown --- plugins/commands/general.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/commands/general.py b/plugins/commands/general.py index 18a0357..a33182a 100644 --- a/plugins/commands/general.py +++ b/plugins/commands/general.py @@ -1,3 +1,4 @@ +import asyncio from os import makedirs from pathlib import Path from time import time @@ -44,4 +45,4 @@ async def cmd_kill(app: PyroClient, msg: Message): Path(f"{app.config['locations']['cache']}/shutdown_time"), ) - exit() + asyncio.get_event_loop().create_task(app.stop()) From 15b272ae3546155c34addea600a2ea25593af183 Mon Sep 17 00:00:00 2001 From: profitroll Date: Sat, 1 Jul 2023 15:43:07 +0200 Subject: [PATCH 23/28] max_concurrent_transmissions is now 3 by default --- config_example.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config_example.json b/config_example.json index 186f3c9..a8ecfc0 100644 --- a/config_example.json +++ b/config_example.json @@ -7,7 +7,7 @@ "api_id": 0, "api_hash": "", "bot_token": "", - "max_concurrent_transmissions": 5, + "max_concurrent_transmissions": 3, "scoped_commands": true }, "database": { From c2619a1370da85fbba3f5dd153d816eb5d4a19c6 Mon Sep 17 00:00:00 2001 From: Renovate Date: Mon, 3 Jul 2023 11:37:48 +0300 Subject: [PATCH 24/28] Update dependency pillow to v10 (#29) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Update | Change | |---|---|---| | [pillow](https://python-pillow.org) ([source](https://github.com/python-pillow/Pillow), [changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)) | major | `~=9.4.0` -> `~=10.0.0` | --- ### Release Notes
python-pillow/Pillow ### [`v10.0.0`](https://github.com/python-pillow/Pillow/blob/HEAD/CHANGES.rst#​1000-2023-07-01) [Compare Source](https://github.com/python-pillow/Pillow/compare/9.5.0...10.0.0) - Fixed deallocating mask images [#​7246](https://github.com/python-pillow/Pillow/issues/7246) \[radarhere] - Added ImageFont.MAX_STRING_LENGTH [#​7244](https://github.com/python-pillow/Pillow/issues/7244) \[radarhere, hugovk] - Fix Windows build with pyproject.toml [#​7230](https://github.com/python-pillow/Pillow/issues/7230) \[hugovk, nulano, radarhere] - Do not close provided file handles with libtiff [#​7199](https://github.com/python-pillow/Pillow/issues/7199) \[radarhere] - Convert to HSV if mode is HSV in getcolor() [#​7226](https://github.com/python-pillow/Pillow/issues/7226) \[radarhere] - Added alpha_only argument to getbbox() [#​7123](https://github.com/python-pillow/Pillow/issues/7123) \[radarhere. hugovk] - Prioritise speed in *repr_png* [#​7242](https://github.com/python-pillow/Pillow/issues/7242) \[radarhere] - Do not use CFFI access by default on PyPy [#​7236](https://github.com/python-pillow/Pillow/issues/7236) \[radarhere] - Limit size even if one dimension is zero in decompression bomb check [#​7235](https://github.com/python-pillow/Pillow/issues/7235) \[radarhere] - Use --config-settings instead of deprecated --global-option [#​7171](https://github.com/python-pillow/Pillow/issues/7171) \[radarhere] - Better C integer definitions [#​6645](https://github.com/python-pillow/Pillow/issues/6645) \[Yay295, hugovk] - Fixed finding dependencies on Cygwin [#​7175](https://github.com/python-pillow/Pillow/issues/7175) \[radarhere] - Changed grabclipboard() to use PNG instead of JPG compression on macOS [#​7219](https://github.com/python-pillow/Pillow/issues/7219) \[abey79, radarhere] - Added in_place argument to ImageOps.exif_transpose() [#​7092](https://github.com/python-pillow/Pillow/issues/7092) \[radarhere] - Fixed calling putpalette() on L and LA images before load() [#​7187](https://github.com/python-pillow/Pillow/issues/7187) \[radarhere] - Fixed saving TIFF multiframe images with LONG8 tag types [#​7078](https://github.com/python-pillow/Pillow/issues/7078) \[radarhere] - Fixed combining single duration across duplicate APNG frames [#​7146](https://github.com/python-pillow/Pillow/issues/7146) \[radarhere] - Remove temporary file when error is raised [#​7148](https://github.com/python-pillow/Pillow/issues/7148) \[radarhere] - Do not use temporary file when grabbing clipboard on Linux [#​7200](https://github.com/python-pillow/Pillow/issues/7200) \[radarhere] - If the clipboard fails to open on Windows, wait and try again [#​7141](https://github.com/python-pillow/Pillow/issues/7141) \[radarhere] - Fixed saving multiple 1 mode frames to GIF [#​7181](https://github.com/python-pillow/Pillow/issues/7181) \[radarhere] - Replaced absolute PIL import with relative import [#​7173](https://github.com/python-pillow/Pillow/issues/7173) \[radarhere] - Replaced deprecated Py_FileSystemDefaultEncoding for Python >= 3.12 [#​7192](https://github.com/python-pillow/Pillow/issues/7192) \[radarhere] - Improved wl-paste mimetype handling in ImageGrab [#​7094](https://github.com/python-pillow/Pillow/issues/7094) \[rrcgat, radarhere] - Added *repr_jpeg*() for IPython display_jpeg [#​7135](https://github.com/python-pillow/Pillow/issues/7135) \[n3011, radarhere, nulano] - Use "/sbin/ldconfig" if ldconfig is not found [#​7068](https://github.com/python-pillow/Pillow/issues/7068) \[radarhere] - Prefer screenshots using XCB over gnome-screenshot [#​7143](https://github.com/python-pillow/Pillow/issues/7143) \[nulano, radarhere] - Fixed joined corners for ImageDraw rounded_rectangle() odd dimensions [#​7151](https://github.com/python-pillow/Pillow/issues/7151) \[radarhere] - Support reading signed 8-bit TIFF images [#​7111](https://github.com/python-pillow/Pillow/issues/7111) \[radarhere] - Added width argument to ImageDraw regular_polygon [#​7132](https://github.com/python-pillow/Pillow/issues/7132) \[radarhere] - Support I mode for ImageFilter.BuiltinFilter [#​7108](https://github.com/python-pillow/Pillow/issues/7108) \[radarhere] - Raise error from stderr of Linux ImageGrab.grabclipboard() command [#​7112](https://github.com/python-pillow/Pillow/issues/7112) \[radarhere] - Added unpacker from I;16B to I;16 [#​7125](https://github.com/python-pillow/Pillow/issues/7125) \[radarhere] - Support float font sizes [#​7107](https://github.com/python-pillow/Pillow/issues/7107) \[radarhere] - Use later value for duplicate xref entries in PdfParser [#​7102](https://github.com/python-pillow/Pillow/issues/7102) \[radarhere] - Load before getting size in **getstate** [#​7105](https://github.com/python-pillow/Pillow/issues/7105) \[bigcat88, radarhere] - Fixed type handling for include and lib directories [#​7069](https://github.com/python-pillow/Pillow/issues/7069) \[adisbladis, radarhere] - Remove deprecations for Pillow 10.0.0 [#​7059](https://github.com/python-pillow/Pillow/issues/7059), [#​7080](https://github.com/python-pillow/Pillow/issues/7080) \[hugovk, radarhere] - Drop support for soon-EOL Python 3.7 [#​7058](https://github.com/python-pillow/Pillow/issues/7058) \[hugovk, radarhere] ### [`v9.5.0`](https://github.com/python-pillow/Pillow/blob/HEAD/CHANGES.rst#​950-2023-04-01) [Compare Source](https://github.com/python-pillow/Pillow/compare/9.4.0...9.5.0) - Added ImageSourceData to TAGS_V2 [#​7053](https://github.com/python-pillow/Pillow/issues/7053) \[radarhere] - Clear PPM half token after use [#​7052](https://github.com/python-pillow/Pillow/issues/7052) \[radarhere] - Removed absolute path to ldconfig [#​7044](https://github.com/python-pillow/Pillow/issues/7044) \[radarhere] - Support custom comments and PLT markers when saving JPEG2000 images [#​6903](https://github.com/python-pillow/Pillow/issues/6903) \[joshware, radarhere, hugovk] - Load before getting size in **array_interface** [#​7034](https://github.com/python-pillow/Pillow/issues/7034) \[radarhere] - Support creating BGR;15, BGR;16 and BGR;24 images, but drop support for BGR;32 [#​7010](https://github.com/python-pillow/Pillow/issues/7010) \[radarhere] - Consider transparency when applying APNG blend mask [#​7018](https://github.com/python-pillow/Pillow/issues/7018) \[radarhere] - Round duration when saving animated WebP images [#​6996](https://github.com/python-pillow/Pillow/issues/6996) \[radarhere] - Added reading of JPEG2000 comments [#​6909](https://github.com/python-pillow/Pillow/issues/6909) \[radarhere] - Decrement reference count [#​7003](https://github.com/python-pillow/Pillow/issues/7003) \[radarhere, nulano] - Allow libtiff_support_custom_tags to be missing [#​7020](https://github.com/python-pillow/Pillow/issues/7020) \[radarhere] - Improved I;16N support [#​6834](https://github.com/python-pillow/Pillow/issues/6834) \[radarhere] - Added QOI reading [#​6852](https://github.com/python-pillow/Pillow/issues/6852) \[radarhere, hugovk] - Added saving RGBA images as PDFs [#​6925](https://github.com/python-pillow/Pillow/issues/6925) \[radarhere] - Do not raise an error if os.environ does not contain PATH [#​6935](https://github.com/python-pillow/Pillow/issues/6935) \[radarhere, hugovk] - Close OleFileIO instance when closing or exiting FPX or MIC [#​7005](https://github.com/python-pillow/Pillow/issues/7005) \[radarhere] - Added **int** to IFDRational for Python >= 3.11 [#​6998](https://github.com/python-pillow/Pillow/issues/6998) \[radarhere] - Added memoryview support to Dib.frombytes() [#​6988](https://github.com/python-pillow/Pillow/issues/6988) \[radarhere, nulano] - Close file pointer copy in the libtiff encoder if still open [#​6986](https://github.com/python-pillow/Pillow/issues/6986) \[fcarron, radarhere] - Raise an error if ImageDraw co-ordinates are incorrectly ordered [#​6978](https://github.com/python-pillow/Pillow/issues/6978) \[radarhere] - Added "corners" argument to ImageDraw rounded_rectangle() [#​6954](https://github.com/python-pillow/Pillow/issues/6954) \[radarhere] - Added memoryview support to frombytes() [#​6974](https://github.com/python-pillow/Pillow/issues/6974) \[radarhere] - Allow comments in FITS images [#​6973](https://github.com/python-pillow/Pillow/issues/6973) \[radarhere] - Support saving PDF with different X and Y resolutions [#​6961](https://github.com/python-pillow/Pillow/issues/6961) \[jvanderneutstulen, radarhere, hugovk] - Fixed writing int as UNDEFINED tag [#​6950](https://github.com/python-pillow/Pillow/issues/6950) \[radarhere] - Raise an error if EXIF data is too long when saving JPEG [#​6939](https://github.com/python-pillow/Pillow/issues/6939) \[radarhere] - Handle more than one directory returned by pkg-config [#​6896](https://github.com/python-pillow/Pillow/issues/6896) \[sebastic, radarhere] - Do not retry past formats when loading all formats for the first time [#​6902](https://github.com/python-pillow/Pillow/issues/6902) \[radarhere] - Do not retry specified formats if they failed when opening [#​6893](https://github.com/python-pillow/Pillow/issues/6893) \[radarhere] - Do not unintentionally load TIFF format at first [#​6892](https://github.com/python-pillow/Pillow/issues/6892) \[radarhere] - Stop reading when EPS line becomes too long [#​6897](https://github.com/python-pillow/Pillow/issues/6897) \[radarhere] - Allow writing IFDRational to BYTE tag [#​6890](https://github.com/python-pillow/Pillow/issues/6890) \[radarhere] - Raise ValueError for BoxBlur filter with negative radius [#​6874](https://github.com/python-pillow/Pillow/issues/6874) \[hugovk, radarhere] - Support arbitrary number of loaded modules on Windows [#​6761](https://github.com/python-pillow/Pillow/issues/6761) \[javidcf, radarhere, nulano]
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). Co-authored-by: Renovate Reviewed-on: https://git.end-play.xyz/profitroll/TelegramPoster/pulls/29 Co-authored-by: Renovate Co-committed-by: Renovate --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index bee2675..432c156 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ aiohttp~=3.8.4 black~=23.3.0 convopyro==0.5 -pillow~=9.4.0 +pillow~=10.0.0 psutil~=5.9.4 pykeyboard==0.1.5 pymongo~=4.4.0 From f7df4d8ddc6d706ab0dd2e93eea1110f98b25859 Mon Sep 17 00:00:00 2001 From: profitroll Date: Mon, 3 Jul 2023 11:04:39 +0200 Subject: [PATCH 25/28] Bump libbot to 1.8 --- config_example.json | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config_example.json b/config_example.json index a8ecfc0..796082e 100644 --- a/config_example.json +++ b/config_example.json @@ -18,7 +18,7 @@ "name": "tgposter" }, "reports": { - "chat_id": 0, + "chat_id": "owner", "sent": false, "error": true, "update": true, diff --git a/requirements.txt b/requirements.txt index 432c156..5b5c163 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,5 +11,5 @@ pytimeparse~=1.1.8 tgcrypto==1.2.5 uvloop==0.17.0 --extra-index-url https://git.end-play.xyz/api/packages/profitroll/pypi/simple -libbot[speed,pyrogram]==1.7 +libbot[speed,pyrogram]==1.8 photosapi_client==0.5.0 \ No newline at end of file From 987f6425784d63159f80ec4ab6a154fecf3c5de8 Mon Sep 17 00:00:00 2001 From: profitroll Date: Mon, 3 Jul 2023 11:27:15 +0200 Subject: [PATCH 26/28] CLI is back and updated --- main.py | 4 ++ modules/api_client.py | 2 + modules/cli.py | 117 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+) create mode 100644 modules/cli.py diff --git a/main.py b/main.py index c590bca..f350574 100644 --- a/main.py +++ b/main.py @@ -4,6 +4,10 @@ from os import getpid from convopyro import Conversation +# This import MUST be done earlier than PyroClient! +# Even if isort does not like it... +from modules.cli import * + from classes.pyroclient import PyroClient from modules.scheduler import scheduler diff --git a/modules/api_client.py b/modules/api_client.py index 211e290..bc64ea0 100644 --- a/modules/api_client.py +++ b/modules/api_client.py @@ -126,6 +126,7 @@ unauthorized_client = Client( timeout=5.0, verify_ssl=True, raise_on_unexpected_status=True, + follow_redirects=False, ) login_token = login( @@ -150,6 +151,7 @@ client = AuthenticatedClient( verify_ssl=True, raise_on_unexpected_status=True, token=login_token.access_token, + follow_redirects=False, ) if __name__ == "__main__": diff --git a/modules/cli.py b/modules/cli.py new file mode 100644 index 0000000..e21ee0e --- /dev/null +++ b/modules/cli.py @@ -0,0 +1,117 @@ +import asyncio +from argparse import ArgumentParser +from sys import exit +from traceback import print_exc + +from libbot import config_get, config_set, sync +from photosapi_client.api.default.album_create_albums_post import ( + asyncio as album_create, +) +from photosapi_client.api.default.login_for_access_token_token_post import ( + asyncio as login, +) +from photosapi_client.api.default.user_create_users_post import asyncio as user_create +from photosapi_client.client import AuthenticatedClient, Client +from photosapi_client.models.body_login_for_access_token_token_post import ( + BodyLoginForAccessTokenTokenPost, +) +from photosapi_client.models.body_user_create_users_post import BodyUserCreateUsersPost + +parser = ArgumentParser( + prog="Telegram Poster", + description="Bot for posting some of your stuff and also receiving submissions.", +) + +parser.add_argument("--create-user", action="store_true") +parser.add_argument("--create-album", action="store_true") + +args = parser.parse_args() + +unauthorized_client = Client( + base_url=sync.config_get("address", "posting", "api"), + timeout=5.0, + verify_ssl=True, + raise_on_unexpected_status=True, + follow_redirects=False, +) + + +async def cli_create_user() -> None: + print( + "To set up Photos API connection you need to create a new user.\nIf you have email confirmation enabled in your Photos API config - you need to use a real email that will get a confirmation code afterwards.", + flush=True, + ) + username = input("Choose username for new Photos API user: ").strip() + email = input(f"Choose email for user '{username}': ").strip() + password = input(f"Choose password for user '{username}': ").strip() + try: + result_1 = await user_create( + client=unauthorized_client, + form_data=BodyUserCreateUsersPost( + user=username, email=email, password=password + ), + ) + # asyncio.run(create_user(username, email, password)) + await config_set("username", username, "posting", "api") + await config_set("password", password, "posting", "api") + none = input( + "Alright. If you have email confirmation enabled - please confirm registration by using the link in your email. After that press Enter. Otherwise just press Enter." + ) + except Exception as exp: + print(f"Could not create a user due to {exp}", flush=True) + print_exc() + exit() + if not args.create_album: + print("You're done!", flush=True) + exit() + return None + + +async def cli_create_album() -> None: + print( + "To use Photos API your user needs to have an album to store its data.\nThis wizard will help you to create a new album with its name and title.", + flush=True, + ) + name = input("Choose a name for your album: ").strip() + title = input(f"Choose a title for album '{name}': ").strip() + try: + login_token = await login( + client=unauthorized_client, + form_data=BodyLoginForAccessTokenTokenPost( + grant_type="password", + scope="me albums.list albums.read albums.write photos.list photos.read photos.write videos.list videos.read videos.write", + username=await config_get("username", "posting", "api"), + password=await config_get("password", "posting", "api"), + ), + ) + client = AuthenticatedClient( + base_url=await config_get("address", "posting", "api"), + timeout=5.0, + verify_ssl=True, + raise_on_unexpected_status=True, + token=login_token.access_token, + follow_redirects=False, + ) + + result_2 = await album_create(client=client, name=name, title=title) + # asyncio.run(create_album(name, title)) + await config_set("album", name, "posting", "api") + except Exception as exp: + print(f"Could not create an album due to {exp}", flush=True) + print_exc() + exit() + print("You're done!", flush=True) + exit() + + +if args.create_user or args.create_album: + loop = asyncio.get_event_loop() + tasks = [] + + if args.create_user: + loop.run_until_complete(asyncio.wait([loop.create_task(cli_create_user())])) + + if args.create_album: + loop.run_until_complete(asyncio.wait([loop.create_task(cli_create_album())])) + + loop.close() From dc774262f8dfca7f40ccf1656ef7e5af722ee35f Mon Sep 17 00:00:00 2001 From: profitroll Date: Mon, 3 Jul 2023 11:42:28 +0200 Subject: [PATCH 27/28] Locale for console is gone for good --- config_example.json | 1 - locale/en.json | 44 --------------------------------- locale/uk.json | 44 --------------------------------- modules/api_client.py | 13 +++------- modules/sender.py | 19 ++++++++------ plugins/callbacks/submission.py | 42 ++++++++----------------------- 6 files changed, 25 insertions(+), 138 deletions(-) diff --git a/config_example.json b/config_example.json index 796082e..1f9671f 100644 --- a/config_example.json +++ b/config_example.json @@ -1,6 +1,5 @@ { "locale": "en", - "locale_log": "en", "bot": { "owner": 0, "admins": [], diff --git a/locale/en.json b/locale/en.json index 50ae6fb..cb1c080 100644 --- a/locale/en.json +++ b/locale/en.json @@ -101,49 +101,5 @@ "sub_duplicates_found": "There're duplicates in bot's database", "locale_set": "Your language now is: {locale}", "nothing": "🏁 This action is already finished" - }, - "console": { - "shutdown": "Shutting down bot with pid {0}", - "startup": "Starting with pid {0}", - "keyboard_interrupt": "\nShutting down...", - "exception_occured": "Exception {0} happened on task execution", - "post_sent": "Sent {0} to {1} with caption {2} and silently {3}", - "post_exception": "Could not send content due to {0}. Traceback: {1}", - "post_invalid_pic": "Error while sending photo HTTP {0}: {1}", - "post_empty": "Could not send content due to queue empty or contains only forbidden extensions", - "sub_mime_not_allowed": "Got submission from {0} but type of {1} which is not allowed", - "sub_document_too_large": "Got submission from {0} but but file is too large ({1} > {2})", - "sub_received": "Got submission from {0} with a caption {1}", - "sub_cooldown": "Got submission from {0} but user is on a cooldown", - "sub_no_id": "from_user in function get_submission does not contain id (maybe user posted in a channel)", - "sub_msg_unavail": "Could not download submission {0} from user {1}: message not available", - "sub_media_unavail": "Could not download submission {0} from user {1}: media not available", - "sub_media_downloading": "Downloading media of submission {0} from user {1}...", - "sub_media_downloaded": "Downloaded media of submission {0} from user {1}", - "sub_accepted": "Accepted submission {0} from user {1}", - "sub_declined": "Declined submission {0} from user {1}", - "sub_blocked": "Blocked user {0}", - "sub_unblocked": "Unblocked user {0}", - "deps_missing": "Required modules are not installed. Run 'pip3 install -r requirements.txt' and restart the program.", - "passed_norun": "Argument --norun passed, not running the main script", - "move_sent_doesnt_exist": "File '{0}' is already moved or does not exist", - "move_sent_doesnt_exception": "Could not move sent file '{0}' to '{1}' due to {2}", - "move_sent_completed": "Moved all sent files to the sent folder", - "cleanup_exception": "Could not remove '{0}' due to {1}", - "cleanup_completed": "Performed cleanup of the sent files", - "cleanup_unathorized": "Requested cleanup of sent files but not authorized. Please pass '--confirm' to perform that", - "cleanup_index_completed": "Performed cleanup of sent files index", - "cleanup_index_unathorized": "Requested cleanup of sent files index but not authorized. Please pass '--confirm' to perform that", - "random_pic_response": "Random pic response: {0}", - "random_pic_error_code": "Could not get photos from album {0}: HTTP {1}", - "random_pic_error_debug": "Could not get photos from '{0}/albums/{1}/photos?q=&page_size={2}&caption=queue' using token '{3}': HTTP {4}", - "find_pic_error": "Could not find image with name '{0}' and caption '{1}' due to: {2}", - "pic_upload_error": "Could not upload '{0}' to API: HTTP {1} with message '{2}'", - "api_creds_invalid": "Incorrect API credentials! Could not login into '{0}' using login '{1}': HTTP {2}", - "user_blocked": "User {0} has been blocked", - "user_unblocked": "User {0} has been unblocked", - "submission_accepted": "Submission with ID '{0}' accepted and uploaded with ID '{1}'", - "submission_rejected": "Submission with ID '{0}' rejected", - "submission_duplicate": "Submission with ID '{0}' could not be accepted because of the duplicates: {1}" } } \ No newline at end of file diff --git a/locale/uk.json b/locale/uk.json index 6e562c6..4ea1778 100644 --- a/locale/uk.json +++ b/locale/uk.json @@ -100,49 +100,5 @@ "sub_duplicates_found": "Знайдено дублікати в базі даних бота", "locale_set": "Встановлено мову: {locale}", "nothing": "🏁 Цю дію вже було завершено" - }, - "console": { - "shutdown": "Вимкнення бота з підом {0}", - "startup": "Запуск бота з підом {0}", - "keyboard_interrupt": "\nВимикаюсь...", - "exception_occured": "Помилка {0} сталась під час виконання", - "post_sent": "Надіслано {0} у {1} з підписом {2} та без звуку {3}", - "post_exception": "Не вдалося надіслати контент через {0}. Traceback: {1}", - "post_invalid_pic": "Помилка надсилання фото HTTP {0}: {1}", - "post_empty": "Не вдалося надіслати контент адже черга з дозволеними розширеннями порожня", - "sub_mime_not_allowed": "Отримано подання від {0} але типу {1} який не є дозволеним", - "sub_document_too_large": "Отримано подання від {0} але файл завеликий({1} > {2})", - "sub_received": "Отримано подання від {0} з підписом {1}", - "sub_cooldown": "Отримано подання від {0} але користувач на тайм-ауті", - "sub_no_id": "from_user у функції get_submission не має атрибуту id (можливо, користувач запостив щось у канал)", - "sub_msg_unavail": "Не вдалося завантажити подання {0} від користувача {1}: повідомлення більше не існує", - "sub_media_unavail": "Не вдалося завантажити подання {0} від користувача {1}: медіафайл більше не існує", - "sub_media_downloading": "Завантажуємо медіа з подання {0} від користувача{1}...", - "sub_media_downloaded": "Завантажено медіа з подання{0} від користувача{1}", - "sub_accepted": "Прийнято подання {0} від користувача {1}", - "sub_declined": "Відхилено подання {0} від користувача {1}", - "sub_blocked": "Заблоковано користувача {0}", - "sub_unblocked": "Розблоковано користувача {0}", - "deps_missing": "Необхідні модулі не встановлені. Запустіть 'pip3 install -r requirements.txt' і перезапустіть програму.", - "passed_norun": "Аргумент --norun надано, основний скрипт не запускається", - "move_sent_doesnt_exist": "Файл '{0}' уже переміщено або він не існує", - "move_sent_doesnt_exception": "Неможливо перемістити надісланий файл '{0}' до '{1}' через {2}", - "move_sent_completed": "Переміщено всі надіслані файли до папки надісланих", - "cleanup_exception": "Не вдалося видалити '{0}' через {1}", - "cleanup_completed": "Виконано очищення надісланих файлів", - "cleanup_unathorized": "Надіслано запит на очищення надісланих файлів, але не авторизовано. Для цього надайте аргумент '--confirm'", - "cleanup_index_completed": "Виконано очищення індексу надісланих файлів", - "cleanup_index_unathorized": "Надіслано запит на очищення індексу надісланих файлів, але не авторизовано. Для цього надайте аргумент '--confirm'", - "random_pic_response": "Відповідь на пошук випадкової картинки: {0}", - "random_pic_error_code": "Не вдалося отримати фото з альбому {0}: HTTP {1}", - "random_pic_error_debug": "Не вдалося отримати фотографії з '{0}/albums/{1}/photos?q=&page_size={2}&caption=queue', використовуючи токен '{3}': HTTP {4}", - "find_pic_error": "Не вдалося знайти зображення з назвою '{0}' та підписом '{1}' через: {2}", - "pic_upload_error": "Не вдалося завантажити '{0}' до API: HTTP {1} з повідомленням '{2}'", - "api_creds_invalid": "Невірні облікові дані API! Не вдалося увійти в '{0}' за допомогою логіна '{1}': HTTP {2}", - "user_blocked": "Користувача {0} було заблоковано", - "user_unblocked": "Користувача {0} було розблоковано", - "submission_accepted": "Подання з ID '{0}' прийнято та завантажено з ID '{1}'", - "submission_rejected": "Подання з ID '{0}' відхилено", - "submission_duplicate": "Подання з ID '{0}' не може бути прийнято через наявність дублікатів: {1}" } } \ No newline at end of file diff --git a/modules/api_client.py b/modules/api_client.py index bc64ea0..ca08626 100644 --- a/modules/api_client.py +++ b/modules/api_client.py @@ -100,15 +100,10 @@ async def authorize(custom_session: Union[ClientSession, None] = None) -> str: ) if not response.ok: logger.warning( - i18n._( - "api_creds_invalid", - "console", - locale=(await config_get("locale_log")).format( - await config_get("address", "posting", "api"), - await config_get("username", "posting", "api"), - response.status, - ), - ) + "Incorrect API credentials! Could not login into '%s' using login '%s': HTTP %s", + await config_get("address", "posting", "api"), + await config_get("username", "posting", "api"), + response.status, ) raise ValueError async with aiofiles.open( diff --git a/modules/sender.py b/modules/sender.py index 9455ca9..0d9af0a 100644 --- a/modules/sender.py +++ b/modules/sender.py @@ -111,7 +111,9 @@ async def send_content(app: PyroClient, http_session: ClientSession) -> None: return except (KeyError, AttributeError, TypeError, IndexError): - logger.info(app._("post_empty", "console")) + logger.info( + "Could not send content due to queue empty or contains only forbidden extensions" + ) if app.config["reports"]["error"]: await app.send_message( app.owner, @@ -244,16 +246,17 @@ async def send_content(app: PyroClient, http_session: ClientSession) -> None: rmtree(path.join(app.config["locations"]["tmp"], tmp_dir), ignore_errors=True) logger.info( - app._("post_sent", "console").format( - media.id, - str(app.config["posting"]["channel"]), - caption.replace("\n", "%n"), - str(app.config["posting"]["silent"]), - ) + "Sent %s to %s with caption %s and silently %s", + media.id, + str(app.config["posting"]["channel"]), + caption.replace("\n", "%n"), + str(app.config["posting"]["silent"]), ) except Exception as exp: - logger.error(app._("post_exception", "console").format(str(exp), format_exc())) + logger.error( + "Could not send content due to %s. Traceback: %s", exp, format_exc() + ) if app.config["reports"]["error"]: await app.send_message( app.owner, diff --git a/plugins/callbacks/submission.py b/plugins/callbacks/submission.py index a113805..47f70a0 100644 --- a/plugins/callbacks/submission.py +++ b/plugins/callbacks/submission.py @@ -55,14 +55,9 @@ async def callback_query_yes(app: PyroClient, clb: CallbackQuery): quote=True, ) logger.info( - app._( - "submission_duplicate", - "console", - locale=app.config["locale_log"], - ).format( - fullclb[2], - str(exp.duplicates), - ), + "Submission with ID '%s' could not be accepted because of the duplicates: %s", + fullclb[2], + str(exp.duplicates), ) return @@ -121,11 +116,9 @@ async def callback_query_yes(app: PyroClient, clb: CallbackQuery): ) logger.info( - app._( - "submission_accepted", - "console", - locale=app.config["locale_log"], - ).format(fullclb[2], submission[1]), + "Submission with ID '%s' accepted and uploaded with ID '%s'", + fullclb[2], + submission[1], ) @@ -197,11 +190,8 @@ async def callback_query_no(app: PyroClient, clb: CallbackQuery): reply_markup=InlineKeyboardMarkup(edited_markup) ) logger.info( - app._( - "submission_rejected", - "console", - locale=app.config["locale_log"], - ).format(fullclb[2]), + "Submission with ID '%s' rejected", + fullclb[2], ) @@ -237,13 +227,7 @@ async def callback_query_block(app: PyroClient, clb: CallbackQuery): await clb.message.edit_reply_markup( reply_markup=InlineKeyboardMarkup(edited_markup) ) - logger.info( - app._( - "user_blocked", - "console", - locale=app.config["locale_log"], - ).format(fullclb[2]), - ) + logger.info("User %s has been blocked", fullclb[2]) @Client.on_callback_query(filters.regex("sub_unblock_[\s\S]*")) @@ -279,10 +263,4 @@ async def callback_query_unblock(app: PyroClient, clb: CallbackQuery): await clb.message.edit_reply_markup( reply_markup=InlineKeyboardMarkup(edited_markup) ) - logger.info( - app._( - "user_unblocked", - "console", - locale=app.config["locale_log"], - ).format(fullclb[2]), - ) + logger.info("User %s has been unblocked", fullclb[2]) From a7e79eb254e8eb709be1861a1744b2f248421236 Mon Sep 17 00:00:00 2001 From: profitroll Date: Mon, 3 Jul 2023 12:34:55 +0200 Subject: [PATCH 28/28] Updated README --- README.md | 32 +++++++------- README_uk.md | 121 +++++++++++++++++++++++++++++++++------------------ 2 files changed, 95 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 4a0058f..41ae074 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@

TelegramPoster

-License: GPL +License: GPL Code style: black

-> Шукаєш інструкцію українською? А вона [ось тут](https://git.end-play.xyz/profitroll/TelegramPoster/src/branch/dev/README_uk.md) знаходиться) +> Шукаєш інструкцію українською? А вона [ось тут](https://git.end-play.xyz/profitroll/TelegramPoster/src/branch/master/README_uk.md) знаходиться) This bot is used for one and only task - post pictures from my personal archive. Here's its source code so you can also host a bot and have fun with it. Just don't exepect it to be brilliant. It is not. But hey, you can always fork it ;) @@ -21,7 +21,7 @@ Please note that Photos API also requires MongoDB so it makes sense to install a ## Installation -To make this bot run at first you need to have a Python interpreter, Photos API, MongoDB and optionally git. You can also ignore git and simply download source code, should also work fine. After that you're ready to go. +To make this bot run at first you need to have a Python interpreter, Photos API, MongoDB and optionally git (if you want to update using `git pull`). You can also ignore git and simply download source code, should also work fine. After that you're ready to go. > In this README I assume that you're using default python in your > system and your system's PATH contains it. If your default python @@ -29,7 +29,7 @@ To make this bot run at first you need to have a Python interpreter, Photos API, > If it's non-standard executable path - you should also change > it in scripts you will use (`loop.sh`, `loop.bat`, `start.sh` and `start.bat`). -1. Install Mongo and Photos API: +1. Install MongoDB and Photos API: 1. Install MongoDB by following [official installation manual](https://www.mongodb.com/docs/manual/installation) 2. Install Photos API by following [Photos API's README](https://git.end-play.xyz/profitroll/PhotosAPI/src/branch/master/README.md) @@ -42,19 +42,19 @@ To make this bot run at first you need to have a Python interpreter, Photos API, 3. Create virtual environment [Optional]: 1. Install virtualenv module: `pip install virtualenv` - 2. Create venv: `python -m venv env` - 3. Activate it using `source venv/bin/activate` on Linux, `venv\Scripts\activate.bat` in CMD or `venv\Scripts\Activate.ps1` in PowerShell. + 2. Create venv: `python -m venv .venv` + 3. Activate it using `source .venv/bin/activate` on Linux, `.venv\Scripts\activate.bat` in CMD or `.venv\Scripts\Activate.ps1` in PowerShell. 4. Install project's dependencies: `python -m pip install -r requirements.txt` Without installing those - bot cannot work at all. -5. Configure "bot" and "owner" with your favorite text editor: +5. Configure required keys with your favorite text editor: - 1. Copy file `config_example.json` to `config.json` - 2. Open `config.json` using your favorite text editor. For example `nano config.json`, but you can edit with vim, nano, on Windows it's Notepad or Notepad++. Whatever - 3. Change `"bot.owner"`, `"reports.chat_id"`, `"bot.api_id"`, `"bot.api_hash"` and `"bot.bot_token"` keys' values. + 1. Copy config file: `cp config_example.json config.json` + 2. Open `config.json` using your favorite text editor. For example `nano config.json`, but you can also edit it with vim, mcedit, or Notepad/Notepad++ on Windows + 3. Change `"bot.owner"`, `"bot.api_id"`, `"bot.api_hash"` and `"bot.bot_token"` keys' values. If you don't know where to find bot_token and your id - here you can find some hints: [get bot token](https://www.siteguarding.com/en/how-to-get-telegram-bot-api-token), [get your id](https://www.alphr.com/telegram-find-user-id), [get api_hash and api_id](https://core.telegram.org/api/obtaining_api_id). @@ -66,26 +66,26 @@ To make this bot run at first you need to have a Python interpreter, Photos API, 3. If you've changed user and password to access the db, you should also change `"database.user"` and `"database.password"` keys, otherwise leave them `null` (default). 2. Configure Photos API: - 1. Change `"posting.api.address"` to the one your API servers uses + 1. Change `"posting.api.address"` and `"posting.api.address_external"` to the ones your API server uses 2. Run your bot using `python main.py --create-user --create-album` to configure its new user and album. You can also use manual user and album creation described [in the wiki](https://git.end-play.xyz/profitroll/TelegramPoster/wiki/Configuring-API). You can also change username, password and album in`"posting.api"` to the user and album you have if you already have Photos API album and user set up. In that case you don't need to create a new one. 7. Add bot to the channel: - To use your bot of course you need to have a channel or group otherwise makes no sense to have such a bot. [Here](https://stackoverflow.com/a/33497769) you can find a quick guide how to add your bot to a channel. After that simply set `"posting.channel"` to your channel's ID and `"posting.comments"` to comments group's ID. + To use your bot of course you need to have a channel or group otherwise it makes no sense to have such a bot. [Here](https://stackoverflow.com/a/33497769) you can find a quick guide how to add your bot to a channel. After that simply set `"posting.channel"` to your channel's ID and `"posting.comments"` to comments group's ID. 8. Configure posting time: - To make your bot post random content you need to configure `"posting.time"` with a list of "DD:MM" formatted strings or use `"posting.interval"` formatted as "XdXhXmXs". To use interval instead of selected time set `"posting.use_interval"` to `true`. + To make your bot post random content you need to configure `"posting.time"` with a list of "DD:MM" formatted strings or use `"posting.interval"` formatted as "XdXhXmXs". To use interval instead of selected time, set `"posting.use_interval"` to `true`. 9. Good to go, run it! - Make sure MongoDB and Photos API are running and use `python main.py` to start it. + Make sure MongoDB and Photos API are running and use `python main.py` to start the bot. Or you can also use `.\start.bat` on Windows and `bash ./start.sh` on Linux. Additionally there are `loop.sh` and `loop.bat` available if you want your bot to start again after being stopped or after using `/shutdown` command. If you need any further instructions on how to configure your bot or you had any difficulties doing so - please use [wiki in this repository](https://git.end-play.xyz/profitroll/TelegramPoster/wiki) to get more detailed instructions. -## Command line arguments +## CLI arguments Of course bot also has them. You can perform some actions with them. @@ -105,6 +105,6 @@ Examples: Bot is capable of using custom locales. There are some that are pre-installed (English and Ukrainian), however you can add your own locales too. -All localization files are located in the `locale` folder, otherwise in folder specified in config file. Just copy locale file of your choice, name it in accordance to [IETF language tags](https://en.wikipedia.org/wiki/IETF_language_tag) (if you want your locale to be compatible with Telegram's locales) or define your own name. Save it as json and you're good to go. If you want to change default locale for messages, that cannot determine admin's locale - edit `"locale"` parameter in the `config.json`. If this locale is not available - `"locale_fallback"` will be used instead. If both are not available - error will be shown. For console output and logging locale you should edit `"locale_log"`. +All localization files are located in the `locale`. Just copy locale file of your choice, name it in accordance to [IETF language tags](https://en.wikipedia.org/wiki/IETF_language_tag) (if you want your locale to be compatible with Telegram's locales) or define your own name. Save it as json and you're good to go. If you want to change default locale for messages - edit `"locale"` parameter in the `config.json`. We recommend to only make changes to your custom locale. Or at least always have your backup of for example `en.json` as your fallback. diff --git a/README_uk.md b/README_uk.md index 28e7a29..7ea4dc8 100644 --- a/README_uk.md +++ b/README_uk.md @@ -5,67 +5,104 @@ Code style: black

-## ⚠️ Українська версія README dev гілки ще не готова! Користуйтесь англійською! ⚠️ +Цей бот використовується для однієї-єдиної задачі - публікувати фотографії з мого особистого архіву. Ось його код, тож ви також можете запустити бота і погратися з ним самостійно. Тільки не очікуйте, що він буде ідеальним. Це не так. Але ви завжди можете його форкнути ;) -Цей бот використовується для однієї-єдиної задачі - розміщувати фотографії з мого особистого архіву. Ось його код, тож Ви також можете захостити бота самостійно та розважитися з ним. Тільки не очікуйте, що він ідеальним. Не буде. Але гей, Ви завжди можете його доробити під себе ;) +## Залежності -## Установка +* [Python 3.8+](https://www.python.org) (рекомендується 3.9+) +* [MongoDB](https://www.mongodb.com) +* [PhotosAPI](https://git.end-play.xyz/profitroll/PhotosAPI) -Для запуску цього бота спочатку потрібно мати інтерпретатор Python та встановлений git. Google — Ваш друг у пошуках. Ви також можете ігнорувати git і просто завантажити код, також має спрацювати добре. Після цього Ви готові до встановлення. +Користуйтесь [інструкцією зі встановлення MongoDB](https://www.mongodb.com/docs/manual/installation) та [README Photos API](https://git.end-play.xyz/profitroll/PhotosAPI/src/branch/master/README.md). -> У цьому README я вважаю, що Ви використовуєте python за замовчуванням у своїй -> системі і PATH Вашої системи містить його. Якщо Ваш python за замовчуванням -> є `python3` або, наприклад, `/home/user/.local/bin/python3.9` - використовуйте його натомість. -> Якщо це нестандартний шлях до виконуваного файлу - Вам також слід змінити -> це у скриптах, які Ви використовуватимете (`loop.sh`, `loop.bat`, `start.sh` та `start.bat`). +Зверніть увагу, що Photos API також потребує MongoDB, тому має сенс спочатку встановити й налаштувати Mongo. -1. Завантажте бота: - 1. `git clone https://git.end-play.xyz/profitroll/TelegramSender.git` (якщо хочете використовувати git) - 2. `cd ./TelegramSender` +## Встановлення -2. Встановіть залежності: - `python -m pip install -r requirements.txt` - Без їх установки бот не зможе працювати взагалі +Щоб запустити бота, вам потрібно мати інтерпретатор Python, Photos API, MongoDB і, за бажанням, git (якщо ви хочете оновлювати за допомогою `git pull`). Ви також можете проігнорувати git і просто завантажити вихідний код, це також повинно спрацювати. Після цього ви готові до роботи. -3. Встановіть додаткові залежності [Не обов'язково]: - `python -m pip install -r requirements-optional.txt` - Вони не є обов’язковими, але можуть прискорити роботу бота +> У цьому README я припускаю, що ви використовуєте python за замовчуванням у вашій +> системі, і він міститься у вашому системному PATH. Якщо ваш python за замовчуванням +> це `python3` або, наприклад, `/home/user/.local/bin/python3.9` - використовуйте його. +> Якщо це нестандартний шлях до виконуваного файлу - вам також слід змінити +> його у скриптах, які ви будете використовувати (`loop.sh`, `loop.bat`, `start.sh` та `start.bat`). -4. Налаштуйте свого бота за допомогою текстового редактора: - `nano config.json` - Ви можете редагувати за допомогою vim, nano, у Windows це Notepad або Notepad++. На Ваш смак. - Якщо Ви не знаєте, де знайти bot_token і свій ідентифікатор, тут Ви можете знайти кілька підказок: [отримати токен бота](https://www.siteguarding.com/en/how-to-get-telegram-bot-api-token), [отримати свій ідентифікатор](https://www.alphr.com/telegram-find-user-id/), [отримати api_hash і api_id](https://core.telegram.org/api/obtaining_api_id). - Також не забудьте змінити режим роботи бота. Ключ `"mode"` має в собі ключі `"post"` та `"submit"`, кожен з який може бути `true` або `false`. +1. Встановіть MongoDB та Photos API: -5. Додайте бота на канал: - Звичайно, щоб використовувати свого бота, Вам потрібно мати канал або групу, інакше немає сенсу мати такого бота. [Тут](https://stackoverflow.com/a/33497769) Ви можете знайти короткий гайд, як додати свого бота до каналу. + 1. Встановіть MongoDB, дотримуючись [офіційного посібника зі встановлення](https://www.mongodb.com/docs/manual/installation) + 2. Встановіть Photos API, дотримуючись [README Photos API](https://git.end-play.xyz/profitroll/PhotosAPI/src/branch/master/README.md) -6. Заповніть папку вмістом: - Звичайно, бот не може опублікувати щось із нічого. Налаштуйте свій `config.json`, які медіа-типи бот повинен публікувати (`"posting", "extensions"`), коли їх публікувати (`"posting", "time"`), а також де їх знайти (`"locations"`). Ви також можете переміщати їх після надсилання, встановивши для `"posting", "move_sent"` значення `true`. +2. Завантажте бота: -6. Готово, запускайте! - `python ./main.py` - Або ви також можете використовувати `.\start.bat` на Windows і `bash ./start.sh` на Linux. - Крім того, доступні `loop.sh` і `loop.bat`, якщо ви хочете, щоб ваш бот запускався знову після зупинки або після використання команди `/reboot`. + 1. `git clone https://git.end-play.xyz/profitroll/TelegramPoster.git` (якщо ви використовуєте git) + 2. `cd TelegramPoster` -## Аргументи командного рядка +3. Створіть віртуальне середовище [Необов'язково]: -Звичайно, у бота вони також є. З ними можна виконувати деякі дії. + 1. Встановіть модуль virtualenv: `pip install virtualenv` + 2. Створіть venv: `python -m venv .venv` + 3. Активуйте його за допомогою `ource .venv/bin/activate` в Linux, `.venv\Scripts\activate.bat` в CMD або `.venv\Scripts\Activate.ps1` в PowerShell. -* `--move-sent` - дозволяє перемістити всі надіслані файли з черги до папки надісланих -* `--cleanup` - очистити файли в папках `queue` і `sent`, якщо вони вже надіслані. Потрібен аргумент `--confirm` -* `--cleanup-index` - видалити всі надіслані записи з індексу. Потрібен аргумент `--confirm` -* `--norun` - дозволяє виконувати наведені вище аргументи, не запускаючи самого бота +4. Встановіть залежності проекту: + + `python -m pip install -r requirements.txt`. + Без їх встановлення бот не зможе працювати взагалі. + +5. Налаштуйте необхідні ключі за допомогою вашого улюбленого текстового редактора: + + 1. Скопіюйте конфігураційний файл: `cp config_example.json config.json` + 2. Відкрийте `config.json` за допомогою вашого улюбленого текстового редактора. Наприклад, `nano config.json`, але ви також можете відредагувати його за допомогою vim, mcedit або Notepad/Notepad++ на Windows + 3. Змініть значення ключів `"bot.owner"`, `"bot.api_id"`, `"bot.api_hash"` і `"bot.bot_token"`. + + Якщо ви не знаєте, де знайти bot_token і ваш id - тут ви можете знайти кілька підказок: [отримати токен бота](https://www.siteguarding.com/en/how-to-get-telegram-bot-api-token), [отримати свій id](https://www.alphr.com/telegram-find-user-id), [отримати api_hash та api_id](https://core.telegram.org/api/obtaining_api_id). + +6. Налаштування бази даних та API: + + 1. Налаштуйте базу даних: + 1. Змініть хост і порт бази даних у ключах `"database.host"` і `"database.port"`. Для локальної установки за замовчуванням це будуть `127.0.0.1` і `27017` відповідно + 2. Змініть ім'я бази даних в `"database.name"`. Вона буде автоматично створена при запуску + 3. Якщо ви змінили користувача та пароль для доступу до бази даних, вам також слід змінити ключі `"database.user"` та `"database.password"`, інакше залиште їх `null` (за замовчуванням). + + 2. Налаштуйте Photos API: + 1. Змініть `"posting.api.address"` та `"posting.api.address_external"` на ті, що використовує ваш сервер API + 2. Запустіть бота за допомогою `python main.py --create-user --create-album`, щоб налаштувати нового користувача та альбом. Ви також можете скористатися ручним створенням користувача і альбому, описаним [у вікі](https://git.end-play.xyz/profitroll/TelegramPoster/wiki/Configuring-API). Ви також можете змінити ім'я користувача, пароль і альбом у `"posting.api"` на користувача і альбом, які у вас є, якщо у вас вже налаштовані альбом і користувач Photos API. У цьому випадку вам не потрібно створювати нові. + +7. Додайте бота до каналу: + + Щоб використовувати бота, вам, звичайно, потрібно мати канал або групу, інакше немає сенсу мати такого бота. [Тут](https://stackoverflow.com/a/33497769) ви можете знайти короткий посібник, як додати бота до каналу. Після цього просто встановіть `"posting.channel"` на ID вашого каналу і `"posting.comments"` на ID групи коментарів. + +8. Налаштуйте час публікації: + + Щоб ваш бот публікував випадковий контент, вам потрібно налаштувати `"posting.time"` зі списком рядків у форматі "ДД:ММ" або використовувати `"posting.interval"` у форматі "XdXhXmXs". Щоб використовувати інтервал замість вибраного часу, встановіть `"posting.use_interval"` у значення `true`. + +9. Готово, запускайте! + + Переконайтеся, що MongoDB і Photos API запущені і використовуйте `python main.py` для запуску бота. + Або ви також можете використовувати `.\start.bat` в Windows і `bash ./start.sh` в Linux. + Додатково доступні `loop.sh` і `loop.bat`, якщо ви хочете, щоб ваш бот запустився знову після зупинки або після використання команди `/shutdown`. + +Якщо вам потрібні додаткові інструкції щодо налаштування бота або у вас виникли труднощі - скористайтеся [вікі в цьому репозиторії](https://git.end-play.xyz/profitroll/TelegramPoster/wiki), щоб отримати детальніші інструкції. + +## CLI аргументи + +Звичайно, бот також має CLI аргументи. За допомогою них можна виконувати деякі дії. + +* `--create-user` - створити нового користувача API. Потребує встановленого конфігураційного ключа `"posting.api.address"`; +* `--create-album` - створити новий альбом API. Вимагає заповнених адреси API та конфігурації користувача (`"posting.api"`). Приклади: -* `python3 ./main.py --move-sent --norun` -* `python3 ./main.py --cleanup --confirm` +* `python main.py --create-user` +* `python main.py --create-user --create-album` + +## Поради та покращення + +* Можливо, ви захочете налаштувати бота для роботи як системну службу. У вікі є [сторінка з цього питання](https://git.end-play.xyz/profitroll/TelegramPoster/wiki/Configuring-Service). ## Локалізація -Бот може використовувати різні мови. Є деякі попередньо встановлені (Англійська та Українська), однак Ви можете додавати свої власні локалізації теж. +Бот може використовувати файли локалізації. Деякі з них встановлено за замовчуванням (англійська та українська), але ви також можете додавати свої власні. -Всі файли локалізації знаходяться у папці `locale`, якщо в конфігураційному файлі не вказано іншу. Просто скопіюйте цікавлячий Вас файл, назвіть його відповідно до [тегів мови IETF](https://en.wikipedia.org/wiki/IETF_language_tag) (якщо Ви хочете, щоб переклад був сумісним з перекладами Telegram) або просто вкажіть свою власну назву. Збережіть свій переклад як json файл і все готово. Якщо ви хочете змінити мову за замовчуванням для повідомлень самого бота, які не можуть визначити мову адміністратора, відредагуйте параметр `"locale"` у `config.json`. Якщо ця мова недоступна, замість неї буде використано `"locale_fallback"`. Якщо обидві мови недоступні - буде показано помилку. Для зміни мови виведення консолі та логування вам слід відредагувати `"locale_log"`. +Усі файли локалізації знаходяться у теці `locale`. Просто скопіюйте файл локалі за вашим вибором, назвіть його відповідно до [мовних кодів IETF](https://en.wikipedia.org/wiki/IETF_language_tag) (якщо ви хочете, щоб ваша локаль була сумісна з локалями Telegram) або дайте йому власну назву. Збережіть переклад у форматі json, і все буде готово. Якщо ви хочете змінити локаль за замовчуванням для повідомлень - відредагуйте параметр `"locale"` у файлі `config.json`. -Ми рекомендуємо вносити будь-які зміни лише до вашої окремої мови. Або, принаймні, завжди мати резервну копію, наприклад, `en.json` як запасний варіант. +Ми рекомендуємо вносити зміни лише у вашу власну локаль. Або, принаймні, завжди мати резервну копію, наприклад, `en.json` як запасний варіант.