lang and dev_username hotfix

This commit is contained in:
2026-02-06 12:02:48 +03:00
parent f68d97d09b
commit e076d4ab28

499
Limoka.py
View File

@@ -2,7 +2,7 @@
# requires: whoosh cryptography # requires: whoosh cryptography
import datetime from collections import Counter, defaultdict
from whoosh.index import create_in, open_dir from whoosh.index import create_in, open_dir
from whoosh.fields import Schema, TEXT, ID from whoosh.fields import Schema, TEXT, ID
from whoosh.qparser import QueryParser, OrGroup from whoosh.qparser import QueryParser, OrGroup
@@ -15,27 +15,41 @@ import html
import json import json
import re import re
import asyncio import asyncio
from typing import Union, List, Dict, Any, Optional from typing import Iterable, Union, List, Dict, Any, Optional
import hashlib import hashlib
from telethon.types import Message from telethon.types import Message
from telethon.errors.rpcerrorlist import WebpageMediaEmptyError from telethon.errors.rpcerrorlist import WebpageMediaEmptyError
from telethon import TelegramClient from telethon import TelegramClient
from telethon.errors.rpcerrorlist import YouBlockedUserError from telethon.errors.rpcerrorlist import YouBlockedUserError
from telethon import functions, types from telethon import functions
try: try:
from aiogram.utils.exceptions import BadRequest from aiogram.utils.exceptions import BadRequest
except ImportError: except ImportError:
from aiogram.exceptions import TelegramBadRequest as BadRequest from aiogram.exceptions import TelegramBadRequest as BadRequest
from .. import utils, loader from .. import utils, loader
from ..types import InlineCall from ..types import InlineCall
logger = logging.getLogger("Limoka") logger = logging.getLogger("Limoka")
__version__ = (1, 4, 0) __version__ = (1, 4, 1)
WEIGHTS = {
"inline.token_obtainment": 15,
"main": 10,
"inline": 7,
"translations": 5,
"security": 3,
}
DEFAULT_WEIGHT = 1
def _get_lang_value(data: Dict[str, Any], lang: str) -> str: def _get_lang_value(data: Dict[str, Any], lang: str) -> str:
if not isinstance(data, dict): if not isinstance(data, dict):
return str(data) if data else "" return str(data) if data else ""
return data.get(lang, data.get("default", data.get("en", ""))) return data.get(lang, data.get("default", data.get("en", "")))
class Search: class Search:
def __init__(self, query, ix): def __init__(self, query, ix):
self.schema = Schema( self.schema = Schema(
@@ -56,6 +70,7 @@ class Search:
return list(set(result["path"] for result in results)) return list(set(result["path"] for result in results))
return [] return []
class LimokaAPI: class LimokaAPI:
async def fetch_json(self, base_url, path): async def fetch_json(self, base_url, path):
url = f"{base_url}{path}" url = f"{base_url}{path}"
@@ -63,9 +78,11 @@ class LimokaAPI:
async with session.get(url) as response: async with session.get(url) as response:
return json.loads(await response.text()) return json.loads(await response.text())
@loader.tds @loader.tds
class Limoka(loader.Module): class Limoka(loader.Module):
"""Modules are now in one place with easy searching!""" """Modules are now in one place with easy searching!"""
strings = { strings = {
"name": "Limoka", "name": "Limoka",
"wait": ( "wait": (
@@ -81,9 +98,7 @@ class Limoka(loader.Module):
"<b><emoji document_id=5418299289141004396>🧑‍💻</emoji> Developer:</b> {username}\n\n" "<b><emoji document_id=5418299289141004396>🧑‍💻</emoji> Developer:</b> {username}\n\n"
"<b><emoji document_id=5418376169055602355>🏷</emoji> Tags:</b> {tags}\n\n" "<b><emoji document_id=5418376169055602355>🏷</emoji> Tags:</b> {tags}\n\n"
), ),
"found_body": ( "found_body": ("{commands}"),
"{commands}"
),
"found_footer": ( "found_footer": (
"\n<emoji document_id=5411143117711624172>🪄</emoji> <code>{prefix}dlm {url}{module_path}</code>" "\n<emoji document_id=5411143117711624172>🪄</emoji> <code>{prefix}dlm {url}{module_path}</code>"
), ),
@@ -168,8 +183,8 @@ class Limoka(loader.Module):
"hikkatrusted": "Hikka Trusted", "hikkatrusted": "Hikka Trusted",
"nonactive": "Non-Active Repository", "nonactive": "Non-Active Repository",
"nonlongermaintained": "No Longer Maintained Repository", "nonlongermaintained": "No Longer Maintained Repository",
"newbie": "Newbie" "newbie": "Newbie",
} },
} }
strings_ru = { strings_ru = {
"name": "Limoka", "name": "Limoka",
@@ -185,9 +200,7 @@ class Limoka(loader.Module):
"<b><emoji document_id=5418299289141004396>🧑‍💻</emoji> Разработчик:</b> {username}\n\n" "<b><emoji document_id=5418299289141004396>🧑‍💻</emoji> Разработчик:</b> {username}\n\n"
"<b><emoji document_id=5418376169055602355>🏷</emoji> Теги:</b> {tags}\n\n" "<b><emoji document_id=5418376169055602355>🏷</emoji> Теги:</b> {tags}\n\n"
), ),
"found_body": ( "found_body": ("{commands}"),
"{commands}"
),
"found_footer": ( "found_footer": (
"\n<emoji document_id=5411143117711624172>🪄</emoji> <code>{prefix}dlm {url}{module_path}</code>" "\n<emoji document_id=5411143117711624172>🪄</emoji> <code>{prefix}dlm {url}{module_path}</code>"
), ),
@@ -220,7 +233,7 @@ class Limoka(loader.Module):
( (
"<emoji document_id=5188311512791393083>🔎</emoji> Limoka имеет лучший поиск*!\n" "<emoji document_id=5188311512791393083>🔎</emoji> Limoka имеет лучший поиск*!\n"
"<i>* В сравнении с предыдущей версией Limoka</i>" "<i>* В сравнении с предыдущей версией Limoka</i>"
) ),
], ],
"inline404": "Не найдено", "inline404": "Не найдено",
"inline?": "Запрос слишком короткий / не найден", "inline?": "Запрос слишком короткий / не найден",
@@ -276,7 +289,7 @@ class Limoka(loader.Module):
"hikkatrusted": "Hikka Trusted", "hikkatrusted": "Hikka Trusted",
"nonactive": "Неактивный репозиторий", "nonactive": "Неактивный репозиторий",
"nonlongermaintained": "Неподдерживаемый репозиторий", "nonlongermaintained": "Неподдерживаемый репозиторий",
"newbie": "Новичок" "newbie": "Новичок",
}, },
"_cls_doc": "Модули теперь в одном месте с простым и удобным поиском!", "_cls_doc": "Модули теперь в одном месте с простым и удобным поиском!",
} }
@@ -307,22 +320,22 @@ class Limoka(loader.Module):
self._invalid_banners = set() self._invalid_banners = set()
self._bot_username = "limoka_bbot" self._bot_username = "limoka_bbot"
self._base_url = self.config["limokaurl"] self._base_url = self.config["limokaurl"]
# Search session states # Search session states
self.SEARCH_STATES = { self.SEARCH_STATES = {
"no_banner": "no_banner", # 404 - Нет баннера "no_banner": "no_banner", # 404 - Нет баннера
"global_search": "global_search", # Глобальный поиск "global_search": "global_search", # Глобальный поиск
"not_found": "not_found", # Не найдено (модуль) "not_found": "not_found", # Не найдено (модуль)
"filter_select": "filter_select", # Выбор категорий (фильтров) "filter_select": "filter_select", # Выбор категорий (фильтров)
} }
# State banners - placeholders for now # State banners - placeholders for now
self.state_banners = { self.state_banners = {
"no_banner": "https://raw.githubusercontent.com/MuRuLOSE/hikka-assets/refs/heads/main/Limoka%20-%20No%20banner.png", "no_banner": "https://raw.githubusercontent.com/MuRuLOSE/hikka-assets/refs/heads/main/Limoka%20-%20No%20banner.png",
"global_search": "https://raw.githubusercontent.com/MuRuLOSE/hikka-assets/main/Limoka%20-%20Global%20Search.png", "global_search": "https://raw.githubusercontent.com/MuRuLOSE/hikka-assets/main/Limoka%20-%20Global%20Search.png",
"not_found": "https://raw.githubusercontent.com/MuRuLOSE/hikka-assets/main/Limoka%20-%20Not%20Found.png", "not_found": "https://raw.githubusercontent.com/MuRuLOSE/hikka-assets/main/Limoka%20-%20Not%20Found.png",
"filter_select": "https://raw.githubusercontent.com/MuRuLOSE/hikka-assets/main/Limoka%20-%20Categories.png", "filter_select": "https://raw.githubusercontent.com/MuRuLOSE/hikka-assets/main/Limoka%20-%20Categories.png",
} }
def _filter_newbies(self, modules: Dict[str, Any]) -> Dict[str, Any]: def _filter_newbies(self, modules: Dict[str, Any]) -> Dict[str, Any]:
"""Filter out modules which belong to repositories tagged as 'newbie'. """Filter out modules which belong to repositories tagged as 'newbie'.
@@ -357,7 +370,7 @@ class Limoka(loader.Module):
current_index: int = 0, current_index: int = 0,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Create a search session dictionary to track state across callbacks. """Create a search session dictionary to track state across callbacks.
Args: Args:
state: Current search state (one of SEARCH_STATES values) state: Current search state (one of SEARCH_STATES values)
query: Current search query query: Current search query
@@ -365,7 +378,7 @@ class Limoka(loader.Module):
results: Search results list results: Search results list
current_index: Index of current result being displayed current_index: Index of current result being displayed
banner_url: Banner image URL for current state banner_url: Banner image URL for current state
Returns: Returns:
Dictionary containing the complete session state Dictionary containing the complete session state
""" """
@@ -393,16 +406,13 @@ class Limoka(loader.Module):
else: else:
self.ix = open_dir("limoka_search") self.ix = open_dir("limoka_search")
self._history = self.pointer("history", []) self._history = self.pointer("history", [])
self.modules = (await self.api.fetch_json( self.modules = (await self.api.fetch_json(self._base_url, "modules.json")).get(
self._base_url, "modules.json" "modules", {}
)).get("modules", {}) )
raw = (await self.api.fetch_json( raw = (await self.api.fetch_json(self._base_url, "repositories.json")).get(
self._base_url, "repositories.json" "repositories", []
)).get("repositories", []) )
self.repositories = { self.repositories = {repo["url"]: repo for repo in raw}
repo["url"]: repo
for repo in raw
}
# Apply newbie filter if enabled # Apply newbie filter if enabled
try: try:
self.modules = self._filter_newbies(self.modules) self.modules = self._filter_newbies(self.modules)
@@ -414,24 +424,28 @@ class Limoka(loader.Module):
try: try:
message = await self.client.get_messages(self._bot_username, limit=1) message = await self.client.get_messages(self._bot_username, limit=1)
if not message: if not message:
message = await self.client.send_message(self._bot_username, "/start") message = await self.client.send_message(
self._bot_username, "/start"
)
await message.delete() await message.delete()
await self.client(functions.messages.DeleteHistoryRequest( await self.client(
peer=self._bot_username, functions.messages.DeleteHistoryRequest(
max_id=0, peer=self._bot_username,
just_clear=True, max_id=0,
revoke=True, just_clear=True,
)) revoke=True,
)
)
except YouBlockedUserError: except YouBlockedUserError:
logger.warning(f"Please unblock {self._bot_username} to enable external installation feature. Or disable external_install_allowed in Limoka settings to get rid of this message.") logger.warning(
f"Please unblock {self._bot_username} to enable external installation feature. Or disable external_install_allowed in Limoka settings to get rid of this message."
)
self._userbot_bot_username = (await self.inline.bot.get_me()).username self._userbot_bot_username = (await self.inline.bot.get_me()).username
@loader.loop(interval=3600) @loader.loop(interval=3600)
async def _update_modules_loop(self): async def _update_modules_loop(self):
self.modules = await self.api.fetch_json( self.modules = await self.api.fetch_json(self._base_url, "modules.json")
self._base_url, "modules.json"
)
# Re-apply newbie filter after modules refresh # Re-apply newbie filter after modules refresh
try: try:
self.modules = self._filter_newbies(self.modules) self.modules = self._filter_newbies(self.modules)
@@ -446,7 +460,18 @@ class Limoka(loader.Module):
writer.add_document( writer.add_document(
title=module_data["name"], title=module_data["name"],
path=module_path, path=module_path,
content=module_data["name"] + " " + (module_data.get("description") or "" + " " + ((module_data.get("meta").get("developer") or "") if module_data.get("meta") else "")), content=module_data["name"]
+ " "
+ (
module_data.get("description")
or ""
+ " "
+ (
(module_data.get("meta").get("developer") or "")
if module_data.get("meta")
else ""
)
),
) )
for func in module_data.get("commands", []): for func in module_data.get("commands", []):
for command, description in func.items(): for command, description in func.items():
@@ -462,7 +487,9 @@ class Limoka(loader.Module):
return None return None
try: try:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.head(url, timeout=5, allow_redirects=True) as response: async with session.head(
url, timeout=5, allow_redirects=True
) as response:
if response.status != 200: if response.status != 200:
self._invalid_banners.add(url) self._invalid_banners.add(url)
return None return None
@@ -475,9 +502,43 @@ class Limoka(loader.Module):
if url: if url:
self._invalid_banners.add(url) self._invalid_banners.add(url)
return None return None
def find_userbot(self, keys: Iterable[str]) -> str | None:
scores = defaultdict(int)
for key in keys:
parts = key.split(".")
# проходим все префиксы
for i in range(1, len(parts)):
prefix = ".".join(parts[:i])
suffix = ".".join(parts[i:])
weight = WEIGHTS.get(suffix, DEFAULT_WEIGHT)
scores[prefix] += weight
if not scores:
return None
return max(scores, key=scores.get)
@property
def user_lang(self) -> str: def user_lang(self) -> str:
self.db.get("heroku.translations", "lang")
userbot = self.find_userbot(self.db.keys())
if not userbot:
logger.warning(
"Cannot determine userbot type. "
"Probably not FTG-like Userbot? "
"Defaulting language to English. "
"If this is unexpected, please report to the module developer."
)
return "en"
return self.db.get(f"{userbot}.translations", "lang")
def generate_commands(self, module_info, lang: str = "en"): def generate_commands(self, module_info, lang: str = "en"):
commands = [] commands = []
@@ -507,7 +568,6 @@ class Limoka(loader.Module):
) )
return commands return commands
def _format_module_content( def _format_module_content(
self, self,
module_info: Dict[str, Any], module_info: Dict[str, Any],
@@ -520,12 +580,14 @@ class Limoka(loader.Module):
name = html.escape(module_info.get("name") or self.strings["no_info"]) name = html.escape(module_info.get("name") or self.strings["no_info"])
cls_doc = module_info.get("cls_doc", {}) cls_doc = module_info.get("cls_doc", {})
description = html.escape( description = html.escape(
_get_lang_value(cls_doc, lang) or _get_lang_value(cls_doc, lang)
_get_lang_value(module_info.get("description", ""), lang) or or _get_lang_value(module_info.get("description", ""), lang)
self.strings["no_info"] or self.strings["no_info"]
)
dev_username = html.escape(module_info["meta"].get("developer") or "Unknown")
raw_path = (
module_path if module_path is not None else module_info.get("path", "")
) )
dev_username = html.escape(module_info["meta"].get("developer", "Unknown"))
raw_path = module_path if module_path is not None else module_info.get("path", "")
clean_module_path = (raw_path or "").replace("\\", "/") clean_module_path = (raw_path or "").replace("\\", "/")
commands = self.generate_commands(module_info, lang) commands = self.generate_commands(module_info, lang)
categories_text = "" categories_text = ""
@@ -537,7 +599,9 @@ class Limoka(loader.Module):
) )
if len(description) > 300: if len(description) > 300:
description = description[:297] + "" description = description[:297] + ""
repo_key = "/".join(module_path.split("/")[:2]) if "/" in module_path else module_path repo_key = (
"/".join(module_path.split("/")[:2]) if "/" in module_path else module_path
)
tags_list = [] tags_list = []
for x in self.repositories: for x in self.repositories:
if x.replace("https://github.com/", "") == repo_key: if x.replace("https://github.com/", "") == repo_key:
@@ -577,14 +641,12 @@ class Limoka(loader.Module):
) )
return header, body_pages, footer, categories_text return header, body_pages, footer, categories_text
def _build_navigation_markup( def _build_navigation_markup(self, session: Dict[str, Any]) -> list:
self, session: Dict[str, Any]
) -> list:
result = session["results"] result = session["results"]
index = session["current_index"] index = session["current_index"]
query = session["query"] query = session["query"]
filters = session["filters"] filters = session["filters"]
page = index + 1 page = index + 1
markup = [ markup = [
[ [
@@ -596,7 +658,11 @@ class Limoka(loader.Module):
{"text": f"{page}/{len(result)}", "callback": self._inline_void}, {"text": f"{page}/{len(result)}", "callback": self._inline_void},
{ {
"text": "" if index + 1 < len(result) else "🚫", "text": "" if index + 1 < len(result) else "🚫",
"callback": self._next_page if index + 1 < len(result) else self._inline_void, "callback": (
self._next_page
if index + 1 < len(result)
else self._inline_void
),
"args": (session,) if index + 1 < len(result) else (), "args": (session,) if index + 1 < len(result) else (),
}, },
], ],
@@ -625,60 +691,93 @@ class Limoka(loader.Module):
return markup return markup
def _build_module_markup( def _build_module_markup(
self, session: Dict[str, Any], body_pages: List[str], page_body: int, module_path: str self,
session: Dict[str, Any],
body_pages: List[str],
page_body: int,
module_path: str,
) -> list: ) -> list:
result = session["results"] result = session["results"]
index = session["current_index"] index = session["current_index"]
query = session["query"] query = session["query"]
filters = session["filters"] filters = session["filters"]
markup = [] markup = []
if len(body_pages) > 1: if len(body_pages) > 1:
markup.append([ markup.append(
{ [
"text": "◀️" if page_body > 0 else "🚫", {
"callback": self._previous_body_page if page_body > 0 else self._inline_void, "text": "◀️" if page_body > 0 else "🚫",
"args": (session, module_path, page_body) if page_body > 0 else (), "callback": (
}, self._previous_body_page
{"text": f"Body {page_body + 1}/{len(body_pages)}", "callback": self._inline_void}, if page_body > 0
{ else self._inline_void
"text": "▶️" if page_body + 1 < len(body_pages) else "🚫", ),
"callback": self._next_body_page if page_body + 1 < len(body_pages) else self._inline_void, "args": (
"args": (session, module_path, page_body) if page_body + 1 < len(body_pages) else (), (session, module_path, page_body) if page_body > 0 else ()
}, ),
]) },
{
"text": f"Body {page_body + 1}/{len(body_pages)}",
"callback": self._inline_void,
},
{
"text": "▶️" if page_body + 1 < len(body_pages) else "🚫",
"callback": (
self._next_body_page
if page_body + 1 < len(body_pages)
else self._inline_void
),
"args": (
(session, module_path, page_body)
if page_body + 1 < len(body_pages)
else ()
),
},
]
)
page = index + 1 page = index + 1
markup.append([ markup.append(
{ [
"text": "" if index > 0 else "🚫", {
"callback": self._previous_page if index > 0 else self._inline_void, "text": "" if index > 0 else "🚫",
"args": (session,) if index > 0 else (), "callback": self._previous_page if index > 0 else self._inline_void,
}, "args": (session,) if index > 0 else (),
{"text": f"{page}/{len(result)}", "callback": self._inline_void}, },
{ {"text": f"{page}/{len(result)}", "callback": self._inline_void},
"text": "" if index + 1 < len(result) else "🚫", {
"callback": self._next_page if index + 1 < len(result) else self._inline_void, "text": "" if index + 1 < len(result) else "🚫",
"args": (session,) if index + 1 < len(result) else (), "callback": (
}, self._next_page
]) if index + 1 < len(result)
markup.append([ else self._inline_void
{ ),
"text": "🔍 " + self.strings["filter_menu"].split(":")[0], "args": (session,) if index + 1 < len(result) else (),
"callback": self._display_filter_menu, },
"args": (session,), ]
}, )
{ markup.append(
"text": "🔄 " + self.strings["change_query"], [
"callback": self._enter_query, {
}, "text": "🔍 " + self.strings["filter_menu"].split(":")[0],
]) "callback": self._display_filter_menu,
markup.append([ "args": (session,),
{ },
"text": self.strings["global_button"], {
"callback": self._show_global_results, "text": "🔄 " + self.strings["change_query"],
"args": (session,), "callback": self._enter_query,
}, },
]) ]
)
markup.append(
[
{
"text": self.strings["global_button"],
"callback": self._show_global_results,
"args": (session,),
},
]
)
markup.append( markup.append(
[{"text": self.strings.get("close", "❌ Close"), "action": "close"}] [{"text": self.strings.get("close", "❌ Close"), "action": "close"}]
) )
@@ -709,7 +808,9 @@ class Limoka(loader.Module):
) )
else: else:
if photo is not None: if photo is not None:
await message_or_call.edit(text=text, reply_markup=markup, photo=photo) await message_or_call.edit(
text=text, reply_markup=markup, photo=photo
)
else: else:
await message_or_call.edit(text=text, reply_markup=markup) await message_or_call.edit(text=text, reply_markup=markup)
except (BadRequest, WebpageMediaEmptyError) as e: except (BadRequest, WebpageMediaEmptyError) as e:
@@ -730,8 +831,11 @@ class Limoka(loader.Module):
try: try:
query = session["query"] query = session["query"]
filters = session["filters"] filters = session["filters"]
lang = self.user_lang() lang = self.user_lang
logger.info(
f"Displaying module: {module_path} for query: {query} with filters: {filters} in language: {lang}"
)
module_banner_raw = module_info.get("meta", {}).get("banner") module_banner_raw = module_info.get("meta", {}).get("banner")
photo = await self._validate_url(module_banner_raw) photo = await self._validate_url(module_banner_raw)
@@ -750,11 +854,11 @@ class Limoka(loader.Module):
current_body = body_pages[min(page_body, len(body_pages) - 1)] current_body = body_pages[min(page_body, len(body_pages) - 1)]
full_message = header + current_body + footer + categories_text full_message = header + current_body + footer + categories_text
markup = self._build_module_markup(session, body_pages, page_body, module_path) markup = self._build_module_markup(
session, body_pages, page_body, module_path
await self._safe_display(
message_or_call, full_message, markup, photo
) )
await self._safe_display(message_or_call, full_message, markup, photo)
except Exception as e: except Exception as e:
logger.exception(f"Error in _display_module: {e}") logger.exception(f"Error in _display_module: {e}")
if isinstance(message_or_call, Message): if isinstance(message_or_call, Message):
@@ -763,30 +867,45 @@ class Limoka(loader.Module):
await message_or_call.edit(self.strings["error_occurred"]) await message_or_call.edit(self.strings["error_occurred"])
async def _previous_body_page( async def _previous_body_page(
self, call: InlineCall, session: Dict[str, Any], module_path: str, page_body: int self,
call: InlineCall,
session: Dict[str, Any],
module_path: str,
page_body: int,
): ):
module_info = self.modules[module_path] module_info = self.modules[module_path]
new_page_body = max(page_body - 1, 0) new_page_body = max(page_body - 1, 0)
await self._display_module(call, module_info, module_path, session, page_body=new_page_body) await self._display_module(
call, module_info, module_path, session, page_body=new_page_body
)
async def _next_body_page( async def _next_body_page(
self, call: InlineCall, session: Dict[str, Any], module_path: str, page_body: int self,
call: InlineCall,
session: Dict[str, Any],
module_path: str,
page_body: int,
): ):
module_info = self.modules[module_path] module_info = self.modules[module_path]
query = session["query"] query = session["query"]
filters = session["filters"] filters = session["filters"]
header, body_pages, footer, categories_text = self._format_module_content( header, body_pages, footer, categories_text = self._format_module_content(
module_info, query, filters, include_categories=True, module_path=module_path, lang=self.user_lang() module_info,
query,
filters,
include_categories=True,
module_path=module_path,
lang=self.user_lang,
) )
new_page_body = min(page_body + 1, len(body_pages) - 1) new_page_body = min(page_body + 1, len(body_pages) - 1)
await self._display_module(call, module_info, module_path, session, page_body=new_page_body) await self._display_module(
call, module_info, module_path, session, page_body=new_page_body
)
async def _display_filter_menu( async def _display_filter_menu(self, call: InlineCall, session: Dict[str, Any]):
self, call: InlineCall, session: Dict[str, Any]
):
query = session["query"] query = session["query"]
current_filters = session["filters"] current_filters = session["filters"]
categories = current_filters.get("category", []) categories = current_filters.get("category", [])
filters_text = self.strings["selected_categories"].format( filters_text = self.strings["selected_categories"].format(
categories=( categories=(
@@ -823,14 +942,14 @@ class Limoka(loader.Module):
[{"text": self.strings.get("close", "❌ Close"), "action": "close"}], [{"text": self.strings.get("close", "❌ Close"), "action": "close"}],
] ]
text = self.strings["filter_menu"].format(query=query) + f"\n{filters_text}" text = self.strings["filter_menu"].format(query=query) + f"\n{filters_text}"
await call.edit(text, reply_markup=markup, photo=self._get_banner_for_state("filter_select")) await call.edit(
text, reply_markup=markup, photo=self._get_banner_for_state("filter_select")
)
async def _select_category( async def _select_category(self, call: InlineCall, session: Dict[str, Any]):
self, call: InlineCall, session: Dict[str, Any]
):
query = session["query"] query = session["query"]
current_filters = session["filters"] current_filters = session["filters"]
all_categories = set() all_categories = set()
for module_data in self.modules.values(): for module_data in self.modules.values():
all_categories.update(module_data.get("category", ["No category"])) all_categories.update(module_data.get("category", ["No category"]))
@@ -860,7 +979,7 @@ class Limoka(loader.Module):
) )
if cat in selected_categories: if cat in selected_categories:
button_text = "" + button_text button_text = "" + button_text
# Create new session with updated filters # Create new session with updated filters
new_session = session.copy() new_session = session.copy()
row.append( row.append(
@@ -893,7 +1012,7 @@ class Limoka(loader.Module):
): ):
query = session["query"] query = session["query"]
current_filters = session["filters"] current_filters = session["filters"]
new_filters = current_filters.copy() new_filters = current_filters.copy()
selected_categories = new_filters.get("category", []) selected_categories = new_filters.get("category", [])
if category in selected_categories: if category in selected_categories:
@@ -921,7 +1040,7 @@ class Limoka(loader.Module):
): ):
query = session["query"] query = session["query"]
filters = session["filters"] filters = session["filters"]
searcher = Search(query.lower(), self.ix) searcher = Search(query.lower(), self.ix)
try: try:
result = searcher.search_module() result = searcher.search_module()
@@ -983,7 +1102,7 @@ class Limoka(loader.Module):
return return
module_path = filtered_result[0] module_path = filtered_result[0]
module_info = self.modules[module_path] module_info = self.modules[module_path]
# Create session for displaying module # Create session for displaying module
display_session = self._create_search_session( display_session = self._create_search_session(
state=self.SEARCH_STATES["global_search"], state=self.SEARCH_STATES["global_search"],
@@ -992,9 +1111,7 @@ class Limoka(loader.Module):
results=filtered_result, results=filtered_result,
current_index=0, current_index=0,
) )
await self._display_module( await self._display_module(call, module_info, module_path, display_session, 0)
call, module_info, module_path, display_session, 0
)
async def _enter_query_handler( async def _enter_query_handler(
self, call_or_query, query: Optional[str] = None, *args, **kwargs self, call_or_query, query: Optional[str] = None, *args, **kwargs
@@ -1074,7 +1191,7 @@ class Limoka(loader.Module):
return return
module_path = result[0] module_path = result[0]
module_info = self.modules[module_path] module_info = self.modules[module_path]
# Create session for displaying module # Create session for displaying module
display_session = self._create_search_session( display_session = self._create_search_session(
state=self.SEARCH_STATES["global_search"], state=self.SEARCH_STATES["global_search"],
@@ -1098,11 +1215,13 @@ class Limoka(loader.Module):
{ {
"text": self.strings["back_to_results"], "text": self.strings["back_to_results"],
"callback": self._show_results, "callback": self._show_results,
"args": (self._create_search_session( "args": (
state=self.SEARCH_STATES["global_search"], self._create_search_session(
query=query or "", state=self.SEARCH_STATES["global_search"],
filters={}, query=query or "",
),), filters={},
),
),
} }
], ],
[ [
@@ -1116,7 +1235,7 @@ class Limoka(loader.Module):
async def _show_global_results(self, call: InlineCall, session: Dict[str, Any]): async def _show_global_results(self, call: InlineCall, session: Dict[str, Any]):
query = session["query"] query = session["query"]
searcher = Search(query.lower(), self.ix) searcher = Search(query.lower(), self.ix)
try: try:
result = searcher.search_module() result = searcher.search_module()
@@ -1145,7 +1264,7 @@ class Limoka(loader.Module):
if not info: if not info:
continue continue
name = info.get("name", "Unknown") name = info.get("name", "Unknown")
global_session = self._create_search_session( global_session = self._create_search_session(
state=self.SEARCH_STATES["global_search"], state=self.SEARCH_STATES["global_search"],
query=query, query=query,
@@ -1171,47 +1290,37 @@ class Limoka(loader.Module):
self, call: InlineCall, module_path: str, session: Dict[str, Any] self, call: InlineCall, module_path: str, session: Dict[str, Any]
): ):
module_info = self.modules[module_path] module_info = self.modules[module_path]
await self._display_module( await self._display_module(call, module_info, module_path, session, 0)
call, module_info, module_path, session, 0
)
async def _next_page( async def _next_page(self, call: InlineCall, session: Dict[str, Any]):
self, call: InlineCall, session: Dict[str, Any]
):
result = session["results"] result = session["results"]
index = session["current_index"] index = session["current_index"]
if index + 1 >= len(result): if index + 1 >= len(result):
await call.answer(self.strings["last_page"]) await call.answer(self.strings["last_page"])
return return
index += 1 index += 1
module_path = result[index] module_path = result[index]
module_info = self.modules[module_path] module_info = self.modules[module_path]
new_session = session.copy() new_session = session.copy()
new_session["current_index"] = index new_session["current_index"] = index
await self._display_module( await self._display_module(call, module_info, module_path, new_session, 0)
call, module_info, module_path, new_session, 0
)
async def _previous_page( async def _previous_page(self, call: InlineCall, session: Dict[str, Any]):
self, call: InlineCall, session: Dict[str, Any]
):
result = session["results"] result = session["results"]
index = session["current_index"] index = session["current_index"]
if index - 1 < 0: if index - 1 < 0:
await call.answer(self.strings["first_page"]) await call.answer(self.strings["first_page"])
return return
index -= 1 index -= 1
module_path = result[index] module_path = result[index]
module_info = self.modules[module_path] module_info = self.modules[module_path]
new_session = session.copy() new_session = session.copy()
new_session["current_index"] = index new_session["current_index"] = index
await self._display_module( await self._display_module(call, module_info, module_path, new_session, 0)
call, module_info, module_path, new_session, 0
)
async def _inline_void(self, call: InlineCall): async def _inline_void(self, call: InlineCall):
await call.answer() await call.answer()
@@ -1244,7 +1353,7 @@ class Limoka(loader.Module):
text=self.strings["start_search_form"], text=self.strings["start_search_form"],
message=message, message=message,
reply_markup=markup, reply_markup=markup,
photo=self._get_banner_for_state("global_search") photo=self._get_banner_for_state("global_search"),
) )
return return
history = self.get("history", []) history = self.get("history", [])
@@ -1269,7 +1378,7 @@ class Limoka(loader.Module):
return await utils.answer(message, self.strings["404"].format(query=args)) return await utils.answer(message, self.strings["404"].format(query=args))
module_path = result[0] module_path = result[0]
module_info = self.modules[module_path] module_info = self.modules[module_path]
# Create session for displaying module # Create session for displaying module
display_session = self._create_search_session( display_session = self._create_search_session(
state=self.SEARCH_STATES["global_search"], state=self.SEARCH_STATES["global_search"],
@@ -1278,7 +1387,9 @@ class Limoka(loader.Module):
results=result, results=result,
current_index=0, current_index=0,
) )
await self._display_module(message, module_info, module_path, display_session, 0) await self._display_module(
message, module_info, module_path, display_session, 0
)
async def _show_global_form(self, call: InlineCall, message: Message): async def _show_global_form(self, call: InlineCall, message: Message):
markup = [ markup = [
@@ -1309,12 +1420,12 @@ class Limoka(loader.Module):
self, call: InlineCall, query: str, message: Message, *args, **kwargs self, call: InlineCall, query: str, message: Message, *args, **kwargs
): ):
global_session = self._create_search_session( global_session = self._create_search_session(
state=self.SEARCH_STATES["global_search"], state=self.SEARCH_STATES["global_search"],
query=query, query=query,
filters={}, filters={},
results=[], results=[],
current_index=0, current_index=0,
) # idk what is that crap but it works lol ) # idk what is that crap but it works lol
if len(query) <= 1: if len(query) <= 1:
await call.edit( await call.edit(
self.strings["?"], self.strings["?"],
@@ -1442,16 +1553,20 @@ class Limoka(loader.Module):
elif hasattr(message.from_id, "channel_id"): elif hasattr(message.from_id, "channel_id"):
sender_id = message.from_id.channel_id sender_id = message.from_id.channel_id
if sender_id != self._service_bot_id: if sender_id != self._service_bot_id:
logger.debug("Message not from official bot, ignoring") # logger.debug("Message not from official bot, ignoring")
return return
if not self.config["external_install_allowed"]: if not self.config["external_install_allowed"]:
return return
try: try:
clean_text = getattr(message, "raw_text", None) or getattr( clean_text = (
message, "message", None getattr(message, "raw_text", None)
) or message.text or "" or getattr(message, "message", None)
or message.text
or ""
)
if message.entities: if message.entities:
from html import unescape from html import unescape
clean_text = unescape(clean_text) clean_text = unescape(clean_text)
clean_text = re.sub(r"<[^>]+>", "", clean_text) clean_text = re.sub(r"<[^>]+>", "", clean_text)
match = re.search(r"#limoka:([^\s\"'<>]+)", clean_text) match = re.search(r"#limoka:([^\s\"'<>]+)", clean_text)
@@ -1480,25 +1595,37 @@ class Limoka(loader.Module):
if not found: if not found:
logger.warning(f"Module not found after cleanup: {module_path}") logger.warning(f"Module not found after cleanup: {module_path}")
await utils.answer( await utils.answer(
message, self.strings["watcher_module_not_found"].format(path=html.escape(module_path)) message,
self.strings["watcher_module_not_found"].format(
path=html.escape(module_path)
),
) )
return return
try: try:
import base64 import base64
from cryptography.hazmat.primitives.asymmetric import ed25519 from cryptography.hazmat.primitives.asymmetric import ed25519
PUB_KEY_B64 = "MCowBQYDK2VwAyEA1ltSnqtf3pGBuctuAYqHivCXsaRtKOVxavai7yin7ZE="
PUB_KEY_B64 = (
"MCowBQYDK2VwAyEA1ltSnqtf3pGBuctuAYqHivCXsaRtKOVxavai7yin7ZE="
)
der_bytes = base64.b64decode(PUB_KEY_B64) der_bytes = base64.b64decode(PUB_KEY_B64)
raw_pubkey = der_bytes[-32:] raw_pubkey = der_bytes[-32:]
module_url = self.config["limokaurl"] + module_path module_url = self.config["limokaurl"] + module_path
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get(module_url, timeout=10) as resp: async with session.get(module_url, timeout=10) as resp:
if resp.status != 200: if resp.status != 200:
logger.error(f"Failed to fetch module for verification: {module_url} (HTTP {resp.status})") logger.error(
await utils.answer(message, self.strings["watcher_loader_missing"]) f"Failed to fetch module for verification: {module_url} (HTTP {resp.status})"
)
await utils.answer(
message, self.strings["watcher_loader_missing"]
)
return return
module_bytes = await resp.read() module_bytes = await resp.read()
sha256 = hashlib.sha256(module_bytes).hexdigest() sha256 = hashlib.sha256(module_bytes).hexdigest()
public_key = ed25519.Ed25519PublicKey.from_public_bytes(raw_pubkey) public_key = ed25519.Ed25519PublicKey.from_public_bytes(
raw_pubkey
)
signature = bytes.fromhex(signature_hex) signature = bytes.fromhex(signature_hex)
signed_payload = f"{module_path}|{sha256}".encode() signed_payload = f"{module_path}|{sha256}".encode()
public_key.verify(signature, signed_payload) public_key.verify(signature, signed_payload)
@@ -1522,21 +1649,27 @@ class Limoka(loader.Module):
if status: if status:
try: try:
bot_peer = await self.client.get_entity(self._service_bot_id) bot_peer = await self.client.get_entity(self._service_bot_id)
await self.client.send_message(bot_peer, f"#limoka:sucsess:{message.id}") await self.client.send_message(
bot_peer, f"#limoka:sucsess:{message.id}"
)
except Exception as e: except Exception as e:
logger.error(f"Failed to send success confirmation: {e}") logger.error(f"Failed to send success confirmation: {e}")
else: else:
logger.error(f"Installation failed with status: {status}") logger.error(f"Installation failed with status: {status}")
try: try:
bot_peer = await self.client.get_entity(self._service_bot_id) bot_peer = await self.client.get_entity(self._service_bot_id)
await self.client.send_message(bot_peer, f"#limoka:failed:{message.id}") await self.client.send_message(
bot_peer, f"#limoka:failed:{message.id}"
)
except Exception as e: except Exception as e:
logger.error(f"Failed to send failure notification: {e}") logger.error(f"Failed to send failure notification: {e}")
except Exception as e: except Exception as e:
logger.exception(f"CRITICAL ERROR in secure_install_watcher: {e}") logger.exception(f"CRITICAL ERROR in secure_install_watcher: {e}")
try: try:
await utils.answer(message, self.strings["watcher_critical"].format(error=str(e)[:100])) await utils.answer(
message, self.strings["watcher_critical"].format(error=str(e)[:100])
)
await asyncio.sleep(5) await asyncio.sleep(5)
await message.delete() await message.delete()
except Exception: except Exception:
pass pass