Merge pull request #179 from MuRuLOSE/update-submodules_a7b8c740b42879d9b3c0d577d275d5b42e4db254

Update of repositories 2026-02-07 01:23:55
This commit is contained in:
Macsim
2026-02-07 10:34:25 +03:00
committed by GitHub
2 changed files with 195 additions and 62 deletions

View File

@@ -3,7 +3,7 @@
# This software is released under the MIT License. # This software is released under the MIT License.
# https://opensource.org/licenses/MIT # https://opensource.org/licenses/MIT
__version__ = (5, 8, 1) #фыр __version__ = (6, 0, 0) #фыр
# meta developer: @SenkoGuardianModules # meta developer: @SenkoGuardianModules
@@ -22,12 +22,10 @@ import socket
import base64 import base64
import uuid import uuid
import json import json
from PIL import Image
import asyncio import asyncio
import logging import logging
import tempfile import tempfile
import aiohttp import aiohttp
from datetime import datetime
from markdown_it import MarkdownIt from markdown_it import MarkdownIt
import pytz import pytz
@@ -41,6 +39,8 @@ except ImportError:
GOOGLE_AVAILABLE = False GOOGLE_AVAILABLE = False
google_exceptions = None google_exceptions = None
from PIL import Image
from datetime import datetime
from telethon import types as tg_types from telethon import types as tg_types
from telethon.tl.types import Message, DocumentAttributeFilename, DocumentAttributeSticker from telethon.tl.types import Message, DocumentAttributeFilename, DocumentAttributeSticker
from telethon.utils import get_display_name, get_peer_id from telethon.utils import get_display_name, get_peer_id
@@ -84,7 +84,15 @@ class Gemini(loader.Module):
"cfg_google_search_doc": "Включить поиск Google (Grounding) для актуальной информации.", "cfg_google_search_doc": "Включить поиск Google (Grounding) для актуальной информации.",
"cfg_image_model_doc": "Модель Gemini для генерации изображений (например: gemini-2.5-flash-image).", "cfg_image_model_doc": "Модель Gemini для генерации изображений (например: gemini-2.5-flash-image).",
"cfg_inline_pagination_doc": "Использовать инлайн-кнопки для длинных ответов.", "cfg_inline_pagination_doc": "Использовать инлайн-кнопки для длинных ответов.",
"no_api_key": '❗️ <b>Api ключ(и) не настроен(ы).</b>\nПолучить Api ключ можно <a href="https://aistudio.google.com/app/apikey">здесь</a>.\n<b>Добавьте ключ(и) в конфиге модуля:</b> <code>.cfg gemini api_key</code>', "no_api_key": (
'❗️ <b>Api ключ(и) не настроен(ы).</b>\nПолучить Api ключ можно <a href="https://aistudio.google.com/app/apikey">здесь</a>.\n'
'<b>Добавьте ключ(и) в конфиге модуля:</b> <code>.cfg gemini api_key</code>\n'
'Так же можно использовать провайдера Openrouter <code>.cfg gemini provider</code>\n'
' Получить Openrouter ключ можно <a href="https://openrouter.ai/settings/keys">здесь</a>'
),
"no_api_key_Openrouter": '❗️ <b>API ключ для OpenRouter не настроен.</b>\nПолучить ключ можно <a href="https://openrouter.ai/settings/keys">здесь</a>.\n<b>Добавьте ключ в конфиге модуля:</b> <code>.cfg gemini Openrouter_api_key</code>',
"invalid_api_key_Openrouter": '❗️ <b>Предоставленный API ключ OpenRouter недействителен.</b>\nУбедитесь, что он правильно скопирован из <a href="https://openrouter.ai/settings/keys">OpenRouter</a>.',
"gmodel_list_title_Openrouter": "📋 <b>Доступные модели OpenRouter:</b>",
"invalid_api_key": '❗️ <b>Предоставленный API ключ недействителен.</b>\nУбедитесь, что он правильно скопирован из <a href="https://aistudio.google.com/app/apikey">Google AI Studio</a> и что для него включен Gemini API.', "invalid_api_key": '❗️ <b>Предоставленный API ключ недействителен.</b>\nУбедитесь, что он правильно скопирован из <a href="https://aistudio.google.com/app/apikey">Google AI Studio</a> и что для него включен Gemini API.',
"all_keys_exhausted": "❗️ <b>Все доступные API ключи ({}) исчерпали свою квоту.</b>\nПопробуйте позже или добавьте новые ключи в конфиге: <code>.cfg gemini api_key</code>", "all_keys_exhausted": "❗️ <b>Все доступные API ключи ({}) исчерпали свою квоту.</b>\nПопробуйте позже или добавьте новые ключи в конфиге: <code>.cfg gemini api_key</code>",
"no_prompt_or_media": "⚠️ <i>Нужен текст или ответ на медиа/файл.</i>", "no_prompt_or_media": "⚠️ <i>Нужен текст или ответ на медиа/файл.</i>",
@@ -159,6 +167,8 @@ class Gemini(loader.Module):
def __init__(self): def __init__(self):
self.config = loader.ModuleConfig( self.config = loader.ModuleConfig(
loader.ConfigValue("api_key", "", self.strings["cfg_api_key_doc"], validator=loader.validators.Hidden()), loader.ConfigValue("api_key", "", self.strings["cfg_api_key_doc"], validator=loader.validators.Hidden()),
loader.ConfigValue("Openrouter_api_key", "", "API Key от OpenRouter (получить <a href='https://openrouter.ai/settings/keys'>тут</a>).", validator=loader.validators.Hidden()),
loader.ConfigValue("provider", "google", "Провайдер API: 'google' или 'openrouter'.", validator=loader.validators.Choice(["google", "openrouter"])),
loader.ConfigValue("model_name", "gemini-2.5-flash", self.strings["cfg_model_name_doc"]), loader.ConfigValue("model_name", "gemini-2.5-flash", self.strings["cfg_model_name_doc"]),
loader.ConfigValue("interactive_buttons", True, self.strings["cfg_buttons_doc"], validator=loader.validators.Boolean()), loader.ConfigValue("interactive_buttons", True, self.strings["cfg_buttons_doc"], validator=loader.validators.Boolean()),
loader.ConfigValue("system_instruction", "", self.strings["cfg_system_instruction_doc"], validator=loader.validators.String()), loader.ConfigValue("system_instruction", "", self.strings["cfg_system_instruction_doc"], validator=loader.validators.String()),
@@ -323,6 +333,56 @@ class Gemini(loader.Module):
except Exception: msg_obj = None except Exception: msg_obj = None
else: else:
chat_id = utils.get_chat_id(message); base_message_id = message.id; msg_obj = message chat_id = utils.get_chat_id(message); base_message_id = message.id; msg_obj = message
if self.config["provider"] == "openrouter":
if regeneration:
current_turn_parts, request_text_for_display = self.last_requests.get(f"{chat_id}:{base_message_id}", (parts, "[регенерация]"))
else:
current_turn_parts = parts
user_text_from_parts = " ".join([p.text for p in parts if hasattr(p, "text") and p.text])
request_text_for_display = display_prompt or user_text_from_parts or "[медиа-запрос]"
self.last_requests[f"{chat_id}:{base_message_id}"] = (current_turn_parts, request_text_for_display)
try:
sys_instruct = self.config["system_instruction"] or None
if impersonation_mode:
my_name = get_display_name(self.me)
chat_history_text = await self._get_recent_chat_text(chat_id)
sys_instruct = self.config["impersonation_prompt"].format(my_name=my_name, chat_history=chat_history_text)
raw_hist = self._get_structured_history(chat_id, gauto=impersonation_mode)
if regeneration and raw_hist: raw_hist = raw_hist[:-2]
openai_messages = self._convert_google_history_to_openai(raw_hist, sys_instruct)
user_text_prompt = " ".join([p.text for p in current_turn_parts if hasattr(p, "text") and p.text])
if not user_text_prompt: user_text_prompt = request_text_for_display
openai_messages.append({"role": "user", "content": user_text_prompt})
target_model = self.config["model_name"]
result_text = await self._send_to_Openrouter_api(target_model, openai_messages, self.config["temperature"])
if self._is_memory_enabled(str(chat_id)):
self._update_history(chat_id, current_turn_parts, result_text, regeneration, msg_obj, gauto=impersonation_mode)
if impersonation_mode: return result_text
hist_len = len(self._get_structured_history(chat_id)) // 2
mem_ind = self.strings["memory_status"].format(hist_len, self.config["max_history_length"])
model_info = f"<i>OpenRouter: <code>{target_model}</code></i>"
response_html = self._markdown_to_html(result_text)
formatted_body = self._format_response_with_smart_separation(response_html)
question_html = f"<blockquote>{utils.escape_html(request_text_for_display[:200])}</blockquote>"
text_to_send = f"{mem_ind}\n{model_info}\n\n{self.strings['question_prefix']}\n{question_html}\n\n{self.strings['response_prefix']}\n{formatted_body}"
buttons = self._get_inline_buttons(chat_id, base_message_id) if self.config["interactive_buttons"] else None
if len(text_to_send) > 4096:
file = io.BytesIO(result_text.encode("utf-8")); file.name = "Gemini_response.txt"
if call: await self.client.send_file(call.chat_id, file, caption="Response too long", reply_to=call.message_id)
elif status_msg:
await status_msg.delete()
await self.client.send_file(chat_id, file, caption="Response too long", reply_to=base_message_id)
else:
if call: await call.edit(text_to_send, reply_markup=buttons)
elif status_msg: await utils.answer(status_msg, text_to_send, reply_markup=buttons)
return ""
except Exception as e:
error_text = self._handle_error(e)
if impersonation_mode: logger.error(f"Gauto/Openrouter error: {error_text}")
elif call: await call.edit(error_text)
elif status_msg: await utils.answer(status_msg, error_text)
return None
api_key_str = self.config["api_key"] api_key_str = self.config["api_key"]
self.api_keys = [k.strip() for k in api_key_str.split(",") if k.strip()] if api_key_str else [] self.api_keys = [k.strip() for k in api_key_str.split(",") if k.strip()] if api_key_str else []
if not self.api_keys: if not self.api_keys:
@@ -814,25 +874,66 @@ class Gemini(loader.Module):
@loader.command() @loader.command()
async def gmodel(self, message: Message): async def gmodel(self, message: Message):
"""[model или пусто] — Узнать/сменить модель. -s — список доступных моделей в файле.""" """[model] [-s] — Узнать/сменить модель. -s — список."""
args = utils.get_args_raw(message).strip().lower() args_raw = utils.get_args_raw(message).strip()
if '-s' in args: args_list = args_raw.split()
if not self.api_keys: return await utils.answer(message, self.strings['no_api_key']) is_list_request = "-s" in [arg.lower() for arg in args_list]
sts = await utils.answer(message, self.strings["processing"]) provider = self.config["provider"]
if is_list_request:
status_msg = await utils.answer(message, self.strings["processing"])
try: try:
if provider == "openrouter":
api_key = self.config["Openrouter_api_key"]
if not api_key: return await utils.answer(status_msg, self.strings['no_api_key_Openrouter'])
async with aiohttp.ClientSession() as session:
async with session.get(
"https://openrouter.ai/api/v1/models",
headers={"Authorization": f"Bearer {api_key}"}
) as resp:
if resp.status != 200: raise ValueError(f"HTTP {resp.status}")
data = await resp.json()
models_data = data.get("data", [])
models_data.sort(key=lambda x: x["id"])
top_list = []
other_list = []
favs = ["google/gemini-2.0-flash-001", "openai/gpt-4o", "anthropic/claude-3.5-sonnet", "deepseek/deepseek-r1"]
for m in models_data:
mid = m["id"]
line = f"• <code>{mid}</code>"
if mid in favs: top_list.append(line)
elif any(x in mid for x in ["gemini", "gpt", "claude", "deepseek"]): other_list.append(line)
text = self.strings.get("gmodel_list_title_Openrouter", "📋 Models:") + "\n" + "\n".join(top_list) + "\n\n" + "\n".join(other_list[:50])
file = io.BytesIO(text.encode("utf-8")); file.name = "openrouter_models.txt"
await self.client.send_file(message.chat_id, file=file, caption="📋 OpenRouter Models", reply_to=message.id)
await status_msg.delete()
else:
if not self.api_keys: return await utils.answer(status_msg, self.strings['no_api_key'])
client = genai.Client(api_key=self.api_keys[0]) client = genai.Client(api_key=self.api_keys[0])
models = await asyncio.to_thread(client.models.list) models = await asyncio.to_thread(client.models.list)
txt = "\n".join([f"• <code>{m.name.split('/')[-1]}</code> ({m.display_name})" for m in models]) txt = "\n".join([f"• <code>{m.name.split('/')[-1]}</code> ({m.display_name})" for m in models])
f = io.BytesIO((self.strings["gmodel_list_title"] + "\n" + txt).encode('utf-8')) f = io.BytesIO((self.strings["gmodel_list_title"] + "\n" + txt).encode('utf-8'))
f.name = "models_list.txt" f.name = "models_list.txt"
await self.client.send_file(message.chat_id, file=f, caption="📋 Список доступных моделей", reply_to=message.id) await self.client.send_file(message.chat_id, file=f, caption="📋 Список доступных моделей", reply_to=message.id)
await sts.delete() await status_msg.delete()
except Exception as e: await utils.answer(sts, self.strings["gmodel_list_error"].format(self._handle_error(e))) except Exception as e:
await utils.answer(status_msg, self.strings["gmodel_list_error"].format(self._handle_error(e)))
return return
if not args_raw:
if not args: return await utils.answer(message, f"Текущая модель: <code>{self.config['model_name']}</code>") return await utils.answer(message, f"🔮 <b>Провайдер:</b> {provider}\n🧠 <b>Модель:</b> <code>{self.config['model_name']}</code>")
self.config["model_name"] = args self.config["model_name"] = args_raw
await utils.answer(message, f"Модель Gemini установлена: <code>{args}</code>") warning = ""
if provider == "google" and ("/" in args_raw or any(x in args_raw.lower() for x in ["gpt", "claude", "deepseek", "llama"])):
warning = (
"\n\n⚠️ <b>Конфликт настроек!</b>\n"
f"Вы установили модель <code>{args_raw}</code>, но провайдер остался <b>Google</b>.\n"
"Смените провайдера командой:\n<code>.cfg gemini provider openrouter</code>"
)
elif provider == "openrouter" and "/" not in args_raw and "gemini" in args_raw.lower():
warning = (
"\n\n⚠️ <b>Совет:</b> Для OpenRouter лучше использовать полные ID.\n"
f"Например: <code>google/{args_raw}</code>"
)
await utils.answer(message, f"✅ Модель установлена: <code>{args_raw}</code>{warning}")
@loader.command() @loader.command()
async def gres(self, message: Message): async def gres(self, message: Message):
@@ -1107,16 +1208,26 @@ class Gemini(loader.Module):
msgs = await self.client.get_messages(cid, limit=lim) msgs = await self.client.get_messages(cid, limit=lim)
if skip_last and msgs: msgs = msgs[1:] if skip_last and msgs: msgs = msgs[1:]
for m in msgs: for m in msgs:
if not m or (not m.text and not m.media): continue if not m: continue
if not (m.text or m.sticker or m.photo or m.file or m.media):
continue
name = get_display_name(await m.get_sender()) or "Unknown" name = get_display_name(await m.get_sender()) or "Unknown"
txt = m.text or ("[Media]" if m.media else "") txt = m.text or ""
if m.sticker: if m.sticker:
alt = "?"
if hasattr(m.sticker, 'attributes'):
alt = next((a.alt for a in m.sticker.attributes if isinstance(a, DocumentAttributeSticker)), "?") alt = next((a.alt for a in m.sticker.attributes if isinstance(a, DocumentAttributeSticker)), "?")
txt += f" [Стикер: {alt}]" txt += f" [Стикер: {alt}]"
elif m.photo: txt += " [Фото]" elif m.photo:
elif m.document and not hasattr(m.media, "webpage"): txt += "айл]" txt += "ото]"
if txt.strip(): lines.append(f"{name}: {txt.strip()}") elif m.file:
except: pass txt += " [Файл]"
elif m.media and not txt:
txt += " [Медиа]"
if txt.strip():
lines.append(f"{name}: {txt.strip()}")
except Exception as e:
pass
return "\n".join(reversed(lines)) return "\n".join(reversed(lines))
def _handle_error(self, e: Exception) -> str: def _handle_error(self, e: Exception) -> str:
@@ -1202,35 +1313,6 @@ class Gemini(loader.Module):
self._clear_history(chat_id, gauto=False) self._clear_history(chat_id, gauto=False)
await call.edit(self.strings["memory_cleared"], reply_markup=None) await call.edit(self.strings["memory_cleared"], reply_markup=None)
async def _get_recent_chat_text(self, chat_id: int, count: int = None, skip_last: bool = False) -> str:
history_limit = count or self.config["impersonation_history_limit"]
fetch_limit = history_limit + 1 if skip_last else history_limit
chat_history_lines = []
try:
messages = await self.client.get_messages(chat_id, limit=fetch_limit)
if skip_last and messages:
messages = messages[1:]
for msg in messages:
if not msg: continue
if not msg.text and not msg.sticker and not msg.photo and not (msg.media and not hasattr(msg.media, "webpage")):
continue
sender = await msg.get_sender()
sender_name = get_display_name(sender) if sender else "Unknown"
text_content = msg.text or ""
if msg.sticker and hasattr(msg.sticker, 'attributes'):
alt_text = next((attr.alt for attr in msg.sticker.attributes if isinstance(attr, DocumentAttributeSticker)), None)
text_content += f" [Стикер: {alt_text or '?'}]"
elif msg.photo:
text_content += " [Фото]"
elif msg.document and not hasattr(msg.media, "webpage"):
text_content += " [Файл]"
if text_content.strip():
chat_history_lines.append(f"{sender_name}: {text_content.strip()}")
except Exception as e:
logger.warning(f"Не удалось получить историю для авто-ответа: {e}")
return "\n".join(reversed(chat_history_lines))
async def _scan_keys(self, force=False): async def _scan_keys(self, force=False):
""" """
Сканирует ключи на валидность. Сканирует ключи на валидность.
@@ -1336,6 +1418,55 @@ class Gemini(loader.Module):
return out.getvalue() return out.getvalue()
except: return img_bytes except: return img_bytes
async def _send_to_Openrouter_api(self, model, messages, temperature):
"""Отправка запроса в OpenRouter (OpenAI format)"""
api_key = self.config["Openrouter_api_key"]
if not api_key:
raise ValueError("Не указан OpenRouter API Key! Установите его в .cfg")
url = "https://openrouter.ai/api/v1/chat/completions"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"HTTP-Referer": "https://github.com/SenkoGuardian",
"X-Title": "Gemini Module for Heroku Telegram-userbot",
}
payload = {
"model": model,
"messages": messages,
"temperature": min(temperature, 1.0)
}
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers, json=payload, timeout=GEMINI_TIMEOUT) as resp:
text = await resp.text()
if resp.status != 200:
try:
err_json = json.loads(text)
err_msg = err_json.get('error', {}).get('message', text)
except:
err_msg = text
raise ConnectionError(f"OpenRouter API Error {resp.status}: {err_msg}")
try:
result = json.loads(text)
except json.JSONDecodeError:
raise ValueError(f"OpenRouter вернул не JSON: {text[:100]}...")
if "choices" not in result or not result["choices"]:
if "error" in result:
raise ValueError(f"OpenRouter Logic Error: {result['error']}")
raise ValueError(f"Пустой ответ (нет 'choices'). Raw: {text}")
return result["choices"][0]["message"]["content"]
def _convert_google_history_to_openai(self, history: list, system_prompt: str) -> list:
"""Конвертирует историю из формата Google в формат OpenAI."""
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
for item in history:
role = "assistant" if item['role'] == "model" else "user"
content = item.get("content", "")
messages.append({"role": role, "content": content})
return messages
def _is_memory_enabled(self, chat_id: str) -> bool: return chat_id not in self.memory_disabled_chats def _is_memory_enabled(self, chat_id: str) -> bool: return chat_id not in self.memory_disabled_chats
def _disable_memory(self, chat_id: int): self.memory_disabled_chats.add(str(chat_id)) def _disable_memory(self, chat_id: int): self.memory_disabled_chats.add(str(chat_id))
def _enable_memory(self, chat_id: int): self.memory_disabled_chats.discard(str(chat_id)) def _enable_memory(self, chat_id: int): self.memory_disabled_chats.discard(str(chat_id))

View File

@@ -131,6 +131,7 @@
"nonlongermaintained": "No Longer Maintained Repository", "nonlongermaintained": "No Longer Maintained Repository",
"newbie": "Newbie" "newbie": "Newbie"
}, },
"indexing_in_progress": "⚠️ Database is busy, try again later. If issue persists, try removing limoka_index in the userbot's root folder. If error persists again, report to developers",
"name_ru": "Limoka", "name_ru": "Limoka",
"wait_ru": "Подождите\n<emoji document_id=5404630946563515782>🔍</emoji> Идёт поиск среди {count} модулей по запросу: <code>{query}</code>\n<i>{fact}</i>", "wait_ru": "Подождите\n<emoji document_id=5404630946563515782>🔍</emoji> Идёт поиск среди {count} модулей по запросу: <code>{query}</code>\n<i>{fact}</i>",
"found_header_ru": "<emoji document_id=5413334818047940135>🔍</emoji> Найден модуль <b>{name}</b> по запросу: <b>{query}</b>\n\n<b><emoji document_id=5418376169055602355></emoji> Описание:</b> {description}\n<b><emoji document_id=5418299289141004396>🧑‍💻</emoji> Разработчик:</b> {username}\n\n<b><emoji document_id=5418376169055602355>🏷</emoji> Теги:</b> {tags}\n\n", "found_header_ru": "<emoji document_id=5413334818047940135>🔍</emoji> Найден модуль <b>{name}</b> по запросу: <b>{query}</b>\n\n<b><emoji document_id=5418376169055602355></emoji> Описание:</b> {description}\n<b><emoji document_id=5418299289141004396>🧑‍💻</emoji> Разработчик:</b> {username}\n\n<b><emoji document_id=5418376169055602355>🏷</emoji> Теги:</b> {tags}\n\n",
@@ -188,7 +189,8 @@
"watcher_signature_invalid_ru": "❌ Неверная подпись! Установка отменена.", "watcher_signature_invalid_ru": "❌ Неверная подпись! Установка отменена.",
"watcher_loader_missing_ru": "❌ Модуль загрузчика не найден.", "watcher_loader_missing_ru": "❌ Модуль загрузчика не найден.",
"watcher_module_not_found_ru": "❌ Модуль не найден в базе Limoka: <code>{path}</code>", "watcher_module_not_found_ru": "❌ Модуль не найден в базе Limoka: <code>{path}</code>",
"watcher_critical_ru": "❌ Критическая ошибка: {error}" "watcher_critical_ru": "❌ Критическая ошибка: {error}",
"indexing_in_progress_ru": "⚠️ База данных занята, попробуйте снова через несколько секунд. Если ошибка сохраняется, попробуйте удалить limoka_index в корневой папке юзербота. Если ошибка сохраняется снова, сообщите разработчикам"
}, },
"has_on_load": false, "has_on_load": false,
"has_on_unload": false, "has_on_unload": false,
@@ -38599,7 +38601,7 @@
"sgamerps": "- Start the game «Rock, Paper, Scissors» | (RU) - Начать игру «Камень, ножницы, бумага» | (UZ) - «Tosh, qog'oz, qaychi» o'yinining boshlanishi | (DE) - Beginn des Spiels «Stein, Papier, Schere» | (ES) - El comienzo del juego «Piedra, papel o tijera»" "sgamerps": "- Start the game «Rock, Paper, Scissors» | (RU) - Начать игру «Камень, ножницы, бумага» | (UZ) - «Tosh, qog'oz, qaychi» o'yinining boshlanishi | (DE) - Beginn des Spiels «Stein, Papier, Schere» | (ES) - El comienzo del juego «Piedra, papel o tijera»"
}, },
{ {
"cleargames": "- Complete all running games. | (RU) <ast.JoinedStr object at 0x7f9ad406ca60> | (UZ) - Barcha faol o'yinlarni tugatish. | (DE) - Alle aktiven Spiele beenden. | (ES) - Completar todos los juegos en curso." "cleargames": "- Complete all running games. | (RU) <ast.JoinedStr object at 0x7fd34fbb6400> | (UZ) - Barcha faol o'yinlarni tugatish. | (DE) - Alle aktiven Spiele beenden. | (ES) - Completar todos los juegos en curso."
} }
], ],
"new_commands": [ "new_commands": [
@@ -38625,7 +38627,7 @@
"original_name": "cleargames", "original_name": "cleargames",
"description": { "description": {
"default": "- Complete all running games.", "default": "- Complete all running games.",
"ru": "<ast.JoinedStr object at 0x7f9ad406ca60>", "ru": "<ast.JoinedStr object at 0x7fd34fbb6400>",
"uz": "- Barcha faol o'yinlarni tugatish.", "uz": "- Barcha faol o'yinlarni tugatish.",
"de": "- Alle aktiven Spiele beenden.", "de": "- Alle aktiven Spiele beenden.",
"es": "- Completar todos los juegos en curso." "es": "- Completar todos los juegos en curso."
@@ -81807,6 +81809,6 @@
}, },
"meta": { "meta": {
"total_modules": 1017, "total_modules": 1017,
"generated_at": "2026-02-06T01:24:40.647732" "generated_at": "2026-02-07T01:23:38.497674"
} }
} }