From 370bcd065354bfcb4d2a43faa71bad453f4a4b37 Mon Sep 17 00:00:00 2001 From: profitroll Date: Fri, 11 Aug 2023 15:04:21 +0200 Subject: [PATCH] Improved locales, added /language --- classes/callbacks.py | 52 +++++++++++++++++ classes/pyroclient.py | 2 +- classes/pyrogroup.py | 72 ++++++++++++++++++++++++ classes/pyrouser.py | 34 +++++------ config_example.json | 17 +++++- locale/en.json | 21 ++++++- locale/uk.json | 18 ++++++ modules/database.py | 13 +++-- plugins/callbacks/ban.py | 32 +++++++---- plugins/callbacks/emoji_button.py | 60 +++++++++++--------- plugins/callbacks/nothing.py | 6 +- plugins/callbacks/verify.py | 26 +++++---- plugins/commands/language_auto.py | 51 +++++++++++++++++ plugins/handlers/bot_join.py | 15 +++++ plugins/handlers/user_join.py | 55 +++++++++--------- plugins/language.py | 93 +++++++++++++++++++++++++++++++ requirements.txt | 2 +- 17 files changed, 465 insertions(+), 104 deletions(-) create mode 100644 classes/callbacks.py create mode 100644 classes/pyrogroup.py create mode 100644 plugins/commands/language_auto.py create mode 100644 plugins/handlers/bot_join.py create mode 100644 plugins/language.py diff --git a/classes/callbacks.py b/classes/callbacks.py new file mode 100644 index 0000000..e367fa2 --- /dev/null +++ b/classes/callbacks.py @@ -0,0 +1,52 @@ +from dataclasses import dataclass + +from pyrogram.types import CallbackQuery + + +@dataclass +class CallbackVerify: + user_id: int + + @classmethod + def from_callback(cls, callback: CallbackQuery): + action, user_id = str(callback.data).split(";") + if action.lower() != "verify": + raise ValueError("Callback provided is not a verification callback") + return cls(int(user_id)) + + +@dataclass +class CallbackEmoji: + user_id: int + emoji: str + + @classmethod + def from_callback(cls, callback: CallbackQuery): + action, user_id, emoji = str(callback.data).split(";") + if action.lower() != "emoji": + raise ValueError("Callback provided is not an emoji button callback") + return cls(int(user_id), emoji) + + +@dataclass +class CallbackBan: + user_id: int + + @classmethod + def from_callback(cls, callback: CallbackQuery): + action, user_id = str(callback.data).split(";") + if action.lower() != "ban": + raise ValueError("Callback provided is not a ban callback") + return cls(int(user_id)) + + +@dataclass +class CallbackLanguage: + language: str + + @classmethod + def from_callback(cls, callback: CallbackQuery): + action, language = str(callback.data).split(";") + if action.lower() != "language": + raise ValueError("Callback provided is not a language callback") + return cls(language) diff --git a/classes/pyroclient.py b/classes/pyroclient.py index 25cddd3..0496d03 100644 --- a/classes/pyroclient.py +++ b/classes/pyroclient.py @@ -18,7 +18,7 @@ class PyroClient(PyroClient): ### Returns: * `PyroUser`: PyroUser object """ - db_record = col_users.find_one( + db_record = await col_users.find_one( {"id": user.id if isinstance(user, User) else user, "group": group} ) diff --git a/classes/pyrogroup.py b/classes/pyrogroup.py new file mode 100644 index 0000000..a1c96e9 --- /dev/null +++ b/classes/pyrogroup.py @@ -0,0 +1,72 @@ +import logging +from dataclasses import dataclass +from typing import Union + +from bson import ObjectId +from pyrogram.types import User + +from classes.pyroclient import PyroClient +from modules.database import col_groups + +logger = logging.getLogger(__name__) + + +@dataclass +class PyroGroup: + """Dataclass of DB entry of a group""" + + __slots__ = ("_id", "id", "locale", "locale_auto") + + _id: ObjectId + id: int + locale: Union[str, None] + locale_auto: bool + + @classmethod + async def create_if_not_exists( + cls, + id: int, + locale: Union[str, None] = None, + locale_auto: bool = True, + ): + db_entry = await col_groups.find_one( + { + "id": id, + } + ) + if db_entry is None: + inserted = await col_groups.insert_one( + {"id": id, "locale": locale, "locale_auto": locale_auto} + ) + db_entry = { + "_id": inserted.inserted_id, + "id": id, + "locale": locale, + "locale_auto": locale_auto, + } + return cls(**db_entry) + + async def set_locale(self, locale: Union[str, None]) -> None: + logger.debug("Locale of group %s has been set to %s", self.id, locale) + await col_groups.update_one({"_id": self._id}, {"$set": {"locale": locale}}) + + async def set_locale_auto(self, enabled: bool) -> None: + logger.debug( + "Automatic locale selection of group %s has been set to %s", + self.id, + enabled, + ) + await col_groups.update_one( + {"_id": self._id}, {"$set": {"locale_auto": enabled}} + ) + + # Group settings + # User locale + def select_locale( + self, app: PyroClient, user: Union[User, None] = None, ignore_auto: bool = False + ) -> str: + if not ignore_auto and self.locale_auto is True: + if user.language_code is not None: + return user.language_code + return self.locale if self.locale is not None else app.default_locale + return self.locale if self.locale is not None else app.default_locale diff --git a/classes/pyrouser.py b/classes/pyrouser.py index 4eb1cd4..41b4480 100644 --- a/classes/pyrouser.py +++ b/classes/pyrouser.py @@ -34,7 +34,7 @@ class PyroUser: mistakes: int @classmethod - def create_if_not_exists( + async def create_if_not_exists( cls, id: int, group: int, @@ -44,9 +44,9 @@ class PyroUser: score: int = 0, mistakes: int = 0, ): - db_entry = col_users.find_one({"id": id, "group": group}) + db_entry = await col_users.find_one({"id": id, "group": group}) if db_entry is None: - inserted = col_users.insert_one( + inserted = await col_users.insert_one( { "id": id, "group": group, @@ -69,36 +69,36 @@ class PyroUser: } return cls(**db_entry) - def set_failed(self, failed: bool = True) -> None: + async def set_failed(self, failed: bool = True) -> None: logger.debug("%s's failure state has been set to %s", self.id, failed) - col_users.update_one({"_id": self._id}, {"$set": {"failed": failed}}) + await col_users.update_one({"_id": self._id}, {"$set": {"failed": failed}}) - def set_emojis(self, emojis: List[str]) -> None: + async def set_emojis(self, emojis: List[str]) -> None: logger.debug("%s's emojis have been set to %s", self.id, emojis) - col_users.update_one({"_id": self._id}, {"$set": {"emojis": emojis}}) + await col_users.update_one({"_id": self._id}, {"$set": {"emojis": emojis}}) - def set_score(self, score: int = 0) -> None: + async def set_score(self, score: int = 0) -> None: logger.debug("%s's score has been set to %s", self.id, score) - col_users.update_one({"_id": self._id}, {"$set": {"score": score}}) + await col_users.update_one({"_id": self._id}, {"$set": {"score": score}}) - def set_mistakes(self, mistakes: int = 0) -> None: + async def set_mistakes(self, mistakes: int = 0) -> None: logger.debug("%s's mistakes count has been set to %s", self.id, mistakes) - col_users.update_one({"_id": self._id}, {"$set": {"mistakes": mistakes}}) + await col_users.update_one({"_id": self._id}, {"$set": {"mistakes": mistakes}}) - def update_score(self, points: int = 1) -> None: + async def update_score(self, points: int = 1) -> None: logger.debug("%s point(s) have been added to %s score", points, self.id) - col_users.update_one( + await col_users.update_one( {"_id": self._id}, {"$set": {"score": self.score + points}} ) - def update_mistakes(self, points: int = 1) -> None: + async def update_mistakes(self, points: int = 1) -> None: logger.debug("%s point(s) have been added to %s mistakes", points, self.id) - col_users.update_one( + await col_users.update_one( {"_id": self._id}, {"$set": {"mistakes": self.mistakes + points}} ) - def update_selected(self, entry: str) -> None: + async def update_selected(self, entry: str) -> None: logger.debug("Emoji %s has been added to %s's selection list", entry, self.id) - col_users.update_one( + await col_users.update_one( {"_id": self._id}, {"$set": {"selected": self.selected + [entry]}} ) diff --git a/config_example.json b/config_example.json index aaf4355..80f2094 100644 --- a/config_example.json +++ b/config_example.json @@ -105,5 +105,20 @@ "πŸ˜€", "πŸ™Š" ], - "commands": {} + "commands": { + "language": { + "scopes": [ + { + "name": "BotCommandScopeAllChatAdministrators" + } + ] + }, + "language_auto": { + "scopes": [ + { + "name": "BotCommandScopeAllChatAdministrators" + } + ] + } + } } \ No newline at end of file diff --git a/locale/en.json b/locale/en.json index a634cb4..d37c6be 100644 --- a/locale/en.json +++ b/locale/en.json @@ -1,19 +1,38 @@ { + "metadata": { + "flag": "πŸ‡¬πŸ‡§", + "name": "English", + "codes": [ + "en", + "en-US", + "en-GB" + ] + }, + "commands": { + "language": "Set bot's language in this group", + "language_auto": "Toggle automatic language selection" + }, "messages": { + "locale_auto_disabled": "Automatic language selection on per-user basis has been **disabled**. Please note that group's language will now be used. You can change it using /language. If \"Default\" language is set – bot's default language ({default_locale}) will be used.", + "locale_auto_enabled": "Automatic language selection on per-user basis has been **enabled**. Please note that if bot does not have a language pack for user – group's or bot's default language will be used. You can change group's language using /language. If \"Default\" language is set – bot's default language ({default_locale}) will be used.", + "locale_choice": "Alright. Please choose the language using keyboard below.\n\nNote, that you can also enable automatic language selection on per-user basis using command /language_auto", + "permission_denied": "You are not allowed to do so.", "verify": "Please, use the buttons below to complete the captcha. Tap on the buttons with emojis you see on this image.", "welcome_verify": "Welcome, {mention}! In order to chat here, we need to verify you're a human. Please, press the button below to start the verification.", "welcome": "Welcome to the chat, {mention}!" }, "callbacks": { - "captcha_failed": "You have failed the captcha.", "captcha_failed_force": "You have forced {user_id} to fail the captcha.", + "captcha_failed": "You have failed the captcha.", "captcha_mistake": "Invalid answer. Remaining attempts: {remaining}.", "captcha_passed": "You have passed the captcha. Welcome!", + "locale_set": "Group's language is now: {locale}", "nothing": "This actions has already been finished.", "wrong_user": "This message is not for you." }, "buttons": { "ban": "Ban (for admins)", + "locale_default": "Default", "verify": "Verify" } } \ No newline at end of file diff --git a/locale/uk.json b/locale/uk.json index 44e035b..6a4fac5 100644 --- a/locale/uk.json +++ b/locale/uk.json @@ -1,5 +1,21 @@ { + "metadata": { + "flag": "πŸ‡ΊπŸ‡¦", + "name": "Π£ΠΊΡ€Π°Ρ—Π½ΡΡŒΠΊΠ°", + "codes": [ + "uk", + "uk-UA" + ] + }, + "commands": { + "language": "Встановити ΠΌΠΎΠ²Ρƒ Π±ΠΎΡ‚Π° Π² Ρ†Ρ–ΠΉ Π³Ρ€ΡƒΠΏΡ–", + "language_auto": "ΠŸΠ΅Ρ€Π΅ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΠΈ Π°Π²Ρ‚ΠΎΠΌΠ°Ρ‚ΠΈΡ‡Π½ΠΈΠΉ Π²ΠΈΠ±Ρ–Ρ€ ΠΌΠΎΠ²ΠΈ" + }, "messages": { + "locale_auto_disabled": "Автоматичний Π²ΠΈΠ±Ρ–Ρ€ ΠΌΠΎΠ²ΠΈ для ΠΊΠΎΠΆΠ½ΠΎΠ³ΠΎ користувача Π±ΡƒΠ»ΠΎ **Π²ΠΈΠΌΠΊΠ½Π΅Π½ΠΎ**. Π—Π²Π΅Ρ€Π½Ρ–Ρ‚ΡŒ ΡƒΠ²Π°Π³Ρƒ, Ρ‰ΠΎ Ρ‚Π΅ΠΏΠ΅Ρ€ Π±ΡƒΠ΄Π΅ використовуватися ΠΌΠΎΠ²Π° Π³Ρ€ΡƒΠΏΠΈ. Π’ΠΈ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ Π·ΠΌΡ–Π½ΠΈΡ‚ΠΈ Ρ—Ρ— Π·Π° допомогою /language. Π―ΠΊΡ‰ΠΎ встановлСно ΠΌΠΎΠ²Ρƒ \"Π—Π° замовчуванням\" – Π±ΡƒΠ΄Π΅ використовуватися ΠΌΠΎΠ²Π° Π±ΠΎΡ‚Π° Π·Π° замовчуванням ({default_locale}).", + "locale_auto_enabled": "Автоматичний Π²ΠΈΠ±Ρ–Ρ€ ΠΌΠΎΠ²ΠΈ для ΠΊΠΎΠΆΠ½ΠΎΠ³ΠΎ користувача Π±ΡƒΠ² **ΡƒΠ²Ρ–ΠΌΠΊΠ½Π΅Π½ΠΈΠΉ**. Π—Π²Π΅Ρ€Π½Ρ–Ρ‚ΡŒ ΡƒΠ²Π°Π³Ρƒ, Ρ‰ΠΎ якщо Π±ΠΎΡ‚ Π½Π΅ ΠΌΠ°Ρ” ΠΌΠΎΠ²Π½ΠΎΠ³ΠΎ ΠΏΠ°ΠΊΠ΅Ρ‚Π° для користувача – Π±ΡƒΠ΄Π΅ використовуватися ΠΌΠΎΠ²Π° Π³Ρ€ΡƒΠΏΠΈ Π°Π±ΠΎ Π±ΠΎΡ‚Π° Π·Π° замовчуванням. Π’ΠΈ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ Π·ΠΌΡ–Π½ΠΈΡ‚ΠΈ ΠΌΠΎΠ²Ρƒ Π³Ρ€ΡƒΠΏΠΈ Π·Π° допомогою /language. Π―ΠΊΡ‰ΠΎ встановлСно ΠΌΠΎΠ²Ρƒ \"Π—Π° замовчуванням\" – Π±ΡƒΠ΄Π΅ використовуватися ΠΌΠΎΠ²Π° Π±ΠΎΡ‚Π° Π·Π° замовчуванням ({default_locale}).", + "locale_choice": "Π“Π°Ρ€Π°Π·Π΄. Π‘ΡƒΠ΄ΡŒ ласка, ΠΎΠ±Π΅Ρ€Ρ–Ρ‚ΡŒ ΠΌΠΎΠ²Ρƒ Π·Π° допомогою ΠΊΠ»Π°Π²Ρ–Π°Ρ‚ΡƒΡ€ΠΈ Π½ΠΈΠΆΡ‡Π΅.\n\nΠ—Π°ΡƒΠ²Π°ΠΆΡ‚Π΅, Ρ‰ΠΎ Π²ΠΈ Ρ‚Π°ΠΊΠΎΠΆ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ ΡƒΠ²Ρ–ΠΌΠΊΠ½ΡƒΡ‚ΠΈ Π°Π²Ρ‚ΠΎΠΌΠ°Ρ‚ΠΈΡ‡Π½ΠΈΠΉ Π²ΠΈΠ±Ρ–Ρ€ ΠΌΠΎΠ²ΠΈ для ΠΊΠΎΠΆΠ½ΠΎΠ³ΠΎ користувача Π·Π° допомогою ΠΊΠΎΠΌΠ°Π½Π΄ΠΈ /language_auto", + "permission_denied": "Π’Π°ΠΌ Π½Π΅ Π΄ΠΎΠ·Π²ΠΎΠ»Π΅Π½ΠΎ Ρ€ΠΎΠ±ΠΈΡ‚ΠΈ Ρ†Π΅.", "verify": "Π‘ΡƒΠ΄ΡŒ ласка, використовуйтС ΠΊΠ½ΠΎΠΏΠΊΠΈ Π½ΠΈΠΆΡ‡Π΅, Ρ‰ΠΎΠ± Π·Π°ΠΏΠΎΠ²Π½ΠΈΡ‚ΠΈ ΠΊΠ°ΠΏΡ‡Ρƒ. ΠΠ°Ρ‚ΠΈΡΠ½Ρ–Ρ‚ΡŒ Π½Π° ΠΊΠ½ΠΎΠΏΠΊΠΈ Π·Ρ– смайликами, які Π²ΠΈ Π±Π°Ρ‡ΠΈΡ‚Π΅ Π½Π° Ρ†ΡŒΠΎΠΌΡƒ Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½Π½Ρ–.", "welcome_verify": "Ласкаво просимо, {mention}! Для Ρ‚ΠΎΠ³ΠΎ, Ρ‰ΠΎΠ± спілкуватися Π² Ρ‡Π°Ρ‚Ρ–, ΠΌΠΈ ΠΏΠΎΠ²ΠΈΠ½Π½Ρ– пСрСконатися, Ρ‰ΠΎ Π²ΠΈ людина. Π‘ΡƒΠ΄ΡŒ ласка, Π½Π°Ρ‚ΠΈΡΠ½Ρ–Ρ‚ΡŒ ΠΊΠ½ΠΎΠΏΠΊΡƒ Π½ΠΈΠΆΡ‡Π΅, Ρ‰ΠΎΠ± ΠΏΠΎΡ‡Π°Ρ‚ΠΈ ΠΏΠ΅Ρ€Π΅Π²Ρ–Ρ€ΠΊΡƒ.", "welcome": "Ласкаво просимо Π΄ΠΎ Ρ‡Π°Ρ‚Ρƒ, {mention}!" @@ -9,11 +25,13 @@ "captcha_failed_force": "Π’ΠΈ змусили {user_id} Π½Π΅ ΠΏΡ€ΠΎΠΉΡ‚ΠΈ ΠΊΠ°ΠΏΡ‡Ρƒ.", "captcha_mistake": "ΠΠ΅ΠΏΡ€Π°Π²ΠΈΠ»ΡŒΠ½Π° Π²Ρ–Π΄ΠΏΠΎΠ²Ρ–Π΄ΡŒ. Π‘ΠΏΡ€ΠΎΠ±ΠΈ, Ρ‰ΠΎ залишилися: {remaining}.", "captcha_passed": "Π’ΠΈ ΠΏΡ€ΠΎΠΉΡˆΠ»ΠΈ ΠΊΠ°ΠΏΡ‡Ρƒ. Ласкаво просимо!", + "locale_set": "ВстановлСно ΠΌΠΎΠ²Ρƒ Π³Ρ€ΡƒΠΏΠΈ: {locale}", "nothing": "Цю Π΄Ρ–ΡŽ Π²ΠΆΠ΅ Π·Π°Π²Π΅Ρ€ΡˆΠ΅Π½ΠΎ.", "wrong_user": "Π¦Π΅ повідомлСння Π½Π΅ для вас." }, "buttons": { "ban": "Π—Π°Π±Π»ΠΎΠΊΡƒΠ²Π°Ρ‚ΠΈ (для Π°Π΄ΠΌΡ–Π½Ρ–Π²)", + "locale_default": "Π—Π° замовчуванням", "verify": "Π’Π΅Ρ€ΠΈΡ„Ρ–ΠΊΡƒΠ²Π°Ρ‚ΠΈΡΡŒ" } } \ No newline at end of file diff --git a/modules/database.py b/modules/database.py index 95992f9..ac8a6e7 100644 --- a/modules/database.py +++ b/modules/database.py @@ -1,6 +1,6 @@ """Module that provides all database columns""" -from pymongo import MongoClient +from async_pymongo import AsyncClient from ujson import loads with open("config.json", "r", encoding="utf-8") as f: @@ -20,14 +20,15 @@ else: db_config["host"], db_config["port"], db_config["name"] ) -db_client = MongoClient(con_string) +db_client = AsyncClient(con_string) db = db_client.get_database(name=db_config["name"]) -collections = db.list_collection_names() +# collections = db.list_collection_names() -for collection in ["users", "schedule"]: - if collection not in collections: - db.create_collection(collection) +# for collection in ["users", "groups", "schedule"]: +# if collection not in collections: +# db.create_collection(collection) col_users = db.get_collection("users") +col_groups = db.get_collection("groups") col_schedule = db.get_collection("schedule") diff --git a/plugins/callbacks/ban.py b/plugins/callbacks/ban.py index c730ad4..67f42f6 100644 --- a/plugins/callbacks/ban.py +++ b/plugins/callbacks/ban.py @@ -4,22 +4,30 @@ from pyrogram import filters from pyrogram.enums.chat_member_status import ChatMemberStatus from pyrogram.types import CallbackQuery, Message +from classes.callbacks import CallbackBan from classes.pyroclient import PyroClient +from classes.pyrogroup import PyroGroup logger = logging.getLogger(__name__) @PyroClient.on_callback_query(filters.regex(r"ban;[\s\S]*")) async def callback_ban(app: PyroClient, callback: CallbackQuery): - if ( - await app.get_chat_member(callback.message.chat.id, callback.from_user.id) - ).status not in [ChatMemberStatus.ADMINISTRATOR, ChatMemberStatus.OWNER]: - await callback.answer(app._("wrong_user", "callbacks"), show_alert=True) + group = await PyroGroup.create_if_not_exists(callback.message.chat.id, None, True) + locale = group.select_locale(app, callback.message.from_user) + + if (await app.get_chat_member(group.id, callback.from_user.id)).status not in [ + ChatMemberStatus.ADMINISTRATOR, + ChatMemberStatus.OWNER, + ]: + await callback.answer( + app._("wrong_user", "callbacks", locale=locale), show_alert=True + ) return - user = await app.find_user( - int(str(callback.data).split(";")[1]), callback.message.chat.id - ) + parsed = CallbackBan.from_callback(callback) + + user = await app.find_user(parsed.user_id, group.id) logger.info( "User %s has been marked as failed the captcha by %s", @@ -27,14 +35,16 @@ async def callback_ban(app: PyroClient, callback: CallbackQuery): callback.from_user.id, ) - user.set_mistakes(3) - user.set_failed(True) + await user.set_mistakes(3) + await user.set_failed(True) await callback.answer( - app._("captcha_failed_force", "callbacks").format(user_id=user.id), + app._("captcha_failed_force", "callbacks", locale=locale).format( + user_id=user.id + ), show_alert=True, ) - banned = await app.ban_chat_member(callback.message.chat.id, user.id) + banned = await app.ban_chat_member(group.id, user.id) if isinstance(banned, Message): await banned.delete() diff --git a/plugins/callbacks/emoji_button.py b/plugins/callbacks/emoji_button.py index 1db2e0c..eb29b43 100644 --- a/plugins/callbacks/emoji_button.py +++ b/plugins/callbacks/emoji_button.py @@ -9,52 +9,58 @@ from pyrogram.types import ( Message, ) +from classes.callbacks import CallbackEmoji from classes.pyroclient import PyroClient +from classes.pyrogroup import PyroGroup logger = logging.getLogger(__name__) @PyroClient.on_callback_query(filters.regex(r"emoji;[\s\S]*")) async def callback_emoji_button(app: PyroClient, callback: CallbackQuery): - user_id = int(str(callback.data).split(";")[1]) - emoji = str(callback.data).split(";")[2] + parsed = CallbackEmoji.from_callback(callback) + group = await PyroGroup.create_if_not_exists(callback.message.chat.id, None, True) + locale = group.select_locale(app, callback.message.from_user) - if callback.from_user.id != user_id: - await callback.answer(app._("wrong_user", "callbacks"), show_alert=True) + if callback.from_user.id != parsed.user_id: + await callback.answer( + app._("wrong_user", "callbacks", locale=locale), show_alert=True + ) return - user = await app.find_user(callback.from_user, callback.message.chat.id) + user = await app.find_user(callback.from_user, group.id) logger.debug( "User %s has pressed the %s emoji '%s'", user.id, - "correct" if emoji in user.emojis else "wrong", - emoji, + "correct" if parsed.emoji in user.emojis else "wrong", + parsed.emoji, ) - if emoji in user.selected: + if parsed.emoji in user.selected: await callback.answer() return - user.update_selected(emoji) + await user.update_selected(parsed.emoji) - if emoji in user.emojis: - user.update_score(1) + if parsed.emoji in user.emojis: + await user.update_score(1) if user.score >= 5: logger.info("User %s has passed the captcha", user.id) await callback.message.delete() - await callback.answer(app._("captcha_passed", "callbacks"), show_alert=True) + await callback.answer( + app._("captcha_passed", "callbacks", locale=locale), show_alert=True + ) await app.send_message( - callback.message.chat.id, - app._( - "welcome", - "messages", - ).format(mention=callback.from_user.mention), + group.id, + app._("welcome", "messages", locale=locale).format( + mention=callback.from_user.mention + ), ) await app.restrict_chat_member( - chat_id=callback.message.chat.id, + chat_id=group.id, user_id=callback.from_user.id, permissions=ChatPermissions(can_send_messages=True), ) @@ -71,20 +77,20 @@ async def callback_emoji_button(app: PyroClient, callback: CallbackQuery): if user.mistakes >= 2: logger.info("User %s has failed the captcha", user.id) - user.set_failed(True) - await callback.answer(app._("captcha_failed", "callbacks"), show_alert=True) - - banned = await app.ban_chat_member( - callback.message.chat.id, callback.from_user.id + await user.set_failed(True) + await callback.answer( + app._("captcha_failed", "callbacks", locale=locale), show_alert=True ) + banned = await app.ban_chat_member(group.id, callback.from_user.id) + if isinstance(banned, Message): await banned.delete() await callback.message.delete() return - user.update_mistakes(1) + await user.update_mistakes(1) logger.info( "User %s has made a mistake and has %s attempt(s) left", user.id, @@ -92,7 +98,9 @@ async def callback_emoji_button(app: PyroClient, callback: CallbackQuery): ) await callback.answer( - app._("captcha_mistake", "callbacks").format(remaining=2 - user.mistakes), + app._("captcha_mistake", "callbacks", locale=locale).format( + remaining=2 - user.mistakes + ), show_alert=True, ) @@ -102,7 +110,7 @@ async def callback_emoji_button(app: PyroClient, callback: CallbackQuery): for row_index, row in enumerate(callback.message.reply_markup.inline_keyboard): for button_index, button in enumerate(row): - if button.text == emoji: + if button.text == parsed.emoji: button_replace = (row_index, button_index) new_keyboard = callback.message.reply_markup.inline_keyboard diff --git a/plugins/callbacks/nothing.py b/plugins/callbacks/nothing.py index 4a6e2a2..4d41f6d 100644 --- a/plugins/callbacks/nothing.py +++ b/plugins/callbacks/nothing.py @@ -2,8 +2,12 @@ from pyrogram import filters from pyrogram.types import CallbackQuery from classes.pyroclient import PyroClient +from classes.pyrogroup import PyroGroup @PyroClient.on_callback_query(filters.regex(r"nothing")) async def callback_nothing(app: PyroClient, callback: CallbackQuery): - await callback.answer(app._("nothing", "callbacks")) + group = await PyroGroup.create_if_not_exists(callback.message.chat.id, None, True) + locale = group.select_locale(app, callback.message.from_user) + + await callback.answer(app._("nothing", "callbacks", locale=locale)) diff --git a/plugins/callbacks/verify.py b/plugins/callbacks/verify.py index 386c0af..b53b294 100644 --- a/plugins/callbacks/verify.py +++ b/plugins/callbacks/verify.py @@ -7,7 +7,9 @@ from pykeyboard import InlineButton, InlineKeyboard from pyrogram import filters from pyrogram.types import CallbackQuery +from classes.callbacks import CallbackVerify from classes.pyroclient import PyroClient +from classes.pyrogroup import PyroGroup from modules.database import col_schedule from modules.kicker import kick_unverified from modules.utils import get_captcha_image @@ -17,13 +19,17 @@ logger = logging.getLogger(__name__) @PyroClient.on_callback_query(filters.regex(r"verify;[\s\S]*")) async def callback_verify(app: PyroClient, callback: CallbackQuery): - user_id = int(str(callback.data).split(";")[1]) + parsed = CallbackVerify.from_callback(callback) + group = await PyroGroup.create_if_not_exists(callback.message.chat.id, None, True) + locale = group.select_locale(app, callback.message.from_user) - if callback.from_user.id != user_id: - await callback.answer(app._("wrong_user", "callbacks"), show_alert=True) + if callback.from_user.id != parsed.user_id: + await callback.answer( + app._("wrong_user", "callbacks", locale=locale), show_alert=True + ) return - user = await app.find_user(callback.from_user, callback.message.chat.id) + user = await app.find_user(callback.from_user, group.id) captcha = get_captcha_image(app.config["emojis"]) logger.info( @@ -33,15 +39,15 @@ async def callback_verify(app: PyroClient, callback: CallbackQuery): captcha.emojis_correct, ) - scheduled_job = col_schedule.find_one_and_delete( - {"user": user_id, "group": callback.message.chat.id} + scheduled_job = await col_schedule.find_one_and_delete( + {"user": parsed.user_id, "group": group.id} ) if scheduled_job is not None and app.scheduler is not None: with contextlib.suppress(JobLookupError): app.scheduler.remove_job(scheduled_job["job_id"]) - user.set_emojis(captcha.emojis_correct) + await user.set_emojis(captcha.emojis_correct) buttons = [ InlineButton(emoji, f"emoji;{user.id};{emoji}") for emoji in captcha.emojis_all @@ -53,9 +59,9 @@ async def callback_verify(app: PyroClient, callback: CallbackQuery): await callback.message.delete() captcha_message = await app.send_photo( - callback.message.chat.id, + group.id, captcha.image, - caption=app._("verify", "messages"), + caption=app._("verify", "messages", locale=locale), reply_markup=keyboard, ) @@ -65,7 +71,7 @@ async def callback_verify(app: PyroClient, callback: CallbackQuery): app.scheduler.add_job( kick_unverified, "date", - [app, user.id, callback.message.chat.id, captcha_message.id], + [app, user.id, group.id, captcha_message.id], run_date=datetime.now() + timedelta(seconds=app.config["timeouts"]["verify"]), ) diff --git a/plugins/commands/language_auto.py b/plugins/commands/language_auto.py new file mode 100644 index 0000000..6cc3cf9 --- /dev/null +++ b/plugins/commands/language_auto.py @@ -0,0 +1,51 @@ +import logging + +from pyrogram import filters +from pyrogram.enums.chat_member_status import ChatMemberStatus +from pyrogram.types import Message + +from classes.pyroclient import PyroClient +from classes.pyrogroup import PyroGroup + +logger = logging.getLogger(__name__) + + +@PyroClient.on_message( + ~filters.scheduled + & filters.group + & filters.command(["language_auto"], prefixes=["/"]) # type: ignore +) +async def command_language(app: PyroClient, message: Message): + group = await PyroGroup.create_if_not_exists(message.chat.id, None, True) + locale = group.select_locale(app, message.from_user) + + if (await app.get_chat_member(group.id, message.from_user.id)).status not in [ + ChatMemberStatus.ADMINISTRATOR, + ChatMemberStatus.OWNER, + ]: + await message.reply_text( + app._("permission_denied", "messages", locale=locale), quote=True + ) + return + + if group.locale_auto: + await message.reply_text( + app._("locale_auto_disabled", "messages", locale=locale).format( + default_locale=f"{app._('flag', 'metadata', locale=app.default_locale)} {app._('name', 'metadata', locale=app.default_locale)}" + ) + ) + else: + await message.reply_text( + app._("locale_auto_enabled", "messages", locale=locale).format( + default_locale=f"{app._('flag', 'metadata', locale=app.default_locale)} {app._('name', 'metadata', locale=app.default_locale)}" + ) + ) + + logger.info( + "Automatic locale selection of group %s has been set to %s (group: %s)", + group.id, + not group.locale_auto, + group.locale, + ) + + await group.set_locale_auto(not group.locale_auto) diff --git a/plugins/handlers/bot_join.py b/plugins/handlers/bot_join.py new file mode 100644 index 0000000..4840588 --- /dev/null +++ b/plugins/handlers/bot_join.py @@ -0,0 +1,15 @@ +import logging + +from pyrogram import filters +from pyrogram.types import Message + +from classes.pyroclient import PyroClient +from classes.pyrogroup import PyroGroup + +logger = logging.getLogger(__name__) + + +@PyroClient.on_message(filters.new_chat_members & filters.group & filters.me) +async def handler_bot_join(app: PyroClient, message: Message): + logger.info("Bot has joined the group %s") + await PyroGroup.create_if_not_exists(message.chat.id, None, True) diff --git a/plugins/handlers/user_join.py b/plugins/handlers/user_join.py index da66d0e..c35bab5 100644 --- a/plugins/handlers/user_join.py +++ b/plugins/handlers/user_join.py @@ -12,6 +12,7 @@ from pyrogram.types import ( ) from classes.pyroclient import PyroClient +from classes.pyrogroup import PyroGroup from classes.pyrouser import PyroUser from modules.database import col_schedule from modules.kicker import kick_unstarted @@ -19,34 +20,37 @@ from modules.kicker import kick_unstarted logger = logging.getLogger(__name__) -@PyroClient.on_message(filters.new_chat_members & ~filters.me) +@PyroClient.on_message( + filters.new_chat_members & filters.group & ~filters.me & ~filters.bot +) async def handler_user_join(app: PyroClient, message: Message): + group = await PyroGroup.create_if_not_exists(message.chat.id, None, True) + locale = group.select_locale(app, message.from_user) + if ( app.config["whitelist"]["enabled"] - and message.chat.id not in app.config["whitelist"]["groups"] + and group.id not in app.config["whitelist"]["groups"] ): logger.info( "User %s has joined the group %s, but it's not whitelisted, ignoring.", message.from_user.id, - message.chat.id, + group.id, ) return - logger.info( - "User %s has joined the group %s", message.from_user.id, message.chat.id - ) + logger.info("User %s has joined the group %s", message.from_user.id, group.id) await message.delete() # If user has already failed the test and joined once more with contextlib.suppress(KeyError): - user = await app.find_user(message.from_user, message.chat.id) + user = await app.find_user(message.from_user, group.id) if user.failed is True: logger.info( "User %s has previously failed the captcha, kicking and banning him", user.id, ) - banned = await app.ban_chat_member(message.chat.id, user.id) + banned = await app.ban_chat_member(group.id, user.id) if isinstance(banned, Message): await banned.delete() @@ -54,42 +58,35 @@ async def handler_user_join(app: PyroClient, message: Message): return await app.restrict_chat_member( - chat_id=message.chat.id, + chat_id=group.id, user_id=message.from_user.id, permissions=ChatPermissions(can_send_messages=False), ) - user = PyroUser.create_if_not_exists(message.from_user.id, message.chat.id) + user = await PyroUser.create_if_not_exists(message.from_user.id, group.id) if user.mistakes > 0 or user.score > 0: - user.set_score(0) - user.set_mistakes(0) + await user.set_score(0) + await user.set_mistakes(0) await asyncio.sleep(2) verification_request = await app.send_message( - chat_id=message.chat.id, - text=app._( - "welcome", - "messages", - ).format(mention=message.from_user.mention), + chat_id=group.id, + text=app._("welcome", "messages", locale=locale).format( + mention=message.from_user.mention + ), reply_markup=InlineKeyboardMarkup( [ [ InlineKeyboardButton( - app._( - "verify", - "buttons", - ), - callback_data=f"verify;{message.from_user.id}", + app._("verify", "buttons", locale=locale), + callback_data=f"verify;{user.id}", ) ], [ InlineKeyboardButton( - app._( - "ban", - "buttons", - ), - callback_data=f"ban;{message.from_user.id}", + app._("ban", "buttons", locale=locale), + callback_data=f"ban;{user.id}", ) ], ], @@ -103,6 +100,6 @@ async def handler_user_join(app: PyroClient, message: Message): [app, user.id, verification_request.chat.id, verification_request.id], run_date=datetime.now() + timedelta(seconds=app.config["timeouts"]["join"]), ) - col_schedule.insert_one( - {"user": message.from_user.id, "group": message.chat.id, "job_id": job.id} + await col_schedule.insert_one( + {"user": user.id, "group": group.id, "job_id": job.id} ) diff --git a/plugins/language.py b/plugins/language.py new file mode 100644 index 0000000..68379d2 --- /dev/null +++ b/plugins/language.py @@ -0,0 +1,93 @@ +import logging + +from pykeyboard import InlineButton, InlineKeyboard +from pyrogram import filters +from pyrogram.client import Client +from pyrogram.enums.chat_member_status import ChatMemberStatus +from pyrogram.types import CallbackQuery, Message + +from classes.callbacks import CallbackLanguage +from classes.pyroclient import PyroClient +from classes.pyrogroup import PyroGroup + +logger = logging.getLogger(__name__) + + +@PyroClient.on_message( + ~filters.scheduled & filters.group & filters.command(["language"], prefixes=["/"]) # type: ignore +) +async def command_language(app: PyroClient, message: Message): + group = await PyroGroup.create_if_not_exists(message.chat.id, None, True) + locale = group.select_locale(app, message.from_user) + + if (await app.get_chat_member(group.id, message.from_user.id)).status not in [ + ChatMemberStatus.ADMINISTRATOR, + ChatMemberStatus.OWNER, + ]: + await message.reply_text( + app._("permission_denied", "messages", locale=locale), quote=True + ) + return + + keyboard = InlineKeyboard(row_width=2) + buttons = [] + + for language, data in app.in_every_locale("metadata").items(): + buttons.append( + InlineButton(f"{data['flag']} {data['name']}", f"language;{language}") + ) + + buttons.append( + InlineButton( + f"πŸ€– {app._('locale_default', 'buttons', locale=locale)}", "language;default" + ) + ) + + keyboard.add(*buttons) + + await message.reply_text( + app._("locale_choice", "messages", locale=locale), + reply_markup=keyboard, + ) + + +@Client.on_callback_query(filters.regex(r"language;[\s\S]*")) # type: ignore +async def callback_language(app: PyroClient, callback: CallbackQuery): + group = await PyroGroup.create_if_not_exists(callback.message.chat.id, None, True) + locale = group.select_locale(app, callback.message.from_user) + + if (await app.get_chat_member(group.id, callback.from_user.id)).status not in [ + ChatMemberStatus.ADMINISTRATOR, + ChatMemberStatus.OWNER, + ]: + await callback.answer( + app._("wrong_user", "callbacks", locale=locale), show_alert=True + ) + return + + parsed = CallbackLanguage.from_callback(callback) + + logger.info( + "Locale of group %s has been set to %s (auto: %s)", + group.id, + parsed.language, + group.locale_auto, + ) + + if parsed.language == "default": + await group.set_locale(None) + await callback.answer( + app._("locale_set", "callbacks", locale=app.default_locale).format( + locale=app._("locale_default", "buttons", locale=locale) + ), + show_alert=True, + ) + return + + await group.set_locale(parsed.language) + await callback.answer( + app._("locale_set", "callbacks", locale=parsed.language).format( + locale=app._("name", "metadata", locale=parsed.language) + ), + show_alert=True, + ) diff --git a/requirements.txt b/requirements.txt index 54206dd..f0effb2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ aiofiles~=23.2.1 apscheduler~=3.10.1 +async_pymongo==0.1.3 black~=23.7.0 Pillow~=10.0.0 pykeyboard==0.1.5 -pymongo==4.4.1 pyrogram==2.0.106 tgcrypto==1.2.5 ujson==5.8.0