From 3aab705032eca09f46be69c8c242ee37bf43de69 Mon Sep 17 00:00:00 2001 From: Macsim Date: Fri, 21 Nov 2025 13:57:41 +0300 Subject: [PATCH] Limoka update: improved query handling, added new features for global search and filtering, fixed bugs. Added new limoka404.png file as a fallback placeholder image. --- Limoka.py | 772 +++++++++++++++++++++++++++---------------- assets/limoka404.png | Bin 0 -> 30363 bytes 2 files changed, 478 insertions(+), 294 deletions(-) create mode 100644 assets/limoka404.png diff --git a/Limoka.py b/Limoka.py index e57414a..3fa3b90 100644 --- a/Limoka.py +++ b/Limoka.py @@ -12,22 +12,25 @@ import logging import os import html import json +import re from datetime import datetime import asyncio +from typing import Union, List, Dict, Any + from telethon.types import Message from telethon.errors.rpcerrorlist import WebpageMediaEmptyError try: from aiogram.utils.exceptions import BadRequest except ImportError: - from aiogram.exceptions import TelegramBadRequest as BadRequest # essential crutch for aiogram 3 in heroku 1.7.0 - + from aiogram.exceptions import TelegramBadRequest as BadRequest + from .. import utils, loader from ..types import InlineQuery, InlineCall logger = logging.getLogger("Limoka") -__version__ = (1, 1, 0) +__version__ = (1, 2, 0) class Search: @@ -40,7 +43,7 @@ class Search: self.query = query self.ix = ix - def search_module(self, content=None): + def search_module(self): with self.ix.searcher() as searcher: parser = QueryParser("content", self.ix.schema, group=OrGroup.factory(0.8)) query = parser.parse(self.query) @@ -51,7 +54,7 @@ class Search: results = searcher.search(search_query) if results: return list(set(result["path"] for result in results)) - return 0 + return [] class LimokaAPI: @@ -73,23 +76,14 @@ class Limoka(loader.Module): "for the query: {query}\n\n{fact}" ), "found": ( - "🔍 Found the module {name} " + "🔍 Found module {name} " "by query: {query}\n\n" "ℹ️ Description: {description}\n" "🧑‍💻 Developer: {username}\n\n" "{commands}\n" "🪄 {prefix}dlm {url}{module_path}" ), - "dotd": ( - "🌟 Module of the Day\n\n" - "🔍 {name}\n" - "ℹ️ Description: {description}\n" - "🧑‍💻 Developer: {username}\n\n" - "{commands}\n" - "🪄 {prefix}dlm {url}{module_path}\n\n" - "Updates daily at midnight!" - ), - "command_template": "{emoji} {prefix}{command} {description}\n", + "command_template": "{emoji} {prefix}{command} — {description}\n", "emojis": { 1: "1️⃣", 2: "2️⃣", @@ -116,62 +110,90 @@ class Limoka(loader.Module): "🔎 Your search history:\n" "{history}" ), - "filter_menu": "Choose filters for query: {query}", + "filter_menu": "Choose filters", "filter_cat": "📑 Filter by Category", "apply_filters": "✅ Apply Filters", "clear_filters": "🗑 Clear Filters", "back_to_results": "🔙 Back to Results", "empty_history": "🔎 Your search history is empty!", + "enter_query": "🔍 Enter new search query:", + "global_search": "🔍 Global search for {query} — found {count} modules", + "change_query": "🔍 Change query", + "no_modules": "No modules available.", + "filter_title": "🏷 Filters", + "category_title": "📂 Categories", + "selected_categories": "✅ Selected categories: {categories}", + "no_categories": "No categories found in the module database", + "select_category": "Select categories for query: {query}\n(You can select multiple)", + "back": "🔙 Back", + "category": "📁 {category}", + "no_category": "No category", + "global_button": "🌍 Results", + "filtered_button": "🏷️ Filtered search", } strings_ru = { + "name": "Limoka", "wait": ( - "Подождите" - "\n🔍 Идёт поиск среди {count} модулей по запросу: {query}" - "\n" - "\n{fact}" + "Подождите\n" + "🔍 Идёт поиск среди {count} модулей по запросу: {query}\n\n" + "{fact}" ), "found": ( - "🔍 Найден модуль {name} по запросу: {query}" - "\n" - "\nℹ️ Описание: {description}" - "\n🧑‍💻 Разработчик: {username}" - "\n" - "\n{commands}" - "\n" - "\n🪄 {prefix}dlm {url}{module_path}" - ), - "dotd": ( - "🌟 Модуль дня\n\n" - "🔍 {name}\n" + "🔍 Найден модуль {name} " + "по запросу: {query}\n\n" "ℹ️ Описание: {description}\n" - "🧑‍💻 Developer: {username}\n\n" + "🧑‍💻 Разработчик: {username}\n\n" "{commands}\n" - "🪄 {prefix}dlm {url}{module_path}\n\n" - "Обновляется ежедневно в полночь!" + "🪄 {prefix}dlm {url}{module_path}" ), - "command_template": "{emoji} {prefix}{command} {description}\n", + "command_template": "{emoji} {prefix}{command} — {description}\n", + "emojis": { + 1: "1️⃣", + 2: "2️⃣", + 3: "3️⃣", + 4: "4️⃣", + 5: "5️⃣", + 6: "6️⃣", + 7: "7️⃣", + 8: "8️⃣", + 9: "9️⃣", + }, "404": " Не найдено по запросу: {query}", "noargs": " Нет аргументов", "?": "🔎 Запрос слишком короткий / не найден", "no_info": "Нет информации", "facts": [ - "🛡 Каталог лимоки тщательно модерируется!", - "🚀 Производительность лимоки позволяет вам искать модули с невероятной скоростью", + "🛡 Каталог Limoka тщательно модерируется!", + "🚀 Limoka позволяет искать модули с невероятной скоростью!", ], "inline404": "Не найдено", "inline?": "Запрос слишком короткий / не найден", "inlinenoargs": "Введите запрос", "history": ( - "🔎 История вашего поиска:\n" + "🔎 История поиска:\n" "{history}" ), - "filter_menu": "Выберите фильтры для запроса: {query}", - "filter_cat": "📑 Фильтр по категории", + "filter_menu": "Выберите фильтры", + "filter_cat": "📑 Фильтр по категориям", "apply_filters": "✅ Применить фильтры", "clear_filters": "🗑 Очистить фильтры", "back_to_results": "🔙 Вернуться к результатам", - "empty_history": "🔎 Ваша история поиска пуста!", + "empty_history": "🔎 История поиска пуста!", + "enter_query": "🔍 Введите новый поисковый запрос:", + "global_search": "🔍 Глобальный поиск по {query} — найдено {count} модулей", + "change_query": "🔍 Изменить запрос", + "no_modules": "Модули недоступны.", + "filter_title": "🏷 Фильтры", + "category_title": "📂 Категории", + "selected_categories": "✅ Выбранные категории: {categories}", + "no_categories": "Категории не найдены в базе модулей", + "select_category": "Выберите категории для запроса: {query}\n(Можно выбрать несколько)", + "back": "🔙 Назад", + "category": "📁 {category}", + "no_category": "Без категории", + "global_button": "🌍 Результаты", + "filtered_button": "🏷️ Поиск с фильтрами", } def __init__(self): @@ -180,13 +202,13 @@ class Limoka(loader.Module): loader.ConfigValue( "limokaurl", "https://raw.githubusercontent.com/MuRuLOSE/limoka/refs/heads/main/", - lambda: "Mirror: https://raw.githubusercontent.com/MuRuLOSE/limoka-mirror/refs/heads/main/ (Dont work)", + lambda: "Зеркало (не работает): https://raw.githubusercontent.com/MuRuLOSE/limoka-mirror/refs/heads/main/", validator=loader.validators.String(), ) ) self.name = self.strings["name"] - self._daily_module = None - self._last_update = None + self._invalid_banners = set() + self.fallback_banner = "https://github.com/MuRuLOSE/limoka/raw/main/assets/limoka404.png" async def client_ready(self, client, db): self.client = client @@ -199,77 +221,51 @@ class Limoka(loader.Module): ) os.makedirs("limoka_search", exist_ok=True) - self.ix = ( - create_in("limoka_search", self.schema) - if not os.path.isdir("limoka_search/index") - else open_dir("limoka_search") - ) + if not os.path.exists("limoka_search/index"): + self.ix = create_in("limoka_search", self.schema) + else: + self.ix = open_dir("limoka_search") self._history = self.pointer("history", []) - self._daily_module_storage = self.pointer("daily_module", {"date": None, "path": None}) self.modules = await self.api.get_all_modules( f"{self.config['limokaurl']}modules.json" ) await self._update_index() - await self._check_daily_module() async def _update_index(self): writer = self.ix.writer() for module_path, module_data in self.modules.items(): - for content in [module_data["name"], module_data["description"]]: - writer.add_document( - title=module_data["name"], - path=module_path, - content=content - ) + writer.add_document( + title=module_data["name"], + path=module_path, + content=module_data["name"] + " " + (module_data["description"] or "") + ) for func in module_data["commands"]: for command, description in func.items(): writer.add_document( title=module_data["name"], path=module_path, - content=command - ) - writer.add_document( - title=module_data["name"], - path=module_path, - content=description + content=f"{command} {description}" ) writer.commit() async def _validate_url(self, url: str) -> str: - if not url: - return None + if not url or url in self._invalid_banners: + return self.fallback_banner try: async with aiohttp.ClientSession() as session: async with session.head(url, timeout=5) as response: if response.status != 200: - return None - content_type = response.headers.get("Content-Type", "") + self._invalid_banners.add(url) + return self.fallback_banner + content_type = response.headers.get("Content-Type", "").lower() if not content_type.startswith("image/"): - return None + self._invalid_banners.add(url) + return self.fallback_banner return url except (aiohttp.ClientError, asyncio.TimeoutError): - return None - - async def _check_daily_module(self): - """Проверяет и обновляет модуль дня если требуется""" - current_date = datetime.now().date() - stored_date = self._daily_module_storage.get("date") - - if not stored_date or datetime.strptime(stored_date, "%Y-%m-%d").date() != current_date: - all_paths = list(self.modules.keys()) - random_path = random.choice(all_paths) - self._daily_module = { - "path": random_path, - "info": self.modules[random_path] - } - self._daily_module_storage["date"] = current_date.strftime("%Y-%m-%d") - self._daily_module_storage["path"] = random_path - else: - self._daily_module = { - "path": self._daily_module_storage["path"], - "info": self.modules[self._daily_module_storage["path"]] - } + self._invalid_banners.add(url) + return self.fallback_banner def generate_commands(self, module_info): commands = [] @@ -279,17 +275,25 @@ class Limoka(loader.Module): break for command, description in func.items(): emoji = self.strings["emojis"].get(i, "") + desc = (description or self.strings["no_info"]).replace("\n", "\n\n")[:200] + if len(desc) > 197: + desc = desc[:197] + "…" commands.append( self.strings["command_template"].format( prefix=self.get_prefix(), command=html.escape(command.replace("cmd", "")), emoji=emoji, - description=html.escape(description or self.strings["no_info"]), + description=html.escape(desc), ) ) return commands async def _display_filter_menu(self, call: InlineCall, query: str, current_filters: dict): + categories = current_filters.get("category", []) + filters_text = self.strings["selected_categories"].format( + categories=', '.join(categories) if categories else self.strings["no_category"] + ) + markup = [ [ {"text": self.strings["filter_cat"], "callback": self._select_category, "args": (query, current_filters)}, @@ -303,36 +307,46 @@ class Limoka(loader.Module): ] ] - categories = current_filters.get("category", []) - filters_text = f"Categories: {', '.join(categories) if categories else 'None'}" - await call.edit( - self.strings["filter_menu"].format(query=query) + f"\n{filters_text}", - reply_markup=markup - ) + text = self.strings["filter_menu"].format(query=query) + f"\n\n{filters_text}" + await call.edit(text, reply_markup=markup) async def _select_category(self, call: InlineCall, query: str, current_filters: dict): all_categories = set() for module_data in self.modules.values(): - all_categories.update(module_data.get("category", [])) + all_categories.update(module_data.get("category", ["No category"])) categories = sorted(all_categories) if not categories: - await call.edit("No categories found in the module database!", reply_markup=[]) + await call.edit(self.strings["no_categories"], reply_markup=[ + [{"text": self.strings["back"], "callback": self._display_filter_menu, "args": (query, current_filters)}] + ]) return selected_categories = current_filters.get("category", []) - markup = [ - [{"text": f"{'✅ ' if cat in selected_categories else ''}{cat}", - "callback": self._toggle_category, - "args": (query, current_filters, cat)}] - for cat in categories - ] - markup.append([{"text": "🔙 Back", "callback": self._display_filter_menu, "args": (query, current_filters)}]) + buttons = [] + row = [] - await call.edit( - f"Select categories for query: {query}\n(You can select multiple)", - reply_markup=markup - ) + for i, cat in enumerate(categories): + button_text = (self.strings["category"].format(category=cat) if "category" in self.strings else f"📁 {cat}") + if cat in selected_categories: + button_text = "✅ " + button_text + + row.append({ + "text": button_text, + "callback": self._toggle_category, + "args": (query, current_filters, cat) + }) + + if (i + 1) % 3 == 0 or i == len(categories) - 1: + buttons.append(row) + row = [] + + buttons.append([ + {"text": self.strings["back"], "callback": self._display_filter_menu, "args": (query, current_filters)} + ]) + + text = self.strings["select_category"].format(query=query) + await call.edit(text, reply_markup=buttons) async def _toggle_category(self, call: InlineCall, query: str, current_filters: dict, category: str): new_filters = current_filters.copy() @@ -360,173 +374,149 @@ class Limoka(loader.Module): searcher = Search(query.lower(), self.ix) try: result = searcher.search_module() - except IndexError: + except Exception: await call.edit(self.strings["?"], reply_markup=[]) return - if not result or result == 0: - if from_filters: - markup = [[{"text": "🔙 Back", "callback": self._display_filter_menu, "args": (query, filters)}]] - await call.edit(self.strings["404"].format(query=query), reply_markup=markup) - else: - await call.edit(self.strings["404"].format(query=query), reply_markup=[]) + if not result: + markup = [[{"text": self.strings["back"], "callback": self._display_filter_menu, "args": (query, filters)}]] if from_filters else [] + await call.edit(self.strings["404"].format(query=query), reply_markup=markup) return if filters.get("category"): filtered_result = [ path for path in result - if any(cat in self.modules.get(path, {}).get("category", []) for cat in filters["category"]) + if any(cat in self.modules.get(path, {}).get("category", ["No category"]) for cat in filters["category"]) ] else: filtered_result = result if not filtered_result: - if from_filters: - markup = [[{"text": "🔙 Back", "callback": self._display_filter_menu, "args": (query, filters)}]] - await call.edit(self.strings["404"].format(query=query), reply_markup=markup) - else: - await call.edit(self.strings["404"].format(query=query), reply_markup=[]) + markup = [[{"text": self.strings["back"], "callback": self._display_filter_menu, "args": (query, filters)}]] if from_filters else [] + await call.edit(self.strings["404"].format(query=query), reply_markup=markup) return module_path = filtered_result[0] module_info = self.modules[module_path] await self._display_module(call, module_info, module_path, query, filtered_result, 0, filters) - @loader.command() - async def limokacmd(self, message: Message): - """[query] - Search module with filter options""" - args = utils.get_args_raw(message) - if len(self._history) == 10: - self._history.pop(0) - - if len(args) <= 1: - return await utils.answer(message, self.strings["?"]) - if not args: - return await utils.answer(message, self.strings["noargs"]) - - self._history.append(args) - - await utils.answer( - message, - self.strings["wait"].format( - count=len(self.modules), - fact=random.choice(self.strings["facts"]), - query=args, - ), - ) - - searcher = Search(args.lower(), self.ix) + async def _enter_query_handler(self, call: InlineCall, query: str, *args, **kwargs): + """Handler for inline query input""" + if len(query) <= 1: + await call.edit(self.strings["?"], reply_markup=[[{"text": "🔄 " + self.strings["change_query"], "callback": self._enter_query}]]) + return + searcher = Search(query.lower(), self.ix) try: result = searcher.search_module() - except IndexError: - return await utils.answer(message, self.strings["?"]) + except Exception: + await call.edit(self.strings["?"], reply_markup=[[{"text": "🔄 " + self.strings["change_query"], "callback": self._enter_query}]]) + return - if not result or result == 0: - return await utils.answer(message, self.strings["404"].format(query=args)) + if not result: + await call.edit( + self.strings["404"].format(query=query), + reply_markup=[[{"text": "🔄 " + self.strings["change_query"], "callback": self._enter_query}]] + ) + return module_path = result[0] module_info = self.modules[module_path] - await self._display_module(message, module_info, module_path, args, result, 0, {}) + await self._display_module(call, module_info, module_path, query, result, 0, {}) - @loader.command() - async def lshistorycmd(self, message: Message): - """ - Showing the last 10 requests""" - if not self._history: - await utils.answer(message, self.strings["empty_history"]) - return - - formatted_history = [f"{i+1}. {history}" for i, history in enumerate(self._history)] - await utils.answer( - message, - self.strings["history"].format( - history='\n'.join(formatted_history) - ) + async def _enter_query(self, call: InlineCall): + """Show input form for new query""" + markup = [ + [ + { + "text": "✍️ " + self.strings["enter_query"], + "input": self.strings["enter_query"], + "handler": self._enter_query_handler, + } + ], + [ + { + "text": self.strings["back_to_results"], + "callback": self._inline_void, + } + ] + ] + + await call.edit( + self.strings["enter_query"], + reply_markup=markup ) - @loader.command() - async def limokadotd(self, message: Message): - """- Show the Module of the Day""" - await self._check_daily_module() + async def _display_module( + self, + message_or_call: Union[Message, InlineCall], + module_info: Dict[str, Any], + module_path: str, + query: str, + result: List[Any], + index: int, + filters: Dict[str, List[str]] + ): + name = html.escape(module_info.get("name") or self.strings["no_info"]) + description = html.escape(module_info.get("description") or self.strings["no_info"]) + dev_username = html.escape(module_info["meta"].get("developer", "Unknown")) - if not self._daily_module: - await utils.answer(message, "Error loading module of the day!") - return - - module_info = self._daily_module["info"] - module_path = self._daily_module["path"] - - dev_username = module_info["meta"].get("developer", "Unknown") - name = module_info["name"] or self.strings["no_info"] - description = html.escape(module_info["description"] or self.strings["no_info"]) - commands = self.generate_commands(module_info) - banner = await self._validate_url(module_info["meta"].get("banner")) - - formatted_message = self.strings["dotd"].format( - name=name, - description=description, - url=self.config["limokaurl"], - username=dev_username, - commands="".join(commands), - prefix=self.get_prefix(), - module_path=module_path.replace("\\", "/"), - ) - - try: - await self.inline.form( - formatted_message, - message, - photo=banner or None - ) - except (BadRequest, WebpageMediaEmptyError) as e: - await self.inline.form( - formatted_message, - message, - photo=None - ) - - async def _display_module(self, message_or_call, module_info, module_path, query, result, index, filters): - dev_username = module_info["meta"].get("developer", "Unknown") - name = module_info["name"] or self.strings["no_info"] - description = html.escape(module_info["description"] or self.strings["no_info"]) - banner = await self._validate_url(module_info["meta"].get("banner")) + clean_module_path = module_path.replace("\\", "/") + banner_url = await self._validate_url(module_info["meta"].get("banner")) commands = self.generate_commands(module_info) page = index + 1 - clean_module_path = module_path.replace('\\', '/') - - formatted_message = self.strings["found"].format( - query=query, - name=name, - description=description, - url=self.config["limokaurl"], - username=dev_username, - commands="".join(commands), - prefix=self.get_prefix(), - module_path=clean_module_path, + categories = filters.get("category", []) + filters_text = self.strings["selected_categories"].format( + categories=', '.join(html.escape(c) for c in categories) if categories else self.strings["no_category"] ) - categories = filters.get("category", []) - filters_text = f"Categories: {', '.join(categories) if categories else 'None'}" + core_message = self.strings["found"].format( + query=html.escape(query), + name=name, + description=description, + url=html.escape(self.config["limokaurl"]), + username=dev_username, + commands="".join(commands), + prefix=html.escape(self.get_prefix()), + module_path=html.escape(clean_module_path), + ) - full_message = formatted_message + f"\n{filters_text}" - if len(full_message) > 1024: - download_command = f"🪄 {self.get_prefix()}dlm {self.config['limokaurl']}{clean_module_path}" - max_content_length = 1024 - len(f"\n{download_command}\n{filters_text}") - 50 - if max_content_length < 100: - max_content_length = 100 - - description = (description[:max_content_length//2] + html.escape("...")) if len(description) > max_content_length//2 else description - commands = commands[:3] if len(commands) > 3 else commands - formatted_message = ( - f"🔍 Found the module {name} " - f"by query: {query}\n\n" - f"ℹ️ Description: {description}\n" - f"🧑‍💻 Developer: {dev_username}\n\n" - f"{''.join(commands)}\n" - ).strip() - full_message = f"{formatted_message[:max_content_length]}{'...' if len(formatted_message) > max_content_length else ''}\n\n{download_command}\n{filters_text}" - else: - full_message = formatted_message + f"\n{filters_text}" + static_suffix = f"\n{filters_text}" + max_core_len = 1024 - len(static_suffix) + + if max_core_len < 50: + max_core_len = 50 + + if len(core_message) > max_core_len: + safe_query = html.escape(query[:30]) + ("..." if len(query) > 30 else "") + safe_name = name[:40] + ("..." if len(name) > 40 else "") + safe_dev = dev_username[:30] + ("..." if len(dev_username) > 30 else "") + + desc_max = max(50, (max_core_len - 250) // 2) + safe_desc = description[:desc_max] + ("…" if len(description) > desc_max else "") + + safe_commands = [] + for cmd in commands[:3]: + if len(cmd) > 150: + cmd = cmd[:147] + "…" + safe_commands.append(cmd) + + core_message = ( + f"🔍 " + f"Found module {safe_name} by query: {safe_query}\n\n" + f"ℹ️ Description: {safe_desc}\n" + f"🧑‍💻 Developer: {safe_dev}\n\n" + f"{''.join(safe_commands)}" + ) + + core_message = re.sub(r'\n\s*\n', '\n\n', core_message) + core_message = "\n".join(line.strip() for line in core_message.splitlines()) + core_message = core_message.rstrip("\n") + + if len(core_message) > max_core_len: + core_message = core_message[:max_core_len - 3] + "…" + + full_message = (core_message + static_suffix)[:1024] markup = [ [ @@ -543,42 +533,88 @@ class Limoka(loader.Module): }, ], [ - {"text": "🔍 Filters", "callback": self._display_filter_menu, "args": (query, filters)}, + {"text": "🔍 " + self.strings["filter_menu"].split(":")[0], "callback": self._display_filter_menu, "args": (query, filters)}, + {"text": "🔄 " + self.strings["change_query"], "callback": self._enter_query}, + ], + [ + {"text": self.strings["global_button"], "callback": self._show_global_results, "args": (query,)}, ] ] try: if isinstance(message_or_call, Message): await self.inline.form( - full_message, - message_or_call, + text=full_message, + message=message_or_call, reply_markup=markup, - photo=banner or None + photo=banner_url ) else: await message_or_call.edit( - full_message, + text=full_message, reply_markup=markup, - photo=banner or None + photo=banner_url ) - except (BadRequest, WebpageMediaEmptyError) as e: + except (BadRequest, WebpageMediaEmptyError): if isinstance(message_or_call, Message): await self.inline.form( - full_message, - message_or_call, + text=full_message, + message=message_or_call, reply_markup=markup, - photo=None + photo=self.fallback_banner ) else: await message_or_call.edit( - full_message, + text=full_message, reply_markup=markup, - photo=None + photo=self.fallback_banner ) + async def _show_global_results(self, call: InlineCall, query: str): + searcher = Search(query.lower(), self.ix) + try: + result = searcher.search_module() + except Exception: + await call.edit(self.strings["?"], reply_markup=[]) + return + + if not result: + await call.edit(self.strings["404"].format(query=query), reply_markup=[ + [{"text": "🔄 " + self.strings["change_query"], "callback": self._enter_query}] + ]) + return + + text = self.strings["global_search"].format( + query=html.escape(query), + count=len(result) + ) + buttons = [] + for i, path in enumerate(result[:15]): + info = self.modules.get(path) + if not info: + continue + name = info.get("name", "Unknown") + buttons.append([ + { + "text": f"{i+1}. {name}", + "callback": self._display_module_from_global, + "args": (path, query, result) + } + ]) + buttons.append([{"text": self.strings["change_query"], "callback": self._enter_query}]) + + await call.edit( + text=text, + reply_markup=buttons + ) + + async def _display_module_from_global(self, call: InlineCall, module_path: str, query: str, result: list): + module_info = self.modules[module_path] + await self._display_module(call, module_info, module_path, query, result, result.index(module_path), {}) + async def _next_page(self, call: InlineCall, result: list, index: int, query: str, filters: dict): if index + 1 >= len(result): - await call.answer("This is the last page!") + await call.answer("This is the last page!" if not hasattr(self, "strings_ru") else "Это последняя страница!") return index += 1 @@ -588,7 +624,7 @@ class Limoka(loader.Module): async def _previous_page(self, call: InlineCall, result: list, index: int, query: str, filters: dict): if index - 1 < 0: - await call.answer("This is the first page!") + await call.answer("This is the first page!" if not hasattr(self, "strings_ru") else "Это первая страница!") return index -= 1 @@ -599,60 +635,208 @@ class Limoka(loader.Module): async def _inline_void(self, call: InlineCall): await call.answer() + @loader.command(ru_doc="[запрос] — Поиск модулей (без аргументов для формы ввода)") + async def limokacmd(self, message: Message): + """[query] - Search modules (no args for input form)""" + args = utils.get_args_raw(message) + + if not args: + # No arguments - show input form + markup = [ + [ + { + "text": "✍️ " + self.strings["enter_query"], + "input": self.strings["enter_query"], + "handler": self._enter_query_handler, + } + ], + [ + { + "text": self.strings["global_button"], + "callback": self._show_global_form, + "args": (message,), + } + ] + ] + + await self.inline.form( + text="🔍 Limoka Search\n\n" + "Enter your query to search for Hikka modules:", + message=message, + reply_markup=markup, + photo=self.fallback_banner + ) + return + + # With arguments - perform search + if len(self._history) >= 10: + self._history = self._history[-9:] + self._history.append(args) + self.pointer("history", self._history) + + if len(args) <= 1: + return await utils.answer(message, self.strings["?"]) + + await utils.answer( + message, + self.strings["wait"].format( + count=len(self.modules), + fact=random.choice(self.strings["facts"]), + query=args, + ), + ) + + searcher = Search(args.lower(), self.ix) + try: + result = searcher.search_module() + except Exception: + return await utils.answer(message, self.strings["?"]) + + if not result: + return await utils.answer(message, self.strings["404"].format(query=args)) + + module_path = result[0] + module_info = self.modules[module_path] + await self._display_module(message, module_info, module_path, args, result, 0, {}) + + async def _show_global_form(self, call: InlineCall, message: Message): + markup = [ + [ + { + "text": "✍️ " + self.strings["enter_query"], + "input": self.strings["enter_query"], + "handler": self._global_search_handler, + "args": (message,), + } + ], + [ + { + "text": "🔙 Back", + "callback": self._inline_void, + } + ] + ] + + await call.edit( + "🔍 Global Search\n\n" + "Enter your query to search ALL modules without filters:", + reply_markup=markup + ) + + async def _global_search_handler(self, call: InlineCall, query: str, message: Message, *args, **kwargs): + if len(query) <= 1: + await call.edit(self.strings["?"], reply_markup=[[{"text": "🔄 Try again", "callback": lambda c: self._show_global_form(c, message)}]]) + return + + searcher = Search(query.lower(), self.ix) + try: + result = searcher.search_module() + except Exception: + await call.edit(self.strings["?"], reply_markup=[[{"text": "🔄 Try again", "callback": lambda c: self._show_global_form(c, message)}]]) + return + + if not result: + await call.edit( + self.strings["404"].format(query=query), + reply_markup=[[{"text": "🔄 Try again", "callback": lambda c: self._show_global_form(c, message)}]] + ) + return + + text = self.strings["global_search"].format( + query=html.escape(query), + count=len(result) + ) + buttons = [] + for i, path in enumerate(result[:15]): + info = self.modules.get(path) + if not info: + continue + name = info.get("name", "Unknown") + buttons.append([ + { + "text": f"{i+1}. {name}", + "callback": self._display_module_from_global, + "args": (path, query, result) + } + ]) + buttons.append([{"text": "🔄 " + self.strings["change_query"], "callback": lambda c: self._show_global_form(c, message)}]) + + await call.edit( + text=text, + reply_markup=buttons + ) + + @loader.command(ru_doc="— Показать историю поиска") + async def lshistorycmd(self, message: Message): + """ - Show search history""" + if not self._history: + await utils.answer(message, self.strings["empty_history"]) + return + + formatted_history = [f"{i+1}. {utils.escape_html(h)}" for i, h in enumerate(self._history[-10:])] + await utils.answer( + message, + self.strings["history"].format( + history='\n'.join(formatted_history) + ) + ) + @loader.inline_handler() async def limoka(self, query: InlineQuery): - """[query] - Inline search modules""" - if not query.args: + """[query] - Search modules inline""" + q = query.args or "" + if not q: return { - "title": "No query", - "description": self.strings["inlinenoargs"], + "title": "Limoka Search", + "description": "Enter module name or keyword to search", "thumb": "https://img.icons8.com/?size=100&id=NIWYFnJlcBfr&format=png&color=000000", - "message": self.strings["inlinenoargs"], + "message": "🔍 Limoka Inline Search\n\nEnter your query to search for Hikka modules:", } - searcher = Search(query.args.lower(), self.ix) + searcher = Search(q.lower(), self.ix) try: results = searcher.search_module() - except IndexError: + except Exception: return { - "title": "Something went wrong...", - "description": self.strings["inline?"], + "title": "Error", + "description": "Search error occurred", "thumb": "https://img.icons8.com/?size=100&id=rUSWMuGVdxJj&format=png&color=000000", - "message": self.strings["inline?"], + "message": " Search error\nTry again later", } if not results: return { - "title": "No results", - "description": self.strings["inline404"], + "title": "No results found", + "description": "No modules match your query", "thumb": "https://img.icons8.com/?size=100&id=olDsW0G3zz22&format=png&color=000000", - "message": self.strings["inline404"], + "message": " No results found\nTry different keywords", } inline_results = [] - for path in results: + for path in results[:10]: module_info = self.modules.get(path) - if module_info and module_info.get("commands"): - banner = await self._validate_url(module_info["meta"].get("banner")) - thumb = await self._validate_url( - module_info["meta"].get("pic", "https://img.icons8.com/?size=100&id=olDsW0G3zz22&format=png&color=000000") - ) - inline_results.append( - { - "title": utils.escape_html(module_info["name"]), - "description": utils.escape_html(module_info["description"]), - "thumb": thumb or "https://img.icons8.com/?size=100&id=olDsW0G3zz22&format=png&color=000000", - "photo": banner or "https://habrastorage.org/getpro/habr/upload_files/9c7/5fa/c54/9c75fac54ebb0beaf89abd7d86b4787c.jpg", - "message": self.strings["found"].format( - name=module_info["name"], - query=query.args, - url=self.config["limokaurl"], - description=module_info["description"], - username=module_info["meta"].get("developer", "Unknown"), - commands="".join(self.generate_commands(module_info)), - module_path=path.replace("\\", "/"), - prefix=self.get_prefix(), - ), - } - ) + if not module_info: + continue + banner = await self._validate_url(module_info["meta"].get("banner")) + thumb = await self._validate_url( + module_info["meta"].get("pic", "https://img.icons8.com/?size=100&id=olDsW0G3zz22&format=png&color=000000") + ) + inline_results.append( + { + "title": module_info["name"], + "description": module_info["description"] or "No description available", + "thumb": thumb or "https://img.icons8.com/?size=100&id=olDsW0G3zz22&format=png&color=000000", + "photo": banner, + "message": self.strings["found"].format( + name=html.escape(module_info["name"]), + query=html.escape(q), + url=html.escape(self.config["limokaurl"]), + description=html.escape(module_info["description"] or self.strings["no_info"]), + username=html.escape(module_info["meta"].get("developer", "Unknown")), + commands="".join(self.generate_commands(module_info)), + module_path=path.replace("\\", "/"), + prefix=html.escape(self.get_prefix()), + ), + } + ) return inline_results \ No newline at end of file diff --git a/assets/limoka404.png b/assets/limoka404.png new file mode 100644 index 0000000000000000000000000000000000000000..cd287f440b3422ab28b80701359e92778d7c7548 GIT binary patch literal 30363 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 R10OA6dEM?>?NzVG{{t?~LyZ6c literal 0 HcmV?d00001