diff --git a/classes/holo_user.py b/classes/holo_user.py index 0c719a9..d0a4537 100644 --- a/classes/holo_user.py +++ b/classes/holo_user.py @@ -6,7 +6,7 @@ from pyrogram.types import User, ChatMember, ChatPrivileges, Chat, Message, Phot from pyrogram.errors import bad_request_400 from dateutil.relativedelta import relativedelta from classes.errors.geo import PlaceNotFoundError -from modules.database import col_tmp, col_users, col_applications, col_sponsorships, col_messages +from modules.database import col_tmp, col_users, col_applications, col_sponsorships, col_messages, col_spoilers from modules.logging import logWrite from modules.utils import configGet, create_tmp, download_tmp, find_location, locale, should_quote @@ -544,4 +544,12 @@ class HoloUser(): logWrite(f"User {self.id} completed stage {stage} of sponsorship") else: - return \ No newline at end of file + return + + def spoiler_state(self) -> bool: + """Check if user has any started but not finished spoilers + + ### Returns: + * `bool`: `True` if any not finished spoilers available and `False` if none. + """ + return False if col_spoilers.find_one({"user": self.id, "completed": False}) is None else True \ No newline at end of file diff --git a/config_example.json b/config_example.json index 34751e0..40f265c 100644 --- a/config_example.json +++ b/config_example.json @@ -87,6 +87,12 @@ "general" ] }, + "spoiler": { + "permissions": [ + "users", + "admins" + ] + }, "cancel": { "permissions": [ "users", diff --git a/holochecker.py b/holochecker.py index c2ad999..997357d 100644 --- a/holochecker.py +++ b/holochecker.py @@ -20,6 +20,7 @@ from modules.commands.nearby import * from modules.commands.reapply import * from modules.commands.reboot import * from modules.commands.rules import * +from modules.commands.spoiler import * from modules.commands.sponsorship import * from modules.commands.start import * from modules.commands.warn import * @@ -28,6 +29,7 @@ from modules.commands.warnings import * from modules.callbacks.nothing import * from modules.callbacks.reapply import * from modules.callbacks.rules import * +from modules.callbacks.sid import * from modules.callbacks.sponsorship import * from modules.callbacks.sub import * from modules.callbacks.sus import * @@ -35,6 +37,7 @@ from modules.callbacks.sus import * from modules.handlers.confirmation import * from modules.handlers.contact import * from modules.handlers.group_join import * +from modules.handlers.spoiler import * from modules.handlers.sponsorship import * from modules.handlers.voice import * from modules.handlers.welcome import * diff --git a/locale/uk.json b/locale/uk.json index c5674a8..2ebc1a0 100644 --- a/locale/uk.json +++ b/locale/uk.json @@ -100,6 +100,15 @@ "identify_invalid_syntax": "Неправильний синтаксис!\nТреба: `/identify ID/NAME/USERNAME`", "identify_not_found": "Не знайдено користувачів за запитом **{0}**", "identify_success": "Користувач `{0}`\n\nІм'я: {1}\nЮзернейм: {2}\nЄ в чаті: {3}\nЄ адміном: {4}\nРоль: {5}\nНаявна анкета: {6}\nНаявне спонсорство: {7}", + "spoiler_started": "Розпочато створення спойлера. Надішліть щось", + "spoiler_unfinished": "У вас ще є незавершений спойлер. Надішліть /cancel щоб зупинити його створення", + "spoiler_cancel": "Створення спойлера було припинено", + "spoiler_empty": "Спойлер без опису", + "spoiler_described": "Спойлер: {0}", + "spoiler_description_enter": "Добре, введіть бажаний опис спойлера", + "spoiler_using_description": "Встановлено опис спойлера: {0}", + "spoiler_send_description": "Майже впорались. Тепер треба надіслати коротенький опис спойлера, щоб люди розуміли що під ним варто очкувати. Надішли мінус (-) щоб пропустити цей крок.", + "spoiler_ready": "Успіх! Спойлер створено. Користуйтесь кнопкою нижче щоб надіслати його.", "yes": "Так", "no": "Ні", "voice_message": [ @@ -146,6 +155,17 @@ [ "Ні, повторно заповнити" ] + ], + "spoiler_description": [ + [ + "NSFW контент" + ], + [ + "Деанон холо-учасників" + ], + [ + "Інше (надішліть свій текст)" + ] ] }, "force_reply": { @@ -162,7 +182,9 @@ "sponsor1": "Ім'я дівчини", "sponsor2": "Дата до якої підписка", "sponsor3": "Фото-підтвердження", - "sponsor4": "Бажана роль" + "sponsor4": "Бажана роль", + "spoiler_content": "Вміст спойлера", + "spoiler_description": "Опис спойлера" }, "button": { "sub_yes": "✅ Прийняти", @@ -189,7 +211,9 @@ "applying_stop": "🛑 Перервати заповнення", "done": "✅ Готово", "sponsor_apply": "Заповнити форму", - "sponsor_started": "Форму розпочато" + "sponsor_started": "Форму розпочато", + "spoiler_send": "Надіслати", + "spoiler_view": "Переглянути" }, "callback": { "sub_accepted": "✅ Анкету {0} схвалено", @@ -222,6 +246,10 @@ "title": "", "description": "Переглянути анкету {0} (@{1})", "message_content": "{0} (@{1})\n\n**Дані анкети:**\n{2}" + }, + "spoiler": { + "title": "Відправити", + "description": "Надіслати цей спойлер до чату" } }, "rules_msg": "📢Правила можуть доповнюватись та змінюватись, залежно від потреби. У такому разі, порушення, які були вчинені до введення (змінення) правила, порушеннями вважатися не будуть. Про всі зміни в правилах, ви будете проінформовані за допомогою закріплених повідомлень. Але вони не будуть закріплені на постійній основі, тому, час від часу, перевіряйте актуальність правил у боті.\n\n🔔Якщо ви бачите, як хтось із учасників порушив правила, тегніть одного із адмінів, у відповідь на повідомлення, яке, на вашу думку, є порушенням. У дописі до тегу, вкажіть, по якому пункту ви побачили порушення. Або перешліть повідомлення до будь кого із адміністраторів у особисті повідомлення, та коротко опишіть ситуацію.\nСписок адміністраторів: @Chirkopol @Za_NerZula @Denialvapr\nЗ питань функціонування бота звертайтесь до @Profitroll2281337\n\n❗️Будь-який заборонений контент, може бути відправлений до чату за допомогою бота - https://t.me/spoilerobot з повним описом контенту, що міститься під спойлером. За неправильний або некоректний опис, може бути видане попередження.\n\n‼️Видалені або змінені повідомлення, все ще є повідомленнями від вашого імені, які могли побачити учасники чату, і які можуть бути відстежені через адмінську панель.\n\n🔨 За порушення - ви отримаєте попередження. За наявності 3-х попереджень - мут на добу. За повторні порушення, ви одразу отримаєте покарання, без додаткових попереджень.", @@ -248,6 +276,7 @@ "reapply": "Повторно заповнити анкету", "reboot": "Перезапустити бота", "rules": "Правила спільноти", + "spoiler": "Почати створювати спойлер", "sponsorship": "Отримати роль за спонсорство", "warn": "Попередити користувача", "warnings": "Переглянути попередження користувача" diff --git a/modules/callbacks/sid.py b/modules/callbacks/sid.py new file mode 100644 index 0000000..434690e --- /dev/null +++ b/modules/callbacks/sid.py @@ -0,0 +1,10 @@ +from app import app +from pyrogram.types import CallbackQuery +from pyrogram.client import Client +from pyrogram import filters + +# Callback rule ================================================================================================================ +@app.on_callback_query(filters.regex("sid_[\s\S]*")) +async def callback_query_rule(app: Client, clb: CallbackQuery): + await clb.answer(url=f'https://t.me/{(await app.get_me()).username}?start={clb.data.split("_")[1]}') +# ============================================================================================================================== \ No newline at end of file diff --git a/modules/commands/cancel.py b/modules/commands/cancel.py index 4880b2d..f0755af 100644 --- a/modules/commands/cancel.py +++ b/modules/commands/cancel.py @@ -3,11 +3,12 @@ from pyrogram import filters from pyrogram.types import Message from pyrogram.client import Client from modules.utils import should_quote, logWrite, locale -from modules.database import col_tmp +from modules.database import col_tmp, col_spoilers from modules import custom_filters @app.on_message((custom_filters.enabled_applications | custom_filters.enabled_sponsorships) & ~filters.scheduled & filters.command("cancel", prefixes=["/"])) async def command_cancel(app: Client, msg: Message): col_tmp.delete_many( {"user": msg.from_user.id} ) + col_spoilers.delete_many( {"user": msg.from_user.id, "completed": False} ) await msg.reply_text(locale("cancel", "message", locale=msg.from_user), quote=should_quote(msg)) logWrite(f"Cancelling all ongoing tmp operations for {msg.from_user.id}") \ No newline at end of file diff --git a/modules/commands/spoiler.py b/modules/commands/spoiler.py new file mode 100644 index 0000000..120ea40 --- /dev/null +++ b/modules/commands/spoiler.py @@ -0,0 +1,39 @@ +from app import app +from pyrogram import filters +from pyrogram.types import Message, ForceReply +from pyrogram.client import Client +from classes.holo_user import HoloUser, UserInvalidError, UserNotFoundError +from modules.utils import locale +from modules.database import col_spoilers +from modules import custom_filters + +# Spoiler command ============================================================================================================== +@app.on_message(custom_filters.member & ~filters.scheduled & filters.private & filters.command(["spoiler"], prefixes=["/"])) +async def cmd_spoiler(app: Client, msg: Message): + + try: + holo_user = HoloUser(msg.from_user) + except (UserInvalidError, UserNotFoundError): + return + + if holo_user.application_state()[0] != "fill" and holo_user.sponsorship_state()[0] != "fill": + + if col_spoilers.find_one( {"user": msg.from_user.id, "completed": False} ) is None: + + col_spoilers.insert_one( + { + "user": msg.from_user.id, + "completed": False, + "description": None, + "photo": None, + "video": None, + "animation": None, + "text": None + } + ) + + await msg.reply_text(locale("spoiler_started", "message", locale=msg.from_user), reply_markup=ForceReply(placeholder=locale("spoiler_content", "force_reply", locale=msg.from_user))) + + else: + await msg.reply_text(locale("spoiler_unfinished", "message", locale=msg.from_user)) +# ============================================================================================================================== \ No newline at end of file diff --git a/modules/commands/start.py b/modules/commands/start.py index e51ac02..66cb464 100644 --- a/modules/commands/start.py +++ b/modules/commands/start.py @@ -3,8 +3,10 @@ from pyrogram import filters from pyrogram.types import ReplyKeyboardMarkup, Message from pyrogram.client import Client from modules.utils import locale, logWrite -from modules.database import col_users +from modules.database import col_users, col_spoilers from modules import custom_filters +from bson.objectid import ObjectId +from bson.errors import InvalidId # Start command ================================================================================================================ @app.on_message(custom_filters.enabled_applications & ~filters.scheduled & filters.private & filters.command(["start"], prefixes=["/"])) @@ -26,4 +28,18 @@ async def cmd_start(app: Client, msg: Message): logWrite(f"User {msg.from_user.id} started bot interaction") await msg.reply_text(locale("start", "message", locale=msg.from_user), reply_markup=ReplyKeyboardMarkup(locale("welcome", "keyboard", locale=msg.from_user), resize_keyboard=True)) + + if len(msg.command) > 1: + try: + spoiler = col_spoilers.find_one( {"_id": ObjectId(msg.command[1])} ) + if spoiler["photo"] is not None: + await msg.reply_photo(spoiler["photo"]) + if spoiler["video"] is not None: + await msg.reply_video(spoiler["video"]) + if spoiler["animation"] is not None: + await msg.reply_animation(spoiler["animation"]) + if spoiler["text"] is not None: + await msg.reply_text(spoiler["text"]) + except InvalidId: + await msg.reply_text(f"Got an invalid ID {msg.command[1]}") # ============================================================================================================================== \ No newline at end of file diff --git a/modules/custom_filters.py b/modules/custom_filters.py index dc54ef4..59baff2 100644 --- a/modules/custom_filters.py +++ b/modules/custom_filters.py @@ -1,8 +1,9 @@ """Custom message filters made to improve commands usage in context of Holo Users.""" +from os import path from app import isAnAdmin -from modules.utils import configGet +from modules.utils import configGet, jsonLoad from modules.database import col_applications from pyrogram import filters from pyrogram.types import Message @@ -10,6 +11,9 @@ from pyrogram.types import Message async def admin_func(_, __, msg: Message): return await isAnAdmin(msg.from_user.id) +async def member_func(_, __, msg: Message): + return True if (msg.from_user.id in jsonLoad(path.join(configGet("cache", "locations"), "group_members"))) else False + async def allowed_func(_, __, msg: Message): return True if (col_applications.find_one({"user": msg.from_user.id}) is not None) else False @@ -32,6 +36,7 @@ async def enabled_dinovoice_func(_, __, msg: Message): return configGet("enabled", "features", "dinovoice") admin = filters.create(admin_func) +member = filters.create(member_func) allowed = filters.create(allowed_func) enabled_general = filters.create(enabled_general_func) diff --git a/modules/database.py b/modules/database.py index f68db38..f518dc1 100644 --- a/modules/database.py +++ b/modules/database.py @@ -28,13 +28,14 @@ db = db_client.get_database(name=db_config["name"]) collections = db.list_collection_names() -for collection in ["tmp", "users", "context", "messages", "warnings", "applications", "sponsorships"]: +for collection in ["tmp", "users", "context", "spoilers", "messages", "warnings", "applications", "sponsorships"]: if not collection in collections: db.create_collection(collection) col_tmp = db.get_collection("tmp") col_users = db.get_collection("users") col_context = db.get_collection("context") +col_spoilers = db.get_collection("spoilers") col_messages = db.get_collection("messages") col_warnings = db.get_collection("warnings") col_applications = db.get_collection("applications") diff --git a/modules/handlers/spoiler.py b/modules/handlers/spoiler.py new file mode 100644 index 0000000..4e4c9e0 --- /dev/null +++ b/modules/handlers/spoiler.py @@ -0,0 +1,86 @@ +from app import app +from pyrogram import filters +from pyrogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton, ForceReply, ReplyKeyboardMarkup, ReplyKeyboardRemove +from pyrogram.client import Client +from classes.holo_user import HoloUser, UserInvalidError, UserNotFoundError +from modules.utils import locale, all_locales +from modules.database import col_spoilers +from modules import custom_filters + +# Any other input ============================================================================================================== +@app.on_message(custom_filters.member & ~filters.scheduled & filters.private & filters.text) +async def handler_spoiler_text(app: Client, msg: Message): + + try: + holo_user = HoloUser(msg.from_user) + except (UserInvalidError, UserNotFoundError): + return + + if holo_user.application_state()[0] != "fill" and holo_user.sponsorship_state()[0] != "fill": + + spoiler = col_spoilers.find_one( {"user": msg.from_user.id, "completed": False} ) + + if spoiler is None: + return + + if spoiler["photo"] is None and spoiler["video"] is None and spoiler["animation"] is None and spoiler["text"] is None: + col_spoilers.find_one_and_update( {"user": msg.from_user.id, "completed": False}, {"$set": {"text": msg.text}} ) + await msg.reply_text(locale("spoiler_send_description", "message", locale=msg.from_user), reply_markup=ReplyKeyboardMarkup(locale("spoiler_description", "keyboard"), resize_keyboard=True, one_time_keyboard=True, placeholder=locale("spoiler_description", "force_reply", locale=msg.from_user))) + else: + for lc in all_locales("spoiler_description", "keyboard"): + if msg.text == lc[-1][0]: + await msg.reply_text(locale("spoiler_description_enter", "message", locale=msg.from_user), reply_markup=ForceReply(placeholder=locale("spoiler_description", "force_reply", locale=msg.from_user))) + return + if msg.text != "-": + col_spoilers.find_one_and_update( {"user": msg.from_user.id, "completed": False}, {"$set": {"description": msg.text, "completed": True}} ) + await msg.reply_text(locale("spoiler_using_description", "message", locale=msg.from_user).format(msg.text), reply_markup=ReplyKeyboardRemove()) + await msg.reply_text(locale("spoiler_ready", "message", locale=msg.from_user), quote=False, reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(locale("spoiler_send", "button", locale=msg.from_user), switch_inline_query=f"spoiler:{spoiler['_id'].__str__()}")]])) + +@app.on_message(custom_filters.member & ~filters.scheduled & filters.private & filters.photo) +async def handler_spoiler_photo(app: Client, msg: Message): + + try: + holo_user = HoloUser(msg.from_user) + except (UserInvalidError, UserNotFoundError): + return + + if holo_user.application_state()[0] != "fill" and holo_user.sponsorship_state()[0] != "fill": + + + if col_spoilers.find_one( {"user": msg.from_user.id, "completed": False} ) is None: + return + + col_spoilers.find_one_and_update( {"user": msg.from_user.id, "completed": False}, {"$set": {"photo": msg.photo.file_id}} ) + await msg.reply_text(locale("spoiler_send_description", "message", locale=msg.from_user), reply_markup=ReplyKeyboardMarkup(locale("spoiler_description", "keyboard"), resize_keyboard=True, one_time_keyboard=True, placeholder=locale("spoiler_description", "force_reply", locale=msg.from_user))) + +@app.on_message(custom_filters.member & ~filters.scheduled & filters.private & filters.video) +async def handler_spoiler_video(app: Client, msg: Message): + + try: + holo_user = HoloUser(msg.from_user) + except (UserInvalidError, UserNotFoundError): + return + + if holo_user.application_state()[0] != "fill" and holo_user.sponsorship_state()[0] != "fill": + + if col_spoilers.find_one( {"user": msg.from_user.id, "completed": False} ) is None: + return + + col_spoilers.find_one_and_update( {"user": msg.from_user.id, "completed": False}, {"$set": {"video": msg.video.file_id}} ) + await msg.reply_text(locale("spoiler_send_description", "message", locale=msg.from_user), reply_markup=ReplyKeyboardMarkup(locale("spoiler_description", "keyboard"), resize_keyboard=True, one_time_keyboard=True, placeholder=locale("spoiler_description", "force_reply", locale=msg.from_user))) + +@app.on_message(custom_filters.member & ~filters.scheduled & filters.private & filters.animation) +async def handler_spoiler_animation(app: Client, msg: Message): + + try: + holo_user = HoloUser(msg.from_user) + except (UserInvalidError, UserNotFoundError): + return + + if holo_user.application_state()[0] != "fill" and holo_user.sponsorship_state()[0] != "fill": + + if col_spoilers.find_one( {"user": msg.from_user.id, "completed": False} ) is None: + return + + col_spoilers.find_one_and_update( {"user": msg.from_user.id, "completed": False}, {"$set": {"animation": msg.animation.file_id}} ) + await msg.reply_text(locale("spoiler_send_description", "message", locale=msg.from_user), reply_markup=ReplyKeyboardMarkup(locale("spoiler_description", "keyboard"), resize_keyboard=True, one_time_keyboard=True, placeholder=locale("spoiler_description", "force_reply", locale=msg.from_user))) \ No newline at end of file diff --git a/modules/inline.py b/modules/inline.py index ac61ea9..45e25aa 100644 --- a/modules/inline.py +++ b/modules/inline.py @@ -4,18 +4,51 @@ all inline queries that bot receives""" from datetime import datetime from os import path, sep from app import app, isAnAdmin -from pyrogram.types import InlineQueryResultArticle, InputTextMessageContent, InlineQuery +from pyrogram.types import InlineQueryResultArticle, InputTextMessageContent, InlineQuery, InlineKeyboardMarkup, InlineKeyboardButton from pyrogram.client import Client from pyrogram.enums.chat_type import ChatType from pyrogram.enums.chat_members_filter import ChatMembersFilter from dateutil.relativedelta import relativedelta from classes.holo_user import HoloUser, UserInvalidError, UserNotFoundError from modules.utils import configGet, locale -from modules.database import col_applications +from modules.database import col_applications, col_spoilers +from bson.objectid import ObjectId +from bson.errors import InvalidId @app.on_inline_query() async def inline_answer(client: Client, inline_query: InlineQuery): + results = [] + + if inline_query.query.startswith("spoiler:"): + + try: + + spoil = col_spoilers.find_one( {"_id": ObjectId(inline_query.query.removeprefix("spoiler:"))} ) + + if spoil is not None: + + desc = locale("spoiler_empty", "message", locale=inline_query.from_user) if spoil["description"] is None else locale("spoiler_described", "message", locale=inline_query.from_user).format(spoil["description"]) + + results = [ + InlineQueryResultArticle( + title=locale("title", "inline", "spoiler", locale=inline_query.from_user), + description=locale("description", "inline", "spoiler", locale=inline_query.from_user), + input_message_content=InputTextMessageContent(desc, disable_web_page_preview=True), + reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(locale("spoiler_view", "button", locale=inline_query.from_user), callback_data=f'sid_{inline_query.query.removeprefix("spoiler:")}')]]) + ) + ] + + except InvalidId: + results = [] + + + await inline_query.answer( + results=results + ) + + return + if inline_query.chat_type in [ChatType.CHANNEL]: await inline_query.answer( results=[ @@ -54,8 +87,6 @@ async def inline_answer(client: Client, inline_query: InlineQuery): async for m in app.get_chat_members(configGet("admin", "users"), limit=max_results, filter=ChatMembersFilter.SEARCH, query=inline_query.query): list_of_users.append(m) - results = [] - for match in list_of_users: application = col_applications.find_one({"user": match.user.id})