mirror of
https://github.com/MuRuLOSE/limoka.git
synced 2026-06-18 15:14:18 +02:00
Compare commits
49 Commits
update-sub
...
update-sub
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
430f49f345 | ||
| 54229a8266 | |||
|
|
6cb625acd8 | ||
|
|
bf03595075 | ||
|
|
8bef604170 | ||
| 61ebb7fa47 | |||
|
|
2702cd2356 | ||
|
|
37f53375bc | ||
|
|
23dade9b9e | ||
|
|
ab5aaf579b | ||
|
|
170809519a | ||
| 78a9b0cb76 | |||
|
|
c1d407f885 | ||
|
|
cf230d2922 | ||
| 7cda8f2685 | |||
|
|
92bdd1510d | ||
|
|
52f6fc53c3 | ||
|
|
5c25eaad81 | ||
| a7b8c740b4 | |||
| 44ca8633c6 | |||
| e076d4ab28 | |||
|
|
634861943a | ||
|
|
d20d019c32 | ||
|
|
041e6ec5f7 | ||
| f68d97d09b | |||
| e624861b64 | |||
|
|
d3f035ec58 | ||
|
|
1ced5efae6 | ||
|
|
3bf476c8a9 | ||
|
|
abff91c013 | ||
| 5c2de322d3 | |||
| 5b2fe145ff | |||
| 64d8ceb354 | |||
| 537ae4c485 | |||
| 52018c8e14 | |||
| 6f95292df1 | |||
| 4a5d4883a9 | |||
| ce59b13c94 | |||
| 91351cb83d | |||
|
|
118a800856 | ||
|
|
f14a99c640 | ||
|
|
c76dcc0fc1 | ||
| 51bbb98711 | |||
| edd70ad020 | |||
|
|
2c33b08ea0 | ||
|
|
e9bb89cf62 | ||
|
|
51055e6427 | ||
| 88ab265755 | |||
|
|
baada2d019 |
19
.github/workflows/ci.yml
vendored
19
.github/workflows/ci.yml
vendored
@@ -10,8 +10,10 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
types: [opened, synchronize, reopened, closed]
|
||||||
workflow_dispatch: # Allows manual triggering from GitHub UI
|
workflow_dispatch: # Allows manual triggering from GitHub UI
|
||||||
|
|
||||||
|
|
||||||
# Environment variables available to all jobs
|
# Environment variables available to all jobs
|
||||||
env:
|
env:
|
||||||
BRANCH_NAME: "update-submodules_${{ github.sha }}"
|
BRANCH_NAME: "update-submodules_${{ github.sha }}"
|
||||||
@@ -207,16 +209,14 @@ jobs:
|
|||||||
|
|
||||||
notify_diffs:
|
notify_diffs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: |
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
|
|
||||||
(github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged == true)
|
|
||||||
needs: parse
|
needs: parse
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: ${{ env.GIT_DEPTH }}
|
fetch-depth: 0
|
||||||
- name: Configure Git for github-actions[bot]
|
- name: Configure Git
|
||||||
run: |
|
run: |
|
||||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
git config --global user.name "github-actions[bot]"
|
git config --global user.name "github-actions[bot]"
|
||||||
@@ -224,15 +224,18 @@ jobs:
|
|||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: '3.9'
|
python-version: '3.9'
|
||||||
- name: Install Python dependencies
|
- name: Install dependencies
|
||||||
run: pip install aiohttp
|
run: pip install aiohttp
|
||||||
- name: Send module diffs to channel
|
- name: Send module diffs to channel
|
||||||
env:
|
env:
|
||||||
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||||
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID_UPDATE }}
|
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID_UPDATE }}
|
||||||
run: |
|
run: |
|
||||||
git fetch origin main
|
LAST_COMMIT_MESSAGE=$(git log -1 --pretty=%B)
|
||||||
python3 update_diffs.py --token ${{ secrets.TELEGRAM_BOT_TOKEN }} --chat_id ${{ secrets.TELEGRAM_CHAT_ID_UPDATE }} --base_commit HEAD~1
|
if [[ "$LAST_COMMIT_MESSAGE" == Merge* ]]; then
|
||||||
|
python3 update_diffs.py --token ${{ secrets.TELEGRAM_BOT_TOKEN }} --chat_id ${{ secrets.TELEGRAM_CHAT_ID_UPDATE }} --base_commit HEAD~1
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
backup:
|
backup:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
541
Limoka.py
541
Limoka.py
@@ -2,10 +2,13 @@
|
|||||||
# requires: whoosh cryptography
|
# requires: whoosh cryptography
|
||||||
|
|
||||||
|
|
||||||
|
from collections import Counter, defaultdict
|
||||||
|
import shutil
|
||||||
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
|
||||||
from whoosh.query import FuzzyTerm, Wildcard
|
from whoosh.query import FuzzyTerm, Wildcard
|
||||||
|
from whoosh.writing import LockError
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import random
|
import random
|
||||||
import logging
|
import logging
|
||||||
@@ -14,26 +17,43 @@ 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
|
||||||
|
|
||||||
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, 3, 1)
|
__version__ = (1, 4, 2)
|
||||||
|
|
||||||
|
WEIGHTS = {
|
||||||
|
"inline.token_obtainment": 15,
|
||||||
|
"main": 10,
|
||||||
|
"inline": 7,
|
||||||
|
"translations": 5,
|
||||||
|
"security": 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFAULT_WEIGHT = 1
|
||||||
|
|
||||||
|
BASE_DIR = os.getcwd()
|
||||||
|
|
||||||
|
|
||||||
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(
|
||||||
@@ -54,6 +74,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}"
|
||||||
@@ -61,9 +82,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": (
|
||||||
@@ -79,9 +102,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>"
|
||||||
),
|
),
|
||||||
@@ -166,8 +187,15 @@ 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",
|
||||||
}
|
},
|
||||||
|
"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"
|
||||||
|
),
|
||||||
}
|
}
|
||||||
strings_ru = {
|
strings_ru = {
|
||||||
"name": "Limoka",
|
"name": "Limoka",
|
||||||
@@ -183,9 +211,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>"
|
||||||
),
|
),
|
||||||
@@ -218,7 +244,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?": "Запрос слишком короткий / не найден",
|
||||||
@@ -274,8 +300,15 @@ class Limoka(loader.Module):
|
|||||||
"hikkatrusted": "Hikka Trusted",
|
"hikkatrusted": "Hikka Trusted",
|
||||||
"nonactive": "Неактивный репозиторий",
|
"nonactive": "Неактивный репозиторий",
|
||||||
"nonlongermaintained": "Неподдерживаемый репозиторий",
|
"nonlongermaintained": "Неподдерживаемый репозиторий",
|
||||||
"newbie": "Новичок"
|
"newbie": "Новичок",
|
||||||
},
|
},
|
||||||
|
"indexing_in_progress": (
|
||||||
|
"⚠️ База данных занята, "
|
||||||
|
"попробуйте снова через несколько секунд. "
|
||||||
|
"Если ошибка сохраняется, попробуйте "
|
||||||
|
"удалить limoka_index в корневой папке юзербота. "
|
||||||
|
"Если ошибка сохраняется снова, сообщите разработчикам"
|
||||||
|
),
|
||||||
"_cls_doc": "Модули теперь в одном месте с простым и удобным поиском!",
|
"_cls_doc": "Модули теперь в одном месте с простым и удобным поиском!",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,18 +336,18 @@ class Limoka(loader.Module):
|
|||||||
)
|
)
|
||||||
self.name = self.strings["name"]
|
self.name = self.strings["name"]
|
||||||
self._invalid_banners = set()
|
self._invalid_banners = set()
|
||||||
self._service_bot_id = 8581621390
|
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 - No banner
|
||||||
"global_search": "global_search", # Глобальный поиск
|
"global_search": "global_search", # Global search
|
||||||
"not_found": "not_found", # Не найдено (модуль)
|
"not_found": "not_found", # Not found (module)
|
||||||
"filter_select": "filter_select", # Выбор категорий (фильтров)
|
"filter_select": "filter_select", # Select categories (filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
# State banners - placeholders for now
|
# State banners
|
||||||
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",
|
||||||
@@ -391,38 +424,49 @@ 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", {}
|
||||||
)
|
)
|
||||||
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["path"]: 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)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
self._bot_username = (await self.client.get_entity(self._service_bot_id)).username
|
self._service_bot_id = (await self.client.get_entity(self._bot_username)).id
|
||||||
await self._update_index()
|
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
self.ix_task = loop.run_in_executor(None, lambda: asyncio.run(self._update_index()))
|
||||||
|
|
||||||
if self.config["external_install_allowed"]:
|
if self.config["external_install_allowed"]:
|
||||||
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._service_bot_id, "/start")
|
message = await self.client.send_message(
|
||||||
|
self._bot_username, "/start"
|
||||||
|
)
|
||||||
await message.delete()
|
await message.delete()
|
||||||
|
await self.client(
|
||||||
|
functions.messages.DeleteHistoryRequest(
|
||||||
|
peer=self._bot_username,
|
||||||
|
max_id=0,
|
||||||
|
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)
|
||||||
@@ -431,29 +475,56 @@ class Limoka(loader.Module):
|
|||||||
await self._update_index()
|
await self._update_index()
|
||||||
|
|
||||||
async def _update_index(self):
|
async def _update_index(self):
|
||||||
writer = self.ix.writer()
|
try:
|
||||||
modules_to_index = self._filter_newbies(self.modules)
|
writer = self.ix.writer()
|
||||||
for module_path, module_data in modules_to_index.items():
|
modules_to_index = self._filter_newbies(self.modules)
|
||||||
writer.add_document(
|
for module_path, module_data in modules_to_index.items():
|
||||||
title=module_data["name"],
|
writer.add_document(
|
||||||
path=module_path,
|
title=module_data["name"],
|
||||||
content=module_data["name"] + " " + (module_data.get("description") or "" + " " + ((module_data.get("meta").get("developer") or "") if module_data.get("meta") else "")),
|
path=module_path,
|
||||||
)
|
content=module_data["name"]
|
||||||
for func in module_data.get("commands", []):
|
+ " "
|
||||||
for command, description in func.items():
|
+ (
|
||||||
writer.add_document(
|
module_data.get("description")
|
||||||
title=module_data["name"],
|
or ""
|
||||||
path=module_path,
|
+ " "
|
||||||
content=f"{command} {description}",
|
+ (
|
||||||
|
(module_data.get("meta").get("developer") or "")
|
||||||
|
if module_data.get("meta")
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for func in module_data.get("commands", []):
|
||||||
|
for command, description in func.items():
|
||||||
|
writer.add_document(
|
||||||
|
title=module_data["name"],
|
||||||
|
path=module_path,
|
||||||
|
content=f"{command} {description}",
|
||||||
|
)
|
||||||
|
writer.commit()
|
||||||
|
except LockError:
|
||||||
|
folder = os.path.join(BASE_DIR, "limoka_search")
|
||||||
|
|
||||||
|
if os.path.commonpath([folder, BASE_DIR]) == BASE_DIR and os.path.exists(folder):
|
||||||
|
shutil.rmtree(folder)
|
||||||
|
await self._update_index()
|
||||||
|
else:
|
||||||
|
logger.error(
|
||||||
|
(
|
||||||
|
f"Skipping unsafe rmtree for {folder}. Please, report this to developer. ",
|
||||||
|
f"Debug info: folder={folder}, base_dir={BASE_DIR}, common_path={os.path.commonpath([folder, BASE_DIR])}, exists={os.path.exists(folder)}",
|
||||||
)
|
)
|
||||||
writer.commit()
|
)
|
||||||
|
|
||||||
async def _validate_url(self, url: str) -> Optional[str]:
|
async def _validate_url(self, url: str) -> Optional[str]:
|
||||||
if not url or url in self._invalid_banners:
|
if not url or url in self._invalid_banners:
|
||||||
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
|
||||||
@@ -467,23 +538,57 @@ class Limoka(loader.Module):
|
|||||||
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 = []
|
||||||
for i, func in enumerate(module_info.get("commands", []), 1):
|
for i, cmd in enumerate(module_info.get("new_commands", []), 1):
|
||||||
for command, description in func.items():
|
name = cmd.get("name", "")
|
||||||
emoji = self.strings["emojis"].get(i, "")
|
desc_map = cmd.get("description", {})
|
||||||
desc = description or self.strings["no_info"]
|
emoji = self.strings["emojis"].get(i, "")
|
||||||
commands.append(
|
desc = _get_lang_value(desc_map, lang) or self.strings["no_info"]
|
||||||
self.strings["command_template"].format(
|
commands.append(
|
||||||
prefix=self.get_prefix(),
|
self.strings["command_template"].format(
|
||||||
command=html.escape(command.replace("cmd", "")),
|
prefix=self.get_prefix(),
|
||||||
emoji=emoji,
|
command=html.escape(name),
|
||||||
description=html.escape(desc),
|
emoji=emoji,
|
||||||
)
|
description=html.escape(desc),
|
||||||
)
|
)
|
||||||
|
)
|
||||||
for i, handler in enumerate(module_info.get("inline_handlers", []), 1):
|
for i, handler in enumerate(module_info.get("inline_handlers", []), 1):
|
||||||
name = handler.get("name", "")
|
name = handler.get("name", "")
|
||||||
desc_map = handler.get("description", {})
|
desc_map = handler.get("description", {})
|
||||||
@@ -509,12 +614,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 = ""
|
||||||
@@ -526,10 +633,12 @@ 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 == repo_key:
|
if x.replace("https://github.com/", "") == repo_key:
|
||||||
tags_list = self.repositories.get(x, {}).get("tags", [])
|
tags_list = self.repositories.get(x, {}).get("tags", [])
|
||||||
break
|
break
|
||||||
tags_text = ", ".join(self.strings["tags"].get(tag, tag) for tag in tags_list)
|
tags_text = ", ".join(self.strings["tags"].get(tag, tag) for tag in tags_list)
|
||||||
@@ -566,9 +675,7 @@ 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"]
|
||||||
@@ -585,7 +692,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 (),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -614,7 +725,11 @@ 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"]
|
||||||
@@ -623,51 +738,80 @@ class Limoka(loader.Module):
|
|||||||
|
|
||||||
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"}]
|
||||||
)
|
)
|
||||||
@@ -698,7 +842,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:
|
||||||
@@ -720,7 +866,10 @@ class Limoka(loader.Module):
|
|||||||
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)
|
||||||
|
|
||||||
@@ -739,11 +888,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):
|
||||||
@@ -752,27 +901,42 @@ 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"]
|
||||||
|
|
||||||
@@ -812,11 +976,11 @@ 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"]
|
||||||
|
|
||||||
@@ -981,9 +1145,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
|
||||||
@@ -1087,11 +1249,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={},
|
||||||
|
),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@@ -1160,13 +1324,9 @@ 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"]
|
||||||
|
|
||||||
@@ -1179,13 +1339,9 @@ class Limoka(loader.Module):
|
|||||||
|
|
||||||
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"]
|
||||||
|
|
||||||
@@ -1198,9 +1354,7 @@ class Limoka(loader.Module):
|
|||||||
|
|
||||||
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()
|
||||||
@@ -1209,6 +1363,11 @@ class Limoka(loader.Module):
|
|||||||
async def limokacmd(self, message: Message):
|
async def limokacmd(self, message: Message):
|
||||||
"""[query / nothing] - Search modules"""
|
"""[query / nothing] - Search modules"""
|
||||||
args = utils.get_args_raw(message)
|
args = utils.get_args_raw(message)
|
||||||
|
lock_path = os.path.join(BASE_DIR, "limoka_search", "index.lock")
|
||||||
|
|
||||||
|
if os.path.exists(lock_path):
|
||||||
|
await utils.answer(message, self.strings["indexing_in_progress"])
|
||||||
|
return
|
||||||
if not args:
|
if not args:
|
||||||
markup = [
|
markup = [
|
||||||
[
|
[
|
||||||
@@ -1233,7 +1392,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", [])
|
||||||
@@ -1267,7 +1426,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 = [
|
||||||
@@ -1298,12 +1459,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["?"],
|
||||||
@@ -1431,16 +1592,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)
|
||||||
@@ -1469,25 +1634,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)
|
||||||
@@ -1511,20 +1688,26 @@ 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:
|
||||||
|
|||||||
1420
LimokaLegacy.py
Normal file
1420
LimokaLegacy.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -23,4 +23,4 @@ class K(loader.Module):
|
|||||||
"""K"""
|
"""K"""
|
||||||
raise Exception("Testing error handling")
|
raise Exception("Testing error handling")
|
||||||
await utils.answer(message, "K")
|
await utils.answer(message, "K")
|
||||||
# why
|
# why FUCK YOU BILL GATES ЛЛЛЛ
|
||||||
|
|||||||
@@ -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, 1, 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
|
||||||
@@ -59,6 +59,7 @@ logger = logging.getLogger(__name__)
|
|||||||
DB_HISTORY_KEY = "gemini_conversations_v4"
|
DB_HISTORY_KEY = "gemini_conversations_v4"
|
||||||
DB_GAUTO_HISTORY_KEY = "gemini_gauto_conversations_v1"
|
DB_GAUTO_HISTORY_KEY = "gemini_gauto_conversations_v1"
|
||||||
DB_IMPERSONATION_KEY = "gemini_impersonation_chats"
|
DB_IMPERSONATION_KEY = "gemini_impersonation_chats"
|
||||||
|
DB_PRESETS_KEY = "gemini_prompt_presets"
|
||||||
GEMINI_TIMEOUT = 840
|
GEMINI_TIMEOUT = 840
|
||||||
MAX_FFMPEG_SIZE = 90 * 1024 * 1024
|
MAX_FFMPEG_SIZE = 90 * 1024 * 1024
|
||||||
DB_KEY_MAP_KEY = "gemini_key_model_map"
|
DB_KEY_MAP_KEY = "gemini_key_model_map"
|
||||||
@@ -84,7 +85,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>",
|
||||||
@@ -150,6 +159,21 @@ class Gemini(loader.Module):
|
|||||||
"gmodel_no_models": "⚠️ Не удалось получить список моделей.",
|
"gmodel_no_models": "⚠️ Не удалось получить список моделей.",
|
||||||
"gmodel_list_error": "❗️ Ошибка получения списка: {}",
|
"gmodel_list_error": "❗️ Ошибка получения списка: {}",
|
||||||
"gimg_process": "<emoji document_id=5325547803936572038>✨</emoji> <b>Генерация...</b>\n🧠 <i>Модель: {model}</i>",
|
"gimg_process": "<emoji document_id=5325547803936572038>✨</emoji> <b>Генерация...</b>\n🧠 <i>Модель: {model}</i>",
|
||||||
|
"gprompt_usage": "ℹ️ <b>Использование:</b>\n<code>.gprompt <текст/пресет></code> — установить.\n<code>.gprompt -c</code> — очистить.\n<code>.gpresets</code> — база пресетов.",
|
||||||
|
"gpresets_usage": (
|
||||||
|
"ℹ️ <b>Управление пресетами:</b>\n"
|
||||||
|
"• <code>.gpresets save [Имя] текст</code> — сохранить (имя в скобках, если с пробелами).\n"
|
||||||
|
"• <code>.gpresets load 1</code> или <code>имя</code> — загрузить по номеру/имени.\n"
|
||||||
|
"• <code>.gpresets del 1</code> или <code>имя</code> — удалить.\n"
|
||||||
|
"• <code>.gpresets list</code> — список."
|
||||||
|
),
|
||||||
|
"gpreset_loaded": "✅ <b>Установлен пресет:</b> [<code>{}</code>]\nДлина: {} симв.",
|
||||||
|
"gpreset_saved": "💾 <b>Пресет сохранен!</b>\n🏷 <b>Имя:</b> {}\n№ <b>Индекс:</b> {}",
|
||||||
|
"gpreset_deleted": "🗑 <b>Пресет удален:</b> {}",
|
||||||
|
"gpreset_not_found": "🚫 Пресет с таким именем или индексом не найден.",
|
||||||
|
"gpreset_list_head": "📋 <b>Ваши пресеты:</b>\n",
|
||||||
|
"gpreset_empty": "📂 Список пресетов пуст.",
|
||||||
|
|
||||||
}
|
}
|
||||||
TEXT_MIME_TYPES = {
|
TEXT_MIME_TYPES = {
|
||||||
"text/plain", "text/markdown", "text/html", "text/css", "text/csv",
|
"text/plain", "text/markdown", "text/html", "text/css", "text/csv",
|
||||||
@@ -159,6 +183,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()),
|
||||||
@@ -184,6 +210,7 @@ class Gemini(loader.Module):
|
|||||||
loader.ConfigValue("inline_pagination", False, self.strings["cfg_inline_pagination_doc"], validator=loader.validators.Boolean()),
|
loader.ConfigValue("inline_pagination", False, self.strings["cfg_inline_pagination_doc"], validator=loader.validators.Boolean()),
|
||||||
loader.ConfigValue("image_model_name", "gemini-2.5-flash-image", self.strings["cfg_image_model_doc"]),
|
loader.ConfigValue("image_model_name", "gemini-2.5-flash-image", self.strings["cfg_image_model_doc"]),
|
||||||
)
|
)
|
||||||
|
self.prompt_presets = []
|
||||||
self.conversations = {}
|
self.conversations = {}
|
||||||
self.gauto_conversations = {}
|
self.gauto_conversations = {}
|
||||||
self.last_requests = {}
|
self.last_requests = {}
|
||||||
@@ -211,6 +238,9 @@ class Gemini(loader.Module):
|
|||||||
return
|
return
|
||||||
self.current_api_key_index = 0
|
self.current_api_key_index = 0
|
||||||
self.conversations = self._load_history_from_db(DB_HISTORY_KEY)
|
self.conversations = self._load_history_from_db(DB_HISTORY_KEY)
|
||||||
|
self.prompt_presets = self.db.get(self.strings["name"], DB_PRESETS_KEY, [])
|
||||||
|
if isinstance(self.prompt_presets, dict):
|
||||||
|
self.prompt_presets = [{"name": k, "content": v} for k, v in self.prompt_presets.items()]
|
||||||
self.gauto_conversations = self._load_history_from_db(DB_GAUTO_HISTORY_KEY)
|
self.gauto_conversations = self._load_history_from_db(DB_GAUTO_HISTORY_KEY)
|
||||||
self.impersonation_chats = set(self.db.get(self.strings["name"], DB_IMPERSONATION_KEY, []))
|
self.impersonation_chats = set(self.db.get(self.strings["name"], DB_IMPERSONATION_KEY, []))
|
||||||
if not self.api_keys:
|
if not self.api_keys:
|
||||||
@@ -323,6 +353,74 @@ 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)
|
||||||
|
history_key = "global_context" if (self.config.get("global_memory") and not impersonation_mode) else str(chat_id)
|
||||||
|
raw_hist = self._get_structured_history(history_key, 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)
|
||||||
|
content_list = []
|
||||||
|
for p in current_turn_parts:
|
||||||
|
if hasattr(p, "text") and p.text:
|
||||||
|
content_list.append({"type": "text", "text": p.text})
|
||||||
|
elif hasattr(p, "inline_data") and p.inline_data:
|
||||||
|
mime = p.inline_data.mime_type
|
||||||
|
data = p.inline_data.data
|
||||||
|
if mime.startswith("image/"):
|
||||||
|
b64_img = base64.b64encode(data).decode("utf-8")
|
||||||
|
content_list.append({
|
||||||
|
"type": "image_url",
|
||||||
|
"image_url": {"url": f"data:{mime};base64,{b64_img}"}
|
||||||
|
})
|
||||||
|
if not content_list:
|
||||||
|
content_list = request_text_for_display
|
||||||
|
openai_messages.append({"role": "user", "content": content_list})
|
||||||
|
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(history_key, current_turn_parts, result_text, regeneration, msg_obj, gauto=impersonation_mode)
|
||||||
|
if impersonation_mode: return result_text
|
||||||
|
hist_len = len(self._get_structured_history(history_key)) // 2
|
||||||
|
mem_ind_fmt = self.strings.get("memory_status_global", self.strings["memory_status"])
|
||||||
|
if self.config.get("global_memory"):
|
||||||
|
mem_ind = mem_ind_fmt.format(hist_len)
|
||||||
|
else:
|
||||||
|
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:
|
||||||
@@ -616,32 +714,39 @@ class Gemini(loader.Module):
|
|||||||
|
|
||||||
@loader.command()
|
@loader.command()
|
||||||
async def gprompt(self, message: Message):
|
async def gprompt(self, message: Message):
|
||||||
"""[текст / -c / ответ на файл] — [-c (очистить)] / (ничего. увидеть промпт) Установить системный промпт (инструкцию/system_instruction)."""
|
"""<текст/-c/ответ на файл> — Установить промпт."""
|
||||||
args = utils.get_args_raw(message)
|
args = utils.get_args_raw(message)
|
||||||
reply = await message.get_reply_message()
|
reply = await message.get_reply_message()
|
||||||
if args == "-c":
|
if args == "-c":
|
||||||
self.config["system_instruction"] = ""
|
self.config["system_instruction"] = ""
|
||||||
return await utils.answer(message, self.strings["gprompt_cleared"])
|
return await utils.answer(message, self.strings["gprompt_cleared"])
|
||||||
new_p = None
|
new_prompt = None
|
||||||
if reply and reply.file:
|
preset = self._find_preset(args)
|
||||||
|
if preset:
|
||||||
|
new_prompt = preset['content']
|
||||||
|
elif reply and reply.file:
|
||||||
if reply.file.size > 1024 * 1024:
|
if reply.file.size > 1024 * 1024:
|
||||||
return await utils.answer(message, self.strings["gprompt_file_too_big"])
|
return await utils.answer(message, self.strings["gprompt_file_too_big"])
|
||||||
try:
|
try:
|
||||||
data = await self.client.download_file(reply.media, bytes)
|
file_data = await self.client.download_file(reply.media, bytes)
|
||||||
try: new_p = data.decode("utf-8")
|
try: new_prompt = file_data.decode("utf-8")
|
||||||
except UnicodeDecodeError: return await utils.answer(message, self.strings["gprompt_not_text"])
|
except UnicodeDecodeError: return await utils.answer(message, self.strings["gprompt_not_text"])
|
||||||
except Exception as e: return await utils.answer(message, self.strings["gprompt_file_error"].format(e))
|
except Exception as e:
|
||||||
elif args: new_p = args
|
return await utils.answer(message, self.strings["gprompt_file_error"].format(e))
|
||||||
if new_p:
|
elif args:
|
||||||
self.config["system_instruction"] = new_p
|
new_prompt = args
|
||||||
return await utils.answer(message, self.strings["gprompt_updated"].format(len(new_p)))
|
if new_prompt is not None:
|
||||||
cur = self.config["system_instruction"]
|
self.config["system_instruction"] = new_prompt
|
||||||
if not cur: return await utils.answer(message, self.strings["gprompt_usage"])
|
return await utils.answer(message, self.strings["gprompt_updated"].format(len(new_prompt)))
|
||||||
if len(cur) > 4000:
|
current_prompt = self.config["system_instruction"]
|
||||||
file = io.BytesIO(cur.encode("utf-8")); file.name = "system_instruction.txt"
|
if not current_prompt:
|
||||||
|
return await utils.answer(message, self.strings["gprompt_usage"])
|
||||||
|
if len(current_prompt) > 4000:
|
||||||
|
file = io.BytesIO(current_prompt.encode("utf-8"))
|
||||||
|
file.name = "system_instruction.txt"
|
||||||
await utils.answer(message, self.strings["gprompt_current"], file=file)
|
await utils.answer(message, self.strings["gprompt_current"], file=file)
|
||||||
else:
|
else:
|
||||||
await utils.answer(message, f"{self.strings['gprompt_current']}\n<code>{utils.escape_html(cur)}</code>")
|
await utils.answer(message, f"{self.strings['gprompt_current']}\n<code>{utils.escape_html(current_prompt)}</code>")
|
||||||
|
|
||||||
@loader.command()
|
@loader.command()
|
||||||
async def gauto(self, message: Message):
|
async def gauto(self, message: Message):
|
||||||
@@ -700,6 +805,64 @@ class Gemini(loader.Module):
|
|||||||
else:
|
else:
|
||||||
await utils.answer(message, self.strings["gclear_usage"])
|
await utils.answer(message, self.strings["gclear_usage"])
|
||||||
|
|
||||||
|
@loader.command()
|
||||||
|
async def gpresets(self, message: Message):
|
||||||
|
"""<save/load/del/list> — Управление пресетами (профилями)."""
|
||||||
|
args = utils.get_args_raw(message)
|
||||||
|
if not args: return await utils.answer(message, self.strings["gpresets_usage"])
|
||||||
|
match = re.match(r"^(\w+)(?:\s+\[(.+?)\]|\s+(\S+))?(?:\s+(.*))?$", args, re.DOTALL)
|
||||||
|
if not match: return await utils.answer(message, self.strings["gpresets_usage"])
|
||||||
|
action = match.group(1).lower()
|
||||||
|
name = match.group(2) or match.group(3)
|
||||||
|
content = match.group(4)
|
||||||
|
if action == "list":
|
||||||
|
if not self.prompt_presets: return await utils.answer(message, self.strings["gpreset_empty"])
|
||||||
|
text = self.strings["gpreset_list_head"]
|
||||||
|
for idx, p in enumerate(self.prompt_presets, 1):
|
||||||
|
text += f"<b>{idx}.</b> <code>{p['name']}</code> ({len(p['content'])} симв.)\n"
|
||||||
|
return await utils.answer(message, text)
|
||||||
|
if action == "save":
|
||||||
|
if not name: return await utils.answer(message, "❌ Укажите имя: <code>.gpresets save [Имя] текст</code>")
|
||||||
|
reply = await message.get_reply_message()
|
||||||
|
if not content and reply:
|
||||||
|
if reply.text: content = reply.text
|
||||||
|
elif reply.file:
|
||||||
|
try: content = (await self.client.download_file(reply.media, bytes)).decode("utf-8", errors="ignore")
|
||||||
|
except: pass
|
||||||
|
if not content: return await utils.answer(message, "❌ Нет текста для сохранения.")
|
||||||
|
existing = self._find_preset(name)
|
||||||
|
if existing:
|
||||||
|
existing['content'] = content
|
||||||
|
else:
|
||||||
|
self.prompt_presets.append({"name": name, "content": content})
|
||||||
|
self.db.set(self.strings["name"], DB_PRESETS_KEY, self.prompt_presets)
|
||||||
|
await utils.answer(message, self.strings["gpreset_saved"].format(name, len(self.prompt_presets)))
|
||||||
|
elif action == "load":
|
||||||
|
target = self._find_preset(name)
|
||||||
|
if not target: return await utils.answer(message, self.strings["gpreset_not_found"])
|
||||||
|
self.config["system_instruction"] = target['content']
|
||||||
|
await utils.answer(message, self.strings["gpreset_loaded"].format(target['name'], len(target['content'])))
|
||||||
|
elif action == "del":
|
||||||
|
target = self._find_preset(name)
|
||||||
|
if not target: return await utils.answer(message, self.strings["gpreset_not_found"])
|
||||||
|
self.prompt_presets.remove(target)
|
||||||
|
self.db.set(self.strings["name"], DB_PRESETS_KEY, self.prompt_presets)
|
||||||
|
await utils.answer(message, self.strings["gpreset_deleted"].format(target['name']))
|
||||||
|
else:
|
||||||
|
await utils.answer(message, self.strings["gpresets_usage"])
|
||||||
|
|
||||||
|
def _find_preset(self, query):
|
||||||
|
"Ищет пресет по номеру (строка '1') или имени."
|
||||||
|
if not query: return None
|
||||||
|
if str(query).isdigit():
|
||||||
|
idx = int(query) - 1
|
||||||
|
if 0 <= idx < len(self.prompt_presets):
|
||||||
|
return self.prompt_presets[idx]
|
||||||
|
for p in self.prompt_presets:
|
||||||
|
if p['name'].lower() == str(query).lower():
|
||||||
|
return p
|
||||||
|
return None
|
||||||
|
|
||||||
@loader.command()
|
@loader.command()
|
||||||
async def gmemdel(self, message: Message):
|
async def gmemdel(self, message: Message):
|
||||||
"""[N] — удалить последние N пар сообщений из памяти."""
|
"""[N] — удалить последние N пар сообщений из памяти."""
|
||||||
@@ -814,25 +977,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:
|
||||||
client = genai.Client(api_key=self.api_keys[0])
|
if provider == "openrouter":
|
||||||
models = await asyncio.to_thread(client.models.list)
|
api_key = self.config["Openrouter_api_key"]
|
||||||
txt = "\n".join([f"• <code>{m.name.split('/')[-1]}</code> ({m.display_name})" for m in models])
|
if not api_key: return await utils.answer(status_msg, self.strings['no_api_key_Openrouter'])
|
||||||
f = io.BytesIO((self.strings["gmodel_list_title"] + "\n" + txt).encode('utf-8'))
|
async with aiohttp.ClientSession() as session:
|
||||||
f.name = "models_list.txt"
|
async with session.get(
|
||||||
await self.client.send_file(message.chat_id, file=f, caption="📋 Список доступных моделей", reply_to=message.id)
|
"https://openrouter.ai/api/v1/models",
|
||||||
await sts.delete()
|
headers={"Authorization": f"Bearer {api_key}"}
|
||||||
except Exception as e: await utils.answer(sts, self.strings["gmodel_list_error"].format(self._handle_error(e)))
|
) 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])
|
||||||
|
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])
|
||||||
|
f = io.BytesIO((self.strings["gmodel_list_title"] + "\n" + txt).encode('utf-8'))
|
||||||
|
f.name = "models_list.txt"
|
||||||
|
await self.client.send_file(message.chat_id, file=f, caption="📋 Список доступных моделей", reply_to=message.id)
|
||||||
|
await status_msg.delete()
|
||||||
|
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 +1311,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 = next((a.alt for a in m.sticker.attributes if isinstance(a, DocumentAttributeSticker)), "?")
|
alt = "?"
|
||||||
|
if hasattr(m.sticker, 'attributes'):
|
||||||
|
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 +1416,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 +1521,54 @@ 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))
|
||||||
|
|||||||
267
ZetGoHack/nullmod/Gradientor.py
Normal file
267
ZetGoHack/nullmod/Gradientor.py
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
#░░░███░███░███░███░███
|
||||||
|
#░░░░░█░█░░░░█░░█░░░█░█
|
||||||
|
#░░░░█░░███░░█░░█░█░█░█
|
||||||
|
#░░░█░░░█░░░░█░░█░█░█░█
|
||||||
|
#░░░███░███░░█░░███░███
|
||||||
|
|
||||||
|
# meta developer: @nullmod
|
||||||
|
|
||||||
|
__version__ = (1, 0, 1)
|
||||||
|
|
||||||
|
import io
|
||||||
|
import math
|
||||||
|
|
||||||
|
from PIL import Image, ImageDraw
|
||||||
|
|
||||||
|
from herokutl.tl.custom import Message
|
||||||
|
from herokutl.tl.functions.help import (
|
||||||
|
GetPeerProfileColorsRequest
|
||||||
|
)
|
||||||
|
from herokutl.tl.types import (
|
||||||
|
EmojiStatusCollectible
|
||||||
|
)
|
||||||
|
|
||||||
|
from .. import loader, utils
|
||||||
|
|
||||||
|
def resize_image(image: Image.Image, max_size: int = 1280) -> Image.Image:
|
||||||
|
w, h = image.size
|
||||||
|
if max(w, h) <= max_size:
|
||||||
|
return image
|
||||||
|
else:
|
||||||
|
scale = max_size / max(w, h)
|
||||||
|
new_w = int(w * scale)
|
||||||
|
new_h = int(h * scale)
|
||||||
|
|
||||||
|
return image.resize((new_w, new_h), Image.LANCZOS)
|
||||||
|
|
||||||
|
# Source: https://gist.github.com/weihanglo/1e754ec47fdd683a42fdf6a272904535#file-draw_gradient_pillow-py
|
||||||
|
def get_gradient(size: tuple, color1: tuple, color2: tuple, gradient_type: str = "linear") -> Image.Image:
|
||||||
|
def interpolate(f_co, t_co, interval):
|
||||||
|
if interval <= 1:
|
||||||
|
yield list(t_co)
|
||||||
|
return
|
||||||
|
|
||||||
|
det_co = [(t - f) / (interval - 1) for f, t in zip(f_co, t_co)]
|
||||||
|
for i in range(interval):
|
||||||
|
yield [round(f + det * i) for f, det in zip(f_co, det_co)]
|
||||||
|
|
||||||
|
gradient = Image.new('RGB', size, color=(0, 0, 0))
|
||||||
|
draw = ImageDraw.Draw(gradient)
|
||||||
|
|
||||||
|
if gradient_type == "linear":
|
||||||
|
top_color, bottom_color = color1, color2
|
||||||
|
|
||||||
|
for y, color in enumerate(interpolate(top_color, bottom_color, max(1, size[1]))):
|
||||||
|
draw.line([(0, y), (size[0], y)], fill=tuple(color), width=1)
|
||||||
|
|
||||||
|
elif gradient_type == "radial":
|
||||||
|
center_color, edge_color = color1, color2
|
||||||
|
|
||||||
|
max_radius = math.hypot(size[0], size[1]) / 2.0
|
||||||
|
interval = max(1, int(math.ceil(max_radius)) + 1)
|
||||||
|
|
||||||
|
colors = list(interpolate(center_color, edge_color, interval))
|
||||||
|
|
||||||
|
cx = size[0] / 2
|
||||||
|
cy = size[1] / 2
|
||||||
|
|
||||||
|
for r_index, color in enumerate(colors):
|
||||||
|
r = interval - 1 - r_index
|
||||||
|
if r < 0:
|
||||||
|
continue
|
||||||
|
bbox = [
|
||||||
|
int(round(cx - r)),
|
||||||
|
int(round(cy - r)),
|
||||||
|
int(round(cx + r)),
|
||||||
|
int(round(cy + r))
|
||||||
|
]
|
||||||
|
draw.ellipse(bbox, fill=tuple(color))
|
||||||
|
|
||||||
|
return gradient
|
||||||
|
|
||||||
|
def set_gradient(im: io.BytesIO, gradient: Image.Image) -> io.BytesIO:
|
||||||
|
img = resize_image(Image.open(im).convert('RGBA'))
|
||||||
|
|
||||||
|
max_size = max(img.width, img.height)
|
||||||
|
gradient = gradient.resize((max_size, max_size), Image.LANCZOS).convert('RGBA')
|
||||||
|
left = (max_size - img.width) // 2
|
||||||
|
top = (max_size - img.height) // 2
|
||||||
|
gradient.paste(img, (left, top), img)
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
|
||||||
|
gradient.save(buffer, format='PNG')
|
||||||
|
|
||||||
|
buffer.seek(0)
|
||||||
|
return buffer
|
||||||
|
|
||||||
|
def crop_by_bbox(img: Image.Image, bbox: tuple = None):
|
||||||
|
img_w, img_h = img.size
|
||||||
|
x, y, w, h = bbox or BBOX_TGA_TGD
|
||||||
|
|
||||||
|
left = int(round(x * img_w))
|
||||||
|
top = int(round(y * img_h))
|
||||||
|
right = int(round((x + w) * img_w))
|
||||||
|
bottom = int(round((y + h) * img_h))
|
||||||
|
|
||||||
|
return img.crop((left, top, right, bottom))
|
||||||
|
|
||||||
|
|
||||||
|
def hex_to_rgb(value: int):
|
||||||
|
return ((value >> 16) & 255, (value >> 8) & 255, value & 255)
|
||||||
|
|
||||||
|
def hexes_to_rgbs(value: list):
|
||||||
|
if len(value) > 1:
|
||||||
|
res = list()
|
||||||
|
for i in value:
|
||||||
|
res.append(hex_to_rgb(i))
|
||||||
|
|
||||||
|
return tuple(res)
|
||||||
|
else:
|
||||||
|
res = hex_to_rgb(value[0])
|
||||||
|
return (res, res)
|
||||||
|
|
||||||
|
SHAPES = {
|
||||||
|
# TODO: фигуры для создания масок на авы
|
||||||
|
}
|
||||||
|
|
||||||
|
BBOX_TGA_TGD = (
|
||||||
|
2894 / 8268,
|
||||||
|
1260 / 8268,
|
||||||
|
2504 / 8268,
|
||||||
|
2504 / 8268,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@loader.tds
|
||||||
|
class Gradientor(loader.Module):
|
||||||
|
strings = {
|
||||||
|
"name": "Gradientor",
|
||||||
|
"_cls_doc": "A module to create your profile picture with a background from your profile",
|
||||||
|
"gradient_creating": "<tg-emoji emoji-id=5886667040432853038>🔁</tg-emoji> Creating gradient...",
|
||||||
|
"gradient_created": "<tg-emoji emoji-id=5818804345247894731>✅</tg-emoji> Gradient created!",
|
||||||
|
}
|
||||||
|
strings_ru = {
|
||||||
|
"_cls_doc": "Модуль для создания вашей аватарки на фоне из вашего профиля",
|
||||||
|
"gradient_creating": "<tg-emoji emoji-id=5886667040432853038>🔁</tg-emoji> Создание градиента...",
|
||||||
|
"gradient_created": "<tg-emoji emoji-id=5818804345247894731>✅</tg-emoji> Градиент создан!",
|
||||||
|
}
|
||||||
|
|
||||||
|
async def client_ready(self):
|
||||||
|
self.colors = self.get("PROFILE_COLORS", None)
|
||||||
|
if not self.colors or not self.colors.get("light", None):
|
||||||
|
raw_colors = (await self.client(GetPeerProfileColorsRequest(0))).colors
|
||||||
|
self.colors = {
|
||||||
|
"dark": {
|
||||||
|
str(col.color_id): hexes_to_rgbs(col.dark_colors.bg_colors) for col
|
||||||
|
in raw_colors
|
||||||
|
},
|
||||||
|
"light": {
|
||||||
|
str(col.color_id): hexes_to_rgbs(col.colors.bg_colors) for col
|
||||||
|
in raw_colors
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
self.set("PROFILE_COLORS", self.colors)
|
||||||
|
|
||||||
|
@loader.command(
|
||||||
|
ru_doc="[фотография/reply] - создать аватарку с градиентом из цвета профиля\n"
|
||||||
|
"--update-cache - обновить кеш профиля, если вы только что сменили фон профиля\n"
|
||||||
|
"--linear - использовать линейный градиент\n"
|
||||||
|
"--light - использовать светлую тему"
|
||||||
|
)
|
||||||
|
async def makepp(self, message: Message):
|
||||||
|
"""[photo/reply] - create a profile picture with a gradient from profile color\n
|
||||||
|
--update-cache - update profile cache if you just changed profile background\n
|
||||||
|
--linear - use linear gradient\n
|
||||||
|
--light - use light theme"""
|
||||||
|
reply: Message = await message.get_reply_message()
|
||||||
|
args = utils.get_args(message)
|
||||||
|
|
||||||
|
if "--update-cache" in args:
|
||||||
|
upd_cache = True
|
||||||
|
args.remove("--update-cache")
|
||||||
|
else:
|
||||||
|
upd_cache = False
|
||||||
|
|
||||||
|
if "--linear" in args:
|
||||||
|
force_linear = True
|
||||||
|
args.remove("--linear")
|
||||||
|
else:
|
||||||
|
force_linear = False
|
||||||
|
|
||||||
|
if "--light" in args:
|
||||||
|
theme = "light"
|
||||||
|
args.remove("--light")
|
||||||
|
else:
|
||||||
|
theme = "dark"
|
||||||
|
|
||||||
|
if "--full" in args:
|
||||||
|
_full = True
|
||||||
|
args.remove("--full")
|
||||||
|
else:
|
||||||
|
_full = False
|
||||||
|
|
||||||
|
user = None
|
||||||
|
background_only = False
|
||||||
|
|
||||||
|
if args:
|
||||||
|
user = await self.client.get_entity(int(args[0]) if args[0].isdigit() else args[0])
|
||||||
|
|
||||||
|
photo_source = (
|
||||||
|
message
|
||||||
|
if (not reply or not (reply.photo or reply.document and "image/" in getattr(reply.document, "mime_type", "")))
|
||||||
|
else reply
|
||||||
|
)
|
||||||
|
if not (photo_source.photo or photo_source.document and "image/" in getattr(photo_source.document, "mime_type", "")):
|
||||||
|
background_only = True
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
if upd_cache:
|
||||||
|
user = self.client.hikka_me = await self.client.get_me()
|
||||||
|
elif reply:
|
||||||
|
user = reply.sender
|
||||||
|
else:
|
||||||
|
user = self.client.hikka_me
|
||||||
|
|
||||||
|
if not user.premium:
|
||||||
|
color1, color2 = (28, 28, 28), (28, 28, 28)
|
||||||
|
|
||||||
|
elif user.emoji_status and isinstance(user.emoji_status, EmojiStatusCollectible):
|
||||||
|
color1, color2 = (
|
||||||
|
user.emoji_status.edge_color, user.emoji_status.center_color
|
||||||
|
)
|
||||||
|
color1 = hex_to_rgb(color1)
|
||||||
|
color2 = hex_to_rgb(color2)
|
||||||
|
|
||||||
|
elif user.profile_color:
|
||||||
|
color_variant = user.profile_color.color
|
||||||
|
|
||||||
|
color1, color2 = self.colors.get(theme).get(
|
||||||
|
str(color_variant),
|
||||||
|
((28, 28, 28), (28, 28, 28))
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
color1, color2 = (28, 28, 28), (28, 28, 28)
|
||||||
|
|
||||||
|
await utils.answer(message, self.strings["gradient_creating"])
|
||||||
|
|
||||||
|
gradient = get_gradient((1280, 1280), color1, color2, "linear" if force_linear else "radial")
|
||||||
|
if not _full:
|
||||||
|
gradient = crop_by_bbox(gradient)
|
||||||
|
|
||||||
|
if not background_only and not _full:
|
||||||
|
p_b = await photo_source.download_media(bytes)
|
||||||
|
p_b_io = io.BytesIO(p_b)
|
||||||
|
p_b_io.seek(0)
|
||||||
|
|
||||||
|
result = set_gradient(p_b_io, gradient)
|
||||||
|
|
||||||
|
else:
|
||||||
|
result = io.BytesIO()
|
||||||
|
gradient.save(result, format='PNG')
|
||||||
|
result.seek(0)
|
||||||
|
|
||||||
|
result.name = "grad @nullmod.png"
|
||||||
|
|
||||||
|
await utils.answer(message, self.strings["gradient_created"], file=result, force_document=True)
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
Chess
|
Chess
|
||||||
HaremManager
|
HaremManager
|
||||||
SchedulePlus
|
SchedulePlus
|
||||||
|
Gradientor
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
__version__ = (3, 1, 1)
|
__version__ = (3, 1, 1)
|
||||||
# meta banner: https://raw.githubusercontent.com/kamekuro/hikka-mods/main/banners/yamusic.png
|
# meta banner: https://raw.githubusercontent.com/kamekuro/hikka-mods/main/banners/yamusic.png
|
||||||
# packurl: https://raw.githubusercontent.com/coddrago/assets/refs/heads/main/modules/yamusic.yml
|
# packurl: https://raw.githubusercontent.com/coddrago/assets/refs/heads/main/modules/yamusic.yml
|
||||||
# meta pic: https://raw.githubusercontent.com/kamekuro/hikka-mods/main/icons/yamusic.png
|
# meta banner: https://raw.githubusercontent.com/coddrago/modules/refs/heads/main/banner.png
|
||||||
# meta developer: @codrago
|
# meta developer: @codrago_m
|
||||||
# old meta dev: @kamekuro xuesos
|
# old meta dev: @kamekuro xuesos
|
||||||
# scope: heroku_only
|
# scope: heroku_only
|
||||||
# scope: heroku_min 1.7.2
|
# scope: heroku_min 1.7.2
|
||||||
@@ -41,6 +41,7 @@ class Banners:
|
|||||||
meta_info: str = "Music",
|
meta_info: str = "Music",
|
||||||
is_liked: bool = False,
|
is_liked: bool = False,
|
||||||
repeat_mode: str = "NONE",
|
repeat_mode: str = "NONE",
|
||||||
|
blur: int = 0,
|
||||||
):
|
):
|
||||||
self.title = title
|
self.title = title
|
||||||
self.artists = artists
|
self.artists = artists
|
||||||
@@ -52,6 +53,7 @@ class Banners:
|
|||||||
self.meta_info = meta_info
|
self.meta_info = meta_info
|
||||||
self.is_liked = is_liked
|
self.is_liked = is_liked
|
||||||
self.repeat_mode = repeat_mode
|
self.repeat_mode = repeat_mode
|
||||||
|
self.blur = blur
|
||||||
|
|
||||||
def ultra(self) -> io.BytesIO:
|
def ultra(self) -> io.BytesIO:
|
||||||
WIDTH, HEIGHT = 2560, 1220
|
WIDTH, HEIGHT = 2560, 1220
|
||||||
@@ -96,7 +98,9 @@ class Banners:
|
|||||||
background = background.crop((0, offset, bg_w, offset + new_h))
|
background = background.crop((0, offset, bg_w, offset + new_h))
|
||||||
|
|
||||||
background = background.resize((WIDTH, HEIGHT), Image.Resampling.LANCZOS)
|
background = background.resize((WIDTH, HEIGHT), Image.Resampling.LANCZOS)
|
||||||
background = background.filter(ImageFilter.GaussianBlur(radius=0))
|
|
||||||
|
if self.blur > 0:
|
||||||
|
background = background.filter(ImageFilter.GaussianBlur(radius=self.blur))
|
||||||
|
|
||||||
dark_overlay = Image.new("RGBA", (WIDTH, HEIGHT), (0, 0, 0, 180))
|
dark_overlay = Image.new("RGBA", (WIDTH, HEIGHT), (0, 0, 0, 180))
|
||||||
background = Image.alpha_composite(background, dark_overlay)
|
background = Image.alpha_composite(background, dark_overlay)
|
||||||
@@ -296,30 +300,6 @@ class Banners:
|
|||||||
(heart_x, icon_y_center + heart_size + 5),
|
(heart_x, icon_y_center + heart_size + 5),
|
||||||
]
|
]
|
||||||
|
|
||||||
if self.is_liked:
|
|
||||||
draw.ellipse(c1_box, fill="red", outline="red")
|
|
||||||
draw.ellipse(c2_box, fill="red", outline="red")
|
|
||||||
draw.polygon(tri_points, fill="red", outline="red")
|
|
||||||
else:
|
|
||||||
draw.ellipse(c1_box, fill=None, outline="red", width=3)
|
|
||||||
draw.ellipse(c2_box, fill=None, outline="red", width=3)
|
|
||||||
draw.line(
|
|
||||||
[
|
|
||||||
(heart_x - c_r * 2 + 1, icon_y_center),
|
|
||||||
(heart_x, icon_y_center + heart_size + 5),
|
|
||||||
],
|
|
||||||
fill="red",
|
|
||||||
width=3,
|
|
||||||
)
|
|
||||||
draw.line(
|
|
||||||
[
|
|
||||||
(heart_x + c_r * 2 - 1, icon_y_center),
|
|
||||||
(heart_x, icon_y_center + heart_size + 5),
|
|
||||||
],
|
|
||||||
fill="red",
|
|
||||||
width=3,
|
|
||||||
)
|
|
||||||
|
|
||||||
by = io.BytesIO()
|
by = io.BytesIO()
|
||||||
background.save(by, format="PNG")
|
background.save(by, format="PNG")
|
||||||
by.seek(0)
|
by.seek(0)
|
||||||
@@ -378,8 +358,13 @@ class YaMusicMod(loader.Module):
|
|||||||
option="banner_version",
|
option="banner_version",
|
||||||
default="ultra",
|
default="ultra",
|
||||||
doc=lambda: self.strings["_cfg"]["banner_version"],
|
doc=lambda: self.strings["_cfg"]["banner_version"],
|
||||||
validator=loader.validators.Choice(["old", "new", "ultra"]),
|
validator=loader.validators.Choice(["ultra"]),
|
||||||
),)
|
),
|
||||||
|
loader.ConfigValue(
|
||||||
|
option="blur",
|
||||||
|
default=0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
self.ym_client = None
|
self.ym_client = None
|
||||||
self.device_id = "".join(random.choices(string.ascii_lowercase, k=16))
|
self.device_id = "".join(random.choices(string.ascii_lowercase, k=16))
|
||||||
@@ -392,6 +377,7 @@ class YaMusicMod(loader.Module):
|
|||||||
#"now_play", self._now_play_placeholder, "placeholder for nowplay music"
|
#"now_play", self._now_play_placeholder, "placeholder for nowplay music"
|
||||||
# Heroku 2.0.0 feature
|
# Heroku 2.0.0 feature
|
||||||
#)
|
#)
|
||||||
|
#utils.register_placeholder("duration", self._duration_placeholder, "progress bar")
|
||||||
|
|
||||||
if not self.get("guide_sent", False):
|
if not self.get("guide_sent", False):
|
||||||
await self.inline.bot.send_message(self._tg_id, self.strings("iguide"))
|
await self.inline.bot.send_message(self._tg_id, self.strings("iguide"))
|
||||||
@@ -437,7 +423,7 @@ class YaMusicMod(loader.Module):
|
|||||||
me = await self._client.get_me()
|
me = await self._client.get_me()
|
||||||
self._premium = me.premium if hasattr(me, "premium") else False
|
self._premium = me.premium if hasattr(me, "premium") else False
|
||||||
|
|
||||||
@loader.loop(30)
|
@loader.loop(15)
|
||||||
async def autobio(self):
|
async def autobio(self):
|
||||||
if not self.config["token"]:
|
if not self.config["token"]:
|
||||||
self.autobio.stop()
|
self.autobio.stop()
|
||||||
@@ -547,6 +533,88 @@ class YaMusicMod(loader.Module):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _duration_placeholder(self):
|
||||||
|
"""Placeholder for {duration} with custom emoji bar"""
|
||||||
|
if not self.config["token"]:
|
||||||
|
return "No Token"
|
||||||
|
|
||||||
|
try:
|
||||||
|
now = await self.__get_now_playing()
|
||||||
|
if not now or now.get("paused"):
|
||||||
|
return "<code>Not Playing</code>"
|
||||||
|
|
||||||
|
duration = now.get("duration_ms", 0)
|
||||||
|
progress = now.get("progress_ms", 0)
|
||||||
|
|
||||||
|
if duration == 0:
|
||||||
|
return "0%"
|
||||||
|
|
||||||
|
percent = (progress / duration) * 100
|
||||||
|
|
||||||
|
s_less_10 = (
|
||||||
|
"<emoji document_id=5454137780454067986>➖</emoji>"
|
||||||
|
"<emoji document_id=6158923355173949539>⭐</emoji>"
|
||||||
|
"<emoji document_id=6159012102083188132>⭐</emoji>"
|
||||||
|
"<emoji document_id=6159012102083188132>⭐</emoji>"
|
||||||
|
"<emoji document_id=6158753257289158944>⭐</emoji>"
|
||||||
|
"<emoji document_id=6156700344526049665>⭐</emoji>"
|
||||||
|
)
|
||||||
|
|
||||||
|
s_10_to_20 = (
|
||||||
|
"<emoji document_id=5454137780454067986>➖</emoji>"
|
||||||
|
"<emoji document_id=6159095673556840262>⭐</emoji>"
|
||||||
|
"<emoji document_id=6159012102083188132>⭐</emoji>"
|
||||||
|
"<emoji document_id=6156933677214341691>⭐</emoji>"
|
||||||
|
"<emoji document_id=6158753257289158944>⭐</emoji>"
|
||||||
|
"<emoji document_id=6156700344526049665>⭐</emoji>"
|
||||||
|
)
|
||||||
|
|
||||||
|
s_30_to_40 = (
|
||||||
|
"<emoji document_id=5454137780454067986>➖</emoji>"
|
||||||
|
"<emoji document_id=5454397458471750662>➖</emoji>"
|
||||||
|
"<emoji document_id=5454397458471750662>➖</emoji>"
|
||||||
|
"<emoji document_id=6158923355173949539>⭐</emoji>"
|
||||||
|
"<emoji document_id=6159012102083188132>⭐</emoji>"
|
||||||
|
"<emoji document_id=6156700344526049665>⭐</emoji>"
|
||||||
|
)
|
||||||
|
|
||||||
|
s_over_50 = (
|
||||||
|
"<emoji document_id=5454137780454067986>➖</emoji>"
|
||||||
|
"<emoji document_id=5454397458471750662>➖</emoji>"
|
||||||
|
"<emoji document_id=5454397458471750662>➖</emoji>"
|
||||||
|
"<emoji document_id=5454397458471750662>➖</emoji>"
|
||||||
|
"<emoji document_id=6156933677214341691>⭐</emoji>"
|
||||||
|
"<emoji document_id=6156700344526049665>⭐</emoji>"
|
||||||
|
)
|
||||||
|
|
||||||
|
s_over_80 = (
|
||||||
|
"<emoji document_id=5454137780454067986>➖</emoji>"
|
||||||
|
"<emoji document_id=5454397458471750662>➖</emoji>"
|
||||||
|
"<emoji document_id=5454397458471750662>➖</emoji>"
|
||||||
|
"<emoji document_id=5454397458471750662>➖</emoji>"
|
||||||
|
"<emoji document_id=5454397458471750662>➖</emoji>"
|
||||||
|
"<emoji document_id=6156700344526049665>⭐</emoji>"
|
||||||
|
)
|
||||||
|
|
||||||
|
if percent < 10:
|
||||||
|
return s_less_10
|
||||||
|
elif percent < 20:
|
||||||
|
return s_10_to_20
|
||||||
|
elif percent < 30:
|
||||||
|
return s_10_to_20
|
||||||
|
elif percent < 40:
|
||||||
|
return s_30_to_40
|
||||||
|
elif percent < 50:
|
||||||
|
return s_30_to_40
|
||||||
|
elif percent < 80:
|
||||||
|
return s_over_50
|
||||||
|
else:
|
||||||
|
return s_over_80
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return f"Error: {e}"
|
||||||
|
|
||||||
async def _download_bytes(self, url: str) -> typing.Optional[bytes]:
|
async def _download_bytes(self, url: str) -> typing.Optional[bytes]:
|
||||||
try:
|
try:
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
@@ -686,8 +754,10 @@ class YaMusicMod(loader.Module):
|
|||||||
meta_info=meta_info,
|
meta_info=meta_info,
|
||||||
is_liked=is_liked,
|
is_liked=is_liked,
|
||||||
repeat_mode=repeat_mode,
|
repeat_mode=repeat_mode,
|
||||||
|
blur=self.config["blur"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
file = await utils.run_sync(
|
file = await utils.run_sync(
|
||||||
getattr(banners, self.config["banner_version"], banners.ultra)
|
getattr(banners, self.config["banner_version"], banners.ultra)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ figlet
|
|||||||
promoclaimer
|
promoclaimer
|
||||||
passwordgen
|
passwordgen
|
||||||
send
|
send
|
||||||
lastfm
|
|
||||||
dbmod
|
dbmod
|
||||||
chatmodule
|
chatmodule
|
||||||
stats
|
stats
|
||||||
|
|||||||
@@ -1,124 +0,0 @@
|
|||||||
# ---------------------------------------------------------------------------------
|
|
||||||
#░█▀▄░▄▀▀▄░█▀▄░█▀▀▄░█▀▀▄░█▀▀▀░▄▀▀▄░░░█▀▄▀█
|
|
||||||
#░█░░░█░░█░█░█░█▄▄▀░█▄▄█░█░▀▄░█░░█░░░█░▀░█
|
|
||||||
#░▀▀▀░░▀▀░░▀▀░░▀░▀▀░▀░░▀░▀▀▀▀░░▀▀░░░░▀░░▒▀
|
|
||||||
# Name: LastFM
|
|
||||||
# Description: Module for music from different services
|
|
||||||
# Author: @codrago_m
|
|
||||||
# ---------------------------------------------------------------------------------
|
|
||||||
# 🔒 Licensed under the GNU AGPLv3
|
|
||||||
# 🌐 https://www.gnu.org/licenses/agpl-3.0.html
|
|
||||||
# ---------------------------------------------------------------------------------
|
|
||||||
# Author: @codrago
|
|
||||||
# Commands: nowplay
|
|
||||||
# scope: heroku_only
|
|
||||||
# meta developer: @codrago_m
|
|
||||||
# meta banner: https://raw.githubusercontent.com/coddrago/modules/refs/heads/main/banner.png
|
|
||||||
# meta pic: https://envs.sh/Hob.webp
|
|
||||||
# ---------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
from .. import loader, utils
|
|
||||||
from herokutl import events
|
|
||||||
import requests
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
|
|
||||||
@loader.tds
|
|
||||||
class lastfmmod(loader.Module):
|
|
||||||
"""Module for music from different services"""
|
|
||||||
def __init__(self):
|
|
||||||
self.config = loader.ModuleConfig(
|
|
||||||
loader.ConfigValue(
|
|
||||||
"username_lastfm",
|
|
||||||
None,
|
|
||||||
lambda: self.strings["_doc_username_lastfm"],
|
|
||||||
),
|
|
||||||
loader.ConfigValue(
|
|
||||||
"text",
|
|
||||||
"<emoji document_id=6007938409857815902>🎧</emoji> <b>now playing...</b>\n"
|
|
||||||
"<emoji document_id=5915480455603295660>🎶</emoji><b> playlist: </b><code>{song_album}</code>\n"
|
|
||||||
"<emoji document_id=5891249688933305846>🎵</emoji> <b>track:</b> <code>{song_name}</code>\n"
|
|
||||||
"<emoji document_id=5897554554894946515>🎤</emoji> <b>artist:</b> <code>{song_artist}</code>",
|
|
||||||
lambda: self.strings["_doc_text"],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
strings = {
|
|
||||||
"name": "LastFm",
|
|
||||||
"loading":"<emoji document_id=5873204392429096339>⌨️</emoji> Loading song...",
|
|
||||||
"bot_no_result": "<emoji document_id=5465665476971471368>❌</emoji> Nothing found.\nTitle: {song_name}\nAuthor: {song_artist}\nAlbum:{song_album}",
|
|
||||||
"_doc_text": "The text that will be written next to the file",
|
|
||||||
"_doc_username_lastfm": "Your username from last.fm",
|
|
||||||
"nick_error": "<emoji document_id=5465665476971471368>❌</emoji> Put your nickname from last.fm",
|
|
||||||
"tutorial": "Go to last.fm and register.\nBE SURE to remember the username and password, they will come in handy later.\nLet's look at the VK version\nAfter that, go to the @vkxci channel, download VK X and log in to your VK account, then go to settings and click «Integrations», select Last FM.\nEnter the username and password.\nThen you're almost done!\nWrite <code>{prefix}fcfg lastfm username_lastfm</code> {username}\nUse the <code>{prefix}nowplay</code> command and enjoy life!",
|
|
||||||
}
|
|
||||||
|
|
||||||
strings_ru = {
|
|
||||||
"name": "LastFm",
|
|
||||||
"loading": "<emoji document_id=5873204392429096339>⌨️</emoji> Загрузка трека...",
|
|
||||||
"bot_no_result": "<emoji document_id=5465665476971471368>❌</emoji> Ничего не найдено.\nНазвание: {song_name}\nИсполнитель: {song_artist}\nАльбом: {song_album}",
|
|
||||||
"_doc_text": "Текст, который будет написан рядом с файлом",
|
|
||||||
"_doc_username_lastfm": "Ваш username с last.fm",
|
|
||||||
"nick_error": "<emoji document_id=5465665476971471368>❌</emoji> Укажите ваш никнейм с last.fm",
|
|
||||||
"tutorial": "Зайдите на last.fm и зарегистрируйтесь.\nОБЯЗАТЕЛЬНО запомните логин и пароль, они пригодятся позже.\nРассмотрим вариант для VK\nПосле этого зайдите в канал @vkxci, скачайте VK X и авторизуйтесь в своём аккаунте VK, затем зайдите в настройки и нажмите «Интеграции», выберите Last FM.\nВведите логин и пароль.\nЗатем вы почти закончили!\nНапишите <code>{prefix}fcfg lastfm username_lastfm</code> {username}\nИспользуйте команду <code>{prefix}nowplay</code> и наслаждайтесь жизнью!",
|
|
||||||
}
|
|
||||||
|
|
||||||
@loader.command(alias="np")
|
|
||||||
async def nowplay(self, message):
|
|
||||||
"""| send playing track"""
|
|
||||||
|
|
||||||
lastfm_username = self.config["username_lastfm"]
|
|
||||||
API_KEY = "460cda35be2fbf4f28e8ea7a38580730" # Облегчение жизни школьникам
|
|
||||||
|
|
||||||
if not lastfm_username:
|
|
||||||
response_text = self.strings["nick_error"]
|
|
||||||
await self.invoke("config", "lastfm", message=message)
|
|
||||||
await utils.answer(message, response_text)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
current_track_url = f'http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&nowplaying=true&user={lastfm_username}&api_key={API_KEY}&format=json'
|
|
||||||
response = requests.get(current_track_url)
|
|
||||||
data = response.json()
|
|
||||||
|
|
||||||
if 'recenttracks' in data and 'track' in data['recenttracks'] and data['recenttracks']['track']:
|
|
||||||
nowplaying_track = None
|
|
||||||
for track in data['recenttracks']['track']:
|
|
||||||
if '@attr' in track and 'nowplaying' in track['@attr']:
|
|
||||||
nowplaying_track = track
|
|
||||||
break
|
|
||||||
|
|
||||||
if nowplaying_track:
|
|
||||||
song_name = nowplaying_track.get('name', 'Unknown song')
|
|
||||||
song_artist = nowplaying_track.get('artist', {}).get('#text', 'Unknown Artist')
|
|
||||||
if nowplaying_track.get('album', {}).get('#text') == nowplaying_track.get('name'):
|
|
||||||
song_album = "single"
|
|
||||||
else:
|
|
||||||
song_album = nowplaying_track.get('album', {}).get('#text', 'Unknown Album')
|
|
||||||
response_text = f"/search {song_name} - {song_artist}"
|
|
||||||
|
|
||||||
try:
|
|
||||||
async with message.client.conversation("@LyaDownbot") as conv:
|
|
||||||
await conv.send_message(response_text)
|
|
||||||
while True:
|
|
||||||
response_bot = await conv.get_response()
|
|
||||||
if "Не удалось найти трек" in response_bot.text:
|
|
||||||
await utils.answer(message, self.strings["bot_no_result"])
|
|
||||||
return
|
|
||||||
|
|
||||||
if "Ищем треки..." in response_bot.text:
|
|
||||||
await utils.answer(message, self.strings["loading"])
|
|
||||||
|
|
||||||
if response_bot.media:
|
|
||||||
await message.client.send_file(message.chat_id, response_bot.media, caption = self.config["text"].format(song_artist=song_artist, song_album=song_album, song_name=song_name))
|
|
||||||
await message.delete()
|
|
||||||
return
|
|
||||||
except Exception as e:
|
|
||||||
await utils.answer(message, f"<pre><code class='language-python'>{e}</code></pre>")
|
|
||||||
except Exception as e:
|
|
||||||
await utils.answer(message, f"<pre><code class='language-python'>{e}</code></pre>")
|
|
||||||
|
|
||||||
@loader.command()
|
|
||||||
async def tutorl(self, message):
|
|
||||||
"""| tutorial"""
|
|
||||||
|
|
||||||
await utils.answer(message, self.strings['tutorial'].format(prefix = self.get_prefix(), username="{username}"))
|
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
# meta developer: @codrago_m
|
# meta developer: @codrago_m
|
||||||
# scope: heroku_min 2.0.0
|
# scope: heroku_min 2.0.0
|
||||||
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from .. import utils, loader, main
|
|
||||||
from telethon.tl.functions.messages import MarkDialogUnreadRequest
|
from telethon.tl.functions.messages import MarkDialogUnreadRequest
|
||||||
|
|
||||||
|
from .. import loader, main, utils
|
||||||
|
|
||||||
logger = logging.getLogger("TagWatcher")
|
logger = logging.getLogger("TagWatcher")
|
||||||
|
|
||||||
|
|
||||||
@@ -124,6 +127,7 @@ class TagWatcher(loader.Module):
|
|||||||
description="Here will be notifications about mentions in chats.",
|
description="Here will be notifications about mentions in chats.",
|
||||||
icon_emoji_id=5409025823388741707,
|
icon_emoji_id=5409025823388741707,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.xdlib = await self.import_lib(
|
self.xdlib = await self.import_lib(
|
||||||
"https://raw.githubusercontent.com/coddrago/modules/refs/heads/main/libs/xdlib.py",
|
"https://raw.githubusercontent.com/coddrago/modules/refs/heads/main/libs/xdlib.py",
|
||||||
suspend_on_error=True,
|
suspend_on_error=True,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -- version --
|
# -- version --
|
||||||
__version__ = (1, 2, 1)
|
__version__ = (1, 2, 2)
|
||||||
# -- version --
|
# -- version --
|
||||||
|
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ __version__ = (1, 2, 1)
|
|||||||
# ██║╚██╔╝██║██╔══╝░░██╔══██║██║░░██║██║░░██║░░████╔═████║░░╚═══██╗░╚═══██╗
|
# ██║╚██╔╝██║██╔══╝░░██╔══██║██║░░██║██║░░██║░░████╔═████║░░╚═══██╗░╚═══██╗
|
||||||
# ██║░╚═╝░██║███████╗██║░░██║██████╔╝╚█████╔╝░░╚██╔╝░╚██╔╝░██████╔╝██████╔╝
|
# ██║░╚═╝░██║███████╗██║░░██║██████╔╝╚█████╔╝░░╚██╔╝░╚██╔╝░██████╔╝██████╔╝
|
||||||
# ╚═╝░░░░░╚═╝╚══════╝╚═╝░░╚═╝╚═════╝░░╚════╝░░░░╚═╝░░░╚═╝░░╚═════╝░╚═════╝░
|
# ╚═╝░░░░░╚═╝╚══════╝╚═╝░░╚═╝╚═════╝░░╚════╝░░░░╚═╝░░░╚═╝░░╚═════╝░╚═════╝░
|
||||||
# © Copyright 2025
|
# © Copyright 2026
|
||||||
# ✈ https://t.me/mead0wssMods
|
# ✈ https://t.me/mead0wssMods
|
||||||
|
|
||||||
|
|
||||||
@@ -33,14 +33,17 @@ class SenderGifts(loader.Module):
|
|||||||
"checking_user": "<emoji document_id=5206634672204829887>🔍</emoji> Проверка пользователя...",
|
"checking_user": "<emoji document_id=5206634672204829887>🔍</emoji> Проверка пользователя...",
|
||||||
"checking_balance": "<emoji document_id=5206634672204829887>🔍</emoji> Проверка баланса...",
|
"checking_balance": "<emoji document_id=5206634672204829887>🔍</emoji> Проверка баланса...",
|
||||||
"user_not_found": "<emoji document_id=4958526153955476488>❌</emoji> Пользователь не найден",
|
"user_not_found": "<emoji document_id=4958526153955476488>❌</emoji> Пользователь не найден",
|
||||||
"gift_menu": "<emoji document_id=5931696400982088015>🎁</emoji> Выберите категорию подарков.\n\n<emoji document_id=6032693626394382504>👤</emoji> Пользователь: {}\n<emoji document_id=5873153278023307367>📄</emoji> Текст: {}\n<emoji document_id=5951810621887484519>⭐</emoji> Баланс: {} звезд",
|
"gift_menu": "<tg-emoji emoji-id=5370781982886220096>🎁</tg-emoji> Выберите категорию подарков.\n\n<tg-emoji emoji-id=6048471184461271609>👤</tg-emoji> Пользователь: {}\n<tg-emoji emoji-id=6048762138430803961>📂</tg-emoji> Текст: {}\n<tg-emoji emoji-id=5321485469249198987>⭐️</tg-emoji> Баланс: {} звезд",
|
||||||
"category_menu": "<emoji document_id=5931696400982088015>🎁</emoji> Подарки за {} ⭐\n\n<emoji document_id=6032693626394382504>👤</emoji> Пользователь: {}\n<emoji document_id=5873153278023307367>📄</emoji> Текст: {}",
|
"category_menu": "<tg-emoji emoji-id=5370781982886220096>🎁</tg-emoji> Подарки за {} ⭐\n\n<tg-emoji emoji-id=6048471184461271609>👤</tg-emoji> Пользователь: {}\n<tg-emoji emoji-id=6048762138430803961>📂</tg-emoji> Текст: {}",
|
||||||
|
"privacy_menu": "<tg-emoji emoji-id=5370781982886220096>🎁</tg-emoji> Выбран подарок: {}\n\nКак отправить подарок?",
|
||||||
"sending_gift": "<emoji document_id=5201691993775818138>🛫</emoji> Отправка подарка...",
|
"sending_gift": "<emoji document_id=5201691993775818138>🛫</emoji> Отправка подарка...",
|
||||||
"gift_sent": "<emoji document_id=5021905410089550576>✅</emoji> Подарок успешно отправлен!",
|
"gift_sent": "<emoji document_id=5021905410089550576>✅</emoji> Подарок успешно отправлен!",
|
||||||
"not_enough_stars": "<emoji document_id=4958526153955476488>❌</emoji> Недостаточно звезд для отправки подарка {}!",
|
"not_enough_stars": "<emoji document_id=4958526153955476488>❌</emoji> Недостаточно звезд для отправки подарка {}!",
|
||||||
"min_stars_error": "<emoji document_id=4958526153955476488>❌</emoji> Недостаточно звезд для отправки минимального подарка!",
|
"min_stars_error": "<emoji document_id=4958526153955476488>❌</emoji> Недостаточно звезд для отправки минимального подарка!",
|
||||||
"no_available_gifts": "<emoji document_id=4958526153955476488>❌</emoji> Нет доступных подарков для вашего баланса",
|
"no_available_gifts": "<emoji document_id=4958526153955476488>❌</emoji> Нет доступных подарков для вашего баланса",
|
||||||
"balance_error": "<emoji document_id=4958526153955476488>❌</emoji> Ошибка при проверке баланса",
|
"balance_error": "<emoji document_id=4958526153955476488>❌</emoji> Ошибка при проверке баланса",
|
||||||
|
"btn_public": "📢 Публично",
|
||||||
|
"btn_anon": "🕵️ Анонимно",
|
||||||
}
|
}
|
||||||
|
|
||||||
gift_categories = {
|
gift_categories = {
|
||||||
@@ -57,6 +60,7 @@ class SenderGifts(loader.Module):
|
|||||||
{"id": 5170314324215857265, "emoji": "💐", "name": "Цветы"},
|
{"id": 5170314324215857265, "emoji": "💐", "name": "Цветы"},
|
||||||
{"id": 5170564780938756245, "emoji": "🚀", "name": "Ракета"},
|
{"id": 5170564780938756245, "emoji": "🚀", "name": "Ракета"},
|
||||||
{"id": 5922558454332916696, "emoji": "🎄", "name": "Ёлка"},
|
{"id": 5922558454332916696, "emoji": "🎄", "name": "Ёлка"},
|
||||||
|
{"id": 5956217000635139069, "emoji": "🧸", "name": "Новогодний мишка"}
|
||||||
],
|
],
|
||||||
100: [
|
100: [
|
||||||
{"id": 5168043875654172773, "emoji": "🏆", "name": "Кубок"},
|
{"id": 5168043875654172773, "emoji": "🏆", "name": "Кубок"},
|
||||||
@@ -136,8 +140,10 @@ class SenderGifts(loader.Module):
|
|||||||
if row:
|
if row:
|
||||||
buttons.append(row)
|
buttons.append(row)
|
||||||
|
|
||||||
|
helper_msg = await self.inline.form("🪐", balance_msg)
|
||||||
|
|
||||||
await utils.answer(
|
await utils.answer(
|
||||||
balance_msg,
|
helper_msg,
|
||||||
self.strings["gift_menu"].format(
|
self.strings["gift_menu"].format(
|
||||||
f"@{user.username}" if user.username else user.first_name,
|
f"@{user.username}" if user.username else user.first_name,
|
||||||
text if text else "-",
|
text if text else "-",
|
||||||
@@ -153,8 +159,8 @@ class SenderGifts(loader.Module):
|
|||||||
for gift in gifts:
|
for gift in gifts:
|
||||||
row.append({
|
row.append({
|
||||||
"text": gift["emoji"],
|
"text": gift["emoji"],
|
||||||
"callback": self._send_gift,
|
"callback": self._select_privacy,
|
||||||
"args": (user_id, gift["id"], text, gift["emoji"], msg_id, balance),
|
"args": (user_id, gift["id"], text, gift["emoji"], msg_id, balance, price),
|
||||||
})
|
})
|
||||||
if len(row) == 3:
|
if len(row) == 3:
|
||||||
buttons.append(row)
|
buttons.append(row)
|
||||||
@@ -183,6 +189,34 @@ class SenderGifts(loader.Module):
|
|||||||
reply_markup=buttons
|
reply_markup=buttons
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def _select_privacy(self, call, user_id, gift_id, text, gift_emoji, msg_id, balance, price):
|
||||||
|
buttons = [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"text": self.strings["btn_public"],
|
||||||
|
"callback": self._send_gift,
|
||||||
|
"args": (user_id, gift_id, text, gift_emoji, msg_id, balance, False) # hide_name=False публично
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": self.strings["btn_anon"],
|
||||||
|
"callback": self._send_gift,
|
||||||
|
"args": (user_id, gift_id, text, gift_emoji, msg_id, balance, True) # hide_name=True анонимно
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"text": "⬅️ Назад",
|
||||||
|
"callback": self._show_category,
|
||||||
|
"args": (user_id, price, text, balance, msg_id)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
await call.edit(
|
||||||
|
self.strings["privacy_menu"].format(gift_emoji),
|
||||||
|
reply_markup=buttons
|
||||||
|
)
|
||||||
|
|
||||||
async def _back_to_categories(self, call, user_id, text, balance, msg_id):
|
async def _back_to_categories(self, call, user_id, text, balance, msg_id):
|
||||||
try:
|
try:
|
||||||
user = await self.client.get_entity(user_id)
|
user = await self.client.get_entity(user_id)
|
||||||
@@ -216,7 +250,7 @@ class SenderGifts(loader.Module):
|
|||||||
reply_markup=buttons
|
reply_markup=buttons
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _send_gift(self, call, user_id, gift_id, text, gift_emoji, msg_id, balance):
|
async def _send_gift(self, call, user_id, gift_id, text, gift_emoji, msg_id, balance, hide_name):
|
||||||
try:
|
try:
|
||||||
await call.edit(
|
await call.edit(
|
||||||
self.strings["sending_gift"],
|
self.strings["sending_gift"],
|
||||||
@@ -227,11 +261,11 @@ class SenderGifts(loader.Module):
|
|||||||
self.client.parse_mode,
|
self.client.parse_mode,
|
||||||
)
|
)
|
||||||
text, entities = parse_mode.parse(text)
|
text, entities = parse_mode.parse(text)
|
||||||
|
|
||||||
user = await self.client.get_input_entity(user_id)
|
user = await self.client.get_input_entity(user_id)
|
||||||
inv = InputInvoiceStarGift(
|
inv = InputInvoiceStarGift(
|
||||||
user,
|
user,
|
||||||
gift_id,
|
gift_id,
|
||||||
|
hide_name=hide_name,
|
||||||
message=TextWithEntities(text, entities) if text else TextWithEntities("", [])
|
message=TextWithEntities(text, entities) if text else TextWithEntities("", [])
|
||||||
)
|
)
|
||||||
form = await self.client(GetPaymentFormRequest(inv))
|
form = await self.client(GetPaymentFormRequest(inv))
|
||||||
|
|||||||
130503
modules.json
130503
modules.json
File diff suppressed because it is too large
Load Diff
31
parse.py
31
parse.py
@@ -10,8 +10,10 @@ logger = logging.getLogger(__name__)
|
|||||||
def safe_unparse(node: ast.AST) -> str:
|
def safe_unparse(node: ast.AST) -> str:
|
||||||
try:
|
try:
|
||||||
return ast.unparse(node)
|
return ast.unparse(node)
|
||||||
except AttributeError:
|
except Exception:
|
||||||
return getattr(node, 'id', str(type(node).__name__))
|
if hasattr(node, "id"):
|
||||||
|
return str(node.id)
|
||||||
|
return str(node)
|
||||||
|
|
||||||
def load_blacklist(file_path):
|
def load_blacklist(file_path):
|
||||||
with open(file_path, "r", encoding="utf-8") as f:
|
with open(file_path, "r", encoding="utf-8") as f:
|
||||||
@@ -27,17 +29,38 @@ def load_blacklist(file_path):
|
|||||||
|
|
||||||
return blacklisted_modules
|
return blacklisted_modules
|
||||||
|
|
||||||
|
def is_loader_tds(deco: ast.AST) -> bool:
|
||||||
|
return (
|
||||||
|
isinstance(deco, ast.Attribute)
|
||||||
|
and isinstance(deco.value, ast.Name)
|
||||||
|
and deco.value.id == "loader"
|
||||||
|
and deco.attr in {"tds", "translatable_docstring"}
|
||||||
|
)
|
||||||
|
|
||||||
def extract_string_value(node: ast.AST) -> Optional[str]:
|
def extract_string_value(node: ast.AST) -> Optional[str]:
|
||||||
try:
|
try:
|
||||||
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
||||||
return node.value
|
return node.value
|
||||||
|
|
||||||
if isinstance(node, ast.Str):
|
if isinstance(node, ast.Str):
|
||||||
return node.s
|
return node.s
|
||||||
|
|
||||||
|
if isinstance(node, ast.JoinedStr):
|
||||||
|
parts = []
|
||||||
|
for v in node.values:
|
||||||
|
if isinstance(v, ast.Constant) and isinstance(v.value, str):
|
||||||
|
parts.append(v.value)
|
||||||
|
elif isinstance(v, ast.FormattedValue):
|
||||||
|
parts.append("{" + safe_unparse(v.value) + "}")
|
||||||
|
return "".join(parts)
|
||||||
|
|
||||||
if isinstance(node, ast.Name):
|
if isinstance(node, ast.Name):
|
||||||
return node.id
|
return node.id
|
||||||
|
|
||||||
if isinstance(node, ast.Attribute):
|
if isinstance(node, ast.Attribute):
|
||||||
return f"{safe_unparse(node.value)}.{node.attr}"
|
return f"{safe_unparse(node.value)}.{node.attr}"
|
||||||
return str(node)
|
|
||||||
|
return None
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -111,7 +134,7 @@ def get_module_info(module_path: str) -> Optional[Dict[str, Any]]:
|
|||||||
|
|
||||||
is_module_class = (
|
is_module_class = (
|
||||||
"Mod" in node.name or
|
"Mod" in node.name or
|
||||||
any(isinstance(d, ast.Attribute) and safe_unparse(d).startswith("loader.tds") for d in node.decorator_list) or
|
any(is_loader_tds(d) for d in node.decorator_list) or
|
||||||
any(isinstance(d, ast.Name) and d.id == "loader" for d in node.decorator_list)
|
any(isinstance(d, ast.Name) and d.id == "loader" for d in node.decorator_list)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
214
radiocycle/Modules/LastFm.py
Normal file
214
radiocycle/Modules/LastFm.py
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
# =======================================
|
||||||
|
# _ __ __ __ _
|
||||||
|
# | |/ /___ | \/ | ___ __| |___
|
||||||
|
# | ' // _ \ | |\/| |/ _ \ / _` / __|
|
||||||
|
# | . \ __/ | | | | (_) | (_| \__ \
|
||||||
|
# |_|\_\___| |_| |_|\___/ \__,_|___/
|
||||||
|
# @ke_mods
|
||||||
|
# =======================================
|
||||||
|
#
|
||||||
|
# LICENSE: CC BY-ND 4.0 (Attribution-NoDerivatives 4.0 International)
|
||||||
|
# --------------------------------------
|
||||||
|
# https://creativecommons.org/licenses/by-nd/4.0/legalcode
|
||||||
|
# =======================================
|
||||||
|
|
||||||
|
# meta developer: @ke_mods
|
||||||
|
|
||||||
|
from .. import loader, utils
|
||||||
|
import requests
|
||||||
|
import io
|
||||||
|
import textwrap
|
||||||
|
from PIL import Image, ImageDraw, ImageEnhance, ImageFilter, ImageFont, ImageOps
|
||||||
|
|
||||||
|
class Banners:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
title: str,
|
||||||
|
artists: list,
|
||||||
|
track_cover: bytes,
|
||||||
|
font
|
||||||
|
):
|
||||||
|
self.title = title
|
||||||
|
self.artists = ", ".join(artists) if isinstance(artists, list) else artists
|
||||||
|
self.track_cover = track_cover
|
||||||
|
self.font_url = font
|
||||||
|
|
||||||
|
def _get_font(self, size, font_bytes):
|
||||||
|
return ImageFont.truetype(io.BytesIO(font_bytes), size)
|
||||||
|
|
||||||
|
def _prepare_cover(self, size, radius):
|
||||||
|
cover = Image.open(io.BytesIO(self.track_cover)).convert("RGBA")
|
||||||
|
cover = cover.resize((size, size), Image.Resampling.LANCZOS)
|
||||||
|
|
||||||
|
mask = Image.new("L", (size, size), 0)
|
||||||
|
draw = ImageDraw.Draw(mask)
|
||||||
|
draw.rounded_rectangle((0, 0, size, size), radius=radius, fill=255)
|
||||||
|
|
||||||
|
output = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||||
|
output.paste(cover, (0, 0), mask=mask)
|
||||||
|
return output
|
||||||
|
|
||||||
|
def _prepare_background(self, w, h):
|
||||||
|
bg = Image.open(io.BytesIO(self.track_cover)).convert("RGBA")
|
||||||
|
bg = bg.resize((w, h), Image.Resampling.BICUBIC)
|
||||||
|
bg = bg.filter(ImageFilter.GaussianBlur(radius=20))
|
||||||
|
bg = ImageEnhance.Brightness(bg).enhance(0.4)
|
||||||
|
return bg
|
||||||
|
|
||||||
|
def horizontal(self):
|
||||||
|
W, H = 1500, 600
|
||||||
|
padding = 60
|
||||||
|
cover_size = 480
|
||||||
|
|
||||||
|
font_bytes = requests.get(self.font_url).content
|
||||||
|
title_font = self._get_font(55, font_bytes)
|
||||||
|
artist_font = self._get_font(45, font_bytes)
|
||||||
|
lfm_font = self._get_font(35, font_bytes)
|
||||||
|
|
||||||
|
img = self._prepare_background(W, H)
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
|
||||||
|
cover = self._prepare_cover(cover_size, 30)
|
||||||
|
img.paste(cover, (padding, (H - cover_size) // 2), cover)
|
||||||
|
|
||||||
|
text_x = padding + cover_size + 60
|
||||||
|
text_y_start = 100
|
||||||
|
text_width_limit = W - text_x - padding
|
||||||
|
|
||||||
|
display_title = self.title
|
||||||
|
while title_font.getlength(display_title) > text_width_limit and len(display_title) > 0:
|
||||||
|
display_title = display_title[:-1]
|
||||||
|
if len(display_title) < len(self.title): display_title += "…"
|
||||||
|
|
||||||
|
display_artist = self.artists
|
||||||
|
while artist_font.getlength(display_artist) > text_width_limit and len(display_artist) > 0:
|
||||||
|
display_artist = display_artist[:-1]
|
||||||
|
if len(display_artist) < len(self.artists): display_artist += "…"
|
||||||
|
|
||||||
|
draw.text((text_x, text_y_start), display_title, font=title_font, fill="white")
|
||||||
|
draw.text((text_x, text_y_start + 70), display_artist, font=artist_font, fill="#B3B3B3")
|
||||||
|
|
||||||
|
bar_y = 480
|
||||||
|
draw.text((text_x, bar_y), "last.fm", font=lfm_font, fill="white")
|
||||||
|
|
||||||
|
by = io.BytesIO()
|
||||||
|
img.save(by, format="PNG")
|
||||||
|
by.seek(0)
|
||||||
|
by.name = "banner.png"
|
||||||
|
return by
|
||||||
|
|
||||||
|
def vertical(self):
|
||||||
|
W, H = 1000, 1500
|
||||||
|
padding = 80
|
||||||
|
cover_size = 800
|
||||||
|
|
||||||
|
font_bytes = requests.get(self.font_url).content
|
||||||
|
title_font = self._get_font(60, font_bytes)
|
||||||
|
artist_font = self._get_font(45, font_bytes)
|
||||||
|
lfm_font = self._get_font(35, font_bytes)
|
||||||
|
|
||||||
|
img = self._prepare_background(W, H)
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
|
||||||
|
cover = self._prepare_cover(cover_size, 40)
|
||||||
|
cover_x = (W - cover_size) // 2
|
||||||
|
cover_y = 120
|
||||||
|
img.paste(cover, (cover_x, cover_y), cover)
|
||||||
|
|
||||||
|
text_area_y = cover_y + cover_size + 120
|
||||||
|
text_width_limit = W - (padding * 2)
|
||||||
|
|
||||||
|
display_title = self.title
|
||||||
|
while title_font.getlength(display_title) > text_width_limit and len(display_title) > 0:
|
||||||
|
display_title = display_title[:-1]
|
||||||
|
if len(display_title) < len(self.title): display_title += "…"
|
||||||
|
|
||||||
|
display_artist = self.artists
|
||||||
|
while artist_font.getlength(display_artist) > text_width_limit and len(display_artist) > 0:
|
||||||
|
display_artist = display_artist[:-1]
|
||||||
|
if len(display_artist) < len(self.artists): display_artist += "…"
|
||||||
|
|
||||||
|
title_w = title_font.getlength(display_title)
|
||||||
|
draw.text(((W - title_w) / 2, text_area_y), display_title, font=title_font, fill="white")
|
||||||
|
|
||||||
|
artist_w = artist_font.getlength(display_artist)
|
||||||
|
draw.text(((W - artist_w) / 2, text_area_y + 75), display_artist, font=artist_font, fill="#B3B3B3")
|
||||||
|
|
||||||
|
bar_y = text_area_y + 260
|
||||||
|
|
||||||
|
lfm_w = lfm_font.getlength("last.fm")
|
||||||
|
draw.text(((W - lfm_w) / 2, bar_y), "last.fm", font=lfm_font, fill="white")
|
||||||
|
|
||||||
|
by = io.BytesIO()
|
||||||
|
img.save(by, format="PNG")
|
||||||
|
by.seek(0)
|
||||||
|
by.name = "banner.png"
|
||||||
|
return by
|
||||||
|
|
||||||
|
@loader.tds
|
||||||
|
class lastfmmod(loader.Module):
|
||||||
|
"""Module for music from different services"""
|
||||||
|
|
||||||
|
strings = {
|
||||||
|
"name": "LastFm",
|
||||||
|
"no_track": "<emoji document_id=5465665476971471368>❌</emoji> <b>No track is currently playing</b>",
|
||||||
|
"_doc_text": "The text that will be written next to the file",
|
||||||
|
"_doc_username": "Your username from last.fm",
|
||||||
|
"nick_error": "<emoji document_id=5465665476971471368>❌</emoji> <b>Put your nickname from last.fm</b>",
|
||||||
|
"uploading": "<emoji document_id=5841359499146825803>🕔</emoji> <i>Uploading banner...</i>",
|
||||||
|
}
|
||||||
|
strings_ru = {
|
||||||
|
"name": "LastFm",
|
||||||
|
"no_track": "<emoji document_id=5465665476971471368>❌</emoji> <b>Сейчас ничего не играет</b>",
|
||||||
|
"_doc_text": "Текст, который будет написан рядом с файлом",
|
||||||
|
"_doc_username": "Ваш username с last.fm",
|
||||||
|
"nick_error": "<emoji document_id=5465665476971471368>❌</emoji> <b>Укажите ваш никнейм с last.fm</b>",
|
||||||
|
"uploading": "<emoji document_id=5841359499146825803>🕔</emoji> <i>Загрузка баннера...</i>",
|
||||||
|
}
|
||||||
|
strings_jp = {
|
||||||
|
"name": "LastFm",
|
||||||
|
"no_track": "<emoji document_id=5465665476971471368>❌</emoji> <b>現在再生中のトラックはありません</b>",
|
||||||
|
"_doc_text": "ファイルの横に表示されるテキスト",
|
||||||
|
"_doc_username": "Last.fmのユーザー名",
|
||||||
|
"nick_error": "<emoji document_id=5465665476971471368>❌</emoji> <b>Last.fmのニックネームを入力してください</b>",
|
||||||
|
"uploading": "<emoji document_id=5841359499146825803>🕔</emoji> <i>バナーをアップロード中...</i>",
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.config = loader.ModuleConfig(
|
||||||
|
loader.ConfigValue("username", None, lambda: self.strings["_doc_username"]),
|
||||||
|
loader.ConfigValue("custom_text", "<emoji document_id=5413612466208799435>🤩</emoji> <b>{song_name}</b> — <b>{song_artist}</b>", lambda: self.strings["_doc_text"]),
|
||||||
|
loader.ConfigValue("font", "https://raw.githubusercontent.com/kamekuro/assets/master/fonts/Onest-Bold.ttf", "Custom font URL (ttf)"),
|
||||||
|
loader.ConfigValue("banner_version", "horizontal", lambda: "Banner version", validator=loader.validators.Choice(["horizontal", "vertical"])),
|
||||||
|
)
|
||||||
|
|
||||||
|
@loader.command(alias="np")
|
||||||
|
async def nowplay(self, message):
|
||||||
|
"""| send playing track info"""
|
||||||
|
user = self.config["username"]
|
||||||
|
if not user:
|
||||||
|
await self.invoke("config", "lastfm", message=message)
|
||||||
|
return await utils.answer(message, self.strings["nick_error"])
|
||||||
|
|
||||||
|
try:
|
||||||
|
url = f'http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&nowplaying=true&user={user}&api_key=460cda35be2fbf4f28e8ea7a38580730&format=json'
|
||||||
|
data = requests.get(url).json()
|
||||||
|
track = next((t for t in data.get('recenttracks', {}).get('track', []) if t.get('@attr', {}).get('nowplaying')), None)
|
||||||
|
if not track:
|
||||||
|
return await utils.answer(message, self.strings["no_track"])
|
||||||
|
name = track.get('name', 'Unknown')
|
||||||
|
artist = track.get('artist', {}).get('#text', 'Unknown')
|
||||||
|
caption = self.config["custom_text"].format(song_artist=artist, song_name=name)
|
||||||
|
imgs = track.get('image', [])
|
||||||
|
cov_url = next((i['#text'] for i in imgs if i['size'] == 'extralarge'), imgs[-1]['#text'] if imgs else None)
|
||||||
|
|
||||||
|
if not cov_url:
|
||||||
|
return await utils.answer(message, caption)
|
||||||
|
msg = await utils.answer(message, self.strings["uploading"])
|
||||||
|
cov_bytes = await utils.run_sync(requests.get, cov_url)
|
||||||
|
banners = Banners(name, artist, cov_bytes.content, self.config["font"])
|
||||||
|
file = await utils.run_sync(getattr(banners, self.config["banner_version"]))
|
||||||
|
await utils.answer(msg, caption, file=file)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await utils.answer(message, f"<pre><code class='language-python'>{e}</code></pre>")
|
||||||
48
radiocycle/Modules/Neofetch.py
Normal file
48
radiocycle/Modules/Neofetch.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# =======================================
|
||||||
|
# _ __ __ __ _
|
||||||
|
# | |/ /___ | \/ | ___ __| |___
|
||||||
|
# | ' // _ \ | |\/| |/ _ \ / _` / __|
|
||||||
|
# | . \ __/ | | | | (_) | (_| \__ \
|
||||||
|
# |_|\_\___| |_| |_|\___/ \__,_|___/
|
||||||
|
# @ke_mods
|
||||||
|
# =======================================
|
||||||
|
#
|
||||||
|
# LICENSE: CC BY-ND 4.0 (Attribution-NoDerivatives 4.0 International)
|
||||||
|
# --------------------------------------
|
||||||
|
# https://creativecommons.org/licenses/by-nd/4.0/legalcode
|
||||||
|
# =======================================
|
||||||
|
|
||||||
|
# meta developer: @ke_mods
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
from .. import loader, utils
|
||||||
|
|
||||||
|
@loader.tds
|
||||||
|
class NeofetchMod(loader.Module):
|
||||||
|
strings = {
|
||||||
|
"name": "Neofetch",
|
||||||
|
"not_installed": "<b>Please, install</b> <i>Neofetch</i> <b>package</b>",
|
||||||
|
}
|
||||||
|
|
||||||
|
strings_ru = {
|
||||||
|
"not_installed": "<b>Пожалуйста, установите пакет</b> <i>Neofetch</i>",
|
||||||
|
}
|
||||||
|
|
||||||
|
strings_ua = {
|
||||||
|
"not_installed": "<b>Будь ласка, встановіть пакет<b> <i>Neofetch</i>",
|
||||||
|
}
|
||||||
|
|
||||||
|
@loader.command(
|
||||||
|
ru_doc="- запустить команду neofetch",
|
||||||
|
ua_doc="- запустити команду neofetch",
|
||||||
|
)
|
||||||
|
async def neofetchcmd(self, message):
|
||||||
|
"""- run neofetch command"""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(["neofetch", "--stdout"], capture_output=True, text=True)
|
||||||
|
output = result.stdout
|
||||||
|
await utils.answer(message, f"<pre>{utils.escape_html(output)}</pre>")
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
await utils.answer(message, self.strings("not_installed"))
|
||||||
|
|
||||||
211
radiocycle/Modules/PicToStories.py
Normal file
211
radiocycle/Modules/PicToStories.py
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
# =======================================
|
||||||
|
# _ __ __ __ _
|
||||||
|
# | |/ /___ | \/ | ___ __| |___
|
||||||
|
# | ' // _ \ | |\/| |/ _ \ / _` / __|
|
||||||
|
# | . \ __/ | | | | (_) | (_| \__ \
|
||||||
|
# |_|\_\___| |_| |_|\___/ \__,_|___/
|
||||||
|
# @ke_mods
|
||||||
|
# =======================================
|
||||||
|
#
|
||||||
|
# LICENSE: CC BY-ND 4.0 (Attribution-NoDerivatives 4.0 International)
|
||||||
|
# --------------------------------------
|
||||||
|
# https://creativecommons.org/licenses/by-nd/4.0/legalcode
|
||||||
|
# =======================================
|
||||||
|
|
||||||
|
# meta developer: @ke_mods
|
||||||
|
# requires: pillow
|
||||||
|
|
||||||
|
import io
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from telethon import functions, types
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from .. import loader, utils
|
||||||
|
|
||||||
|
|
||||||
|
@loader.tds
|
||||||
|
class PicToStoriesMod(loader.Module):
|
||||||
|
"""Grid for stories"""
|
||||||
|
|
||||||
|
strings = {
|
||||||
|
"name": "PicToStories",
|
||||||
|
"no_rep": (
|
||||||
|
"<emoji document_id=5879813604068298387>❗️</emoji> "
|
||||||
|
"<b>Reply to photo!</b>"
|
||||||
|
),
|
||||||
|
"work": (
|
||||||
|
"<emoji document_id=5841359499146825803>🕔</emoji> "
|
||||||
|
"<b>Processing...</b>"
|
||||||
|
),
|
||||||
|
"done": (
|
||||||
|
"<emoji document_id=5776375003280838798>✅</emoji> "
|
||||||
|
"<b>Done! Check your profile.</b>"
|
||||||
|
),
|
||||||
|
"err": (
|
||||||
|
"<emoji document_id=5778527486270770928>❌</emoji> "
|
||||||
|
"<b>Error:</b> {}"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
strings_ru = {
|
||||||
|
"no_rep": (
|
||||||
|
"<emoji document_id=5879813604068298387>❗️</emoji> "
|
||||||
|
"<b>Реплай на фото!</b>"
|
||||||
|
),
|
||||||
|
"work": (
|
||||||
|
"<emoji document_id=5841359499146825803>🕔</emoji> "
|
||||||
|
"<b>Обрабатываю...</b>"
|
||||||
|
),
|
||||||
|
"done": (
|
||||||
|
"<emoji document_id=5776375003280838798>✅</emoji> "
|
||||||
|
"<b>Готово! Проверяй профиль.</b>"
|
||||||
|
),
|
||||||
|
"err": (
|
||||||
|
"<emoji document_id=5778527486270770928>❌</emoji> "
|
||||||
|
"<b>Ошибка:</b> {}"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.config = loader.ModuleConfig(
|
||||||
|
loader.ConfigValue(
|
||||||
|
"period",
|
||||||
|
48,
|
||||||
|
lambda: "Visibility period in hours",
|
||||||
|
validator=loader.validators.Integer(),
|
||||||
|
),
|
||||||
|
loader.ConfigValue(
|
||||||
|
"blacklist",
|
||||||
|
[],
|
||||||
|
lambda: "Blacklisted user IDs",
|
||||||
|
validator=loader.validators.Series(loader.validators.Integer()),
|
||||||
|
),
|
||||||
|
loader.ConfigValue(
|
||||||
|
"cooldown",
|
||||||
|
0,
|
||||||
|
lambda: "Cooldown between stories in seconds",
|
||||||
|
validator=loader.validators.Integer(minimum=0),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@loader.command(ru_doc="<реплай на фото> [название альбома] - сделать сетку")
|
||||||
|
async def ptscmd(self, message):
|
||||||
|
"""<reply to photo> [album name] - make grid"""
|
||||||
|
args = utils.get_args_raw(message)
|
||||||
|
reply = await message.get_reply_message()
|
||||||
|
if not reply or not reply.media:
|
||||||
|
await utils.answer(message, self.strings("no_rep"))
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
image_bytes = await reply.download_media(file=bytes)
|
||||||
|
img = Image.open(io.BytesIO(image_bytes))
|
||||||
|
except Exception as e:
|
||||||
|
await utils.answer(message, self.strings("err").format(e))
|
||||||
|
return
|
||||||
|
|
||||||
|
await utils.answer(message, self.strings("work"))
|
||||||
|
|
||||||
|
w, h = img.size
|
||||||
|
curr_ratio = w / h
|
||||||
|
variants = [
|
||||||
|
(5 / 4, 2),
|
||||||
|
(4 / 5, 3),
|
||||||
|
(3 / 5, 4),
|
||||||
|
(9 / 16, 5)
|
||||||
|
]
|
||||||
|
best_ratio, rows = min(variants, key=lambda x: abs(curr_ratio - x[0]))
|
||||||
|
|
||||||
|
new_h = int(w / best_ratio)
|
||||||
|
img = img.resize((w, new_h), Image.LANCZOS)
|
||||||
|
w, h = img.size
|
||||||
|
|
||||||
|
parts = []
|
||||||
|
pw, ph = w // 3, h // rows
|
||||||
|
for r in range(rows):
|
||||||
|
for c in range(3):
|
||||||
|
x, y = c * pw, r * ph
|
||||||
|
parts.append(img.crop((x, y, x + pw, y + ph)))
|
||||||
|
|
||||||
|
parts.reverse()
|
||||||
|
|
||||||
|
privacy = [types.InputPrivacyValueAllowAll()]
|
||||||
|
if self.config["blacklist"]:
|
||||||
|
entities = []
|
||||||
|
for uid in self.config["blacklist"]:
|
||||||
|
try:
|
||||||
|
entities.append(await self.client.get_input_entity(uid))
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if entities:
|
||||||
|
privacy.append(types.InputPrivacyValueDisallowUsers(users=entities))
|
||||||
|
|
||||||
|
story_ids = []
|
||||||
|
for i, p in enumerate(parts):
|
||||||
|
out = io.BytesIO()
|
||||||
|
p.save(out, "JPEG", quality=95)
|
||||||
|
out.seek(0)
|
||||||
|
|
||||||
|
uploaded_file = await self.client.upload_file(out, file_name="s.jpg")
|
||||||
|
res = await self.client(
|
||||||
|
functions.stories.SendStoryRequest(
|
||||||
|
peer=types.InputPeerSelf(),
|
||||||
|
media=types.InputMediaUploadedPhoto(uploaded_file),
|
||||||
|
privacy_rules=privacy,
|
||||||
|
period=self.config["period"] * 3600,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
sid = next(
|
||||||
|
(
|
||||||
|
u.story_id if hasattr(u, "story_id") else u.id
|
||||||
|
for u in res.updates
|
||||||
|
if hasattr(u, "story_id") or hasattr(u, "id")
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if sid:
|
||||||
|
story_ids.append(sid)
|
||||||
|
|
||||||
|
if self.config["cooldown"] > 0 and i < len(parts) - 1:
|
||||||
|
await asyncio.sleep(self.config["cooldown"])
|
||||||
|
|
||||||
|
if not story_ids:
|
||||||
|
return
|
||||||
|
|
||||||
|
if args:
|
||||||
|
all_albums = await self.client(
|
||||||
|
functions.stories.GetAlbumsRequest(peer=types.InputPeerSelf(), hash=0)
|
||||||
|
)
|
||||||
|
|
||||||
|
target = next(
|
||||||
|
(a for a in all_albums.albums if getattr(a, 'title', '') == args),
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|
||||||
|
if target:
|
||||||
|
await self.client(
|
||||||
|
functions.stories.UpdateAlbumRequest(
|
||||||
|
peer=types.InputPeerSelf(),
|
||||||
|
album_id=target.album_id,
|
||||||
|
add_stories=story_ids,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await self.client(
|
||||||
|
functions.stories.CreateAlbumRequest(
|
||||||
|
peer=types.InputPeerSelf(),
|
||||||
|
stories=story_ids,
|
||||||
|
title=args,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await self.client(
|
||||||
|
functions.stories.TogglePinnedRequest(
|
||||||
|
peer=types.InputPeerSelf(), id=story_ids, pinned=True
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
await utils.answer(message, self.strings("done"))
|
||||||
1436
radiocycle/Modules/SpotifyMod.py
Normal file
1436
radiocycle/Modules/SpotifyMod.py
Normal file
File diff suppressed because it is too large
Load Diff
74
radiocycle/Modules/UnbanAll.py
Normal file
74
radiocycle/Modules/UnbanAll.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# =======================================
|
||||||
|
# _ __ __ __ _
|
||||||
|
# | |/ /___ | \/ | ___ __| |___
|
||||||
|
# | ' // _ \ | |\/| |/ _ \ / _` / __|
|
||||||
|
# | . \ __/ | | | | (_) | (_| \__ \
|
||||||
|
# |_|\_\___| |_| |_|\___/ \__,_|___/
|
||||||
|
# @ke_mods
|
||||||
|
# =======================================
|
||||||
|
#
|
||||||
|
# LICENSE: CC BY-ND 4.0 (Attribution-NoDerivatives 4.0 International)
|
||||||
|
# --------------------------------------
|
||||||
|
# https://creativecommons.org/licenses/by-nd/4.0/legalcode
|
||||||
|
# =======================================
|
||||||
|
|
||||||
|
# meta developer: @ke_mods
|
||||||
|
|
||||||
|
from .. import loader, utils
|
||||||
|
from telethon.tl.types import ChatBannedRights
|
||||||
|
from telethon.tl.functions.channels import EditBannedRequest
|
||||||
|
from telethon.tl.types import ChannelParticipantsKicked
|
||||||
|
|
||||||
|
@loader.tds
|
||||||
|
class UnbanAllMod(loader.Module):
|
||||||
|
strings = {
|
||||||
|
"name": "UnbanAll",
|
||||||
|
"no_rights": "<b>❌ I don't have administrator rights to remove restrictions.</b>",
|
||||||
|
"success": "<b>✅ All banned chat members have been unbanned.</b>",
|
||||||
|
"unban_in_process": "<b>👀 Unbanning users...</b>",
|
||||||
|
"no_banned": "<b>ℹ️ There are no banned members in this chat.</b>",
|
||||||
|
"error_occured": "<b>💢 An error occurred while unbanning user <code>{}</code>:</b>\n<code>{}</code>",
|
||||||
|
}
|
||||||
|
strings_ru = {
|
||||||
|
"no_rights": "<b>❌ У меня нет прав администратора для снятия ограничений.</b>",
|
||||||
|
"success": "<b>✅ Все забаненные участники чата были разблокированы.</b>",
|
||||||
|
"unban_in_process": "<b>👀 Разбаниваю пользователей...</b>",
|
||||||
|
"no_banned": "<b>ℹ️ В этом чате нет забаненных участников.</b>",
|
||||||
|
"error_occured": "<b>💢 Произошла ошибка при разблокировке пользователя <code>{}</code>:</b>\n<code>{}</code>",
|
||||||
|
}
|
||||||
|
|
||||||
|
@loader.command(ru_doc="- Разбанить всех забаненных пользователей")
|
||||||
|
async def unbanallcmd(self, message):
|
||||||
|
"""- Unban all banned users"""
|
||||||
|
chat = await message.get_chat()
|
||||||
|
|
||||||
|
if not chat.admin_rights and not chat.creator:
|
||||||
|
await utils.answer(message, self.strings("no_rights"))
|
||||||
|
return
|
||||||
|
|
||||||
|
await utils.answer(message, self.strings("unban_in_process"))
|
||||||
|
|
||||||
|
no_banned = True
|
||||||
|
|
||||||
|
async for user in self.client.iter_participants(
|
||||||
|
message.chat_id, filter=ChannelParticipantsKicked
|
||||||
|
):
|
||||||
|
|
||||||
|
no_banned = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self.client(EditBannedRequest(
|
||||||
|
message.chat_id,
|
||||||
|
user.id,
|
||||||
|
ChatBannedRights(until_date=0)
|
||||||
|
))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await utils.answer(message, self.strings("error_occured").format(user.id, e))
|
||||||
|
pass
|
||||||
|
|
||||||
|
if no_banned:
|
||||||
|
await utils.answer(message, self.strings("no_banned"))
|
||||||
|
return
|
||||||
|
|
||||||
|
await utils.answer(message, self.strings("success"))
|
||||||
7
radiocycle/Modules/full.txt
Normal file
7
radiocycle/Modules/full.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Neofetch
|
||||||
|
randomanimepic
|
||||||
|
SpotifyMod
|
||||||
|
UnbanAll
|
||||||
|
voicetotext
|
||||||
|
LastFm
|
||||||
|
PicToStories
|
||||||
65
radiocycle/Modules/randomanimepic.py
Normal file
65
radiocycle/Modules/randomanimepic.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# =======================================
|
||||||
|
# _ __ __ __ _
|
||||||
|
# | |/ /___ | \/ | ___ __| |___
|
||||||
|
# | ' // _ \ | |\/| |/ _ \ / _` / __|
|
||||||
|
# | . \ __/ | | | | (_) | (_| \__ \
|
||||||
|
# |_|\_\___| |_| |_|\___/ \__,_|___/
|
||||||
|
# @ke_mods
|
||||||
|
# =======================================
|
||||||
|
#
|
||||||
|
# LICENSE: CC BY-ND 4.0 (Attribution-NoDerivatives 4.0 International)
|
||||||
|
# --------------------------------------
|
||||||
|
# https://creativecommons.org/licenses/by-nd/4.0/legalcode
|
||||||
|
# =======================================
|
||||||
|
|
||||||
|
# meta developer: @ke_mods
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import traceback
|
||||||
|
from logging import basicConfig
|
||||||
|
from .. import loader, utils
|
||||||
|
|
||||||
|
basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@loader.tds
|
||||||
|
class RandomAnimePicMod(loader.Module):
|
||||||
|
strings = {
|
||||||
|
"name": "RandomAnimePic",
|
||||||
|
"img": "<emoji document_id=4916036072560919511>✅</emoji> <b>Your anime pic</b>\n<emoji document_id=5877465816030515018>🔗</emoji> <b>URL:</b> {}",
|
||||||
|
"loading": "<emoji document_id=4911241630633165627>✨</emoji> <b>Loading image...</b>",
|
||||||
|
"error": "<emoji document_id=5116151848855667552>🚫</emoji> <b>An unexpected error occurred...</b>",
|
||||||
|
}
|
||||||
|
|
||||||
|
strings_ru = {
|
||||||
|
"img": "<emoji document_id=4916036072560919511>✅</emoji> <b>Ваша аниме-картинка</b>\n<emoji document_id=5877465816030515018>🔗</emoji> <b>Ссылка:</b> {}",
|
||||||
|
"loading": "<emoji document_id=4911241630633165627>✨</emoji> <b>Загрузка изображения...</b>",
|
||||||
|
"error": "<emoji document_id=5116151848855667552>🚫</emoji> <b>Произошла непредвиденная ошибка...</b>",
|
||||||
|
}
|
||||||
|
|
||||||
|
@loader.command(
|
||||||
|
ru_doc="- получить рандомную аниме-картинку 👀"
|
||||||
|
)
|
||||||
|
async def rapiccmd(self, message):
|
||||||
|
"""- fetch random anime-pic 👀"""
|
||||||
|
|
||||||
|
await utils.answer(message, self.strings("loading"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
res = requests.get("https://api.nekosia.cat/api/v1/images/cute?count=1")
|
||||||
|
res.raise_for_status()
|
||||||
|
data = res.json()
|
||||||
|
image_url = data['image']['original']['url']
|
||||||
|
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
|
await utils.answer(message, self.strings("img").format(image_url), file=image_url, reply_to=message.reply_to_msg_id)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
logger.error("Error fetching random anime pic: %s", traceback.format_exc())
|
||||||
|
|
||||||
|
await utils.answer(message, self.strings("error"))
|
||||||
|
|
||||||
|
await asyncio.sleep(5)
|
||||||
77
radiocycle/Modules/voicetotext.py
Normal file
77
radiocycle/Modules/voicetotext.py
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# =======================================
|
||||||
|
# _ __ __ __ _
|
||||||
|
# | |/ /___ | \/ | ___ __| |___
|
||||||
|
# | ' // _ \ | |\/| |/ _ \ / _` / __|
|
||||||
|
# | . \ __/ | | | | (_) | (_| \__ \
|
||||||
|
# |_|\_\___| |_| |_|\___/ \__,_|___/
|
||||||
|
# @ke_mods
|
||||||
|
# =======================================
|
||||||
|
#
|
||||||
|
# LICENSE: CC BY-ND 4.0 (Attribution-NoDerivatives 4.0 International)
|
||||||
|
# --------------------------------------
|
||||||
|
# https://creativecommons.org/licenses/by-nd/4.0/legalcode
|
||||||
|
# =======================================
|
||||||
|
|
||||||
|
# meta developer: @ke_mods
|
||||||
|
# scope: ffmpeg
|
||||||
|
# requires: pydub SpeechRecognition
|
||||||
|
|
||||||
|
from .. import loader, utils
|
||||||
|
import os
|
||||||
|
import speech_recognition as sr
|
||||||
|
from pydub import AudioSegment
|
||||||
|
|
||||||
|
@loader.tds
|
||||||
|
class VoiceToTextMod(loader.Module):
|
||||||
|
strings = {
|
||||||
|
"name": "VoiceToText",
|
||||||
|
"process_text": "<emoji document_id=4911241630633165627>✨</emoji> <b>Recognizing the message text...</b>",
|
||||||
|
"vtt_success": "<emoji document_id=5116110535565247270>🔥</emoji> <b>Recognized text:</b>\n<blockquote expandable>{}</blockquote>",
|
||||||
|
"vtt_failure": "<emoji document_id=5116151848855667552>🚫</emoji> <b>Failed to recognize the message.</b>",
|
||||||
|
"vtt_request_error": "<emoji document_id=5116151848855667552>🚫</emoji> <b>Error when contacting the recognition service:</b>\n<code>{}</code>",
|
||||||
|
"vtt_invalid": "<emoji document_id=5116151848855667552>🚫</emoji> <b>Please reply to a voice or video message with the command</b> <code>{}vtt</code>",
|
||||||
|
"vtt_successful": "<emoji document_id=4916036072560919511>✅</emoji> <b>Text recognized successfully</b>",
|
||||||
|
}
|
||||||
|
|
||||||
|
strings_ru = {
|
||||||
|
"process_text": "<emoji document_id=4911241630633165627>✨</emoji> <b>Распознаю текст сообщения...</b>",
|
||||||
|
"vtt_success": "<emoji document_id=5116110535565247270>🔥</emoji> <b>Распознанный текст:</b>\n<blockquote expandable>{}</blockquote>",
|
||||||
|
"vtt_failure": "<emoji document_id=5116151848855667552>🚫</emoji> <b>Не удалось распознать сообщение.</b>",
|
||||||
|
"vtt_request_error": "<emoji document_id=5116151848855667552>🚫</emoji> <b>Ошибка при обращении к сервису распознавания:</b>\n<code>{}</code>",
|
||||||
|
"vtt_invalid": "<emoji document_id=5116151848855667552>🚫</emoji> <b>Пожалуйста, ответьте на голосовое или видеосообщение командой</b> <code>{}vtt</code>",
|
||||||
|
"vtt_successful": "<emoji document_id=4916036072560919511>✅</emoji> <b>Текст успешно распознан</b>",
|
||||||
|
}
|
||||||
|
|
||||||
|
@loader.command(
|
||||||
|
ru_doc="- распознает текст из голосового или видеосообщения.",
|
||||||
|
)
|
||||||
|
async def vttcmd(self, message):
|
||||||
|
"""- recognizes text from voice or video messages."""
|
||||||
|
reply = await message.get_reply_message()
|
||||||
|
|
||||||
|
if not reply or not (reply.voice or reply.video_note):
|
||||||
|
await utils.answer(message, self.strings["vtt_invalid"].format(self.get_prefix()))
|
||||||
|
return
|
||||||
|
|
||||||
|
msg = await utils.answer(
|
||||||
|
message, self.strings["process_text"], reply_to=message.id
|
||||||
|
)
|
||||||
|
|
||||||
|
media_file = await reply.download_media()
|
||||||
|
wav_file = media_file.replace('.mp4', '.wav') if reply.video_note else media_file.replace('.oga', '.wav')
|
||||||
|
|
||||||
|
try:
|
||||||
|
AudioSegment.from_file(media_file).export(wav_file, format='wav')
|
||||||
|
recognizer = sr.Recognizer()
|
||||||
|
with sr.AudioFile(wav_file) as source:
|
||||||
|
audio_data = recognizer.record(source)
|
||||||
|
try:
|
||||||
|
text = recognizer.recognize_google(audio_data, language='ru-RU')
|
||||||
|
await utils.answer(msg, self.strings["vtt_success"].format(text))
|
||||||
|
except sr.UnknownValueError:
|
||||||
|
await utils.answer(msg, self.strings["vtt_failure"])
|
||||||
|
except sr.RequestError as e:
|
||||||
|
await utils.answer(msg, self.strings["vtt_request_error"].format(e))
|
||||||
|
finally:
|
||||||
|
os.remove(media_file)
|
||||||
|
os.remove(wav_file)
|
||||||
@@ -234,6 +234,11 @@
|
|||||||
"url": "https://github.com/yummy1gay/limoka",
|
"url": "https://github.com/yummy1gay/limoka",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"blacklist": []
|
"blacklist": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/radiocycle/Modules",
|
||||||
|
"tags": ["newbie"],
|
||||||
|
"blacklist": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -53,6 +53,7 @@ async def send_document(session, file_path, caption=None):
|
|||||||
data = aiohttp.FormData()
|
data = aiohttp.FormData()
|
||||||
data.add_field('chat_id', arguments.chat_id)
|
data.add_field('chat_id', arguments.chat_id)
|
||||||
data.add_field('document', f, filename=os.path.basename(file_path))
|
data.add_field('document', f, filename=os.path.basename(file_path))
|
||||||
|
data.add_field('parse_mode', 'HTML')
|
||||||
if caption:
|
if caption:
|
||||||
data.add_field('caption', caption)
|
data.add_field('caption', caption)
|
||||||
data.add_field('parse_mode', 'Markdown')
|
data.add_field('parse_mode', 'Markdown')
|
||||||
@@ -133,8 +134,9 @@ async def main():
|
|||||||
|
|
||||||
diff_url = f"https://github.com/MuRuLOSE/limoka/compare/{old_hash}...{new_hash}.diff"
|
diff_url = f"https://github.com/MuRuLOSE/limoka/compare/{old_hash}...{new_hash}.diff"
|
||||||
message = (
|
message = (
|
||||||
f"🪼 Module {module_name} changes approved\n\n"
|
f"🪼 <b>Module <code>{module_name}</code> changes approved</b>\n\n"
|
||||||
f"[File URL]({github_url}) | [Diff URL]({diff_url})\n\n"
|
f"<b><a href=\"{github_url}\">File URL</a></b> | "
|
||||||
|
f"<b><a href=\"{diff_url}\">Diff URL</a></b>"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get diff
|
# Get diff
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
__version__ = (1, 1, 1, 1)
|
__version__ = (1, 2, 0, 0)
|
||||||
|
|
||||||
# This file is a part of Hikka Userbot!
|
# This file is a part of Hikka Userbot!
|
||||||
# This product includes software developed by t.me/Fl1yd and t.me/spypm.
|
# This product includes software developed by t.me/Fl1yd and t.me/spypm.
|
||||||
@@ -15,6 +15,10 @@ __version__ = (1, 1, 1, 1)
|
|||||||
# scope: hikka_only
|
# scope: hikka_only
|
||||||
# scope: hikka_min 1.6.3
|
# scope: hikka_min 1.6.3
|
||||||
|
|
||||||
|
# Changelog v1.2:
|
||||||
|
# - Added: Proxy for users from RF
|
||||||
|
# - Fixed: Correct reply author resolving for forwarded messages
|
||||||
|
|
||||||
# █▄█ █░█ █▀▄▀█ █▀▄▀█ █▄█ █▀▄▀█ █▀█ █▀▄ █▀
|
# █▄█ █░█ █▀▄▀█ █▀▄▀█ █▄█ █▀▄▀█ █▀█ █▀▄ █▀
|
||||||
# ░█░ █▄█ █░▀░█ █░▀░█ ░█░ █░▀░█ █▄█ █▄▀ ▄█
|
# ░█░ █▄█ █░▀░█ █░▀░█ ░█░ █░▀░█ █▄█ █▄▀ ▄█
|
||||||
|
|
||||||
@@ -194,6 +198,12 @@ class Quotes(loader.Module):
|
|||||||
validator=loader.validators.Integer(minimum=1,maximum=50)),
|
validator=loader.validators.Integer(minimum=1,maximum=50)),
|
||||||
loader.ConfigValue("endpoint","https://kok.gay/gayotes/generate",
|
loader.ConfigValue("endpoint","https://kok.gay/gayotes/generate",
|
||||||
lambda:"URL API-эндпоинта (можешь поднять локально - github.com/yummy1gay/quote-api)",
|
lambda:"URL API-эндпоинта (можешь поднять локально - github.com/yummy1gay/quote-api)",
|
||||||
|
validator=loader.validators.Link()),
|
||||||
|
loader.ConfigValue("use_rf_proxy", False,
|
||||||
|
lambda:'Включает прокси для РФ, если основной эндпоинт возвращает ошибку "Нетворк еррорь", и при этом сервер с юзерботом находится в России или ты сам сидишь в России с ограниченным доступом к зарубежным ресурсам (Termux / UserLAnd)',
|
||||||
|
validator=loader.validators.Boolean()),
|
||||||
|
loader.ConfigValue("rf_endpoint", "https://ru.kok.gay/gayotes/generate",
|
||||||
|
lambda:"URL API-эндпоинта для РФ",
|
||||||
validator=loader.validators.Link()))
|
validator=loader.validators.Link()))
|
||||||
|
|
||||||
async def client_ready(self, client, db):
|
async def client_ready(self, client, db):
|
||||||
@@ -226,10 +236,11 @@ class Quotes(loader.Module):
|
|||||||
"format": "webp" if not doc else "png", "type": self.config["type"]}
|
"format": "webp" if not doc else "png", "type": self.config["type"]}
|
||||||
|
|
||||||
await utils.answer(st,self.strings["api_processing"])
|
await utils.answer(st,self.strings["api_processing"])
|
||||||
r=await Dick.post(f"{self.config['endpoint']}.webp",pay)
|
endpoint=self.config['rf_endpoint'] if self.config['use_rf_proxy'] else self.config['endpoint']
|
||||||
|
r=await Dick.post(f"{endpoint}.webp",pay)
|
||||||
if not r or r.status_code!=200:
|
if not r or r.status_code!=200:
|
||||||
try: err=r.json().get("error",f"HTTP {r.status_code}") if r else "Нетворк еррорь"
|
try: err=r.json().get("error",f"HTTP {r.status_code}") if r else "Нетворк еррорь (попробуй включить <code>use_rf_proxy</code> в конфиге)"
|
||||||
except Exception: err=f"HTTP {r.status_code}" if r else "Нетворк еррорь"
|
except Exception: err=f"HTTP {r.status_code}" if r else "Нетворк еррорь (попробуй включить <code>use_rf_proxy</code> в конфиге)"
|
||||||
return await utils.answer(st,self.strings["api_error"].format(err))
|
return await utils.answer(st,self.strings["api_error"].format(err))
|
||||||
|
|
||||||
buf=io.BytesIO(r.content); buf.name="YgQuote"+(".png" if doc else ".webp")
|
buf=io.BytesIO(r.content); buf.name="YgQuote"+(".png" if doc else ".webp")
|
||||||
@@ -259,10 +270,11 @@ class Quotes(loader.Module):
|
|||||||
"format": "webp","type":self.config["type"]}
|
"format": "webp","type":self.config["type"]}
|
||||||
|
|
||||||
await utils.answer(st,self.strings["api_processing"])
|
await utils.answer(st,self.strings["api_processing"])
|
||||||
r=await Dick.post(f"{self.config['endpoint']}.webp",dickk)
|
endpoint=self.config['rf_endpoint'] if self.config['use_rf_proxy'] else self.config['endpoint']
|
||||||
|
r=await Dick.post(f"{endpoint}.webp",dickk)
|
||||||
if not r or r.status_code!=200:
|
if not r or r.status_code!=200:
|
||||||
try: err=r.json().get("error",f"HTTP {r.status_code}") if r else "Нетворк еррорь"
|
try: err=r.json().get("error",f"HTTP {r.status_code}") if r else "Нетворк еррорь (попробуй включить <code>use_rf_proxy</code> в конфиге)"
|
||||||
except Exception: err=f"HTTP {r.status_code}" if r else "Нетворк еррорь"
|
except Exception: err=f"HTTP {r.status_code}" if r else "Нетворк еррорь (попробуй включить <code>use_rf_proxy</code> в конфиге)"
|
||||||
return await utils.answer(st,self.strings["api_error"].format(err))
|
return await utils.answer(st,self.strings["api_error"].format(err))
|
||||||
|
|
||||||
buf=io.BytesIO(r.content); buf.name="YgQuote.webp"
|
buf=io.BytesIO(r.content); buf.name="YgQuote.webp"
|
||||||
@@ -289,7 +301,8 @@ class Quotes(loader.Module):
|
|||||||
try:
|
try:
|
||||||
r=await mm.get_reply_message()
|
r=await mm.get_reply_message()
|
||||||
if r:
|
if r:
|
||||||
rname=telethon.utils.get_display_name(r.sender)
|
ruser = await self.who(r)
|
||||||
|
rname=telethon.utils.get_display_name(ruser)
|
||||||
rtxt=Dick.desc(r,True)
|
rtxt=Dick.desc(r,True)
|
||||||
if r.raw_text: rtxt=(rtxt+". "+r.raw_text) if rtxt else r.raw_text
|
if r.raw_text: rtxt=(rtxt+". "+r.raw_text) if rtxt else r.raw_text
|
||||||
rb={"name":rname,"text":rtxt or "","entities":Dick.ents(r.entities),
|
rb={"name":rname,"text":rtxt or "","entities":Dick.ents(r.entities),
|
||||||
|
|||||||
Reference in New Issue
Block a user