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 0000000..cd287f4 Binary files /dev/null and b/assets/limoka404.png differ