From 813a5ce9fd44afc1b97560b4ff2a21a3b594de85 Mon Sep 17 00:00:00 2001 From: ch3p4ll3 Date: Wed, 3 Jan 2024 21:57:22 +0100 Subject: [PATCH 01/35] start to implement localization --- src/bot/plugins/common.py | 102 ++++++++++++++++++++--------- src/bot/plugins/on_message.py | 41 +++++++----- src/configs/user.py | 1 + src/locales/en.json | 38 +++++++++++ src/translator/__init__.py | 2 + src/translator/strings/__init__.py | 30 +++++++++ src/translator/translator.py | 39 +++++++++++ src/utils.py | 10 +++ 8 files changed, 216 insertions(+), 47 deletions(-) create mode 100644 src/locales/en.json create mode 100644 src/translator/__init__.py create mode 100644 src/translator/strings/__init__.py create mode 100644 src/translator/translator.py diff --git a/src/bot/plugins/common.py b/src/bot/plugins/common.py index 80ea3b8..87fa3af 100644 --- a/src/bot/plugins/common.py +++ b/src/bot/plugins/common.py @@ -9,59 +9,96 @@ from ...client_manager import ClientRepo from ...utils import get_user_from_config from ...configs.enums import UserRolesEnum +from ...translator import Translator, Strings async def send_menu(client: Client, message_id: int, chat_id: int) -> None: user = get_user_from_config(chat_id) buttons = [ - [InlineKeyboardButton("📝 List", "list")] + [InlineKeyboardButton(Translator.translate(Strings.MenuList, user.locale), "list")] ] if user.role in [UserRolesEnum.Manager, UserRolesEnum.Administrator]: buttons += [ - [InlineKeyboardButton("➕ Add Magnet", "category#add_magnet"), - InlineKeyboardButton("➕ Add Torrent", "category#add_torrent")], - [InlineKeyboardButton("⏯ Pause/Resume", "menu_pause_resume")] + [InlineKeyboardButton(Translator.translate(Strings.AddMagnet, user.locale), "category#add_magnet"), + InlineKeyboardButton(Translator.translate(Strings.AddTorrent, user.locale), "category#add_torrent")], + [InlineKeyboardButton(Translator.translate(Strings.PauseResume, user.locale), "menu_pause_resume")] ] if user.role == UserRolesEnum.Administrator: buttons += [ - [InlineKeyboardButton("🗑 Delete", "menu_delete")], - [InlineKeyboardButton("📂 Categories", "menu_categories")], - [InlineKeyboardButton("⚙️ Settings", "settings")] + [InlineKeyboardButton(Translator.translate(Strings.Delete, user.locale), "menu_delete")], + [InlineKeyboardButton(Translator.translate(Strings.Categories, user.locale), "menu_categories")], + [InlineKeyboardButton(Translator.translate(Strings.Settings, user.locale), "settings")] ] db_management.write_support("None", chat_id) try: - await client.edit_message_text(chat_id, message_id, text="Qbittorrent Control", + await client.edit_message_text(chat_id, message_id, text=Translator.translate(Strings.Menu, user.locale), reply_markup=InlineKeyboardMarkup(buttons)) except MessageIdInvalid: - await client.send_message(chat_id, text="Qbittorrent Control", reply_markup=InlineKeyboardMarkup(buttons)) + await client.send_message( + chat_id, + text=Translator.translate(Strings.Menu, user.locale), + reply_markup=InlineKeyboardMarkup(buttons) + ) -async def list_active_torrents(client: Client, chat_id, message_id, callback: Optional[str] = None, status_filter: str = None) -> None: +async def list_active_torrents(client: Client, chat_id: int, message_id: int, callback: Optional[str] = None, status_filter: str = None) -> None: + user = get_user_from_config(chat_id) repository = ClientRepo.get_client_manager(Configs.config.client.type) torrents = repository.get_torrent_info(status_filter=status_filter) def render_categories_buttons(): return [ - InlineKeyboardButton(f"⏳ {'*' if status_filter == 'downloading' else ''} Downloading", - "by_status_list#downloading"), - InlineKeyboardButton(f"✔️ {'*' if status_filter == 'completed' else ''} Completed", - "by_status_list#completed"), - InlineKeyboardButton(f"⏸️ {'*' if status_filter == 'paused' else ''} Paused", "by_status_list#paused"), + InlineKeyboardButton( + Translator.translate( + Strings.ListFilterDownloading, user.locale, active='*' if status_filter == 'downloading' else '' + ), + "by_status_list#downloading" + ), + + InlineKeyboardButton( + Translator.translate( + Strings.ListFilterCompleted, user.locale, active='*' if status_filter == 'completed' else '' + ), + "by_status_list#completed" + ), + + InlineKeyboardButton( + Translator.translate( + Strings.ListFilterPaused, user.locale, active='*' if status_filter == 'paused' else '' + ), + "by_status_list#paused" + ), ] categories_buttons = render_categories_buttons() if not torrents: - buttons = [categories_buttons, [InlineKeyboardButton("🔙 Menu", "menu")]] + buttons = [ + categories_buttons, + [ + InlineKeyboardButton(Translator.translate(Strings.BackToMenu, user.locale), "menu") + ] + ] + try: - await client.edit_message_text(chat_id, message_id, "There are no torrents", - reply_markup=InlineKeyboardMarkup(buttons)) + await client.edit_message_text( + chat_id, + message_id, + Translator.translate(Strings.NoTorrents, user.locale), + reply_markup=InlineKeyboardMarkup(buttons) + ) + except MessageIdInvalid: - await client.send_message(chat_id, "There are no torrents", reply_markup=InlineKeyboardMarkup(buttons)) + await client.send_message( + chat_id, + Translator.translate(Strings.NoTorrents, user.locale), + reply_markup=InlineKeyboardMarkup(buttons) + ) + return buttons = [categories_buttons] @@ -70,20 +107,21 @@ def render_categories_buttons(): for key, i in enumerate(torrents): buttons.append([InlineKeyboardButton(i.name, f"{callback}#{i.info.hash}")]) - buttons.append([InlineKeyboardButton("🔙 Menu", "menu")]) - - try: - await client.edit_message_reply_markup(chat_id, message_id, reply_markup=InlineKeyboardMarkup(buttons)) - except MessageIdInvalid: - await client.send_message(chat_id, "Qbittorrent Control", reply_markup=InlineKeyboardMarkup(buttons)) - else: - for key, i in enumerate(torrents): + for _, i in enumerate(torrents): buttons.append([InlineKeyboardButton(i.name, f"torrentInfo#{i.info.hash}")]) - buttons.append([InlineKeyboardButton("🔙 Menu", "menu")]) + buttons.append( + [ + InlineKeyboardButton(Translator.translate(Strings.BackToMenu, user.locale), "menu") + ] + ) - try: - await client.edit_message_reply_markup(chat_id, message_id, reply_markup=InlineKeyboardMarkup(buttons)) - except MessageIdInvalid: - await client.send_message(chat_id, "Qbittorrent Control", reply_markup=InlineKeyboardMarkup(buttons)) + try: + await client.edit_message_reply_markup(chat_id, message_id, reply_markup=InlineKeyboardMarkup(buttons)) + except MessageIdInvalid: + await client.send_message( + chat_id, + Translator.translate(Strings.Menu, user.locale), + reply_markup=InlineKeyboardMarkup(buttons) + ) diff --git a/src/bot/plugins/on_message.py b/src/bot/plugins/on_message.py index 37e045f..4330b82 100644 --- a/src/bot/plugins/on_message.py +++ b/src/bot/plugins/on_message.py @@ -9,6 +9,7 @@ from ...configs import Configs from ...utils import get_user_from_config, convert_type_from_string from .. import custom_filters +from ...translator import Translator, Strings logger = logging.getLogger(__name__) @@ -17,6 +18,7 @@ @Client.on_message(~filters.me & custom_filters.check_user_filter) async def on_text(client: Client, message: Message) -> None: action = db_management.read_support(message.from_user.id) + user = get_user_from_config(message.from_user.id) if "magnet" in action: if message.text.startswith("magnet:?xt"): @@ -30,14 +32,17 @@ async def on_text(client: Client, message: Message) -> None: ) if not response: - await message.reply_text("Unable to add magnet link") + await message.reply_text(Translator.translate(Strings.UnableToAddMagnet, locale=user.locale)) return await send_menu(client, message.id, message.from_user.id) db_management.write_support("None", message.from_user.id) else: - await client.send_message(message.from_user.id, "This magnet link is invalid! Retry") + await client.send_message( + message.from_user.id, + Translator.translate(Strings.InvalidMagnet, locale=user.locale) + ) elif "torrent" in action and message.document: if ".torrent" in message.document.file_name: @@ -50,18 +55,24 @@ async def on_text(client: Client, message: Message) -> None: response = repository.add_torrent(file_name=name, category=category) if not response: - await message.reply_text("Unable to add magnet link") + await message.reply_text(Translator.translate(Strings.UnableToAddTorrent, locale=user.locale)) return await send_menu(client, message.id, message.from_user.id) db_management.write_support("None", message.from_user.id) else: - await client.send_message(message.from_user.id, "This is not a torrent file! Retry") + await client.send_message( + message.from_user.id, + Translator.translate(Strings.InvalidTorrent, locale=user.locale) + ) elif action == "category_name": db_management.write_support(f"category_dir#{message.text}", message.from_user.id) - await client.send_message(message.from_user.id, f"now send me the path for the category {message.text}") + await client.send_message( + message.from_user.id, + Translator.translate(Strings.CategoryPath, category_name=message.text) + ) elif "category_dir" in action: if os.path.exists(message.text): @@ -80,7 +91,9 @@ async def on_text(client: Client, message: Message) -> None: await send_menu(client, message.id, message.from_user.id) else: - await client.send_message(message.from_user.id, "The path entered does not exist! Retry") + await client.send_message( + message.from_user.id, Translator.translate(Strings.PathNotValid,locale=user.locale) + ) elif "edit_user" in action: data = action.split("#")[1] @@ -91,8 +104,7 @@ async def on_text(client: Client, message: Message) -> None: try: new_value = data_type(message.text) - user_info = get_user_from_config(user_id) - user_from_configs = Configs.config.users.index(user_info) + user_from_configs = Configs.config.users.index(user) if user_from_configs == -1: return @@ -105,9 +117,7 @@ async def on_text(client: Client, message: Message) -> None: await send_menu(client, message.id, message.from_user.id) except Exception as e: - await message.reply_text( - f"Error: {e}" - ) + await message.reply_text(Translator.translate(Strings.GenericError, locale=user.locale, error=e)) logger.exception(f"Error converting value \"{message.text}\" to type \"{data_type}\"", exc_info=True) elif "edit_clt" in action: @@ -126,9 +136,10 @@ async def on_text(client: Client, message: Message) -> None: await send_menu(client, message.id, message.from_user.id) except Exception as e: - await message.reply_text( - f"Error: {e}" - ) + await message.reply_text(Translator.translate(Strings.GenericError, locale=user.locale, error=e)) logger.exception(f"Error converting value \"{message.text}\" to type \"{data_type}\"", exc_info=True) else: - await client.send_message(message.from_user.id, "The command does not exist") + await client.send_message( + message.from_user.id, + Translator.translate(Strings.CommandDoesNotExist, locale=user.locale) + ) diff --git a/src/configs/user.py b/src/configs/user.py index a7d45d3..ace3b4f 100644 --- a/src/configs/user.py +++ b/src/configs/user.py @@ -7,4 +7,5 @@ class User(BaseModel): user_id: int role: UserRolesEnum = UserRolesEnum.Reader + locale: Optional[str] = "en" notify: Optional[bool] = True diff --git a/src/locales/en.json b/src/locales/en.json new file mode 100644 index 0000000..55aa688 --- /dev/null +++ b/src/locales/en.json @@ -0,0 +1,38 @@ +{ + "errors": { + "path_not_valid": "The path entered does not exist! Retry", + "error": "Error: ${error}", + "command_does_not_exist": "The command does not exist" + }, + + "on_message": { + "error_adding_magnet": "Unable to add magnet link", + "invalid_magnet": "This magnet link is invalid! Retry", + "error_adding_torrent": "Unable to add torrent file", + "invalid_torrent": "This is not a torrent file! Retry", + "send_category_path": "Please, send the path for the category ${category_name}" + }, + + + "common": { + "menu": "Welcome to QBittorrent Bot", + "menu_btn": { + "menu_list": "\uD83D\uDCDD List", + "add_magnet": "➕ Add Magnet", + "add_torrent": "➕ Add Torrent", + "pause_resume": "⏯ Pause/Resume", + "delete": "\uD83D\uDDD1 Delete", + "categories": "\uD83D\uDCC2 Categories", + "settings": "⚙\uFE0F Settings" + }, + + "list_filter": { + "downloading": "⏳ ${active} Downloading", + "completed": "✔\uFE0F ${active} Completed", + "paused": "⏸\uFE0F ${active} Paused" + }, + + "no_torrents": "There are no torrents", + "back_to_menu_btn": "\uD83D\uDD19 Menu" + } +} \ No newline at end of file diff --git a/src/translator/__init__.py b/src/translator/__init__.py new file mode 100644 index 0000000..b01d998 --- /dev/null +++ b/src/translator/__init__.py @@ -0,0 +1,2 @@ +from .translator import Translator +from .strings import Strings diff --git a/src/translator/strings/__init__.py b/src/translator/strings/__init__.py new file mode 100644 index 0000000..6f76377 --- /dev/null +++ b/src/translator/strings/__init__.py @@ -0,0 +1,30 @@ +from enum import StrEnum + + +class Strings(StrEnum): + GenericError = "errors.error" + PathNotValid = "errors.path_not_valid" + CommandDoesNotExist = "errors.command_does_not_exist" + + UnableToAddMagnet = "on_message.error_adding_magnet" + InvalidMagnet = "on_message.invalid_magnet" + UnableToAddTorrent = "on_message.error_adding_torrent" + InvalidTorrent = "on_message.invalid_torrent" + CategoryPath = "on_message.send_category_path" + + MenuList = "common.menu_btn.menu_list" + AddMagnet = "common.menu_btn.add_magnet" + AddTorrent = "common.menu_btn.add_torrent" + PauseResume = "common.menu_btn.pause_resume" + Delete = "common.menu_btn.delete" + Categories = "common.menu_btn.categories" + Settings = "common.menu_btn.settings" + + Menu = "common.menu" + + ListFilterDownloading = "common.list_filter.downloading" + ListFilterCompleted = "common.list_filter.completed" + ListFilterPaused = "common.list_filter.paused" + + NoTorrents = "common.no_torrents" + BackToMenu = "common.back_to_menu_btn" diff --git a/src/translator/translator.py b/src/translator/translator.py new file mode 100644 index 0000000..0a68aae --- /dev/null +++ b/src/translator/translator.py @@ -0,0 +1,39 @@ +import json +from pathlib import Path +from typing import Dict +from string import Template +from ..utils import get_value + + +def load_locales(locales_path: Path) -> Dict: + # check if format is supported + # get list of files with specific extensions + data = {} + + for file in locales_path.glob("*.json"): + # get the name of the file without extension, will be used as locale name + locale = file.stem + with open(file, 'r', encoding='utf8') as f: + data[locale] = json.load(f) + + return data + + +class Translator: + locales: Dict = load_locales(Path(__file__).parent.parent / 'locales') + + @classmethod + def translate(cls, key, locale: str = 'en', **kwargs) -> str: + # return the key instead of translation text if locale is not supported + if locale not in cls.locales: + return key + + text = get_value(cls.locales[locale], key) + + return Template(text).safe_substitute(**kwargs) + + @classmethod + def reload_locales(cls) -> Dict: + cls.locales = load_locales(Path(__file__).parent.parent / 'locales') + + return cls.locales diff --git a/src/utils.py b/src/utils.py index b01fcee..e8b3611 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,5 +1,6 @@ from math import log, floor import datetime +from typing import Dict from pydantic import IPvAnyAddress from pyrogram.errors.exceptions import UserIsBlocked @@ -58,3 +59,12 @@ def convert_type_from_string(input_type: str): return UserRolesEnum elif "str" in input_type: return str + + +def get_value(locales_dict: Dict, key_string: str) -> str: + """Function to get value from dictionary using key strings like 'on_message.error_adding_magnet'""" + if '.' not in key_string: + return locales_dict[key_string] + else: + head, tail = key_string.split('.', 1) + return get_value(locales_dict[head], tail) From 7afff115c2a1099157f036ea27fe5bbc2358b93a Mon Sep 17 00:00:00 2001 From: ch3p4ll3 Date: Fri, 5 Jan 2024 15:36:19 +0100 Subject: [PATCH 02/35] inject user + others translations --- .gitignore | 1 + config.json.template | 3 +- .../callbacks/add_torrents_callbacks.py | 13 +++-- src/bot/plugins/callbacks/torrent_info.py | 47 +++++++++++++------ src/bot/plugins/commands.py | 31 +++++++----- src/bot/plugins/on_message.py | 29 +++++------- src/locales/en.json | 27 +++++++++++ src/translator/strings/__init__.py | 23 +++++++++ src/utils.py | 14 ++++-- 9 files changed, 138 insertions(+), 50 deletions(-) diff --git a/.gitignore b/.gitignore index f9de277..d7fbaa2 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,7 @@ coverage.xml .pytest_cache/ cover/ +.vscode/ # Translations *.mo *.pot diff --git a/config.json.template b/config.json.template index 5aa7aa0..1688f40 100644 --- a/config.json.template +++ b/config.json.template @@ -1,7 +1,7 @@ { "client": { "type": "qbittorrent", - "host": "http://192.168.178.102", + "host": "http://192.168.178.102:8080", "user": "admin", "password": "admin" }, @@ -15,6 +15,7 @@ { "user_id": 123456, "notify": false, + "locale": "en" "role": "administrator" } ] diff --git a/src/bot/plugins/callbacks/add_torrents_callbacks.py b/src/bot/plugins/callbacks/add_torrents_callbacks.py index 2035a55..bdb3315 100644 --- a/src/bot/plugins/callbacks/add_torrents_callbacks.py +++ b/src/bot/plugins/callbacks/add_torrents_callbacks.py @@ -2,15 +2,20 @@ from pyrogram.types import CallbackQuery from .... import db_management from ... import custom_filters +from ....configs.user import User +from ....translator import Translator, Strings +from ....utils import inject_user @Client.on_callback_query(custom_filters.add_magnet_filter & custom_filters.check_user_filter & (custom_filters.user_is_administrator | custom_filters.user_is_manager)) -async def add_magnet_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def add_magnet_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: db_management.write_support(f"magnet#{callback_query.data.split('#')[1]}", callback_query.from_user.id) - await client.answer_callback_query(callback_query.id, "Send a magnet link") + await client.answer_callback_query(callback_query.id, Translator.translate(Strings.SendMagnetLink, user.locale)) @Client.on_callback_query(custom_filters.add_torrent_filter & custom_filters.check_user_filter & (custom_filters.user_is_administrator | custom_filters.user_is_manager)) -async def add_torrent_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def add_torrent_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: db_management.write_support(f"torrent#{callback_query.data.split('#')[1]}", callback_query.from_user.id) - await client.answer_callback_query(callback_query.id, "Send a torrent file") + await client.answer_callback_query(callback_query.id, Translator.translate(Strings.SendTorrentFile, user.locale)) diff --git a/src/bot/plugins/callbacks/torrent_info.py b/src/bot/plugins/callbacks/torrent_info.py index 79a7821..6bc3bba 100644 --- a/src/bot/plugins/callbacks/torrent_info.py +++ b/src/bot/plugins/callbacks/torrent_info.py @@ -6,49 +6,68 @@ from ... import custom_filters from ....client_manager import ClientRepo from ....configs import Configs -from ....utils import convert_size, convert_eta +from ....configs.user import User +from ....translator import Translator, Strings +from ....utils import convert_size, convert_eta, inject_user @Client.on_callback_query(custom_filters.torrentInfo_filter & custom_filters.check_user_filter) -async def torrent_info_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def torrent_info_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: repository = ClientRepo.get_client_manager(Configs.config.client.type) torrent = repository.get_torrent_info(callback_query.data.split("#")[1]) text = f"{torrent.name}\n" if torrent.progress == 1: - text += "**COMPLETED**\n" + text += Translator.translate(Strings.TorrentCompleted, user.locale) else: text += f"{tqdm.format_meter(torrent.progress, 1, 0, bar_format='{l_bar}{bar}|')}\n" if "stalled" not in torrent.state: - text += (f"**State:** {torrent.state.capitalize()} \n" - f"**Download Speed:** {convert_size(torrent.dlspeed)}/s\n") - - text += f"**Size:** {convert_size(torrent.size)}\n" + text += Translator.translate( + Strings.TorrentState, + user.locale, + current_state=torrent.state.capitalize(), + download_speed=convert_size(torrent.dlspeed) + ) + + text += Translator.translate( + Strings.TorrentSize, + user.locale, + torrent_size=convert_size(torrent.size) + ) if "stalled" not in torrent.state: - text += f"**ETA:** {convert_eta(int(torrent.eta))}\n" + text += Translator.translate( + Strings.TorrentEta, + user.locale, + torrent_eta=convert_eta(int(torrent.eta)) + ) if torrent.category: - text += f"**Category:** {torrent.category}\n" + text += Translator.translate( + Strings.TorrentCategory, + user.locale, + torrent_category=torrent.category + ) buttons = [ [ - InlineKeyboardButton("💾 Export torrent", f"export#{callback_query.data.split('#')[1]}") + InlineKeyboardButton(Translator.translate(Strings.ExportTorrentBtn, user.locale), f"export#{callback_query.data.split('#')[1]}") ], [ - InlineKeyboardButton("⏸ Pause", f"pause#{callback_query.data.split('#')[1]}") + InlineKeyboardButton(Translator.translate(Strings.PauseTorrentBtn, user.locale), f"pause#{callback_query.data.split('#')[1]}") ], [ - InlineKeyboardButton("▶️ Resume", f"resume#{callback_query.data.split('#')[1]}") + InlineKeyboardButton(Translator.translate(Strings.ResumeTorrentBtn, user.locale), f"resume#{callback_query.data.split('#')[1]}") ], [ - InlineKeyboardButton("🗑 Delete", f"delete_one#{callback_query.data.split('#')[1]}") + InlineKeyboardButton(Translator.translate(Strings.DeleteTorrentBtn, user.locale), f"delete_one#{callback_query.data.split('#')[1]}") ], [ - InlineKeyboardButton("🔙 Menu", "menu") + InlineKeyboardButton(Translator.translate(Strings.BackToMenu, user.locale), "menu") ] ] diff --git a/src/bot/plugins/commands.py b/src/bot/plugins/commands.py index ec66a2b..d9ca5b6 100644 --- a/src/bot/plugins/commands.py +++ b/src/bot/plugins/commands.py @@ -4,15 +4,18 @@ from .. import custom_filters from ...db_management import write_support -from ...utils import convert_size +from ...utils import convert_size, inject_user from .common import send_menu +from ...configs.user import User +from ...translator import Translator, Strings @Client.on_message(~custom_filters.check_user_filter) -async def access_denied_message(client: Client, message: Message) -> None: +@inject_user +async def access_denied_message(client: Client, message: Message, user: User) -> None: button = InlineKeyboardMarkup([[InlineKeyboardButton("Github", url="https://github.com/ch3p4ll3/QBittorrentBot/")]]) - await client.send_message(message.chat.id, "You are not authorized to use this bot", reply_markup=button) + await client.send_message(message.chat.id, Translator.translate(Strings.NotAuthorized, user.locale), reply_markup=button) @Client.on_message(filters.command("start") & custom_filters.check_user_filter) @@ -23,13 +26,19 @@ async def start_command(client: Client, message: Message) -> None: @Client.on_message(filters.command("stats") & custom_filters.check_user_filter) -async def stats_command(client: Client, message: Message) -> None: - stats_text = f"**============SYSTEM============**\n" \ - f"**CPU Usage:** {psutil.cpu_percent(interval=None)}%\n" \ - f"**CPU Temp:** {psutil.sensors_temperatures()['coretemp'][0].current}°C\n" \ - f"**Free Memory:** {convert_size(psutil.virtual_memory().available)} of " \ - f"{convert_size(psutil.virtual_memory().total)} ({psutil.virtual_memory().percent}%)\n" \ - f"**Disks usage:** {convert_size(psutil.disk_usage('/mnt').used)} of " \ - f"{convert_size(psutil.disk_usage('/mnt').total)} ({psutil.disk_usage('/mnt').percent}%)" +@inject_user +async def stats_command(client: Client, message: Message, user: User) -> None: + stats_text = Translator.translate( + Strings.StatsCommand, + user.locale, + cpu_usage=psutil.cpu_percent(interval=None), + cpu_temp=psutil.sensors_temperatures()['coretemp'][0].current, + free_memory=convert_size(psutil.virtual_memory().available), + total_memory=convert_size(psutil.virtual_memory().total), + memory_percent=psutil.virtual_memory().percent, + disk_used=convert_size(psutil.disk_usage('/mnt').used), + disk_total=convert_size(psutil.disk_usage('/mnt').total), + disk_percent=psutil.disk_usage('/mnt').percent + ) await client.send_message(message.chat.id, stats_text) diff --git a/src/bot/plugins/on_message.py b/src/bot/plugins/on_message.py index 4330b82..bcc3adf 100644 --- a/src/bot/plugins/on_message.py +++ b/src/bot/plugins/on_message.py @@ -7,7 +7,8 @@ from ... import db_management from .common import send_menu from ...configs import Configs -from ...utils import get_user_from_config, convert_type_from_string +from ...configs.user import User +from ...utils import convert_type_from_string, inject_user from .. import custom_filters from ...translator import Translator, Strings @@ -16,9 +17,9 @@ @Client.on_message(~filters.me & custom_filters.check_user_filter) -async def on_text(client: Client, message: Message) -> None: +@inject_user +async def on_text(client: Client, message: Message, user: User) -> None: action = db_management.read_support(message.from_user.id) - user = get_user_from_config(message.from_user.id) if "magnet" in action: if message.text.startswith("magnet:?xt"): @@ -75,25 +76,19 @@ async def on_text(client: Client, message: Message) -> None: ) elif "category_dir" in action: - if os.path.exists(message.text): - name = db_management.read_support(message.from_user.id).split("#")[1] + name = db_management.read_support(message.from_user.id).split("#")[1] - repository = ClientRepo.get_client_manager(Configs.config.client.type) - - if "modify" in action: - repository.edit_category(name=name, save_path=message.text) - - await send_menu(client, message.id, message.from_user.id) - return + repository = ClientRepo.get_client_manager(Configs.config.client.type) - repository.create_category(name=name, save_path=message.text) + if "modify" in action: + repository.edit_category(name=name, save_path=message.text) await send_menu(client, message.id, message.from_user.id) + return - else: - await client.send_message( - message.from_user.id, Translator.translate(Strings.PathNotValid,locale=user.locale) - ) + repository.create_category(name=name, save_path=message.text) + + await send_menu(client, message.id, message.from_user.id) elif "edit_user" in action: data = action.split("#")[1] diff --git a/src/locales/en.json b/src/locales/en.json index 55aa688..9335fe2 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -34,5 +34,32 @@ "no_torrents": "There are no torrents", "back_to_menu_btn": "\uD83D\uDD19 Menu" + }, + + "commands": { + "not_authorized": "You are not authorized to use this bot", + "stats_command": "**============SYSTEM============**\n**CPU Usage:** ${cpu_usage}%\n**CPU Temp:** ${cpu_temp}°C\n**Free Memory:** ${free_memory} of ${total_memory} (${memory_percent}%)\n**Disks usage:** ${disk_used} of ${disk_total} (${disk_percent}%)" + }, + + "callbacks": { + "torrent_info": { + "torrent_completed": "**COMPLETED**\n", + "torrent_state": "**State:** ${current_state} \n**Download Speed:** ${download_speed}/s\n", + "torrent_size": "**Size:** ${torrent_size}\n", + "torrent_eta": "**ETA:** ${torrent_eta}\n", + "torrent_category": "**Category:** ${torrent_category}\n", + + "info_btns": { + "export_torrent": "💾 Export torrent", + "pause_torrent": "⏸ Pause", + "resume_torrent": "▶️ Resume", + "delete_torrent": "🗑 Delete" + } + }, + + "add_torrents": { + "send_magnet": "Send a magnet link", + "send_torrent": "Send a torrent file" + } } } \ No newline at end of file diff --git a/src/translator/strings/__init__.py b/src/translator/strings/__init__.py index 6f76377..4f114e6 100644 --- a/src/translator/strings/__init__.py +++ b/src/translator/strings/__init__.py @@ -6,12 +6,14 @@ class Strings(StrEnum): PathNotValid = "errors.path_not_valid" CommandDoesNotExist = "errors.command_does_not_exist" + # On Message UnableToAddMagnet = "on_message.error_adding_magnet" InvalidMagnet = "on_message.invalid_magnet" UnableToAddTorrent = "on_message.error_adding_torrent" InvalidTorrent = "on_message.invalid_torrent" CategoryPath = "on_message.send_category_path" + # Common MenuList = "common.menu_btn.menu_list" AddMagnet = "common.menu_btn.add_magnet" AddTorrent = "common.menu_btn.add_torrent" @@ -28,3 +30,24 @@ class Strings(StrEnum): NoTorrents = "common.no_torrents" BackToMenu = "common.back_to_menu_btn" + + # Commands + NotAuthorized = "commands.not_authorized" + StatsCommand = "commands.stats_command" + + ########################## CALLBACKS ################################### + # Torrent Info + TorrentCompleted = "callbacks.torrent_info.torrent_completed" + TorrentState = "callbacks.torrent_info.torrent_state" + TorrentSize = "callbacks.torrent_info.torrent_size" + TorrentEta = "callbacks.torrent_info.torrent_eta" + TorrentCategory = "callbacks.torrent_info.torrent_category" + + ExportTorrentBtn = "callbacks.torrent_infoinfo_btns.export_torrent" + PauseTorrentBtn = "callbacks.torrent_infoinfo_btns.pause_torrent" + ResumeTorrentBtn = "callbacks.torrent_infoinfo_btns.resume_torrent" + DeleteTorrentBtn = "callbacks.torrent_infoinfo_btns.delete_torrent" + + # add_torrents + SendMagnetLink = "callbacks.add_torrents.send_magnet" + SendTorrentFile = "callbacks.add_torrents.send_torrent" diff --git a/src/utils.py b/src/utils.py index 665bae1..98b68e8 100644 --- a/src/utils.py +++ b/src/utils.py @@ -2,7 +2,7 @@ import datetime from typing import Dict -from pydantic import IPvAnyAddress +from pydantic import HttpUrl from pyrogram.errors.exceptions import UserIsBlocked from src import db_management @@ -52,8 +52,8 @@ def convert_eta(n) -> str: def convert_type_from_string(input_type: str): if "int" in input_type: return int - elif "IPvAnyAddress" in input_type: - return IPvAnyAddress + elif "HttpUrl" in input_type: + return HttpUrl elif "ClientTypeEnum" in input_type: return ClientTypeEnum elif "UserRolesEnum" in input_type: @@ -69,3 +69,11 @@ def get_value(locales_dict: Dict, key_string: str) -> str: else: head, tail = key_string.split('.', 1) return get_value(locales_dict[head], tail) + + +def inject_user(func): + def wrapper(client, message): + user = get_user_from_config(message.from_user.id) + func(client, message, user) + + return wrapper \ No newline at end of file From 11eeaaaf53942fa8ee4cf53cab6f1c87ce39867e Mon Sep 17 00:00:00 2001 From: ch3p4ll3 Date: Fri, 5 Jan 2024 16:05:50 +0100 Subject: [PATCH 03/35] fix inject user --- src/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils.py b/src/utils.py index 98b68e8..974cd16 100644 --- a/src/utils.py +++ b/src/utils.py @@ -72,8 +72,8 @@ def get_value(locales_dict: Dict, key_string: str) -> str: def inject_user(func): - def wrapper(client, message): + async def wrapper(client, message): user = get_user_from_config(message.from_user.id) - func(client, message, user) + await func(client, message, user) return wrapper \ No newline at end of file From fd478dd1f6d5bbb9a55b15b2061dfb66755d6d38 Mon Sep 17 00:00:00 2001 From: ch3p4ll3 Date: Fri, 5 Jan 2024 17:14:17 +0100 Subject: [PATCH 04/35] add client settings translations --- .../plugins/callbacks/settings/__init__.py | 19 ++--- .../settings/client_settings_callbacks.py | 75 ++++++++++++------- src/locales/en.json | 31 ++++++++ src/translator/strings/__init__.py | 25 +++++++ 4 files changed, 115 insertions(+), 35 deletions(-) diff --git a/src/bot/plugins/callbacks/settings/__init__.py b/src/bot/plugins/callbacks/settings/__init__.py index b021c8c..d720db3 100644 --- a/src/bot/plugins/callbacks/settings/__init__.py +++ b/src/bot/plugins/callbacks/settings/__init__.py @@ -1,28 +1,29 @@ from pyrogram import Client from pyrogram.types import CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton from .... import custom_filters +from .....configs.user import User +from .....utils import inject_user +from .....translator import Translator, Strings @Client.on_callback_query(custom_filters.settings_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def settings_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def settings_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: await callback_query.edit_message_text( - "QBittorrentBot Settings", + Translator.translate(Strings.SettingsMenu, user.locale), reply_markup=InlineKeyboardMarkup( [ [ - InlineKeyboardButton("🫂 Users Settings", "get_users") + InlineKeyboardButton(Translator.translate(Strings.UsersSettings, user.locale), "get_users") ], [ - InlineKeyboardButton("📥 Client Settings", "edit_client") + InlineKeyboardButton(Translator.translate(Strings.ClientSettings, user.locale), "edit_client") ], [ - InlineKeyboardButton("🇮🇹 Language Settings", "menu") + InlineKeyboardButton(Translator.translate(Strings.ReloadSettings, user.locale), "reload_settings") ], [ - InlineKeyboardButton("🔄 Reload Settings", "reload_settings") - ], - [ - InlineKeyboardButton("🔙 Menu", "menu") + InlineKeyboardButton(Translator.translate(Strings.BackToMenu, user.locale), "menu") ] ] ) diff --git a/src/bot/plugins/callbacks/settings/client_settings_callbacks.py b/src/bot/plugins/callbacks/settings/client_settings_callbacks.py index 3951af2..e08256b 100644 --- a/src/bot/plugins/callbacks/settings/client_settings_callbacks.py +++ b/src/bot/plugins/callbacks/settings/client_settings_callbacks.py @@ -3,35 +3,44 @@ from .... import custom_filters from .....configs import Configs +from .....configs.user import User from .....client_manager import ClientRepo -from .....utils import convert_type_from_string +from .....utils import convert_type_from_string, inject_user from .....db_management import write_support +from .....translator import Translator, Strings @Client.on_callback_query(custom_filters.edit_client_settings_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def edit_client_settings_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def edit_client_settings_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: confs = '\n- '.join(iter([f"**{key.capitalize()}:** {item}" for key, item in Configs.config.client.model_dump().items()])) repository = ClientRepo.get_client_manager(Configs.config.client.type) speed_limit = repository.get_speed_limit_mode() - confs += f"\n\n**Speed Limit**: {'Enabled' if speed_limit else 'Disabled'}" + speed_limit_status = Translator.translate(Strings.Enabled if speed_limit else Strings.Disabled, user.locale) + + confs += Translator.translate( + Strings.SpeedLimitStatus, + user.locale, + speed_limit_status=speed_limit_status + ) await callback_query.edit_message_text( - f"Edit Qbittorrent Client Settings \n\n**Current Settings:**\n- {confs}", + Translator.translate(Strings.EditClientSettings, user.locale, client_type=Configs.config.client.type, configs=confs), reply_markup=InlineKeyboardMarkup( [ [ - InlineKeyboardButton("📝 Edit Client Settings", "lst_client") + InlineKeyboardButton(Translator.translate(Strings.EditClientSettingsBtn, user.locale), "lst_client") ], [ - InlineKeyboardButton("🐢 Toggle Speed Limit", "toggle_speed_limit") + InlineKeyboardButton(Translator.translate(Strings.ToggleSpeedLimit, user.locale), "toggle_speed_limit") ], [ - InlineKeyboardButton("✅ Check Client connection", "check_connection") + InlineKeyboardButton(Translator.translate(Strings.CheckClientConnection, user.locale), "check_connection") ], [ - InlineKeyboardButton("🔙 Settings", "settings") + InlineKeyboardButton(Translator.translate(Strings.BackToSettings, user.locale), "settings") ] ] ) @@ -39,29 +48,36 @@ async def edit_client_settings_callback(client: Client, callback_query: Callback @Client.on_callback_query(custom_filters.toggle_speed_limit_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def toggle_speed_limit_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def toggle_speed_limit_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: confs = '\n- '.join(iter([f"**{key.capitalize()}:** {item}" for key, item in Configs.config.client.model_dump().items()])) repository = ClientRepo.get_client_manager(Configs.config.client.type) speed_limit = repository.toggle_speed_limit() - confs += f"\n\n**Speed Limit**: {'Enabled' if speed_limit else 'Disabled'}" + speed_limit_status = Translator.translate(Strings.Enabled if speed_limit else Strings.Disabled, user.locale) + + confs += Translator.translate( + Strings.SpeedLimitStatus, + user.locale, + speed_limit_status=speed_limit_status + ) await callback_query.edit_message_text( - f"Edit Qbittorrent Client Settings \n\n**Current Settings:**\n- {confs}", + Translator.translate(Strings.EditClientSettings, user.locale, client_type=Configs.config.client.type, configs=confs), reply_markup=InlineKeyboardMarkup( [ [ - InlineKeyboardButton("📝 Edit Client Settings", "lst_client") + InlineKeyboardButton(Translator.translate(Strings.EditClientSettingsBtn, user.locale), "lst_client") ], [ - InlineKeyboardButton("🐢 Toggle Speed Limit", "toggle_speed_limit") + InlineKeyboardButton(Translator.translate(Strings.ToggleSpeedLimit, user.locale), "toggle_speed_limit") ], [ - InlineKeyboardButton("✅ Check Client connection", "check_connection") + InlineKeyboardButton(Translator.translate(Strings.CheckClientConnection, user.locale), "check_connection") ], [ - InlineKeyboardButton("🔙 Settings", "settings") + InlineKeyboardButton(Translator.translate(Strings.BackToSettings, user.locale), "settings") ] ] ) @@ -69,32 +85,38 @@ async def toggle_speed_limit_callback(client: Client, callback_query: CallbackQu @Client.on_callback_query(custom_filters.check_connection_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def check_connection_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def check_connection_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: try: repository = ClientRepo.get_client_manager(Configs.config.client.type) version = repository.check_connection() - await callback_query.answer(f"✅ The connection works. QBittorrent version: {version}", show_alert=True) + await callback_query.answer(Translator.translate(Strings.ClientConnectionOk, user.locale, version=version), show_alert=True) except Exception: - await callback_query.answer("❌ Unable to establish connection with QBittorrent", show_alert=True) + await callback_query.answer(Translator.translate(Strings.ClientConnectionBad, user.locale), show_alert=True) @Client.on_callback_query(custom_filters.list_client_settings_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def list_client_settings_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def list_client_settings_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: # get all fields of the model dynamically fields = [ - [InlineKeyboardButton(f"Edit {key.replace('_', ' ').capitalize()}", - f"edit_clt#{key}-{item.annotation}")] + [ + InlineKeyboardButton( + Translator.translate(Strings.EditClientSetting, user.locale, setting=key.replace('_', ' ').capitalize()), + f"edit_clt#{key}-{item.annotation}" + ) + ] for key, item in Configs.config.client.model_fields.items() ] await callback_query.edit_message_text( - "Edit Qbittorrent Client", + Translator.translate(Strings.EditClientType, user.locale, client_type=Configs.config.client.type), reply_markup=InlineKeyboardMarkup( fields + [ [ - InlineKeyboardButton("🔙 Settings", "settings") + InlineKeyboardButton(Translator.translate(Strings.BackToSettings, user.locale), "settings") ] ] ) @@ -102,7 +124,8 @@ async def list_client_settings_callback(client: Client, callback_query: Callback @Client.on_callback_query(custom_filters.edit_client_setting_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def edit_client_setting_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def edit_client_setting_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: data = callback_query.data.split("#")[1] field_to_edit = data.split("-")[0] data_type = convert_type_from_string(data.split("-")[1]) @@ -110,11 +133,11 @@ async def edit_client_setting_callback(client: Client, callback_query: CallbackQ write_support(callback_query.data, callback_query.from_user.id) await callback_query.edit_message_text( - f"Send the new value for field \"{field_to_edit}\" for client. \n\n**Note:** the field type is \"{data_type}\"", + Translator.translate(Strings.NewValueForClientField, user.locale, field_to_edit=field_to_edit, data_type=data_type), reply_markup=InlineKeyboardMarkup( [ [ - InlineKeyboardButton("🔙 Settings", "settings") + InlineKeyboardButton(Translator.translate(Strings.BackToSettings, user.locale), "settings") ] ] ) diff --git a/src/locales/en.json b/src/locales/en.json index 9335fe2..6f34d9e 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -60,6 +60,37 @@ "add_torrents": { "send_magnet": "Send a magnet link", "send_torrent": "Send a torrent file" + }, + + "settings": { + "settings_menu": "QBittorrentBot Settings", + "settings_menu_btns": { + "users_settings": "🫂 Users Settings", + "client_settings": "📥 Client Settings", + "reload_settings": "🔄 Reload Settings" + } + }, + + "client_settings": { + "enabled": "Enabled", + "disabled": "Disabled", + "speed_limit_status": "\n\n**Speed Limit**: ${speed_limit_status}", + "edit_client_settings": "Edit ${client_type} client settings \n\n**Current Settings:**\n- ${configs}", + + "edit_client_settings_btns": { + "edit_client_settings": "📝 Edit Client Settings", + "toggle_speed_limit": "🐢 Toggle Speed Limit", + "check_client_connection": "✅ Check Client connection", + "back_to_settings": "🔙 Settings" + }, + + "client_connection_ok": "✅ The connection works. QBittorrent version: ${version}", + "client_connection_bad": "❌ Unable to establish connection with QBittorrent", + + "edit_client_setting": "Edit ${setting}", + "edit_client_type": "Edit ${client_type} Client", + + "new_value_for_field": "Send the new value for field \"${field_to_edit}\" for client. \n\n**Note:** the field type is \"${data_type}\"" } } } \ No newline at end of file diff --git a/src/translator/strings/__init__.py b/src/translator/strings/__init__.py index 4f114e6..1723dba 100644 --- a/src/translator/strings/__init__.py +++ b/src/translator/strings/__init__.py @@ -51,3 +51,28 @@ class Strings(StrEnum): # add_torrents SendMagnetLink = "callbacks.add_torrents.send_magnet" SendTorrentFile = "callbacks.add_torrents.send_torrent" + + # Settings + SettingsMenu = "callbacks.settings.settings_menu" + UsersSettings = "callbacks.settings.settings_menu_btns.users_settings" + ClientSettings = "callbacks.settings.settings_menu_btns.client_settings" + ReloadSettings = "callbacks.settings.settings_menu_btns.reload_settings" + + # Client Settings + Enabled = "callbacks.client_settings.enabled" + Disabled = "callbacks.client_settings.disabled" + SpeedLimitStatus = "callbacks.client_settings.speed_limit_status" + EditClientSettings = "callbacks.client_settings.edit_client_settings" + + EditClientSettingsBtn = "callbacks.client_settings.edit_client_settings_btns.edit_client_settings" + ToggleSpeedLimit = "callbacks.client_settings.edit_client_settings_btns.toggle_speed_limit" + CheckClientConnection = "callbacks.client_settings.edit_client_settings_btns.check_client_connection" + BackToSettings = "callbacks.client_settings.edit_client_settings_btns.back_to_settings" + + ClientConnectionOk = "callbacks.client_settings.client_connection_ok" + ClientConnectionBad = "callbacks.client_settings.client_connection_bad" + + EditClientSetting = "callbacks.client_settings.edit_client_setting" + EditClientType = "callbacks.client_settings.edit_client_type" + + NewValueForClientField = "callbacks.client_settings.new_value_for_field" \ No newline at end of file From dab2f545620e1abbaddf15335660131054b104f6 Mon Sep 17 00:00:00 2001 From: ch3p4ll3 Date: Fri, 5 Jan 2024 20:30:28 +0100 Subject: [PATCH 05/35] add user settings locales --- .../settings/reload_settings_callbacks.py | 9 ++- .../settings/users_settings_callbacks.py | 75 ++++++++++++------- src/locales/en.json | 18 ++++- .../{strings/__init__.py => strings.py} | 19 ++++- src/utils.py | 2 + 5 files changed, 90 insertions(+), 33 deletions(-) rename src/translator/{strings/__init__.py => strings.py} (85%) diff --git a/src/bot/plugins/callbacks/settings/reload_settings_callbacks.py b/src/bot/plugins/callbacks/settings/reload_settings_callbacks.py index 292cc8d..8d206c1 100644 --- a/src/bot/plugins/callbacks/settings/reload_settings_callbacks.py +++ b/src/bot/plugins/callbacks/settings/reload_settings_callbacks.py @@ -3,10 +3,13 @@ from .... import custom_filters from .....configs import Configs +from .....configs.user import User +from .....utils import inject_user +from .....translator import Translator, Strings @Client.on_callback_query(custom_filters.reload_settings_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def reload_settings_callback(client: Client, callback_query: CallbackQuery) -> None: - # TO FIX reload +@inject_user +async def reload_settings_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: Configs.reload_config() - await callback_query.answer("Settings Reloaded", show_alert=True) + await callback_query.answer(Translator.translate(Strings.SettingsReloaded, user.locale), show_alert=True) diff --git a/src/bot/plugins/callbacks/settings/users_settings_callbacks.py b/src/bot/plugins/callbacks/settings/users_settings_callbacks.py index da448ef..c232757 100644 --- a/src/bot/plugins/callbacks/settings/users_settings_callbacks.py +++ b/src/bot/plugins/callbacks/settings/users_settings_callbacks.py @@ -1,28 +1,34 @@ -import typing - from pyrogram import Client from pyrogram.types import CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton from .... import custom_filters from .....configs import Configs +from .....configs.user import User from .....db_management import write_support -from .....utils import get_user_from_config, convert_type_from_string +from .....utils import get_user_from_config, convert_type_from_string, inject_user +from .....translator import Translator, Strings @Client.on_callback_query(custom_filters.get_users_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def get_users_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def get_users_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: users = [ - [InlineKeyboardButton(f"User #{i.user_id}", f"user_info#{i.user_id}")] + [ + InlineKeyboardButton( + Translator.translate(Strings.UserBtn, user.locale, user_id=i.user_id), + f"user_info#{i.user_id}" + ) + ] for i in Configs.config.users ] await callback_query.edit_message_text( - "Authorized users", + Translator.translate(Strings.AuthorizedUsers, user.locale), reply_markup=InlineKeyboardMarkup( users + [ [ - InlineKeyboardButton("🔙 Settings", "settings") + InlineKeyboardButton(Translator.translate(Strings.BackToSettings, user.locale), "settings") ] ] ) @@ -30,7 +36,8 @@ async def get_users_callback(client: Client, callback_query: CallbackQuery) -> N @Client.on_callback_query(custom_filters.user_info_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def get_user_info_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def get_user_info_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: user_id = int(callback_query.data.split("#")[1]) user_info = get_user_from_config(user_id) @@ -39,20 +46,24 @@ async def get_user_info_callback(client: Client, callback_query: CallbackQuery) # get all fields of the model dynamically fields = [ - [InlineKeyboardButton(f"Edit {key.replace('_', ' ').capitalize()}", - f"edit_user#{user_id}-{key}-{item.annotation}")] + [ + InlineKeyboardButton( + Translator.translate(Strings.EditClientSetting, user.locale, setting=key.replace('_', ' ').capitalize()), + f"edit_user#{user_id}-{key}-{item.annotation}" + ) + ] for key, item in user_info.model_fields.items() ] confs = '\n- '.join(iter([f"**{key.capitalize()}:** {item}" for key, item in user_info.model_dump().items()])) await callback_query.edit_message_text( - f"Edit User #{user_id}\n\n**Current Settings:**\n- {confs}", + Translator.translate(Strings.EditUserSetting, user.locale, user_id=user_id, confs=confs), reply_markup=InlineKeyboardMarkup( fields + [ [ - InlineKeyboardButton("🔙 Users", "get_users") + InlineKeyboardButton(Translator.translate(Strings.BackToUsers, user.locale), "get_users") ] ] ) @@ -60,7 +71,8 @@ async def get_user_info_callback(client: Client, callback_query: CallbackQuery) @Client.on_callback_query(custom_filters.edit_user_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def edit_user_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def edit_user_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: data = callback_query.data.split("#")[1] user_id = int(data.split("-")[0]) field_to_edit = data.split("-")[1] @@ -68,18 +80,23 @@ async def edit_user_callback(client: Client, callback_query: CallbackQuery) -> N user_info = get_user_from_config(user_id) - if data_type == bool or data_type == typing.Optional[bool]: + if data_type == bool: + notify_status = Translator.translate(Strings.Enabled if user_info.notify else Strings.Disabled, user.locale) + await callback_query.edit_message_text( - f"Edit User #{user_id} {field_to_edit} Field", + Translator.translate(Strings.EditUserField, user.locale, user_id=user_id, field_to_edit=field_to_edit), reply_markup=InlineKeyboardMarkup( [ [ InlineKeyboardButton( - f"{'✅' if user_info.notify else '❌'} Toggle", + notify_status, f"toggle_user_var#{user_id}-{field_to_edit}") ], [ - InlineKeyboardButton(f"🔙 User#{user_id} info", f"user_info#{user_id}") + InlineKeyboardButton( + Translator.translate(Strings.BackToUSer, user.locale, user_id=user_id), + f"user_info#{user_id}" + ) ] ] ) @@ -90,11 +107,14 @@ async def edit_user_callback(client: Client, callback_query: CallbackQuery) -> N write_support(callback_query.data, callback_query.from_user.id) await callback_query.edit_message_text( - f"Send the new value for field \"{field_to_edit}\" for user #{user_id}. \n\n**Note:** the field type is \"{data_type}\"", + Translator.translate(Strings.NewValueForUserField, user.locale, field_to_edit=field_to_edit, user_id=user_id, data_type=data_type), reply_markup=InlineKeyboardMarkup( [ [ - InlineKeyboardButton(f"🔙 User#{user_id} info", f"user_info#{user_id}") + InlineKeyboardButton( + Translator.translate(Strings.BackToUSer, user.locale, user_id=user_id), + f"user_info#{user_id}" + ) ] ] ) @@ -102,13 +122,13 @@ async def edit_user_callback(client: Client, callback_query: CallbackQuery) -> N @Client.on_callback_query(custom_filters.toggle_user_var_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def toggle_user_var(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def toggle_user_var(client: Client, callback_query: CallbackQuery, user: User) -> None: data = callback_query.data.split("#")[1] user_id = int(data.split("-")[0]) field_to_edit = data.split("-")[1] - user_info = get_user_from_config(user_id) - user_from_configs = Configs.config.users.index(user_info) + user_from_configs = Configs.config.users.index(user) if user_from_configs == -1: return @@ -116,17 +136,22 @@ async def toggle_user_var(client: Client, callback_query: CallbackQuery) -> None Configs.config.users[user_from_configs].notify = not Configs.config.users[user_from_configs].notify Configs.update_config(Configs.config) + notify_status = Translator.translate(Strings.Enabled if user.notify else Strings.Disabled, user.locale) + await callback_query.edit_message_text( - f"Edit User #{user_id} {field_to_edit} Field", + Translator.translate(Strings.EditUserField, user.locale, field_to_edit=field_to_edit, user_id=user_id), reply_markup=InlineKeyboardMarkup( [ [ InlineKeyboardButton( - f"{'✅' if Configs.config.users[user_from_configs].notify else '❌'} Toggle", + notify_status, f"toggle_user_var#{user_id}-{field_to_edit}") ], [ - InlineKeyboardButton(f"🔙 User#{user_id} info", f"user_info#{user_id}") + InlineKeyboardButton( + Translator.translate(Strings.BackToUSer, user.locale, user_id=user_id), + f"user_info#{user_id}" + ) ] ] ) diff --git a/src/locales/en.json b/src/locales/en.json index 6f34d9e..956f495 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -72,8 +72,8 @@ }, "client_settings": { - "enabled": "Enabled", - "disabled": "Disabled", + "enabled": "✅ Enabled", + "disabled": "❌ Disabled", "speed_limit_status": "\n\n**Speed Limit**: ${speed_limit_status}", "edit_client_settings": "Edit ${client_type} client settings \n\n**Current Settings:**\n- ${configs}", @@ -91,6 +91,20 @@ "edit_client_type": "Edit ${client_type} Client", "new_value_for_field": "Send the new value for field \"${field_to_edit}\" for client. \n\n**Note:** the field type is \"${data_type}\"" + }, + + "reload_settings": { + "settings_reloaded": "Settings Reloaded" + }, + + "user_settings": { + "user_btn": "User #${user_id}", + "authorized_users": "Authorized users", + "edit_user_setting": "Edit User #${user_id}\n\n**Current Settings:**\n- ${confs}", + "back_to_users": "\uD83D\uDD19 Users", + "edit_user_field": "Edit User #${user_id} ${field_to_edit} field", + "back_to_user": "\uD83D\uDD19 User#${user_id} info", + "new_value_for_field": "Send the new value for field \"${field_to_edit}\" for user #${user_id}. \n\n**Note:** the field type is \"${data_type}\"" } } } \ No newline at end of file diff --git a/src/translator/strings/__init__.py b/src/translator/strings.py similarity index 85% rename from src/translator/strings/__init__.py rename to src/translator/strings.py index 1723dba..3f1610b 100644 --- a/src/translator/strings/__init__.py +++ b/src/translator/strings.py @@ -42,13 +42,13 @@ class Strings(StrEnum): TorrentSize = "callbacks.torrent_info.torrent_size" TorrentEta = "callbacks.torrent_info.torrent_eta" TorrentCategory = "callbacks.torrent_info.torrent_category" - + ExportTorrentBtn = "callbacks.torrent_infoinfo_btns.export_torrent" PauseTorrentBtn = "callbacks.torrent_infoinfo_btns.pause_torrent" ResumeTorrentBtn = "callbacks.torrent_infoinfo_btns.resume_torrent" DeleteTorrentBtn = "callbacks.torrent_infoinfo_btns.delete_torrent" - # add_torrents + # Add Torrents SendMagnetLink = "callbacks.add_torrents.send_magnet" SendTorrentFile = "callbacks.add_torrents.send_torrent" @@ -75,4 +75,17 @@ class Strings(StrEnum): EditClientSetting = "callbacks.client_settings.edit_client_setting" EditClientType = "callbacks.client_settings.edit_client_type" - NewValueForClientField = "callbacks.client_settings.new_value_for_field" \ No newline at end of file + NewValueForClientField = "callbacks.client_settings.new_value_for_field" + + # Reload Settings + SettingsReloaded = "callbacks.reload_settings.settings_reloaded" + + # User Settings + + UserBtn = "callbacks.user_settings.user_btn" + AuthorizedUsers = "callbacks.user_settings.authorized_users" + EditUserSetting = "callbacks.user_settings.edit_user_setting" + BackToUsers = "callbacks.user_settings.back_to_users" + EditUserField = "callbacks.user_settings.edit_user_field" + BackToUSer = "callbacks.user_settings.back_to_user" + NewValueForUserField = "callbacks.user_settings.new_value_for_field" diff --git a/src/utils.py b/src/utils.py index 974cd16..21d5801 100644 --- a/src/utils.py +++ b/src/utils.py @@ -60,6 +60,8 @@ def convert_type_from_string(input_type: str): return UserRolesEnum elif "str" in input_type: return str + elif "bool" in input_type: + return bool def get_value(locales_dict: Dict, key_string: str) -> str: From a0f5c8073515bee65bee40f795638e838680ac2d Mon Sep 17 00:00:00 2001 From: ch3p4ll3 Date: Fri, 5 Jan 2024 21:20:19 +0100 Subject: [PATCH 06/35] add base torrent class --- src/bot/plugins/callbacks/torrent_info.py | 2 +- src/bot/plugins/common.py | 6 +- src/bot/plugins/on_message.py | 197 ++++++++++-------- src/client_manager/client_manager.py | 7 +- src/client_manager/entities/__init__.py | 0 src/client_manager/entities/torrent.py | 14 ++ src/client_manager/mappers/__init__.py | 0 src/client_manager/mappers/mapper.py | 10 + src/client_manager/mappers/mapper_repo.py | 12 ++ .../mappers/qbittorrent_mapper.py | 35 ++++ src/client_manager/qbittorrent_manager.py | 24 ++- src/translator/strings.py | 8 +- src/utils.py | 2 +- 13 files changed, 214 insertions(+), 103 deletions(-) create mode 100644 src/client_manager/entities/__init__.py create mode 100644 src/client_manager/entities/torrent.py create mode 100644 src/client_manager/mappers/__init__.py create mode 100644 src/client_manager/mappers/mapper.py create mode 100644 src/client_manager/mappers/mapper_repo.py create mode 100644 src/client_manager/mappers/qbittorrent_mapper.py diff --git a/src/bot/plugins/callbacks/torrent_info.py b/src/bot/plugins/callbacks/torrent_info.py index 6bc3bba..aa3a5ef 100644 --- a/src/bot/plugins/callbacks/torrent_info.py +++ b/src/bot/plugins/callbacks/torrent_info.py @@ -15,7 +15,7 @@ @inject_user async def torrent_info_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: repository = ClientRepo.get_client_manager(Configs.config.client.type) - torrent = repository.get_torrent_info(callback_query.data.split("#")[1]) + torrent = repository.get_torrent(callback_query.data.split("#")[1]) text = f"{torrent.name}\n" diff --git a/src/bot/plugins/common.py b/src/bot/plugins/common.py index 568ae03..2d02370 100644 --- a/src/bot/plugins/common.py +++ b/src/bot/plugins/common.py @@ -49,7 +49,7 @@ async def send_menu(client: Client, message_id: int, chat_id: int) -> None: async def list_active_torrents(client: Client, chat_id: int, message_id: int, callback: Optional[str] = None, status_filter: str = None) -> None: user = get_user_from_config(chat_id) repository = ClientRepo.get_client_manager(Configs.config.client.type) - torrents = repository.get_torrent_info(status_filter=status_filter) + torrents = repository.get_torrents(status_filter=status_filter) def render_categories_buttons(): return [ @@ -105,11 +105,11 @@ def render_categories_buttons(): if callback is not None: for _, i in enumerate(torrents): - buttons.append([InlineKeyboardButton(i.name, f"{callback}#{i.info.hash}")]) + buttons.append([InlineKeyboardButton(i.name, f"{callback}#{i.hash}")]) else: for _, i in enumerate(torrents): - buttons.append([InlineKeyboardButton(i.name, f"torrentInfo#{i.info.hash}")]) + buttons.append([InlineKeyboardButton(i.name, f"torrentInfo#{i.hash}")]) buttons.append( [ diff --git a/src/bot/plugins/on_message.py b/src/bot/plugins/on_message.py index bcc3adf..0877c35 100644 --- a/src/bot/plugins/on_message.py +++ b/src/bot/plugins/on_message.py @@ -16,123 +16,148 @@ logger = logging.getLogger(__name__) -@Client.on_message(~filters.me & custom_filters.check_user_filter) -@inject_user -async def on_text(client: Client, message: Message, user: User) -> None: - action = db_management.read_support(message.from_user.id) +async def on_magnet(client, message, user): + if message.text.startswith("magnet:?xt"): + magnet_link = message.text.split("\n") + category = db_management.read_support(message.from_user.id).split("#")[1] - if "magnet" in action: - if message.text.startswith("magnet:?xt"): - magnet_link = message.text.split("\n") + repository = ClientRepo.get_client_manager(Configs.config.client.type) + response = repository.add_magnet( + magnet_link=magnet_link, + category=category + ) + + if not response: + await message.reply_text(Translator.translate(Strings.UnableToAddMagnet, locale=user.locale)) + return + + await send_menu(client, message.id, message.from_user.id) + db_management.write_support("None", message.from_user.id) + + else: + await client.send_message( + message.from_user.id, + Translator.translate(Strings.InvalidMagnet, locale=user.locale) + ) + + +async def on_torrent(client, message, user): + if ".torrent" in message.document.file_name: + with tempfile.TemporaryDirectory() as tempdir: + name = f"{tempdir}/{message.document.file_name}" category = db_management.read_support(message.from_user.id).split("#")[1] + await message.download(name) repository = ClientRepo.get_client_manager(Configs.config.client.type) - response = repository.add_magnet( - magnet_link=magnet_link, - category=category - ) + response = repository.add_torrent(file_name=name, category=category) if not response: - await message.reply_text(Translator.translate(Strings.UnableToAddMagnet, locale=user.locale)) + await message.reply_text(Translator.translate(Strings.UnableToAddTorrent, locale=user.locale)) return - await send_menu(client, message.id, message.from_user.id) - db_management.write_support("None", message.from_user.id) + await send_menu(client, message.id, message.from_user.id) + db_management.write_support("None", message.from_user.id) - else: - await client.send_message( - message.from_user.id, - Translator.translate(Strings.InvalidMagnet, locale=user.locale) - ) + else: + await client.send_message( + message.from_user.id, + Translator.translate(Strings.InvalidTorrent, locale=user.locale) + ) - elif "torrent" in action and message.document: - if ".torrent" in message.document.file_name: - with tempfile.TemporaryDirectory() as tempdir: - name = f"{tempdir}/{message.document.file_name}" - category = db_management.read_support(message.from_user.id).split("#")[1] - await message.download(name) - repository = ClientRepo.get_client_manager(Configs.config.client.type) - response = repository.add_torrent(file_name=name, category=category) +async def on_category_name(client, message): + db_management.write_support(f"category_dir#{message.text}", message.from_user.id) + await client.send_message( + message.from_user.id, + Translator.translate(Strings.CategoryPath, category_name=message.text) + ) - if not response: - await message.reply_text(Translator.translate(Strings.UnableToAddTorrent, locale=user.locale)) - return - await send_menu(client, message.id, message.from_user.id) - db_management.write_support("None", message.from_user.id) +async def on_category_directory(client, message, action): + name = db_management.read_support(message.from_user.id).split("#")[1] - else: - await client.send_message( - message.from_user.id, - Translator.translate(Strings.InvalidTorrent, locale=user.locale) - ) + repository = ClientRepo.get_client_manager(Configs.config.client.type) - elif action == "category_name": - db_management.write_support(f"category_dir#{message.text}", message.from_user.id) - await client.send_message( - message.from_user.id, - Translator.translate(Strings.CategoryPath, category_name=message.text) - ) + if "modify" in action: + repository.edit_category(name=name, save_path=message.text) - elif "category_dir" in action: - name = db_management.read_support(message.from_user.id).split("#")[1] + await send_menu(client, message.id, message.from_user.id) + return - repository = ClientRepo.get_client_manager(Configs.config.client.type) + repository.create_category(name=name, save_path=message.text) + + await send_menu(client, message.id, message.from_user.id) + + +async def on_edit_user(client, message, user, action): + data = action.split("#")[1] + user_id = int(data.split("-")[0]) + field_to_edit = data.split("-")[1] + data_type = convert_type_from_string(data.split("-")[2].replace("", "")) - if "modify" in action: - repository.edit_category(name=name, save_path=message.text) + try: + new_value = data_type(message.text) - await send_menu(client, message.id, message.from_user.id) + user_from_configs = Configs.config.users.index(user) + + if user_from_configs == -1: return - repository.create_category(name=name, save_path=message.text) + setattr(Configs.config.users[user_from_configs], field_to_edit, new_value) + Configs.update_config(Configs.config) + Configs.reload_config() + logger.debug(f"Updating User #{user_id} {field_to_edit} settings to {new_value}") + db_management.write_support("None", message.from_user.id) await send_menu(client, message.id, message.from_user.id) + except Exception as e: + await message.reply_text(Translator.translate(Strings.GenericError, locale=user.locale, error=e)) + logger.exception(f"Error converting value \"{message.text}\" to type \"{data_type}\"", exc_info=True) - elif "edit_user" in action: - data = action.split("#")[1] - user_id = int(data.split("-")[0]) - field_to_edit = data.split("-")[1] - data_type = convert_type_from_string(data.split("-")[2].replace("", "")) - try: - new_value = data_type(message.text) +async def on_edit_client(client: Client, message: Message, user, action): + data = action.split("#")[1] + field_to_edit = data.split("-")[0] + data_type = convert_type_from_string(data.split("-")[1]) - user_from_configs = Configs.config.users.index(user) + try: + new_value = data_type(message.text) - if user_from_configs == -1: - return + setattr(Configs.config.client, field_to_edit, new_value) + Configs.update_config(Configs.config) + Configs.reload_config() + logger.debug(f"Updating Client field \"{field_to_edit}\" to \"{new_value}\"") + db_management.write_support("None", message.from_user.id) - setattr(Configs.config.users[user_from_configs], field_to_edit, new_value) - Configs.update_config(Configs.config) - Configs.reload_config() - logger.debug(f"Updating User #{user_id} {field_to_edit} settings to {new_value}") - db_management.write_support("None", message.from_user.id) + await send_menu(client, message.id, message.from_user.id) + except Exception as e: + await message.reply_text(Translator.translate(Strings.GenericError, locale=user.locale, error=e)) + logger.exception(f"Error converting value \"{message.text}\" to type \"{data_type}\"", exc_info=True) - await send_menu(client, message.id, message.from_user.id) - except Exception as e: - await message.reply_text(Translator.translate(Strings.GenericError, locale=user.locale, error=e)) - logger.exception(f"Error converting value \"{message.text}\" to type \"{data_type}\"", exc_info=True) + +@Client.on_message(~filters.me & custom_filters.check_user_filter) +@inject_user +async def on_text(client: Client, message: Message, user: User) -> None: + action = db_management.read_support(message.from_user.id) + + if "magnet" in action: + await on_magnet(client, message, user) + + elif "torrent" in action and message.document: + await on_torrent(client, message, user) + + elif action == "category_name": + await on_category_name(client, message) + + elif "category_dir" in action: + await on_category_directory(client, message, action) + + elif "edit_user" in action: + await on_edit_user(client, message, user, action) elif "edit_clt" in action: - data = action.split("#")[1] - field_to_edit = data.split("-")[0] - data_type = convert_type_from_string(data.split("-")[1]) - - try: - new_value = data_type(message.text) - - setattr(Configs.config.client, field_to_edit, new_value) - Configs.update_config(Configs.config) - Configs.reload_config() - logger.debug(f"Updating Client field \"{field_to_edit}\" to \"{new_value}\"") - db_management.write_support("None", message.from_user.id) - - await send_menu(client, message.id, message.from_user.id) - except Exception as e: - await message.reply_text(Translator.translate(Strings.GenericError, locale=user.locale, error=e)) - logger.exception(f"Error converting value \"{message.text}\" to type \"{data_type}\"", exc_info=True) + await on_edit_client(client, message, user, action) + else: await client.send_message( message.from_user.id, diff --git a/src/client_manager/client_manager.py b/src/client_manager/client_manager.py index be5633d..e064be9 100644 --- a/src/client_manager/client_manager.py +++ b/src/client_manager/client_manager.py @@ -60,7 +60,12 @@ def get_categories(cls): raise NotImplementedError @classmethod - def get_torrent_info(cls, torrent_hash: str = None, status_filter: str = None): + def get_torrent(cls, torrent_hash: str, status_filter: str = None): + """Get a torrent info with or without a status filter""" + raise NotImplementedError + + @classmethod + def get_torrents(cls, torrent_hash: str = None, status_filter: str = None): """Get a torrent info with or without a status filter""" raise NotImplementedError diff --git a/src/client_manager/entities/__init__.py b/src/client_manager/entities/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/client_manager/entities/torrent.py b/src/client_manager/entities/torrent.py new file mode 100644 index 0000000..25a6622 --- /dev/null +++ b/src/client_manager/entities/torrent.py @@ -0,0 +1,14 @@ +from dataclasses import dataclass +from typing import Union + + +@dataclass +class Torrent: + hash: str + name: str + progress: int + dlspeed: int + state: str + size: int + eta: int + category: Union[str, None] diff --git a/src/client_manager/mappers/__init__.py b/src/client_manager/mappers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/client_manager/mappers/mapper.py b/src/client_manager/mappers/mapper.py new file mode 100644 index 0000000..7020c96 --- /dev/null +++ b/src/client_manager/mappers/mapper.py @@ -0,0 +1,10 @@ +from abc import ABC +from typing import Union, List, Any +from ..entities.torrent import Torrent + + +class Mapper(ABC): + @classmethod + def map(cls, torrents: Union[Any, List[Any]]) -> Union[Torrent, List[Torrent]]: + """Map a torrent or list of torrents to a mapped torrent or list of torrents""" + raise NotImplementedError diff --git a/src/client_manager/mappers/mapper_repo.py b/src/client_manager/mappers/mapper_repo.py new file mode 100644 index 0000000..b432866 --- /dev/null +++ b/src/client_manager/mappers/mapper_repo.py @@ -0,0 +1,12 @@ +from ...configs.enums import ClientTypeEnum +from .qbittorrent_mapper import QBittorrentMapper, Mapper + + +class MapperRepo: + mappers = { + ClientTypeEnum.QBittorrent: QBittorrentMapper + } + + @classmethod + def get_mapper(cls, client_type: ClientTypeEnum): + return cls.mappers.get(client_type, Mapper) diff --git a/src/client_manager/mappers/qbittorrent_mapper.py b/src/client_manager/mappers/qbittorrent_mapper.py new file mode 100644 index 0000000..f1ad1aa --- /dev/null +++ b/src/client_manager/mappers/qbittorrent_mapper.py @@ -0,0 +1,35 @@ +from typing import List, Union + +from ..entities.torrent import Torrent +from .mapper import Mapper +from qbittorrentapi.torrents import TorrentInfoList, TorrentDictionary + + +class QBittorrentMapper(Mapper): + @classmethod + def map(cls, torrents: Union[TorrentDictionary, TorrentInfoList]) -> Union[Torrent, List[Torrent]]: + if type(torrents) is TorrentInfoList: + return [ + Torrent( + torrent.info.hash, + torrent.name, + torrent.progress, + torrent.dlspeed, + torrent.state, + torrent.size, + torrent.eta, + torrent.category + ) + for torrent in torrents + ] + + return Torrent( + torrents.info.hash, + torrents.name, + torrents.progress, + torrents.dlspeed, + torrents.state, + torrents.size, + torrents.eta, + torrents.category + ) diff --git a/src/client_manager/qbittorrent_manager.py b/src/client_manager/qbittorrent_manager.py index 907d2bf..6fcc661 100644 --- a/src/client_manager/qbittorrent_manager.py +++ b/src/client_manager/qbittorrent_manager.py @@ -6,6 +6,9 @@ from typing import Union, List from .client_manager import ClientManager +from .mappers.mapper_repo import MapperRepo +from .entities.torrent import Torrent + logger = logging.getLogger(__name__) @@ -102,18 +105,25 @@ def get_categories(cls): return @classmethod - def get_torrent_info(cls, torrent_hash: str = None, status_filter: str = None): - if torrent_hash is None: - logger.debug("Getting torrents infos") - with qbittorrentapi.Client(**Configs.config.client.connection_string) as qbt_client: - return qbt_client.torrents_info(status_filter=status_filter) - logger.debug(f"Getting infos for torrent with hash {torrent_hash}") + def get_torrent(cls, torrent_hash: str, status_filter: str = None) -> Union[Torrent, None]: + logger.debug(f"Getting torrent with hash {torrent_hash}") with qbittorrentapi.Client(**Configs.config.client.connection_string) as qbt_client: - return next( + mapper = MapperRepo.get_mapper(Configs.config.client.type) + + torrent = next( iter( qbt_client.torrents_info(status_filter=status_filter, torrent_hashes=torrent_hash) ), None ) + return mapper.map(torrent) + + @classmethod + def get_torrents(cls, torrent_hash: str = None, status_filter: str = None) -> List[Torrent]: + if torrent_hash is None: + logger.debug("Getting torrents infos") + with qbittorrentapi.Client(**Configs.config.client.connection_string) as qbt_client: + mapper = MapperRepo.get_mapper(Configs.config.client.type) + return mapper.map(qbt_client.torrents_info(status_filter=status_filter)) @classmethod def edit_category(cls, name: str, save_path: str) -> None: diff --git a/src/translator/strings.py b/src/translator/strings.py index 3f1610b..102917b 100644 --- a/src/translator/strings.py +++ b/src/translator/strings.py @@ -43,10 +43,10 @@ class Strings(StrEnum): TorrentEta = "callbacks.torrent_info.torrent_eta" TorrentCategory = "callbacks.torrent_info.torrent_category" - ExportTorrentBtn = "callbacks.torrent_infoinfo_btns.export_torrent" - PauseTorrentBtn = "callbacks.torrent_infoinfo_btns.pause_torrent" - ResumeTorrentBtn = "callbacks.torrent_infoinfo_btns.resume_torrent" - DeleteTorrentBtn = "callbacks.torrent_infoinfo_btns.delete_torrent" + ExportTorrentBtn = "callbacks.torrent_info.info_btns.export_torrent" + PauseTorrentBtn = "callbacks.torrent_info.info_btns.pause_torrent" + ResumeTorrentBtn = "callbacks.torrent_info.info_btns.resume_torrent" + DeleteTorrentBtn = "callbacks.torrent_info.info_btns.delete_torrent" # Add Torrents SendMagnetLink = "callbacks.add_torrents.send_magnet" diff --git a/src/utils.py b/src/utils.py index 21d5801..b860317 100644 --- a/src/utils.py +++ b/src/utils.py @@ -15,7 +15,7 @@ async def torrent_finished(app): repository = ClientRepo.get_client_manager(Configs.config.client.type) - for i in repository.get_torrent_info(status_filter="completed"): + for i in repository.get_torrents(status_filter="completed"): if db_management.read_completed_torrents(i.hash) is None: for user in Configs.config.users: From 26ce9b2f942e777d2a59fc8db3e6368e12e3f79e Mon Sep 17 00:00:00 2001 From: ch3p4ll3 Date: Sat, 6 Jan 2024 21:24:34 +0100 Subject: [PATCH 07/35] add resume/pause locales + check if locale exists --- .../callbacks/pause_resume/__init__.py | 18 +++++++++++------- .../callbacks/pause_resume/pause_callbacks.py | 13 +++++++++---- .../pause_resume/resume_callbacks.py | 13 +++++++++---- src/bot/plugins/on_message.py | 10 ++++++++++ src/locales/en.json | 19 ++++++++++++++++++- src/translator/strings.py | 14 ++++++++++++++ 6 files changed, 71 insertions(+), 16 deletions(-) diff --git a/src/bot/plugins/callbacks/pause_resume/__init__.py b/src/bot/plugins/callbacks/pause_resume/__init__.py index 0ca1890..b9f4eec 100644 --- a/src/bot/plugins/callbacks/pause_resume/__init__.py +++ b/src/bot/plugins/callbacks/pause_resume/__init__.py @@ -1,24 +1,28 @@ from pyrogram import Client from pyrogram.types import CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton from .... import custom_filters +from .....configs.user import User +from .....utils import inject_user +from .....translator import Translator, Strings @Client.on_callback_query(custom_filters.menu_pause_resume_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def menu_pause_resume_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def menu_pause_resume_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: await callback_query.edit_message_text( - "Pause/Resume a torrent", + Translator.translate(Strings.PauseResumeMenu, user.locale), reply_markup=InlineKeyboardMarkup( [ [ - InlineKeyboardButton("⏸ Pause", "pause"), - InlineKeyboardButton("▶️ Resume", "resume") + InlineKeyboardButton(Translator.translate(Strings.PauseTorrentBtn, user.locale), "pause"), + InlineKeyboardButton(Translator.translate(Strings.ResumeTorrentBtn, user.locale), "resume") ], [ - InlineKeyboardButton("⏸ Pause All", "pause_all"), - InlineKeyboardButton("▶️ Resume All", "resume_all") + InlineKeyboardButton(Translator.translate(Strings.PauseAll, user.locale), "pause_all"), + InlineKeyboardButton(Translator.translate(Strings.ResumeAll, user.locale), "resume_all") ], [ - InlineKeyboardButton("🔙 Menu", "menu") + InlineKeyboardButton(Translator.translate(Strings.BackToMenu, user.locale), "menu") ] ] ) diff --git a/src/bot/plugins/callbacks/pause_resume/pause_callbacks.py b/src/bot/plugins/callbacks/pause_resume/pause_callbacks.py index 332279d..7c1421b 100644 --- a/src/bot/plugins/callbacks/pause_resume/pause_callbacks.py +++ b/src/bot/plugins/callbacks/pause_resume/pause_callbacks.py @@ -5,21 +5,26 @@ from .....client_manager import ClientRepo from ...common import list_active_torrents from .....configs import Configs +from .....configs.user import User +from .....utils import inject_user +from .....translator import Translator, Strings @Client.on_callback_query(custom_filters.pause_all_filter & custom_filters.check_user_filter & (custom_filters.user_is_administrator | custom_filters.user_is_manager)) -async def pause_all_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def pause_all_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: repository = ClientRepo.get_client_manager(Configs.config.client.type) repository.pause_all() - await client.answer_callback_query(callback_query.id, "Paused all torrents") + await client.answer_callback_query(callback_query.id, Translator.translate(Strings.PauseAllTorrents, user.locale)) @Client.on_callback_query(custom_filters.pause_filter & custom_filters.check_user_filter & (custom_filters.user_is_administrator | custom_filters.user_is_manager)) -async def pause_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def pause_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: if callback_query.data.find("#") == -1: await list_active_torrents(client, callback_query.from_user.id, callback_query.message.id, "pause") else: repository = ClientRepo.get_client_manager(Configs.config.client.type) repository.pause(torrent_hash=callback_query.data.split("#")[1]) - await callback_query.answer("Torrent Paused") + await callback_query.answer(Translator.translate(Strings.PauseTorrent, user.locale)) diff --git a/src/bot/plugins/callbacks/pause_resume/resume_callbacks.py b/src/bot/plugins/callbacks/pause_resume/resume_callbacks.py index 477adad..6cb9823 100644 --- a/src/bot/plugins/callbacks/pause_resume/resume_callbacks.py +++ b/src/bot/plugins/callbacks/pause_resume/resume_callbacks.py @@ -5,21 +5,26 @@ from .....client_manager import ClientRepo from ...common import list_active_torrents from .....configs import Configs +from .....configs.user import User +from .....utils import inject_user +from .....translator import Translator, Strings @Client.on_callback_query(custom_filters.resume_all_filter & custom_filters.check_user_filter & (custom_filters.user_is_administrator | custom_filters.user_is_manager)) -async def resume_all_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def resume_all_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: repository = ClientRepo.get_client_manager(Configs.config.client.type) repository.resume_all() - await client.answer_callback_query(callback_query.id, "Resumed all torrents") + await client.answer_callback_query(callback_query.id, Translator.translate(Strings.ResumeAllTorrents, user.locale)) @Client.on_callback_query(custom_filters.resume_filter & custom_filters.check_user_filter & (custom_filters.user_is_administrator | custom_filters.user_is_manager)) -async def resume_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def resume_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: if callback_query.data.find("#") == -1: await list_active_torrents(client, callback_query.from_user.id, callback_query.message.id, "resume") else: repository = ClientRepo.get_client_manager(Configs.config.client.type) repository.resume(torrent_hash=callback_query.data.split("#")[1]) - await callback_query.answer("Torrent Resumed") + await callback_query.answer(Translator.translate(Strings.ResumeTorrent, user.locale)) diff --git a/src/bot/plugins/on_message.py b/src/bot/plugins/on_message.py index 0877c35..eaef6e9 100644 --- a/src/bot/plugins/on_message.py +++ b/src/bot/plugins/on_message.py @@ -103,6 +103,16 @@ async def on_edit_user(client, message, user, action): if user_from_configs == -1: return + if field_to_edit == "locale" and new_value not in Translator.locales.keys(): + await message.reply_text( + Translator.translate( + Strings.LocaleNotFound, + locale=user.locale, + new_locale=new_value + ) + ) + return + setattr(Configs.config.users[user_from_configs], field_to_edit, new_value) Configs.update_config(Configs.config) Configs.reload_config() diff --git a/src/locales/en.json b/src/locales/en.json index 956f495..73bc4c4 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -2,7 +2,8 @@ "errors": { "path_not_valid": "The path entered does not exist! Retry", "error": "Error: ${error}", - "command_does_not_exist": "The command does not exist" + "command_does_not_exist": "The command does not exist", + "locale_not_found": "Locale ${new_locale} not found!" }, "on_message": { @@ -105,6 +106,22 @@ "edit_user_field": "Edit User #${user_id} ${field_to_edit} field", "back_to_user": "\uD83D\uDD19 User#${user_id} info", "new_value_for_field": "Send the new value for field \"${field_to_edit}\" for user #${user_id}. \n\n**Note:** the field type is \"${data_type}\"" + }, + + "pause_resume": { + "pause_resume_menu": "Pause/Resume a torrent", + "pause_all": "⏸ Pause All", + "resume_all": "▶\uFE0F Resume All" + }, + + "pause": { + "pause_all_torrents": "Paused all torrents", + "pause_one_torrent": "Torrent Paused" + }, + + "resume": { + "resume_all_torrents": "Resumed all torrents", + "resume_one_torrent": "Torrent Resumed" } } } \ No newline at end of file diff --git a/src/translator/strings.py b/src/translator/strings.py index 102917b..f954710 100644 --- a/src/translator/strings.py +++ b/src/translator/strings.py @@ -5,6 +5,7 @@ class Strings(StrEnum): GenericError = "errors.error" PathNotValid = "errors.path_not_valid" CommandDoesNotExist = "errors.command_does_not_exist" + LocaleNotFound = "errors.locale_not_found" # On Message UnableToAddMagnet = "on_message.error_adding_magnet" @@ -89,3 +90,16 @@ class Strings(StrEnum): EditUserField = "callbacks.user_settings.edit_user_field" BackToUSer = "callbacks.user_settings.back_to_user" NewValueForUserField = "callbacks.user_settings.new_value_for_field" + + # Pause Resume + PauseResumeMenu = "callbacks.pause_resume.pause_resume_menu" + PauseAll = "callbacks.pause_resume.pause_all" + ResumeAll = "callbacks.pause_resume.resume_all" + + # Pause + PauseAllTorrents = "callbacks.pause.pause_all_torrents" + PauseTorrent = "callbacks.pause.pause_one_torrent" + + # Resume + ResumeAllTorrents = "callbacks.resume.resume_all_torrents" + ResumeTorrent = "callbacks.resume.resume_one_torrent" From d11fe5ff5262bcd998b935ce34f2eda0d11271de Mon Sep 17 00:00:00 2001 From: ch3p4ll3 Date: Mon, 8 Jan 2024 16:32:34 +0100 Subject: [PATCH 08/35] add proxy settings + locales for delete callbacks --- README.md | 5 +++ src/bot/__init__.py | 10 +++++- src/bot/plugins/callbacks/delete/__init__.py | 12 ++++--- .../callbacks/delete/delete_all_callbacks.py | 33 ++++++++++++++----- .../delete/delete_single_callbacks.py | 27 +++++++++++---- src/configs/enums.py | 6 ++++ src/configs/telegram.py | 3 ++ src/configs/telegram_proxy.py | 21 ++++++++++++ src/locales/en.json | 17 ++++++++++ src/translator/strings.py | 12 +++++++ 10 files changed, 127 insertions(+), 19 deletions(-) create mode 100644 src/configs/telegram_proxy.py diff --git a/README.md b/README.md index db832cf..af30299 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,9 @@ +![GitHub License](https://img.shields.io/github/license/ch3p4ll3/QBittorrentBot) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/259099080ca24e029a910e3249d32041)](https://app.codacy.com/gh/ch3p4ll3/QBittorrentBot?utm_source=github.com&utm_medium=referral&utm_content=ch3p4ll3/QBittorrentBot&utm_campaign=Badge_Grade) +![GitHub contributors](https://img.shields.io/github/contributors/ch3p4ll3/QBittorrentBot) +![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/ch3p4ll3/QBittorrentBot/docker-image.yml) + + [![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-) diff --git a/src/bot/__init__.py b/src/bot/__init__.py index 1194ff8..1ab78c0 100644 --- a/src/bot/__init__.py +++ b/src/bot/__init__.py @@ -12,13 +12,21 @@ root="src.bot.plugins" ) + +proxy = None + +if BOT_CONFIGS.telegram.proxy is not None: + proxy = BOT_CONFIGS.telegram.proxy.proxy_settings + + app = Client( "qbittorrent_bot", api_id=BOT_CONFIGS.telegram.api_id, api_hash=BOT_CONFIGS.telegram.api_hash, bot_token=BOT_CONFIGS.telegram.bot_token, parse_mode=ParseMode.MARKDOWN, - plugins=plugins + plugins=plugins, + proxy=proxy ) scheduler = AsyncIOScheduler() diff --git a/src/bot/plugins/callbacks/delete/__init__.py b/src/bot/plugins/callbacks/delete/__init__.py index 9fb3661..d1363d2 100644 --- a/src/bot/plugins/callbacks/delete/__init__.py +++ b/src/bot/plugins/callbacks/delete/__init__.py @@ -2,22 +2,26 @@ from pyrogram.types import CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton from .... import custom_filters +from .....configs.user import User +from .....utils import inject_user +from .....translator import Translator, Strings @Client.on_callback_query(custom_filters.menu_delete_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def menu_delete_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def menu_delete_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: await callback_query.edit_message_text( "Delete a torrent", reply_markup=InlineKeyboardMarkup( [ [ - InlineKeyboardButton("🗑 Delete", "delete_one") + InlineKeyboardButton(Translator.translate(Strings.DeleteTorrentBtn, user.locale), "delete_one") ], [ - InlineKeyboardButton("🗑 Delete All", "delete_all") + InlineKeyboardButton(Translator.translate(Strings.DeleteAllMenuBtn, user.locale), "delete_all") ], [ - InlineKeyboardButton("🔙 Menu", "menu") + InlineKeyboardButton(Translator.translate(Strings.BackToMenu, user.locale), "menu") ] ] ) diff --git a/src/bot/plugins/callbacks/delete/delete_all_callbacks.py b/src/bot/plugins/callbacks/delete/delete_all_callbacks.py index 1d7ed41..a324df7 100644 --- a/src/bot/plugins/callbacks/delete/delete_all_callbacks.py +++ b/src/bot/plugins/callbacks/delete/delete_all_callbacks.py @@ -6,29 +6,46 @@ from ...common import send_menu from .....configs import Configs +from .....configs.user import User +from .....utils import inject_user +from .....translator import Translator, Strings + + @Client.on_callback_query(custom_filters.delete_all_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def delete_all_callback(client: Client, callback_query: CallbackQuery) -> None: - buttons = [[InlineKeyboardButton("🗑 Delete all torrents", "delete_all_no_data")], - [InlineKeyboardButton("🗑 Delete all torrents and data", "delete_all_data")], - [InlineKeyboardButton("🔙 Menu", "menu")]] +@inject_user +async def delete_all_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: + buttons = [ + [ + InlineKeyboardButton(Translator.translate(Strings.DeleteAllBtn, user.locale), "delete_all_no_data") + ], + [ + InlineKeyboardButton(Translator.translate(Strings.DeleteAllData, user.locale), "delete_all_data") + ], + [ + InlineKeyboardButton(Translator.translate(Strings.BackToMenu, user.locale), "menu") + ] + ] + await client.edit_message_reply_markup(callback_query.from_user.id, callback_query.message.id, reply_markup=InlineKeyboardMarkup(buttons)) @Client.on_callback_query(custom_filters.delete_all_no_data_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def delete_all_with_no_data_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def delete_all_with_no_data_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: repository = ClientRepo.get_client_manager(Configs.config.client.type) repository.delete_all_no_data() - await client.answer_callback_query(callback_query.id, "Deleted only torrents") + await client.answer_callback_query(callback_query.id, Translator.translate(Strings.DeletedAll, user.locale)) await send_menu(client, callback_query.message.id, callback_query.from_user.id) @Client.on_callback_query(custom_filters.delete_all_data_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def delete_all_with_data_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def delete_all_with_data_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: repository = ClientRepo.get_client_manager(Configs.config.client.type) repository.delete_all_data() - await client.answer_callback_query(callback_query.id, "Deleted All+Torrents") + await client.answer_callback_query(callback_query.id, Translator.translate(Strings.DeletedAllData, user.locale)) await send_menu(client, callback_query.message.id, callback_query.from_user.id) diff --git a/src/bot/plugins/callbacks/delete/delete_single_callbacks.py b/src/bot/plugins/callbacks/delete/delete_single_callbacks.py index f11c2ca..561c70b 100644 --- a/src/bot/plugins/callbacks/delete/delete_single_callbacks.py +++ b/src/bot/plugins/callbacks/delete/delete_single_callbacks.py @@ -6,25 +6,39 @@ from ...common import send_menu, list_active_torrents from .....configs import Configs +from .....configs.user import User +from .....utils import inject_user +from .....translator import Translator, Strings + + @Client.on_callback_query(custom_filters.delete_one_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def delete_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def delete_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: if callback_query.data.find("#") == -1: await list_active_torrents(client, callback_query.from_user.id, callback_query.message.id, "delete_one") else: buttons = [ - [InlineKeyboardButton("🗑 Delete torrent", f"delete_one_no_data#{callback_query.data.split('#')[1]}")], - [InlineKeyboardButton("🗑 Delete torrent and data", f"delete_one_data#{callback_query.data.split('#')[1]}")], - [InlineKeyboardButton("🔙 Menu", "menu")]] + [ + InlineKeyboardButton(Translator.translate(Strings.DeleteSingleBtn, user.locale), f"delete_one_no_data#{callback_query.data.split('#')[1]}") + ], + [ + InlineKeyboardButton(Translator.translate(Strings.DeleteSingleDataBtn, user.locale), f"delete_one_data#{callback_query.data.split('#')[1]}") + ], + [ + InlineKeyboardButton(Translator.translate(Strings.BackToMenu, user.locale), "menu") + ] + ] await client.edit_message_reply_markup(callback_query.from_user.id, callback_query.message.id, reply_markup=InlineKeyboardMarkup(buttons)) @Client.on_callback_query(custom_filters.delete_one_no_data_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def delete_no_data_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def delete_no_data_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: if callback_query.data.find("#") == -1: await list_active_torrents(client, callback_query.from_user.id, callback_query.message.id, "delete_one_no_data") @@ -36,7 +50,8 @@ async def delete_no_data_callback(client: Client, callback_query: CallbackQuery) @Client.on_callback_query(custom_filters.delete_one_data_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def delete_with_data_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def delete_with_data_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: if callback_query.data.find("#") == -1: await list_active_torrents(client, callback_query.from_user.id, callback_query.message.id, "delete_one_data") diff --git a/src/configs/enums.py b/src/configs/enums.py index 3c58caf..c35a54e 100644 --- a/src/configs/enums.py +++ b/src/configs/enums.py @@ -9,3 +9,9 @@ class UserRolesEnum(str, Enum): Reader = "reader" Manager = "manager" Administrator = "administrator" + + +class TelegramProxyScheme(str, Enum): + Sock4 = "socks4" + Sock5 = "socks5" + Http = "http" \ No newline at end of file diff --git a/src/configs/telegram.py b/src/configs/telegram.py index 64ff2de..a554801 100644 --- a/src/configs/telegram.py +++ b/src/configs/telegram.py @@ -1,10 +1,13 @@ +from typing import Optional from pydantic import BaseModel, field_validator +from .telegram_proxy import TelegramProxy class Telegram(BaseModel): bot_token: str api_id: int api_hash: str + proxy: Optional[TelegramProxy] = None @field_validator('bot_token') def bot_token_validator(cls, v): diff --git a/src/configs/telegram_proxy.py b/src/configs/telegram_proxy.py new file mode 100644 index 0000000..4da033a --- /dev/null +++ b/src/configs/telegram_proxy.py @@ -0,0 +1,21 @@ +from typing import Optional +from pydantic import BaseModel +from .enums import TelegramProxyScheme + + +class TelegramProxy(BaseModel): + scheme: TelegramProxyScheme = TelegramProxyScheme.Http # "socks4", "socks5" and "http" are supported + hostname: str + port: int + username: Optional[str] = None + password: Optional[str] = None + + @property + def proxy_settings(self): + return dict( + scheme=self.scheme.value, + hostname=self.hostname, + port=self.port, + username=self.username, + passowrd=self.password + ) diff --git a/src/locales/en.json b/src/locales/en.json index 73bc4c4..c3362dc 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -122,6 +122,23 @@ "resume": { "resume_all_torrents": "Resumed all torrents", "resume_one_torrent": "Torrent Resumed" + }, + + "delete_delete_all": { + "delete_all": "🗑 Delete All" + }, + + "delete_all": { + "delete_all": "🗑 Delete all torrents", + "delete_all_data": "🗑 Delete all torrents and data", + + "deleted_all": "Deleted all torrents", + "deleted_all_data": "Deleted all torrents and data" + }, + + "delete_single": { + "delete_single": "🗑 Delete torrent", + "delete_single_data": "🗑 Delete torrent and data" } } } \ No newline at end of file diff --git a/src/translator/strings.py b/src/translator/strings.py index f954710..74ab937 100644 --- a/src/translator/strings.py +++ b/src/translator/strings.py @@ -103,3 +103,15 @@ class Strings(StrEnum): # Resume ResumeAllTorrents = "callbacks.resume.resume_all_torrents" ResumeTorrent = "callbacks.resume.resume_one_torrent" + + # Delete + + DeleteAllMenuBtn = "callbacks.delete_delete_all.delete_all" + DeleteAllBtn = "callbacks.delete_all.delete_all" + DeleteAllData = "callbacks.delete_all.delete_all_data" + + DeletedAll = "callbacks.delete_all.deleted_all" + DeletedAllData = "callbacks.delete_all.deleted_all_data" + + DeleteSingleBtn = "callbacks.delete_single.delete_single" + DeleteSingleDataBtn = "callbacks.delete_single.delete_single_data" \ No newline at end of file From cd194c1ea5908e4ad424a8ae28d5603e4b3a2319 Mon Sep 17 00:00:00 2001 From: ch3p4ll3 Date: Mon, 8 Jan 2024 17:25:58 +0100 Subject: [PATCH 09/35] add localization to category --- .../plugins/callbacks/category/__init__.py | 14 ++- .../callbacks/category/category_callbacks.py | 105 +++++++++++++----- src/bot/plugins/on_message.py | 1 - src/locales/en.json | 12 ++ src/translator/strings.py | 13 ++- src/utils.py | 2 +- 6 files changed, 109 insertions(+), 38 deletions(-) diff --git a/src/bot/plugins/callbacks/category/__init__.py b/src/bot/plugins/callbacks/category/__init__.py index 534d78a..cfce486 100644 --- a/src/bot/plugins/callbacks/category/__init__.py +++ b/src/bot/plugins/callbacks/category/__init__.py @@ -2,24 +2,28 @@ from pyrogram.types import CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton from .... import custom_filters +from .....configs.user import User +from .....utils import inject_user +from .....translator import Translator, Strings @Client.on_callback_query(custom_filters.menu_category_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def menu_category_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def menu_category_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: await callback_query.edit_message_text( "Pause/Resume a download", reply_markup=InlineKeyboardMarkup( [ [ - InlineKeyboardButton("➕ Add Category", "add_category"), + InlineKeyboardButton(Translator.translate(Strings.AddCategory, user.locale), "add_category"), ], [ - InlineKeyboardButton("🗑 Remove Category", "select_category#remove_category") + InlineKeyboardButton(Translator.translate(Strings.RemoveCategory, user.locale), "select_category#remove_category") ], [ - InlineKeyboardButton("📝 Modify Category", "select_category#modify_category")], + InlineKeyboardButton(Translator.translate(Strings.EditCategory, user.locale), "select_category#modify_category")], [ - InlineKeyboardButton("🔙 Menu", "menu") + InlineKeyboardButton(Translator.translate(Strings.BackToMenu, user.locale), "menu") ] ] ) diff --git a/src/bot/plugins/callbacks/category/category_callbacks.py b/src/bot/plugins/callbacks/category/category_callbacks.py index 0cbc71d..d727897 100644 --- a/src/bot/plugins/callbacks/category/category_callbacks.py +++ b/src/bot/plugins/callbacks/category/category_callbacks.py @@ -8,68 +8,105 @@ from .....client_manager import ClientRepo from .....configs import Configs +from .....configs.user import User +from .....utils import inject_user +from .....translator import Translator, Strings + @Client.on_callback_query(custom_filters.add_category_filter & custom_filters.check_user_filter & (custom_filters.user_is_administrator | custom_filters.user_is_manager)) -async def add_category_callback(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def add_category_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: db_management.write_support("category_name", callback_query.from_user.id) - button = InlineKeyboardMarkup([[InlineKeyboardButton("🔙 Menu", "menu")]]) + button = InlineKeyboardMarkup([[InlineKeyboardButton(Translator.translate(Strings.BackToMenu, user.locale), "menu")]]) try: - await client.edit_message_text(callback_query.from_user.id, callback_query.message.id, - "Send the category name", reply_markup=button) + await client.edit_message_text( + callback_query.from_user.id, + callback_query.message.id, + Translator.translate(Strings.NewCategoryName, user.locale), + reply_markup=button + ) + except MessageIdInvalid: - await client.send_message(callback_query.from_user.id, "Send the category name", reply_markup=button) + await client.send_message( + callback_query.from_user.id, + Translator.translate(Strings.NewCategoryName, user.locale), + reply_markup=button + ) @Client.on_callback_query(custom_filters.select_category_filter & custom_filters.check_user_filter & (custom_filters.user_is_administrator | custom_filters.user_is_manager)) -async def list_categories(client: Client, callback_query: CallbackQuery): +@inject_user +async def list_categories(client: Client, callback_query: CallbackQuery, user: User): buttons = [] repository = ClientRepo.get_client_manager(Configs.config.client.type) categories = repository.get_categories() if categories is None: - buttons.append([InlineKeyboardButton("🔙 Menu", "menu")]) - await client.edit_message_text(callback_query.from_user.id, callback_query.message.id, - "There are no categories", reply_markup=InlineKeyboardMarkup(buttons)) + buttons.append([InlineKeyboardButton(Translator.translate(Strings.BackToMenu, user.locale), "menu")]) + await client.edit_message_text( + callback_query.from_user.id, + callback_query.message.id, + Translator.translate(Strings.NoCategory, user.locale), + reply_markup=InlineKeyboardMarkup(buttons) + ) + return for _, i in enumerate(categories): buttons.append([InlineKeyboardButton(i, f"{callback_query.data.split('#')[1]}#{i}")]) - buttons.append([InlineKeyboardButton("🔙 Menu", "menu")]) + buttons.append([InlineKeyboardButton(Translator.translate(Strings.BackToMenu, user.locale), "menu")]) try: - await client.edit_message_text(callback_query.from_user.id, callback_query.message.id, - "Choose a category:", reply_markup=InlineKeyboardMarkup(buttons)) + await client.edit_message_text( + callback_query.from_user.id, + callback_query.message.id, + Translator.translate(Strings.ChooseCategory, user.locale), + reply_markup=InlineKeyboardMarkup(buttons) + ) + except MessageIdInvalid: - await client.send_message(callback_query.from_user.id, "Choose a category:", - reply_markup=InlineKeyboardMarkup(buttons)) + await client.send_message( + callback_query.from_user.id, + Translator.translate(Strings.ChooseCategory, user.locale), + reply_markup=InlineKeyboardMarkup(buttons) + ) @Client.on_callback_query(custom_filters.remove_category_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def remove_category_callback(client: Client, callback_query: CallbackQuery) -> None: - buttons = [[InlineKeyboardButton("🔙 Menu", "menu")]] +@inject_user +async def remove_category_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: + buttons = [[InlineKeyboardButton(Translator.translate(Strings.BackToMenu, user.locale), "menu")]] repository = ClientRepo.get_client_manager(Configs.config.client.type) repository.remove_category(callback_query.data.split("#")[1]) - await client.edit_message_text(callback_query.from_user.id, callback_query.message.id, - f"The category {callback_query.data.split('#')[1]} has been removed", - reply_markup=InlineKeyboardMarkup(buttons)) + await client.edit_message_text( + callback_query.from_user.id, + callback_query.message.id, + Translator.translate(Strings.OnCategoryRemoved, user.locale, category_name=callback_query.data.split('#')[1]), + reply_markup=InlineKeyboardMarkup(buttons) + ) @Client.on_callback_query(custom_filters.modify_category_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) -async def modify_category_callback(client: Client, callback_query: CallbackQuery) -> None: - buttons = [[InlineKeyboardButton("🔙 Menu", "menu")]] +@inject_user +async def modify_category_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: + buttons = [[InlineKeyboardButton(Translator.translate(Strings.BackToMenu, user.locale), "menu")]] db_management.write_support(f"category_dir_modify#{callback_query.data.split('#')[1]}", callback_query.from_user.id) - await client.edit_message_text(callback_query.from_user.id, callback_query.message.id, - f"Send new path for category {callback_query.data.split('#')[1]}", - reply_markup=InlineKeyboardMarkup(buttons)) + await client.edit_message_text( + callback_query.from_user.id, + callback_query.message.id, + Translator.translate(Strings.OnCategoryEdited, user.locale, category_name=callback_query.data.split('#')[1]), + reply_markup=InlineKeyboardMarkup(buttons) + ) @Client.on_callback_query(custom_filters.category_filter & custom_filters.check_user_filter & (custom_filters.user_is_administrator | custom_filters.user_is_manager)) -async def category(client: Client, callback_query: CallbackQuery) -> None: +@inject_user +async def category(client: Client, callback_query: CallbackQuery, user: User) -> None: buttons = [] repository = ClientRepo.get_client_manager(Configs.config.client.type) @@ -88,11 +125,19 @@ async def category(client: Client, callback_query: CallbackQuery) -> None: buttons.append([InlineKeyboardButton(i, f"{callback_query.data.split('#')[1]}#{i}")]) buttons.append([InlineKeyboardButton("None", f"{callback_query.data.split('#')[1]}#None")]) - buttons.append([InlineKeyboardButton("🔙 Menu", "menu")]) + buttons.append([InlineKeyboardButton(Translator.translate(Strings.BackToMenu, user.locale), "menu")]) try: - await client.edit_message_text(callback_query.from_user.id, callback_query.message.id, - "Choose a category:", reply_markup=InlineKeyboardMarkup(buttons)) + await client.edit_message_text( + callback_query.from_user.id, + callback_query.message.id, + Translator.translate(Strings.ChooseCategory, user.locale), + reply_markup=InlineKeyboardMarkup(buttons) + ) + except MessageIdInvalid: - await client.send_message(callback_query.from_user.id, "Choose a category:", - reply_markup=InlineKeyboardMarkup(buttons)) + await client.send_message( + callback_query.from_user.id, + Translator.translate(Strings.ChooseCategory, user.locale), + reply_markup=InlineKeyboardMarkup(buttons) + ) diff --git a/src/bot/plugins/on_message.py b/src/bot/plugins/on_message.py index eaef6e9..c546843 100644 --- a/src/bot/plugins/on_message.py +++ b/src/bot/plugins/on_message.py @@ -1,5 +1,4 @@ import logging -import os import tempfile from pyrogram import Client, filters from pyrogram.types import Message diff --git a/src/locales/en.json b/src/locales/en.json index c3362dc..c48835b 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -139,6 +139,18 @@ "delete_single": { "delete_single": "🗑 Delete torrent", "delete_single_data": "🗑 Delete torrent and data" + }, + + "category": { + "add_category_btn": "➕ Add Category", + "remove_category_btn": "🗑 Remove Category", + "modify_category_btn": "📝 Modify Category", + + "send_category_name": "Send the category name", + "no_categories": "There are no categories", + "choose_category": "Choose a category:", + "on_category_removed": "The category ${category_name} has been removed", + "on_category_edited": "Send new path for category ${category_name}" } } } \ No newline at end of file diff --git a/src/translator/strings.py b/src/translator/strings.py index 74ab937..9bf670f 100644 --- a/src/translator/strings.py +++ b/src/translator/strings.py @@ -114,4 +114,15 @@ class Strings(StrEnum): DeletedAllData = "callbacks.delete_all.deleted_all_data" DeleteSingleBtn = "callbacks.delete_single.delete_single" - DeleteSingleDataBtn = "callbacks.delete_single.delete_single_data" \ No newline at end of file + DeleteSingleDataBtn = "callbacks.delete_single.delete_single_data" + + # Categories + AddCategory = "callbacks.category.add_category_btn" + RemoveCategory = "callbacks.category.remove_category_btn" + EditCategory = "callbacks.category.modify_category_btn" + + NewCategoryName = "callbacks.category.send_category_name" + NoCategory = "callbacks.category.no_categories" + ChooseCategory = "callbacks.category.choose_category" + OnCategoryRemoved = "callbacks.category.on_category_removed" + OnCategoryEdited = "callbacks.category.on_category_edited" diff --git a/src/utils.py b/src/utils.py index b860317..f8612a1 100644 --- a/src/utils.py +++ b/src/utils.py @@ -78,4 +78,4 @@ async def wrapper(client, message): user = get_user_from_config(message.from_user.id) await func(client, message, user) - return wrapper \ No newline at end of file + return wrapper From 526655599d78f19a3b7ebdea7b22225525eb6c61 Mon Sep 17 00:00:00 2001 From: ch3p4ll3 Date: Mon, 8 Jan 2024 17:33:29 +0100 Subject: [PATCH 10/35] quick fix --- src/bot/plugins/callbacks/category/category_callbacks.py | 6 +++--- src/client_manager/mappers/qbittorrent_mapper.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/bot/plugins/callbacks/category/category_callbacks.py b/src/bot/plugins/callbacks/category/category_callbacks.py index d727897..35425fa 100644 --- a/src/bot/plugins/callbacks/category/category_callbacks.py +++ b/src/bot/plugins/callbacks/category/category_callbacks.py @@ -114,14 +114,14 @@ async def category(client: Client, callback_query: CallbackQuery, user: User) -> if categories is None: if "magnet" in callback_query.data: - await add_magnet_callback(client, callback_query) + await add_magnet_callback(client, callback_query, user) else: - await add_torrent_callback(client, callback_query) + await add_torrent_callback(client, callback_query, user) return - for key, i in enumerate(categories): + for _, i in enumerate(categories): buttons.append([InlineKeyboardButton(i, f"{callback_query.data.split('#')[1]}#{i}")]) buttons.append([InlineKeyboardButton("None", f"{callback_query.data.split('#')[1]}#None")]) diff --git a/src/client_manager/mappers/qbittorrent_mapper.py b/src/client_manager/mappers/qbittorrent_mapper.py index f1ad1aa..1d33766 100644 --- a/src/client_manager/mappers/qbittorrent_mapper.py +++ b/src/client_manager/mappers/qbittorrent_mapper.py @@ -8,7 +8,7 @@ class QBittorrentMapper(Mapper): @classmethod def map(cls, torrents: Union[TorrentDictionary, TorrentInfoList]) -> Union[Torrent, List[Torrent]]: - if type(torrents) is TorrentInfoList: + if isinstance(torrents, TorrentInfoList): return [ Torrent( torrent.info.hash, From 21372d969729aa6db756812381ca22f2b3fc5681 Mon Sep 17 00:00:00 2001 From: ch3p4ll3 Date: Tue, 9 Jan 2024 09:28:32 +0100 Subject: [PATCH 11/35] add ru_ua + uk_ua locales --- requirements.txt | 2 + .../settings/users_settings_callbacks.py | 4 + src/locales/ru_UA.json | 156 ++++++++++++++++++ src/locales/uk_UA.json | 156 ++++++++++++++++++ 4 files changed, 318 insertions(+) create mode 100644 src/locales/ru_UA.json create mode 100644 src/locales/uk_UA.json diff --git a/requirements.txt b/requirements.txt index 5fbd17e..f78d18b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ APScheduler==3.10.4 async-lru==2.0.4 certifi==2023.11.17 charset-normalizer==3.3.2 +executing==2.0.1 idna==3.6 packaging==23.2 pony==0.7.17 @@ -24,3 +25,4 @@ tzdata==2023.3 tzlocal==5.2 urllib3==2.1.0 uvloop==0.19.0 +varname==0.12.2 diff --git a/src/bot/plugins/callbacks/settings/users_settings_callbacks.py b/src/bot/plugins/callbacks/settings/users_settings_callbacks.py index c232757..740bce9 100644 --- a/src/bot/plugins/callbacks/settings/users_settings_callbacks.py +++ b/src/bot/plugins/callbacks/settings/users_settings_callbacks.py @@ -1,5 +1,6 @@ from pyrogram import Client from pyrogram.types import CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton +from varname import nameof from .... import custom_filters from .....configs import Configs @@ -80,6 +81,9 @@ async def edit_user_callback(client: Client, callback_query: CallbackQuery, user user_info = get_user_from_config(user_id) + # if field_to_edit == nameof(user.locale): + # pass + if data_type == bool: notify_status = Translator.translate(Strings.Enabled if user_info.notify else Strings.Disabled, user.locale) diff --git a/src/locales/ru_UA.json b/src/locales/ru_UA.json new file mode 100644 index 0000000..ec38507 --- /dev/null +++ b/src/locales/ru_UA.json @@ -0,0 +1,156 @@ +{ + "errors": { + "path_not_valid": "Путь не верный! Проверь и повтори", + "error": "Ошибочка: ${error}", + "command_does_not_exist": "Не понимаю что ты хош ", + "locale_not_found": "Локаль ${new_locale} не найдена!" + }, + + "on_message": { + "error_adding_magnet": "Не возможно добавить магнит", + "invalid_magnet": "Твой магнит инвалид, проверь", + "error_adding_torrent": "Не возможно добавить торрент", + "invalid_torrent": "Твой торрент инвалид, проверь", + "send_category_path": "Пж, пришли мне путь к новой категории ${category_name}" + }, + + + "common": { + "menu": "Приветули от QBittorrent Bot", + "menu_btn": { + "menu_list": "📝 Список", + "add_magnet": "➕ Добавить магнит", + "add_torrent": "➕ Добавить торрент", + "pause_resume": "⏯ Пауза\\Продолжить", + "delete": "🗑 Удалить", + "categories": "📂 Категории", + "settings": "⚙️ Настройки" + }, + + "list_filter": { + "downloading": "⏳ ${active} Загружается", + "completed": "✔️ ${active} Загружен", + "paused": "⏸️ ${active} На паузе" + }, + + "no_torrents": "Нет торрентов", + "back_to_menu_btn": "🔙 Меню" + }, + + "commands": { + "not_authorized": "Ты не авторизован в этом боте", + "stats_command": "**============SYSTEM============**\n**CPU Usage:** ${cpu_usage}%\n**CPU Temp:** ${cpu_temp}°C\n**Free Memory:** ${free_memory} of ${total_memory} (${memory_percent}%)\n**Disks usage:** ${disk_used} of ${disk_total} (${disk_percent}%)" + }, + + "callbacks": { + "torrent_info": { + "torrent_completed": "**ВЫПОЛНЕНО**\n", + "torrent_state": "**State:** ${current_state} \n**Download Speed:** ${download_speed}/s\n", + "torrent_size": "**Размер:** ${torrent_size}\n", + "torrent_eta": "**ETA:** ${torrent_eta}\n", + "torrent_category": "**Категория:** ${torrent_category}\n", + + "info_btns": { + "export_torrent": "💾 Экспортировать торрент", + "pause_torrent": "⏸ Пауза", + "resume_torrent": "▶️ Продолжить", + "delete_torrent": "🗑 Удалить" + } + }, + + "add_torrents": { + "send_magnet": "Отправь мне магнит", + "send_torrent": "Отправь мне торрент" + }, + + "settings": { + "settings_menu": "QBittorrentBot Настройки", + "settings_menu_btns": { + "users_settings": "🫂 Настройки пользователей", + "client_settings": "📥 Настройки клиента QBt", + "reload_settings": "🔄 Перезагрузить настройки" + } + }, + + "client_settings": { + "enabled": "✅ Включено", + "disabled": "❌ Выключено", + "speed_limit_status": "\n\n**Ограничение скорости**: ${speed_limit_status}", + "edit_client_settings": "Редактировать ${client_type} настройки QBt \n\n**Настройки:**\n- ${configs}", + + "edit_client_settings_btns": { + "edit_client_settings": "📝 Редактировать настройки QBt", + "toggle_speed_limit": "🐢 Переключить ограничение скорости", + "check_client_connection": "✅ Проверить подключение к QBt", + "back_to_settings": "🔙 Настройки" + }, + + "client_connection_ok": "✅ Подключение в норме. Версия QBt: ${version}", + "client_connection_bad": "❌ Нет подключения к QBt", + + "edit_client_setting": "Редактировать ${setting}", + "edit_client_type": "Редактировать ${client_type} ", + + "new_value_for_field": "Отправь новое значение для поля \"${field_to_edit}\" \n \n**Примечание.** тип поля: \"${data_type}\"" + }, + + "reload_settings": { + "settings_reloaded": "Настройки перезагружены" + }, + + "user_settings": { + "user_btn": "Юзер #${user_id}", + "authorized_users": "Авторизованные пользователи", + "edit_user_setting": "Edit User #${user_id}\n\n**Текущие настройки:**\n- ${confs}", + "back_to_users": "🔙 Пользователи", + "edit_user_field": "Изменить настройки пользователя #${user_id} ${field_to_edit} ", + "back_to_user": "🔙 Инфо о пользователе #${user_id} ", + "new_value_for_field": "Отправь новое значение для поля \"${field_to_edit}\" для пользователя #${user_id}. \n\n**Примечание.** тип поля: \"${data_type}\"" + }, + + "pause_resume": { + "pause_resume_menu": "Пауза/Продолжить торрент", + "pause_all": "⏸ Запаузить все", + "resume_all": "▶️ Продолжить все" + }, + + "pause": { + "pause_all_torrents": "Все торренты на паузе", + "pause_one_torrent": "Торрент на паузе" + }, + + "resume": { + "resume_all_torrents": "Продолжить все торренты", + "resume_one_torrent": "Торрент снова в загрузке" + }, + + "delete_delete_all": { + "delete_all": "🗑 Удалить все" + }, + + "delete_all": { + "delete_all": "🗑 Удалить все *torrents", + "delete_all_data": "🗑 Удалить все *torrents и их файлики", + + "deleted_all": "Все торренты удалены", + "deleted_all_data": "Все файлики и их торренты удалены" + }, + + "delete_single": { + "delete_single": "🗑 Удалить торрент", + "delete_single_data": "🗑 Удалить торрент и файлики" + }, + + "category": { + "add_category_btn": "➕ Добавить категорию", + "remove_category_btn": "🗑 Удалить категорию", + "modify_category_btn": "📝 Изменить категорию", + + "send_category_name": "Пришли мне название категории", + "no_categories": "Нет категорий", + "choose_category": "Выбери категорию:", + "on_category_removed": "Категория ${category_name} удалена", + "on_category_edited": "Отправь мне новый путь к категории ${category_name}" + } + } +} \ No newline at end of file diff --git a/src/locales/uk_UA.json b/src/locales/uk_UA.json new file mode 100644 index 0000000..05e6f61 --- /dev/null +++ b/src/locales/uk_UA.json @@ -0,0 +1,156 @@ +{ + "errors": { + "path_not_valid": "Введений шлях не існує! Перевiр та повтори спробу.", + "error": "Помилка: ${error}", + "command_does_not_exist": "Команда не існує", + "locale_not_found": "Локалізацію ${new_locale} не знайдено!" + }, + + "on_message": { + "error_adding_magnet": "Неможливо додати магніт", + "invalid_magnet": "Цей магніт недійсний! Перевiр та повтори спробу.", + "error_adding_torrent": "Неможливо додати торрент-файл", + "invalid_torrent": "Це не торрент-файл! Перевiр та повтори спробу.", + "send_category_path": "Будьласочка, надішли шлях для категорії ${category_name}" + }, + + + "common": { + "menu": "Ласкаво просимо до QBittorrent Bot", + "menu_btn": { + "menu_list": "📝 Список", + "add_magnet": "➕ Додати магніт", + "add_torrent": "➕ Додати торрент", + "pause_resume": "⏯ Пауза/Продовжити", + "delete": "🗑 Видалити", + "categories": "📂 Категорії", + "settings": "⚙️ Налаштування" + }, + + "list_filter": { + "downloading": "⏳ ${active} Завантаження", + "completed": "✔️ ${active} Завершено", + "paused": "⏸️ ${active} Призупинено" + }, + + "no_torrents": "Немає торрентів", + "back_to_menu_btn": "🔙 Меню" + }, + + "commands": { + "not_authorized": "В тебе не маєте дозволу використовувати цього бота", + "stats_command": "**============SYSTEM============**\n**CPU Usage:** ${cpu_usage}%\n**CPU Temp:** ${cpu_temp}°C\n**Free Memory:** ${free_memory} of ${total_memory} (${memory_percent}%)\n**Disks usage:** ${disk_used} of ${disk_total} (${disk_percent}%)" + }, + + "callbacks": { + "torrent_info": { + "torrent_completed": "**ЗАВЕРШЕНО**\n", + "torrent_state": "**Стан:** ${current_state}\n**Швидкість завантаження:** ${download_speed}/с\n\n", + "torrent_size": "**Розмір:** ${torrent_size}\n", + "torrent_eta": "**ETA:** ${torrent_eta}\n", + "torrent_category": "**Категорія:** ${torrent_category}\n", + + "info_btns": { + "export_torrent": "💾 Експорт торрента", + "pause_torrent": "⏸ Пауза", + "resume_torrent": "▶️ Продовжити", + "delete_torrent": "🗑 Видалити" + } + }, + + "add_torrents": { + "send_magnet": "Надішли магніт", + "send_torrent": "Надішли торрент" + }, + + "settings": { + "settings_menu": "QBittorrentBot Налаштування", + "settings_menu_btns": { + "users_settings": "🫂 Налаштування користувачів", + "client_settings": "📥 Налаштування клієнта", + "reload_settings": "🔄 Перезавантажити налаштування" + } + }, + + "client_settings": { + "enabled": "✅ Увімкнено", + "disabled": "❌ Вимкнено", + "speed_limit_status": "\n\n**Обмеження швидкості**: ${speed_limit_status}", + "edit_client_settings": "Редагувати налаштування клієнта ${client_type}\n\n**Поточні налаштування:**\n- ${configs}", + + "edit_client_settings_btns": { + "edit_client_settings": "📝 Редагувати налаштування клієнта", + "toggle_speed_limit": "🐢 Перемкнути обмеження швидкості", + "check_client_connection": "✅ Перевірка підключення клієнта", + "back_to_settings": "🔙 Налаштування" + }, + + "client_connection_ok": "✅ Підключення працює. Версія QBittorrent: ${version}", + "client_connection_bad": "❌ Неможливо встановити з'єднання з QBittorrent", + + "edit_client_setting": "Редагувати ${setting}", + "edit_client_type": "Редагувати клієнта ${client_type}", + + "new_value_for_field": "Надішли нове значення для поля \"${field_to_edit}\" для клієнта.\n\n**Примітка:** тип поля - \"${data_type}\"" + }, + + "reload_settings": { + "settings_reloaded": "Налаштування перезавантажено" + }, + + "user_settings": { + "user_btn": "Користувач #${user_id}", + "authorized_users": "Авторизовані користувачі", + "edit_user_setting": "Редагувати користувача #${user_id}\n\n**Поточні налаштування:**\n- ${confs}", + "back_to_users": "🔙 Користувачі", + "edit_user_field": "Редагувати поле користувача №${user_id} ${field_to_edit}", + "back_to_user": "🔙 Інформація користувача#${user_id}", + "new_value_for_field": "Надішліть нове значення для поля \"${field_to_edit}\" для користувача №${user_id}.\n\n**Примітка:** тип поля - \"${data_type}\"" + }, + + "pause_resume": { + "pause_resume_menu": "Призупинити/продовжити торрент", + "pause_all": "⏸ Призупинити все", + "resume_all": "▶️ Відновити все" + }, + + "pause": { + "pause_all_torrents": "Призупинено всі торренти", + "pause_one_torrent": "Торрент призупинено" + }, + + "resume": { + "resume_all_torrents": "Відновлено всі торренти", + "resume_one_torrent": "Торрент відновлено" + }, + + "delete_delete_all": { + "delete_all": "🗑 Видалити все" + }, + + "delete_all": { + "delete_all": "🗑 Видалити всі торренти", + "delete_all_data": "🗑 Видалити всі торренти та дані", + + "deleted_all": "Видалено всі торренти", + "deleted_all_data": "Видалено всі торренти та дані" + }, + + "delete_single": { + "delete_single": "🗑 Видалити торрент", + "delete_single_data": "🗑 Видалити торрент та дані" + }, + + "category": { + "add_category_btn": "➕ Додати категорію", + "remove_category_btn": "🗑 Видалити категорію", + "modify_category_btn": "📝 Змінити категорію", + + "send_category_name": "Надішли назву категорії", + "no_categories": "Немає категорій", + "choose_category": "Оберіть категорію.", + "on_category_removed": "Категорію ${category_name} було видалено", + "on_category_edited": "Відправ менi новий шлях для категорії ${category_name}" + } + } +} \ No newline at end of file From b09df488ca946c506489e2152521095d25e99202 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Tue, 9 Jan 2024 10:31:28 +0000 Subject: [PATCH 12/35] Translate src/locales/en.json in it 100% translated source file: 'src/locales/en.json' on 'it'. --- src/locales/it.json | 156 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 src/locales/it.json diff --git a/src/locales/it.json b/src/locales/it.json new file mode 100644 index 0000000..73af30f --- /dev/null +++ b/src/locales/it.json @@ -0,0 +1,156 @@ +{ + "errors": { + "path_not_valid": "Il path non esiste! Riprova", + "error": "Errore: ${error}", + "command_does_not_exist": "Il comando non esiste", + "locale_not_found": "Lingua ${new_locale} non trovata" + }, + + "on_message": { + "error_adding_magnet": "Impossibile aggiungere magnet link", + "invalid_magnet": "Questo magnet link è invalido! Riprova", + "error_adding_torrent": "Impossibile aggiungere file torrent", + "invalid_torrent": "Questo non è un file torrent! Riprova", + "send_category_path": "Per favore, invia il percorso per la categoria ${category_name}" + }, + + + "common": { + "menu": "Benvenuto in QBittorrent Bot", + "menu_btn": { + "menu_list": "📝 Elenco", + "add_magnet": "➕ Aggiungi Magnet", + "add_torrent": "➕ Aggiungi Torrent", + "pause_resume": "⏯ Pausa/Riprendi", + "delete": "🗑 Elimina", + "categories": "📂 Categorie", + "settings": "⚙️ Impostazioni" + }, + + "list_filter": { + "downloading": "⏳ ${active} Downloading", + "completed": "✔️ ${active} Completato", + "paused": "⏸️ ${active} In pausa" + }, + + "no_torrents": "Non ci sono torrent", + "back_to_menu_btn": "🔙 Menu" + }, + + "commands": { + "not_authorized": "Non sei autorizzato a utilizzare questo bot", + "stats_command": "**============SISTEMA============**\n**Utilizzo CPU:** ${cpu_usage}%\n**Temperatura CPU:** ${cpu_temp}°C\n**Memoria libera:** ${free_memory} di ${total_memory} (${memory_percent}%)\n**Utilizzo dischi:** ${disk_used} di ${disk_total} (${disk_percent}%)" + }, + + "callbacks": { + "torrent_info": { + "torrent_completed": "**COMPLETATO**\n", + "torrent_state": "**Stato:** ${current_state}\n**Velocità di download:** ${download_speed}/s\n", + "torrent_size": "**Dimensione:** ${torrent_size}\n", + "torrent_eta": "**ETA:** ${torrent_eta}\n", + "torrent_category": "**Categoria:** ${torrent_category}\n", + + "info_btns": { + "export_torrent": "💾 Esporta torrent", + "pause_torrent": "⏸ Pausa", + "resume_torrent": "▶️ Riprendi", + "delete_torrent": "🗑 Elimina" + } + }, + + "add_torrents": { + "send_magnet": "Invia un link magnet", + "send_torrent": "Invia un file torrent" + }, + + "settings": { + "settings_menu": "Impostazioni QBittorrentBot", + "settings_menu_btns": { + "users_settings": "🫂 Impostazioni utenti", + "client_settings": "📥 Impostazioni client", + "reload_settings": "🔄 Ricarica impostazioni" + } + }, + + "client_settings": { + "enabled": "✅ Abilitato", + "disabled": "❌ Disabilitato", + "speed_limit_status": "\n\n**Limite di velocità**: ${speed_limit_status}", + "edit_client_settings": "Modifica le impostazioni del cliente ${client_type}\n\n**Impostazioni attuali:**\n- ${configs}", + + "edit_client_settings_btns": { + "edit_client_settings": "📝 Modifica impostazioni client", + "toggle_speed_limit": "🐢 Commuta limitazione di velocità", + "check_client_connection": "✅ Verifica la connessione del client", + "back_to_settings": "🔙 Impostazioni" + }, + + "client_connection_ok": "✅ La connessione funziona. Versione di QBittorrent: ${version}", + "client_connection_bad": "❌ Impossibile stabilire una connessione con QBittorrent", + + "edit_client_setting": "Modifica ${setting}", + "edit_client_type": "Modifica ${client_type} client", + + "new_value_for_field": "Invia il nuovo valore per il campo \"${field_to_edit}\" per il cliente.\n\n**Nota:** il tipo di campo è \"${data_type}\"" + }, + + "reload_settings": { + "settings_reloaded": "Impostazioni Ricaricate" + }, + + "user_settings": { + "user_btn": "User #${user_id}", + "authorized_users": "Utenti autorizzati", + "edit_user_setting": "Modifica utente #${user_id}\n\n**Impostazioni attuali:**\n- ${confs}", + "back_to_users": "🔙 Utenti", + "edit_user_field": "Modifica campo utente #${user_id} ${field_to_edit}", + "back_to_user": "🔙 Informazioni utente#${user_id}", + "new_value_for_field": "Invia il nuovo valore per il campo \"${field_to_edit}\" per l'utente #${user_id}.\n\n**Nota:** il tipo di campo è \"${data_type}\"" + }, + + "pause_resume": { + "pause_resume_menu": "Pausa/Riprendi un torrent", + "pause_all": "⏸ Pausa tutto", + "resume_all": "▶️ Riprendi tutto" + }, + + "pause": { + "pause_all_torrents": "Fermati tutti i torrent", + "pause_one_torrent": "Torrent in pausa" + }, + + "resume": { + "resume_all_torrents": "Ripresi tutti i torrent", + "resume_one_torrent": "Torrent Ripreso" + }, + + "delete_delete_all": { + "delete_all": "🗑 Elimina tutto" + }, + + "delete_all": { + "delete_all": "🗑 Elimina tutti i torrent", + "delete_all_data": "🗑 Elimina tutti i torrent ed i dati", + + "deleted_all": "Eliminati tutti i torrent", + "deleted_all_data": "Eliminati tutti i torrent ed i dati." + }, + + "delete_single": { + "delete_single": "🗑 Elimina torrent", + "delete_single_data": "🗑 Elimina torrent e dati" + }, + + "category": { + "add_category_btn": "➕ Aggiungi Categoria", + "remove_category_btn": "🗑 Rimuovi Categoria", + "modify_category_btn": "📝 Modifica Categoria", + + "send_category_name": "Invia il nome della categoria", + "no_categories": "Non ci sono categorie", + "choose_category": "Scegli una categoria.", + "on_category_removed": "La categoria ${category_name} è stata rimossa", + "on_category_edited": "Invia il nuovo percorso per la categoria ${category_name}" + } + } +} \ No newline at end of file From e4c213590c5b0a5d303c42cb36a310adf6f4def2 Mon Sep 17 00:00:00 2001 From: ch3p4ll3 Date: Tue, 9 Jan 2024 11:40:15 +0100 Subject: [PATCH 13/35] en lang name --- src/locales/en.json | 3 +++ src/translator/strings.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/locales/en.json b/src/locales/en.json index c48835b..9cb5037 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1,4 +1,7 @@ { + "lang_name": "English", + "en_lang_name": "English", + "errors": { "path_not_valid": "The path entered does not exist! Retry", "error": "Error: ${error}", diff --git a/src/translator/strings.py b/src/translator/strings.py index 9bf670f..bff167f 100644 --- a/src/translator/strings.py +++ b/src/translator/strings.py @@ -2,6 +2,9 @@ class Strings(StrEnum): + LangName = "lang_name" + EnLangName = "en_lang_name" + # Errors GenericError = "errors.error" PathNotValid = "errors.path_not_valid" CommandDoesNotExist = "errors.command_does_not_exist" From ad98d26548e8273b56088996e62f8dc73c8ebe16 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Tue, 9 Jan 2024 10:41:24 +0000 Subject: [PATCH 14/35] Translate src/locales/en.json in it 100% translated source file: 'src/locales/en.json' on 'it'. --- src/locales/it.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/locales/it.json b/src/locales/it.json index 73af30f..cfcefaf 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -1,4 +1,7 @@ { + "lang_name": "Italiano", + "en_lang_name": "Italian", + "errors": { "path_not_valid": "Il path non esiste! Riprova", "error": "Errore: ${error}", From f1f698316824b5dda0424f0eb892440ab8c07630 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Tue, 9 Jan 2024 11:38:34 +0000 Subject: [PATCH 15/35] Translate src/locales/en.json in ru_UA 100% translated source file: 'src/locales/en.json' on 'ru_UA'. --- src/locales/ru_UA.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/locales/ru_UA.json b/src/locales/ru_UA.json index ec38507..d927629 100644 --- a/src/locales/ru_UA.json +++ b/src/locales/ru_UA.json @@ -1,4 +1,7 @@ { + "lang_name": "English", + "en_lang_name": "English", + "errors": { "path_not_valid": "Путь не верный! Проверь и повтори", "error": "Ошибочка: ${error}", From a6f2d6a2dd4922cc362d091529c2cacce6018a0a Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Tue, 9 Jan 2024 11:38:56 +0000 Subject: [PATCH 16/35] Translate src/locales/en.json in uk_UA 100% translated source file: 'src/locales/en.json' on 'uk_UA'. --- src/locales/uk_UA.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/locales/uk_UA.json b/src/locales/uk_UA.json index 05e6f61..aeb11b1 100644 --- a/src/locales/uk_UA.json +++ b/src/locales/uk_UA.json @@ -1,4 +1,7 @@ { + "lang_name": "English", + "en_lang_name": "English", + "errors": { "path_not_valid": "Введений шлях не існує! Перевiр та повтори спробу.", "error": "Помилка: ${error}", From c0418f838bf4109bb431e32bffe664a87bd14200 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Tue, 9 Jan 2024 12:50:23 +0000 Subject: [PATCH 17/35] Translate src/locales/en.json in uk_UA 100% translated source file: 'src/locales/en.json' on 'uk_UA'. --- src/locales/uk_UA.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/uk_UA.json b/src/locales/uk_UA.json index aeb11b1..8bdd2d2 100644 --- a/src/locales/uk_UA.json +++ b/src/locales/uk_UA.json @@ -1,6 +1,6 @@ { "lang_name": "English", - "en_lang_name": "English", + "en_lang_name": "Ukrainian", "errors": { "path_not_valid": "Введений шлях не існує! Перевiр та повтори спробу.", From 70bcc8fcd704ad96c45c70fe6d223e30531b60d4 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Tue, 9 Jan 2024 12:50:43 +0000 Subject: [PATCH 18/35] Translate src/locales/en.json in ru_UA 100% translated source file: 'src/locales/en.json' on 'ru_UA'. --- src/locales/ru_UA.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/ru_UA.json b/src/locales/ru_UA.json index d927629..d49e9db 100644 --- a/src/locales/ru_UA.json +++ b/src/locales/ru_UA.json @@ -1,6 +1,6 @@ { "lang_name": "English", - "en_lang_name": "English", + "en_lang_name": "Russian(Ukraine)", "errors": { "path_not_valid": "Путь не верный! Проверь и повтори", From 1329d4c1f3f575b9a2be48c57302ca40ed9ec28e Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Tue, 9 Jan 2024 12:53:25 +0000 Subject: [PATCH 19/35] Translate src/locales/en.json in uk_UA 100% translated source file: 'src/locales/en.json' on 'uk_UA'. --- src/locales/uk_UA.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/uk_UA.json b/src/locales/uk_UA.json index 8bdd2d2..5d64db7 100644 --- a/src/locales/uk_UA.json +++ b/src/locales/uk_UA.json @@ -1,5 +1,5 @@ { - "lang_name": "English", + "lang_name": "Українська", "en_lang_name": "Ukrainian", "errors": { From df1152a17ee00e8d478b2c44dca178f6d55dc91d Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Tue, 9 Jan 2024 12:58:51 +0000 Subject: [PATCH 20/35] Translate src/locales/en.json in ru_UA 100% translated source file: 'src/locales/en.json' on 'ru_UA'. --- src/locales/ru_UA.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/ru_UA.json b/src/locales/ru_UA.json index d49e9db..2039e77 100644 --- a/src/locales/ru_UA.json +++ b/src/locales/ru_UA.json @@ -1,5 +1,5 @@ { - "lang_name": "English", + "lang_name": "Русский(Украина)", "en_lang_name": "Russian(Ukraine)", "errors": { From a937968f1b516f5056d1f9252f9887fb5943d4b7 Mon Sep 17 00:00:00 2001 From: ch3p4ll3 Date: Tue, 9 Jan 2024 18:09:54 +0100 Subject: [PATCH 21/35] add Contributing section --- README.md | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index af30299..0c80e50 100755 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ![GitHub License](https://img.shields.io/github/license/ch3p4ll3/QBittorrentBot) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/259099080ca24e029a910e3249d32041)](https://app.codacy.com/gh/ch3p4ll3/QBittorrentBot?utm_source=github.com&utm_medium=referral&utm_content=ch3p4ll3/QBittorrentBot&utm_campaign=Badge_Grade) -![GitHub contributors](https://img.shields.io/github/contributors/ch3p4ll3/QBittorrentBot) ![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/ch3p4ll3/QBittorrentBot/docker-image.yml) +![Docker Pulls](https://img.shields.io/docker/pulls/ch3p4ll3/qbittorrent-bot) @@ -18,6 +18,21 @@ magnet:?xt=... ``` You can also pause, resume, delete and add/remove and modify categories. +# Table Of Contents +- [QBittorrentBot](#qbittorrentbot) +- [Table Of Contents](#table-of-contents) + - [Warning!](#warning) + - [Configuration](#configuration) + - [Retrieve Telegram API ID and API HASH](#retrieve-telegram-api-id-and-api-hash) + - [JSON Configuration](#json-configuration) + - [Running](#running) + - [Build docker](#build-docker) + - [Running without docker](#running-without-docker) + - [Contributing Translations on Transifex](#contributing-translations-on-transifex) + - [How to enable the qBittorrent Web UI](#how-to-enable-the-qbittorrent-web-ui) + - [Contributors ✨](#contributors-) + + ## Warning! Since version V2, the mapping of the configuration file has been changed. Make sure you have modified it correctly before starting the bot @@ -73,6 +88,25 @@ Pull and run the image with: `docker run -d -v /home/user/docker/QBittorrentBot: - Create a config.json file - Start the bot with `python3 main.py` +## Contributing Translations on Transifex +QBittorrentBot is an open-source project that relies on the contributions of its community members to provide translations for its users. If you are multilingual and would like to help us make QBittorrentBot more accessible to a wider audience, you can contribute by adding new translations or improving existing ones using Transifex: + +- Visit the [QBittorrentBot Transifex Project](https://app.transifex.com/ch3p4ll3/qbittorrentbot/). + +- If you don't have a Transifex account, sign up for one. If you already have an account, log in. + +- Navigate to the "Languages" tab to view the available languages. Choose the language you want to contribute to. + +- Locate the specific string you wish to translate. Please note that the text between "${" and "}" should not be edited, as they are placeholders for dynamic content. + +- Click on the string you want to translate, enter your translation in the provided field, and save your changes. + +- If your language is not listed, you can request its addition. + +- Once you have completed your translations, submit them for review. The project maintainers will review and approve your contributions. + +Thank you for helping improve QBittorrentBot with your valuable translations! + ## How to enable the qBittorrent Web UI For the bot to work, it requires qbittorrent to have the web interface active. You can activate it by going on the menu bar, go to **Tools > Options** qBittorrent WEB UI From 2918b6cb69ddda9dada3c5225b0a5392465cb6a7 Mon Sep 17 00:00:00 2001 From: ch3p4ll3 Date: Thu, 11 Jan 2024 08:41:48 +0100 Subject: [PATCH 22/35] fix primary key size --- src/db_management.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/db_management.py b/src/db_management.py index 1f56dc7..ca2c8b4 100644 --- a/src/db_management.py +++ b/src/db_management.py @@ -8,7 +8,7 @@ class Support(db.Entity): - id = PrimaryKey(int) + id = PrimaryKey(int, size=64) Action = Required(str, 255) From 028f0528942d2c9f8279881c9fb0d86a61a1731e Mon Sep 17 00:00:00 2001 From: ch3p4ll3 Date: Thu, 11 Jan 2024 09:24:17 +0100 Subject: [PATCH 23/35] update config.template --- config.json.template | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/config.json.template b/config.json.template index 1688f40..cf8b097 100644 --- a/config.json.template +++ b/config.json.template @@ -8,7 +8,14 @@ "telegram": { "bot_token": "1111111:AAAAAAAA-BBBBBBBBB", "api_id": 1111, - "api_hash": "aaaaaaaa" + "api_hash": "aaaaaaaa", + "proxy": { + "scheme": "http", + "hostname": "myproxy.local", + "port": 8080, + "username": "admin", + "password": "admin" + } }, "users": [ From 3063e305e3133d38e4aff613b93338dbea97a687 Mon Sep 17 00:00:00 2001 From: ch3p4ll3 Date: Thu, 11 Jan 2024 13:30:38 +0100 Subject: [PATCH 24/35] add change language from bot (to be tested) --- src/bot/__init__.py | 3 +- src/bot/custom_filters.py | 1 + .../settings/users_settings_callbacks.py | 52 +++++++++++++++++-- src/locales/en.json | 2 + src/translator/strings.py | 2 + 5 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/bot/__init__.py b/src/bot/__init__.py index 1ab78c0..2f739ef 100644 --- a/src/bot/__init__.py +++ b/src/bot/__init__.py @@ -1,3 +1,4 @@ +import uvloop from pyrogram import Client from pyrogram.enums.parse_mode import ParseMode from apscheduler.schedulers.asyncio import AsyncIOScheduler @@ -18,7 +19,7 @@ if BOT_CONFIGS.telegram.proxy is not None: proxy = BOT_CONFIGS.telegram.proxy.proxy_settings - +uvloop.install() app = Client( "qbittorrent_bot", api_id=BOT_CONFIGS.telegram.api_id, diff --git a/src/bot/custom_filters.py b/src/bot/custom_filters.py index 3479187..142c473 100644 --- a/src/bot/custom_filters.py +++ b/src/bot/custom_filters.py @@ -45,6 +45,7 @@ user_info_filter = filters.regex(r'^user_info(#.+|$)?$') edit_user_filter = filters.regex(r'^edit_user(#.+|$)?$') toggle_user_var_filter = filters.regex(r'^toggle_user_var(#.+|$)?$') +edit_locale_filter = filters.regex(r'^edit_locale(#.+|$)?$') edit_client_settings_filter = filters.regex(r"^edit_client$") list_client_settings_filter = filters.regex(r"^lst_client$") check_connection_filter = filters.regex(r"^check_connection$") diff --git a/src/bot/plugins/callbacks/settings/users_settings_callbacks.py b/src/bot/plugins/callbacks/settings/users_settings_callbacks.py index 740bce9..eeb820c 100644 --- a/src/bot/plugins/callbacks/settings/users_settings_callbacks.py +++ b/src/bot/plugins/callbacks/settings/users_settings_callbacks.py @@ -1,6 +1,7 @@ from pyrogram import Client from pyrogram.types import CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton from varname import nameof +from pykeyboard import InlineKeyboard, InlineButton from .... import custom_filters from .....configs import Configs @@ -81,8 +82,29 @@ async def edit_user_callback(client: Client, callback_query: CallbackQuery, user user_info = get_user_from_config(user_id) - # if field_to_edit == nameof(user.locale): - # pass + if field_to_edit == nameof(user.locale): + keyboard = InlineKeyboard() + keyboard.add( + [ + InlineButton( + f'{Translator.translate(Strings.LangName, i)}/{Translator.translate(Strings.EnLangName, i)}', + f'edit_locale#{user_id}-{i}' + ) + + for i in Translator.locales + ] + ) + + await callback_query.edit_message_text( + Translator.translate(Strings.EditLocale, user.locale), + reply_markup=keyboard.inline_keyboard + + [ + InlineKeyboardButton( + Translator.translate(Strings.BackToUSer, user.locale, user_id=user_id), + f"user_info#{user_id}" + ) + ] + ) if data_type == bool: notify_status = Translator.translate(Strings.Enabled if user_info.notify else Strings.Disabled, user.locale) @@ -132,7 +154,7 @@ async def toggle_user_var(client: Client, callback_query: CallbackQuery, user: U user_id = int(data.split("-")[0]) field_to_edit = data.split("-")[1] - user_from_configs = Configs.config.users.index(user) + user_from_configs = Configs.config.users.index(get_user_from_config(user_id)) if user_from_configs == -1: return @@ -160,3 +182,27 @@ async def toggle_user_var(client: Client, callback_query: CallbackQuery, user: U ] ) ) + + +@Client.on_callback_query(custom_filters.toggle_user_var_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) +@inject_user +async def edit_locale_filter(client: Client, callback_query: CallbackQuery, user: User) -> None: + data = callback_query.data.split("#")[1] + user_id = int(data.split("-")[0]) + new_locale = data.split("-")[1] + + user_from_configs = Configs.config.users.index(get_user_from_config(user_id)) + + if user_from_configs == -1: + return + + Configs.config.users[user_from_configs].locale = new_locale + + await callback_query.answer( + Translator.translate( + Strings.NewLocale, + user.locale, + new_locale=Translator.translate(Strings.LangName, new_locale), + user_id=user_id + ) + ) diff --git a/src/locales/en.json b/src/locales/en.json index 9cb5037..a530b72 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -104,6 +104,8 @@ "user_settings": { "user_btn": "User #${user_id}", "authorized_users": "Authorized users", + "new_locale": "Changed language to ${new_locale} for user #${user_id}", + "edit_user_locale": "Choose a new locale:", "edit_user_setting": "Edit User #${user_id}\n\n**Current Settings:**\n- ${confs}", "back_to_users": "\uD83D\uDD19 Users", "edit_user_field": "Edit User #${user_id} ${field_to_edit} field", diff --git a/src/translator/strings.py b/src/translator/strings.py index bff167f..7177f4d 100644 --- a/src/translator/strings.py +++ b/src/translator/strings.py @@ -87,6 +87,8 @@ class Strings(StrEnum): # User Settings UserBtn = "callbacks.user_settings.user_btn" + NewLocale = "callbacks.user_settings.new_locale" + EditLocale = "callbacks.user_settings.edit_user_locale" AuthorizedUsers = "callbacks.user_settings.authorized_users" EditUserSetting = "callbacks.user_settings.edit_user_setting" BackToUsers = "callbacks.user_settings.back_to_users" From c978397d5a046a26316dcebce3d740894a0b34b8 Mon Sep 17 00:00:00 2001 From: ch3p4ll3 Date: Fri, 12 Jan 2024 11:28:42 +0100 Subject: [PATCH 25/35] update requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index f78d18b..2f47bd2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,6 +11,7 @@ psutil==5.9.6 pyaes==1.6.1 pydantic==2.5.2 pydantic_core==2.14.5 +pykeyboard==0.1.5 Pyrogram==2.0.106 PySocks==1.7.1 pytz==2023.3.post1 From 7e9e98db4fbc44b50f1ada694035d21182472417 Mon Sep 17 00:00:00 2001 From: ch3p4ll3 Date: Fri, 12 Jan 2024 11:49:03 +0100 Subject: [PATCH 26/35] fix locale buttons --- .../settings/users_settings_callbacks.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/bot/plugins/callbacks/settings/users_settings_callbacks.py b/src/bot/plugins/callbacks/settings/users_settings_callbacks.py index eeb820c..8542d2b 100644 --- a/src/bot/plugins/callbacks/settings/users_settings_callbacks.py +++ b/src/bot/plugins/callbacks/settings/users_settings_callbacks.py @@ -97,13 +97,15 @@ async def edit_user_callback(client: Client, callback_query: CallbackQuery, user await callback_query.edit_message_text( Translator.translate(Strings.EditLocale, user.locale), - reply_markup=keyboard.inline_keyboard + - [ - InlineKeyboardButton( - Translator.translate(Strings.BackToUSer, user.locale, user_id=user_id), - f"user_info#{user_id}" - ) - ] + reply_markup=InlineKeyboardMarkup( + keyboard.inline_keyboard + + [ + InlineKeyboardButton( + Translator.translate(Strings.BackToUSer, user.locale, user_id=user_id), + f"user_info#{user_id}" + ) + ] + ) ) if data_type == bool: From 9e4dbbda2e9c07ff54fc536f0a4ec1d8f827ace9 Mon Sep 17 00:00:00 2001 From: ch3p4ll3 Date: Fri, 12 Jan 2024 12:26:32 +0100 Subject: [PATCH 27/35] fix edit locale --- .../callbacks/settings/users_settings_callbacks.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/bot/plugins/callbacks/settings/users_settings_callbacks.py b/src/bot/plugins/callbacks/settings/users_settings_callbacks.py index 8542d2b..5cb35f1 100644 --- a/src/bot/plugins/callbacks/settings/users_settings_callbacks.py +++ b/src/bot/plugins/callbacks/settings/users_settings_callbacks.py @@ -85,7 +85,7 @@ async def edit_user_callback(client: Client, callback_query: CallbackQuery, user if field_to_edit == nameof(user.locale): keyboard = InlineKeyboard() keyboard.add( - [ + *[ InlineButton( f'{Translator.translate(Strings.LangName, i)}/{Translator.translate(Strings.EnLangName, i)}', f'edit_locale#{user_id}-{i}' @@ -100,14 +100,18 @@ async def edit_user_callback(client: Client, callback_query: CallbackQuery, user reply_markup=InlineKeyboardMarkup( keyboard.inline_keyboard + [ - InlineKeyboardButton( - Translator.translate(Strings.BackToUSer, user.locale, user_id=user_id), - f"user_info#{user_id}" - ) + [ + InlineKeyboardButton( + Translator.translate(Strings.BackToUSer, user.locale, user_id=user_id), + f"user_info#{user_id}" + ) + ] ] ) ) + return + if data_type == bool: notify_status = Translator.translate(Strings.Enabled if user_info.notify else Strings.Disabled, user.locale) From 23fa097e8b177e1a2b1810f1a9cbc263df6e54d7 Mon Sep 17 00:00:00 2001 From: ch3p4ll3 Date: Fri, 12 Jan 2024 12:41:48 +0100 Subject: [PATCH 28/35] fix edit locale callback --- src/bot/plugins/callbacks/settings/users_settings_callbacks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bot/plugins/callbacks/settings/users_settings_callbacks.py b/src/bot/plugins/callbacks/settings/users_settings_callbacks.py index 5cb35f1..c79e4b4 100644 --- a/src/bot/plugins/callbacks/settings/users_settings_callbacks.py +++ b/src/bot/plugins/callbacks/settings/users_settings_callbacks.py @@ -190,7 +190,7 @@ async def toggle_user_var(client: Client, callback_query: CallbackQuery, user: U ) -@Client.on_callback_query(custom_filters.toggle_user_var_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) +@Client.on_callback_query(custom_filters.edit_locale_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) @inject_user async def edit_locale_filter(client: Client, callback_query: CallbackQuery, user: User) -> None: data = callback_query.data.split("#")[1] From 1ad5147306b62e58f767cbc5871c21ec10312f90 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Fri, 12 Jan 2024 12:01:42 +0000 Subject: [PATCH 29/35] Translate src/locales/en.json in it 100% translated source file: 'src/locales/en.json' on 'it'. --- src/locales/it.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/locales/it.json b/src/locales/it.json index cfcefaf..8afb0c1 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -104,6 +104,8 @@ "user_settings": { "user_btn": "User #${user_id}", "authorized_users": "Utenti autorizzati", + "new_locale": "La lingua ${new_locale} è stata modificata per l'utente #${user_id}", + "edit_user_locale": "Scegli una nuova lingua.", "edit_user_setting": "Modifica utente #${user_id}\n\n**Impostazioni attuali:**\n- ${confs}", "back_to_users": "🔙 Utenti", "edit_user_field": "Modifica campo utente #${user_id} ${field_to_edit}", From 6f1d6aa285a5b8e194b69ca14867f547e5fccf7b Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Fri, 12 Jan 2024 12:05:01 +0000 Subject: [PATCH 30/35] Translate src/locales/en.json in it 100% translated source file: 'src/locales/en.json' on 'it'. --- src/locales/it.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/it.json b/src/locales/it.json index 8afb0c1..d979081 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -104,7 +104,7 @@ "user_settings": { "user_btn": "User #${user_id}", "authorized_users": "Utenti autorizzati", - "new_locale": "La lingua ${new_locale} è stata modificata per l'utente #${user_id}", + "new_locale": "Modificata la lingua per l'utente #${user_id} in ${new_locale}", "edit_user_locale": "Scegli una nuova lingua.", "edit_user_setting": "Modifica utente #${user_id}\n\n**Impostazioni attuali:**\n- ${confs}", "back_to_users": "🔙 Utenti", From 53b6219e834e427552acfa781438651a245b2744 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Fri, 12 Jan 2024 12:59:38 +0000 Subject: [PATCH 31/35] Translate src/locales/en.json in ru_UA 100% translated source file: 'src/locales/en.json' on 'ru_UA'. --- src/locales/ru_UA.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/locales/ru_UA.json b/src/locales/ru_UA.json index 2039e77..1c87a9a 100644 --- a/src/locales/ru_UA.json +++ b/src/locales/ru_UA.json @@ -104,6 +104,8 @@ "user_settings": { "user_btn": "Юзер #${user_id}", "authorized_users": "Авторизованные пользователи", + "new_locale": "Изменил язык на ${new_locale} для пользователя #${user_id}", + "edit_user_locale": "Выберый новую локаль:", "edit_user_setting": "Edit User #${user_id}\n\n**Текущие настройки:**\n- ${confs}", "back_to_users": "🔙 Пользователи", "edit_user_field": "Изменить настройки пользователя #${user_id} ${field_to_edit} ", From df6251cf291557fe9284da8d884b5b6e3d749100 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Fri, 12 Jan 2024 13:00:11 +0000 Subject: [PATCH 32/35] Translate src/locales/en.json in uk_UA 100% translated source file: 'src/locales/en.json' on 'uk_UA'. --- src/locales/uk_UA.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/locales/uk_UA.json b/src/locales/uk_UA.json index 5d64db7..433ca7a 100644 --- a/src/locales/uk_UA.json +++ b/src/locales/uk_UA.json @@ -104,6 +104,8 @@ "user_settings": { "user_btn": "Користувач #${user_id}", "authorized_users": "Авторизовані користувачі", + "new_locale": "Змінено мову на ${new_locale} для користувача №${user_id}", + "edit_user_locale": "Оберіть нову локаль:", "edit_user_setting": "Редагувати користувача #${user_id}\n\n**Поточні налаштування:**\n- ${confs}", "back_to_users": "🔙 Користувачі", "edit_user_field": "Редагувати поле користувача №${user_id} ${field_to_edit}", From 0ec4d17a260d21d48aef82ddc3efd6ca02e69ae7 Mon Sep 17 00:00:00 2001 From: ch3p4ll3 Date: Sun, 14 Jan 2024 12:34:42 +0100 Subject: [PATCH 33/35] fix user not authorized --- .../settings/users_settings_callbacks.py | 2 +- src/bot/plugins/commands.py | 18 +++++++++++++----- src/locales/en.json | 1 - src/locales/it.json | 1 - src/locales/ru_UA.json | 1 - src/locales/uk_UA.json | 1 - 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/bot/plugins/callbacks/settings/users_settings_callbacks.py b/src/bot/plugins/callbacks/settings/users_settings_callbacks.py index c79e4b4..f91098c 100644 --- a/src/bot/plugins/callbacks/settings/users_settings_callbacks.py +++ b/src/bot/plugins/callbacks/settings/users_settings_callbacks.py @@ -192,7 +192,7 @@ async def toggle_user_var(client: Client, callback_query: CallbackQuery, user: U @Client.on_callback_query(custom_filters.edit_locale_filter & custom_filters.check_user_filter & custom_filters.user_is_administrator) @inject_user -async def edit_locale_filter(client: Client, callback_query: CallbackQuery, user: User) -> None: +async def edit_locale_callback(client: Client, callback_query: CallbackQuery, user: User) -> None: data = callback_query.data.split("#")[1] user_id = int(data.split("-")[0]) new_locale = data.split("-")[1] diff --git a/src/bot/plugins/commands.py b/src/bot/plugins/commands.py index d9ca5b6..c574e63 100644 --- a/src/bot/plugins/commands.py +++ b/src/bot/plugins/commands.py @@ -11,11 +11,19 @@ @Client.on_message(~custom_filters.check_user_filter) -@inject_user -async def access_denied_message(client: Client, message: Message, user: User) -> None: - button = InlineKeyboardMarkup([[InlineKeyboardButton("Github", - url="https://github.com/ch3p4ll3/QBittorrentBot/")]]) - await client.send_message(message.chat.id, Translator.translate(Strings.NotAuthorized, user.locale), reply_markup=button) +async def access_denied_message(client: Client, message: Message) -> None: + button = InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton( + "Github", + url="https://github.com/ch3p4ll3/QBittorrentBot/" + ) + ] + ] + ) + + await client.send_message(message.chat.id, "You are not authorized to use this bot", reply_markup=button) @Client.on_message(filters.command("start") & custom_filters.check_user_filter) diff --git a/src/locales/en.json b/src/locales/en.json index a530b72..fb33f30 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -41,7 +41,6 @@ }, "commands": { - "not_authorized": "You are not authorized to use this bot", "stats_command": "**============SYSTEM============**\n**CPU Usage:** ${cpu_usage}%\n**CPU Temp:** ${cpu_temp}°C\n**Free Memory:** ${free_memory} of ${total_memory} (${memory_percent}%)\n**Disks usage:** ${disk_used} of ${disk_total} (${disk_percent}%)" }, diff --git a/src/locales/it.json b/src/locales/it.json index d979081..d34a12b 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -41,7 +41,6 @@ }, "commands": { - "not_authorized": "Non sei autorizzato a utilizzare questo bot", "stats_command": "**============SISTEMA============**\n**Utilizzo CPU:** ${cpu_usage}%\n**Temperatura CPU:** ${cpu_temp}°C\n**Memoria libera:** ${free_memory} di ${total_memory} (${memory_percent}%)\n**Utilizzo dischi:** ${disk_used} di ${disk_total} (${disk_percent}%)" }, diff --git a/src/locales/ru_UA.json b/src/locales/ru_UA.json index 1c87a9a..acc5e6d 100644 --- a/src/locales/ru_UA.json +++ b/src/locales/ru_UA.json @@ -41,7 +41,6 @@ }, "commands": { - "not_authorized": "Ты не авторизован в этом боте", "stats_command": "**============SYSTEM============**\n**CPU Usage:** ${cpu_usage}%\n**CPU Temp:** ${cpu_temp}°C\n**Free Memory:** ${free_memory} of ${total_memory} (${memory_percent}%)\n**Disks usage:** ${disk_used} of ${disk_total} (${disk_percent}%)" }, diff --git a/src/locales/uk_UA.json b/src/locales/uk_UA.json index 433ca7a..c19e9ca 100644 --- a/src/locales/uk_UA.json +++ b/src/locales/uk_UA.json @@ -41,7 +41,6 @@ }, "commands": { - "not_authorized": "В тебе не маєте дозволу використовувати цього бота", "stats_command": "**============SYSTEM============**\n**CPU Usage:** ${cpu_usage}%\n**CPU Temp:** ${cpu_temp}°C\n**Free Memory:** ${free_memory} of ${total_memory} (${memory_percent}%)\n**Disks usage:** ${disk_used} of ${disk_total} (${disk_percent}%)" }, From beabfc47b7182e8dfca181e2b432919fc72fb42b Mon Sep 17 00:00:00 2001 From: ch3p4ll3 Date: Tue, 16 Jan 2024 09:52:39 +0100 Subject: [PATCH 34/35] update docs --- config.json.template | 2 +- docker-compose.yml.example | 8 +++ docs/advanced/telegram_proxy.md | 1 + docs/getting_started/configuration_file.md | 60 +++++++++++++------ docs/getting_started/index.md | 28 --------- docs/getting_started/installation.md | 69 ++++++++++++++++++++++ docs/getting_started/migrating_to_v2.md | 10 +++- src/bot/__init__.py | 13 ++-- 8 files changed, 136 insertions(+), 55 deletions(-) create mode 100644 docker-compose.yml.example create mode 100644 docs/advanced/telegram_proxy.md create mode 100644 docs/getting_started/installation.md diff --git a/config.json.template b/config.json.template index cf8b097..af41c30 100644 --- a/config.json.template +++ b/config.json.template @@ -22,7 +22,7 @@ { "user_id": 123456, "notify": false, - "locale": "en" + "locale": "en", "role": "administrator" } ] diff --git a/docker-compose.yml.example b/docker-compose.yml.example new file mode 100644 index 0000000..a212b2d --- /dev/null +++ b/docker-compose.yml.example @@ -0,0 +1,8 @@ +version: '3.9' +services: + qbittorrent-bot: + image: 'ch3p4ll3/qbittorrent-bot:latest' + container_name: qbittorrent-bot + restart: unless-stopped + volumes: + - '/home/user/docker/QBittorrentBot:/app/config:rw' diff --git a/docs/advanced/telegram_proxy.md b/docs/advanced/telegram_proxy.md new file mode 100644 index 0000000..ddf699a --- /dev/null +++ b/docs/advanced/telegram_proxy.md @@ -0,0 +1 @@ +# Configure proxy for Telegram \ No newline at end of file diff --git a/docs/getting_started/configuration_file.md b/docs/getting_started/configuration_file.md index 41d4060..54c2ec3 100644 --- a/docs/getting_started/configuration_file.md +++ b/docs/getting_started/configuration_file.md @@ -40,7 +40,7 @@ Here's a brief overview of the configuration file and its key sections: This section defines the configuration for the qBittorrent client that the bot will be interacting with. -Name | Type | Value +Name | Type | Remarks --- |-----------------------------------| --- type | [ClientTypeEnum](#clienttypeenum) | The type of client. host | [HttpUrl](#httpurl) | The IP address of the qBittorrent server. @@ -51,23 +51,24 @@ password | str | The password for the qBittorrent This section defines the configuration for the Telegram bot that the QBittorrentBot will be communicating with. -Name | Type | Value ---- | --- | --- -bot_token | str | The bot token for the QBittorrentBot. This is a unique identifier that is used to authenticate the bot with the Telegram API. -api_id | int | The API ID for the QBittorrentBot. This is a unique identifier that is used to identify the bot to the Telegram API. -api_hash |str | The API hash for the QBittorrentBot. This is a string of characters that is used to verify the authenticity of the bot's requests to the Telegram API. +Name | Type | Remarks +--- | --- | --- +bot_token | str | The bot token for the QBittorrentBot. This is a unique identifier that is used to authenticate the bot with the Telegram API. +api_id | int | The API ID for the QBittorrentBot. This is a unique identifier that is used to identify the bot to the Telegram API. +api_hash | str | The API hash for the QBittorrentBot. This is a string of characters that is used to verify the authenticity of the bot's requests to the Telegram API. +proxy | [TelegramProxySettings](#telegram-proxy-settings) | Optional, the settings for using a proxy to contact telegram servers ## Users This section defines the list of users who are authorized to use the QBittorrentBot. Each user is defined by their Telegram user ID, whether or not they should be notified about completed torrents, and their role. -Name | Type | Value ---- | --- |--- -user_id | int |The Telegram user ID of the user. This is a unique identifier that is used to identify the user to the Telegram API. -notify | bool |Whether or not the user should be notified about new torrents. -role | [UserRolesEnum](#userrolesenum) |The role of the user. - +Name | Type | Remarks +--- | --- | --- +user_id | int | The Telegram user ID of the user. This is a unique identifier that is used to identify the user to the Telegram API. +notify | bool | Whether or not the user should be notified about new torrents. +role | [UserRolesEnum](#userrolesenum) | The role of the user. Default: `administrator` +locale | str | Language used by the user, [list of supported languages](#languages). Default: `en` ## Enums @@ -75,14 +76,21 @@ role | [UserRolesEnum](#userrolesenum) |The role of the user. Name | Type | Value(to be used in json) --- | --- |--- -QBittorrent | str | qbittorrent +QBittorrent | str | `qbittorrent` ### UserRolesEnum Name | Type | Value(to be used in json) | Remarks --- | --- |--- | -Reader | str | reader | Can perform only reading operations(view torrents) -Manager | str | manager | Can perform only managing operations(view torrents + can download files + can add/edit categories + set torrent priority + can stop/start downloads) -Administrator| str | administrator | Can perform all operations (Manager + remove torrent + remove category + edit configs) +Reader | str | `reader` | Can perform only reading operations(view torrents) +Manager | str | `manager` | Can perform only managing operations(view torrents + can download files + can add/edit categories + set torrent priority + can stop/start downloads) +Administrator| str | `administrator` | Can perform all operations (Manager + remove torrent + remove category + edit configs) + +### Telegram Proxy Scheme +Name | Type | Value(to be used in json) +--- | --- |--- +Sock4 | str | `socks4` +Sock5 |str | `socks5` +Http |str | `http` ## Other types @@ -90,4 +98,22 @@ Administrator| str | administrator | Can perform all operations ( A type that will accept any http or https URL. - TLD required - Host required -- Max length 2083 \ No newline at end of file +- Max length 2083 + +### Languages +Name | Type | Value(to be used in json) +--- | --- |--- +English | str | `en` +Italian | str | `it` +Ukrainian | str | `uk_UA` +Russian(Ukraine) | str | `ru_UA` + +### Telegram Proxy Settings +QBittorrentBot supports proxies with and without authentication. This feature allows QBittorrentBot to exchange data with Telegram through an intermediate SOCKS 4/5 or HTTP proxy server. + +Name | Type | Remarks +--- | --- | --- +scheme | [TelegramProxyScheme](#telegram-proxy-scheme) | The scheme to be used to connect to the proxy +hostname | str | The hostname of the proxy +username | str | Optional, the proxy user +password | str | Optional, the proxy password \ No newline at end of file diff --git a/docs/getting_started/index.md b/docs/getting_started/index.md index e68510c..bad5562 100644 --- a/docs/getting_started/index.md +++ b/docs/getting_started/index.md @@ -1,29 +1 @@ # Getting Started - -In order to start using the bot, you must first create a folder where the bot will fish for settings and where it will save logs - -For example: let's create a folder called `QBittorrentBot` in the home of the user `user`. The path to the folder will then be `/home/user/docker/QBittorrentBot`. - -Before starting the bot you need to place the configuration file in this folder. You can rename the `config.json.template` file to `config.json` and change the parameters as desired. Go [here](configuration_file.md) to read more about the configuration file. - -Once that is done you can start the bot using docker, you can use either docker or docker compose. - -+++ Docker -Open your terminal and execute the following command to start the bot container: - -`docker run -d -v /home/user/docker/QBittorrentBot:/app/config:rw --name qbittorrent-bot ch3p4ll3/qbittorrent-bot:latest` -+++ Docker compose -Create a file named `docker-compose.yml` inside a directory with the following content: -``` -version: '3.9' -services: - qbittorrent-bot: - image: 'ch3p4ll3/qbittorrent-bot:latest' - container_name: qbittorrent-bot - restart: unless-stopped - volumes: - - '/home/user/docker/QBittorrentBot:/app/config:rw' -``` - -Run the following command to start the bot using Docker Compose: -`docker compose up -d` \ No newline at end of file diff --git a/docs/getting_started/installation.md b/docs/getting_started/installation.md new file mode 100644 index 0000000..8d99a14 --- /dev/null +++ b/docs/getting_started/installation.md @@ -0,0 +1,69 @@ +--- +label: Installation-Updating +--- + +# Installation + +In order to start using the bot, you must first create a folder where the bot will fish for settings and where it will save logs + +For example: let's create a folder called `QBittorrentBot` in the home of the user `user`. The path to the folder will then be `/home/user/docker/QBittorrentBot`. + +Before starting the bot you need to place the configuration file in this folder. You can rename the `config.json.template` file to `config.json` and change the parameters as desired. Go [here](configuration_file.md) to read more about the configuration file. + +Once that is done you can start the bot using docker, you can use either docker or docker compose. + ++++ Docker +Open your terminal and execute the following command to start the bot container: + +`docker run -d -v /home/user/docker/QBittorrentBot:/app/config:rw --name qbittorrent-bot ch3p4ll3/qbittorrent-bot:latest` ++++ Docker compose +Create a file named `docker-compose.yml` inside a directory with the following content: +``` +version: '3.9' +services: + qbittorrent-bot: + image: 'ch3p4ll3/qbittorrent-bot:latest' + container_name: qbittorrent-bot + restart: unless-stopped + volumes: + - '/home/user/docker/QBittorrentBot:/app/config:rw' +``` + +Run the following command to start the bot using Docker Compose: +`docker compose up -d` ++++ + +# Updating + ++++ Docker +To update to the latest version of QBittorrentBot, use the following commands to stop then remove the old version: +- `docker stop qbittorrent-bot` +- `docker rm qbittorrent-bot` + +Now that you have stopped and removed the old version of QBittorrentBot, you must ensure that you have the latest version of the image locally. You can do this with a docker pull command: + +`docker pull ch3p4ll3/qbittorrent-bot:latest` + +Finally, deploy the updated version of Portainer: + +`docker run -d -v /home/user/docker/QBittorrentBot:/app/config:rw --name qbittorrent-bot ch3p4ll3/qbittorrent-bot:latest` ++++ Docker compose +To update to the latest version of QBittorrentBot, navigate to the folder where you created the `docker-compose.yml` file. + +Then use the following command to pull the latest version of the image: + +`docker compose pull` + +Finally use the following command to start the bot using Docker Compose: +`docker compose up -d` ++++ + +# Running without docker +it is preferable to use the bot using docker, this gives the developers to isolate their app from its environment, solving the “it works on my machine” headache. + +In case you could not use docker you can use the bot without it. To do so, follow the following steps: +- Clone this repo `git clone https://github.com/ch3p4ll3/QBittorrentBot.git` +- Move in the project directory +- Install dependencies with `pip3 install -r requirements.txt` +- Create a config.json file +- Start the bot with `python3 main.py` \ No newline at end of file diff --git a/docs/getting_started/migrating_to_v2.md b/docs/getting_started/migrating_to_v2.md index 876b38e..827b263 100644 --- a/docs/getting_started/migrating_to_v2.md +++ b/docs/getting_started/migrating_to_v2.md @@ -64,13 +64,21 @@ configurations in comparison "telegram": { "bot_token": "1111111:AAAAAAAA-BBBBBBBBB", "api_id": 1111, - "api_hash": "aaaaaaaa" + "api_hash": "aaaaaaaa", + "proxy": { + "scheme": "http", + "hostname": "myproxy.local", + "port": 8080, + "username": "admin", + "password": "admin" + } }, "users": [ { "user_id": 123456, "notify": false, + "locale": "en", "role": "administrator" } ] diff --git a/src/bot/__init__.py b/src/bot/__init__.py index 2f739ef..4f60289 100644 --- a/src/bot/__init__.py +++ b/src/bot/__init__.py @@ -6,9 +6,6 @@ from ..configs import Configs -BOT_CONFIGS = Configs.config - - plugins = dict( root="src.bot.plugins" ) @@ -16,15 +13,15 @@ proxy = None -if BOT_CONFIGS.telegram.proxy is not None: - proxy = BOT_CONFIGS.telegram.proxy.proxy_settings +if Configs.config.telegram.proxy is not None: + proxy = Configs.config.telegram.proxy.proxy_settings uvloop.install() app = Client( "qbittorrent_bot", - api_id=BOT_CONFIGS.telegram.api_id, - api_hash=BOT_CONFIGS.telegram.api_hash, - bot_token=BOT_CONFIGS.telegram.bot_token, + api_id=Configs.config.telegram.api_id, + api_hash=Configs.config.telegram.api_hash, + bot_token=Configs.config.telegram.bot_token, parse_mode=ParseMode.MARKDOWN, plugins=plugins, proxy=proxy From 231441a56503e4db5531a4c360a42a2f4da84835 Mon Sep 17 00:00:00 2001 From: ch3p4ll3 Date: Tue, 16 Jan 2024 13:42:04 +0100 Subject: [PATCH 35/35] add some other docs --- docs/advanced/add_new_client_manager.md | 2 +- docs/advanced/telegram_proxy.md | 33 +++++++++++++++++++++- docs/contributing.md | 30 ++++++++++++++++++++ docs/faq.md | 4 ++- docs/getting_started/configuration_file.md | 2 +- docs/getting_started/index.md | 9 ++++++ 6 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 docs/contributing.md diff --git a/docs/advanced/add_new_client_manager.md b/docs/advanced/add_new_client_manager.md index 0c67cd4..f0f199e 100644 --- a/docs/advanced/add_new_client_manager.md +++ b/docs/advanced/add_new_client_manager.md @@ -37,7 +37,7 @@ class ClientTypeEnum(str, Enum): - Return to the `src/client_manager` folder and edit the `client_repo.py` file by adding to the dictionary named `repositories` an entry associating the newly created enum with the new manager. Example: ```python from ..configs.enums import ClientTypeEnum -from .qbittorrent_manager import QbittorrentManager, ClientManager +from .qbittorrent_manager import QbittorrentManager, ClientManager, UtorrentManager class ClientRepo: diff --git a/docs/advanced/telegram_proxy.md b/docs/advanced/telegram_proxy.md index ddf699a..cf21e65 100644 --- a/docs/advanced/telegram_proxy.md +++ b/docs/advanced/telegram_proxy.md @@ -1 +1,32 @@ -# Configure proxy for Telegram \ No newline at end of file +# Configure proxy for Telegram + +QBittorrent Bot can be configured to use a Telegram proxy to connect to the Telegram API. This can be useful if you are behind a firewall that blocks direct connections to Telegram. + +To configure QBittorrent Bot to use a Telegram proxy, you will need to add a **proxy section** to the `config.json` file in the **telegram section**. The telegram section should have the following format: + +```json5 +"telegram": { + "bot_token": "1111111:AAAAAAAA-BBBBBBBBB", + "api_id": 1111, + "api_hash": "aaaaaaaa", + "proxy": { + "scheme": "http", // http, sock4 or sock5 + "hostname": "myproxy.local", + "port": 8080, + "username": "admin", + "password": "admin" + } +} +``` + +Where: + +- `scheme` is the protocol to use for the proxy connection. This can be `http`, `sock4` or `sock5` +- `hostname` is the hostname or IP address of the proxy server. +- `port` is the port number of the proxy server. +- `username` (optional) is the username for the proxy server. +- `password` (optional) is the password for the proxy server. + +!!! +Once you have added the proxy section to the config.json file, you will need to restart QBittorrent Bot for the changes to take effect. +!!! \ No newline at end of file diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..964aba1 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,30 @@ +--- +order: -10 +--- +# Contributing +QBittorrentBot is an open-source Telegram bot that enables seamless management of qBittorrent downloads directly from Telegram. + +By contributing to QBittorrentBot, you can help improve this valuable tool for qBittorrent users. Your contributions can range from fixing bugs and enhancing existing features to adding new functionalities that enhance the bot's capabilities. + +## Adding translations +If you are multilingual and would like to help us make QBittorrentBot more accessible to a wider audience, you can contribute by adding new translations or improving existing ones using Transifex: + +- Visit the [QBittorrentBot Transifex Project](https://app.transifex.com/ch3p4ll3/qbittorrentbot/). + +- If you don't have a Transifex account, sign up for one. If you already have an account, log in. + +- Navigate to the "Languages" tab to view the available languages. Choose the language you want to contribute to. + +- Locate the specific string you wish to translate. Please note that the text between "${" and "}" should not be edited, as they are placeholders for dynamic content. + +- Click on the string you want to translate, enter your translation in the provided field, and save your changes. + +- If your language is not listed, you can request its addition. + +- Once you have completed your translations, submit them for review. The project maintainers will review and approve your contributions. + +Thank you for helping improve QBittorrentBot with your valuable translations! + + +[!ref](/advanced/add_new_client_manager.md) +[!ref](/advanced/add_entries_configuration.md) diff --git a/docs/faq.md b/docs/faq.md index 36d47ef..84f9f3c 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -61,4 +61,6 @@ Please follow this guide ### How do I contribute to the development of QBittorrentBot? -QBittorrentBot is an open-source project. You can contribute to the development by reporting bugs, suggesting improvements, or submitting pull requests. The project's code is hosted on [GitHub](https://github.com/ch3p4ll3/QBittorrentBot). \ No newline at end of file +QBittorrentBot is an open-source project. You can contribute to the development by reporting bugs, suggesting improvements, or submitting pull requests. The project's code is hosted on [GitHub](https://github.com/ch3p4ll3/QBittorrentBot). + +[!ref](/contributing.md) \ No newline at end of file diff --git a/docs/getting_started/configuration_file.md b/docs/getting_started/configuration_file.md index 54c2ec3..3f2a665 100644 --- a/docs/getting_started/configuration_file.md +++ b/docs/getting_started/configuration_file.md @@ -32,7 +32,7 @@ Here's a brief overview of the configuration file and its key sections: - **Clients Section**: Establishes the connection details for the qBittorrent server, including the hostname, port number, username, and password. This enables the bot to interact with the qBittorrent server and manage torrents. -- **Telegram Section**: Contains the bot token, API ID, and API hash, which are essential for authenticating the bot with the Telegram API. These credentials allow the bot to communicate with the Telegram server and receive user commands. Click [here](https://docs.pyrogram.org/intro/quickstart) to find out how to retrive your API ID and API Hash +- **Telegram Section**: Contains the bot token, API ID, and API hash, which are essential for authenticating the bot with the Telegram API. These credentials allow the bot to communicate with the Telegram server and receive user commands. Click [here](https://core.telegram.org/api/obtaining_api_id) to find out how to retrive your API ID and API Hash - **Users Section**: Lists the authorized users of the QBittorrentBot, along with their Telegram user IDs, notification preferences, and user roles. This section defines the users who can interact with the bot, receive notifications, and manage torrents. diff --git a/docs/getting_started/index.md b/docs/getting_started/index.md index bad5562..991cc5b 100644 --- a/docs/getting_started/index.md +++ b/docs/getting_started/index.md @@ -1 +1,10 @@ # Getting Started +QBittorrentBot is a Telegram bot that allows you to control your qBittorrent client from within the Telegram messaging app. This makes it easy to add new torrents, manage your existing downloads, and get status updates without having to switch between applications. + +## Prerequisites + +- A Telegram account + - A bot token obtained from [botfather](https://core.telegram.org/bots#how-do-i-create-a-bot) + - [Telegram API ID](https://core.telegram.org/api/obtaining_api_id) +- A running qBittorrent instance with WebUI enabled +- Access to your qBittorrent server's IP address and port number