Compare commits

...

41 Commits

Author SHA1 Message Date
2770ae1ccc fix: now supports video / gif baners 2026-06-12 12:15:31 +03:00
Macsim
c3390a5887 Merge pull request #315 from MuRuLOSE/update-submodules_d33e49b6967d97ab3dc83dc74e03537a26defd62
Update of repositories 2026-06-12 08:37:40
2026-06-12 11:59:03 +03:00
github-actions[bot]
2ff79975c2 Updated modules.json after parse 2026-06-12 08:37:15 2026-06-12 08:37:15 +00:00
github-actions[bot]
ab03f6ed94 Added and updated repositories 2026-06-12 08:36:40 2026-06-12 08:36:40 +00:00
d33e49b696 fix 2026-06-12 11:34:55 +03:00
04cc1dc4b3 New developer 2026-06-12 11:24:33 +03:00
6b6afb7493 fix: Ported fix from stable, in python version below 3.12 you can't use " with f-strings 2026-06-08 09:46:37 +03:00
Macsim
2ed246b9ad Merge pull request #301 from MuRuLOSE/update-submodules_d279789b37a939b3d9ececce6b4d0e1992293c23
Update of repositories 2026-05-31 02:48:09
2026-06-01 00:23:40 +03:00
github-actions[bot]
837784206f Updated modules.json after parse 2026-05-31 02:47:46 2026-05-31 02:47:46 +00:00
github-actions[bot]
811beb2b74 Added and updated repositories 2026-05-31 02:47:15 2026-05-31 02:47:16 +00:00
Zahar Vanilovv
d279789b37 Merge pull request #273 from MuRuLOSE/update-submodules_63944822139e8b6869fe2250ad8c650e9db06765
Update of repositories 2026-05-03 02:11:45
2026-05-03 05:14:12 +03:00
github-actions[bot]
74dfe4caf8 Updated modules.json after parse 2026-05-03 02:11:26 2026-05-03 02:11:26 +00:00
github-actions[bot]
18b8247e21 Added and updated repositories 2026-05-03 02:10:53 2026-05-03 02:10:53 +00:00
6394482213 fix: banners now will be visible no matter what 2026-04-25 09:23:54 +03:00
43a0e578c1 fix: 1
Co-authored-by: Copilot <copilot@github.com>
2026-04-24 21:58:27 +03:00
3112e6d4bf python-magic is shit so its replaced with filetype due to magic requires its own binary in order to work 2026-04-24 21:43:29 +03:00
18bb817239 fix: if server does not return Content-Type in header we will try determine MIME-type offline
Co-authored-by: Copilot <copilot@github.com>
2026-04-24 21:37:39 +03:00
24f1983e2a fix: not awaited update function 2026-04-24 21:16:36 +03:00
74638c14d0 feat: now you can see version if module updated
Co-authored-by: Copilot <copilot@github.com>
2026-04-24 20:56:58 +03:00
Macsim
db62a5aee1 Merge pull request #264 from MuRuLOSE/update-submodules_49956ba865b533dca03b344d048f96465a6be62e
Update of repositories 2026-04-24 17:42:48
2026-04-24 20:43:54 +03:00
github-actions[bot]
4d2a2899f4 Updated modules.json after parse 2026-04-24 17:42:27 2026-04-24 17:42:27 +00:00
github-actions[bot]
2a27c56d83 Added and updated repositories 2026-04-24 17:41:57 2026-04-24 17:41:57 +00:00
49956ba865 version bump 2026-04-24 20:33:45 +03:00
f0d2a28105 removed H.modules
feat: added force index update

Co-authored-by: Copilot <copilot@github.com>
2026-04-24 20:10:52 +03:00
13d091c56c removed H.Modules by request 2026-04-19 16:10:58 +03:00
d7cf406b78 feat: Added 'by developer' if meta developer in module and event if module new
fix: if some event happened with module, changes message will not be sended
2026-04-19 14:40:13 +03:00
Macsim
0f30a78990 Merge pull request #257 from MuRuLOSE/update-submodules_e50c7c1688b89901690bc01609544e5c3e238097
Update of repositories 2026-04-19 02:02:27
2026-04-19 14:16:50 +03:00
github-actions[bot]
16adfac8b5 Updated modules.json after parse 2026-04-19 02:02:03 2026-04-19 02:02:03 +00:00
github-actions[bot]
7ddb190b35 Added and updated repositories 2026-04-19 02:01:35 2026-04-19 02:01:35 +00:00
e50c7c1688 drop: debug log 2026-04-18 14:08:04 +03:00
f3682ed87a fix: version displaying wrong and some shit idk 2026-04-18 13:41:49 +03:00
be47e59d97 fix: some broken strings
feat: install button and new limoka version update notification
drop: watcher that installs module  from bot (temporary)
2026-04-18 13:19:24 +03:00
eb71e39fcf Merge branch 'main' of https://github.com/MuRuLOSE/limoka 2026-04-18 11:01:48 +03:00
Macsim
7d713e36c0 Merge pull request #256 from MuRuLOSE/update-submodules_a424d6bac4fb0d52521fca595af24968162bcc87
Update of repositories 2026-04-18 01:50:07
2026-04-18 10:58:20 +03:00
github-actions[bot]
a8fde5e498 Updated modules.json after parse 2026-04-18 01:49:43 2026-04-18 01:49:43 +00:00
github-actions[bot]
4a03c6cb1a Added and updated repositories 2026-04-18 01:49:10 2026-04-18 01:49:10 +00:00
Macsim
a424d6bac4 Merge pull request #254 from MuRuLOSE/update-submodules_59564f07b5b57f12eec78ff6e8f0574c70e37f8b
Update of repositories 2026-04-16 02:02:29
2026-04-16 07:39:02 +03:00
github-actions[bot]
fc8344ca05 Updated modules.json after parse 2026-04-16 02:02:11 2026-04-16 02:02:11 +00:00
github-actions[bot]
3e62dc0b69 Added and updated repositories 2026-04-16 02:01:42 2026-04-16 02:01:42 +00:00
41f253b471 feat: deleted files now got dedicated notification too 2026-04-15 18:02:16 +03:00
Macsim
59564f07b5 Merge pull request #253 from MuRuLOSE/update-submodules_3a193cfb2f731c403bbc542d2408c6144d0bcda7
Update of repositories 2026-04-15 01:54:22
2026-04-15 17:35:17 +03:00
213 changed files with 83389 additions and 94771 deletions

View File

@@ -7,7 +7,7 @@ __version__ = (1, 0, 0)
# 🔑 http://www.apache.org/licenses/LICENSE-2.0
# meta banner: https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/BSR/banner.png
# meta developer: @FModules
# meta developer: @NFModules
# meta fhsdesc: brawlstars, game, funny
from .. import loader, utils
@@ -42,6 +42,7 @@ async def to_code(n: int) -> str:
n_shifted //= 31
return "X" + "".join(reversed(res))
@loader.tds
class BSR(loader.Module):
'''Module for finding nearby game rooms in BrawlStars.'''
@@ -139,7 +140,7 @@ class BSR(loader.Module):
'''(room code/link) (previous) (next) - find rooms.'''
args = utils.get_args_raw(message).split()
if not args:
return await utils.answer(message, self.strings("invalid_args").format(prefix=self.get_prefix()))
return await utils.answer(message, self.strings["invalid_args"].format(prefix=self.get_prefix()))
raw_input = args[0]
before = 0
@@ -161,13 +162,13 @@ class BSR(loader.Module):
nxt = max(0, min(nxt, 5000))
if before == 0 and nxt == 0:
return await utils.answer(message, self.strings("at_least_one"))
return await utils.answer(message, self.strings["at_least_one"])
clean_tag = await extract_code(raw_input)
base_id = await to_id(clean_tag)
if base_id == 0:
return await utils.answer(message, self.strings("invalid_code"))
return await utils.answer(message, self.strings["invalid_code"])
text, page, total_pages = await self.get_page_content(base_id, before, nxt, 0)
kb = self.build_keyboard(base_id, before, nxt, page, total_pages, clean_tag)
@@ -205,10 +206,10 @@ class BSR(loader.Module):
blocks = []
if prev_list:
blocks.append(self.strings("prev_block").format(prev_list="\n".join(prev_list)))
blocks.append(self.strings["prev_block"].format(prev_list="\n".join(prev_list)))
if next_list:
blocks.append(self.strings("next_block").format(next_list="\n".join(next_list)))
blocks.append(self.strings["next_block"].format(next_list="\n".join(next_list)))
res = "\n\n".join(blocks)
if not res.strip():
@@ -220,7 +221,7 @@ class BSR(loader.Module):
kb = [
[
{
"text": self.strings("btn_target"),
"text": self.strings["btn_target"],
"copy": clean_tag
}
]

View File

@@ -1,6 +1,6 @@
__version__ = (9, 3, 9)
# meta developer: @FModules
# meta developer: @NFModules
# meta pic: https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FHeta/logo.png
# meta banner: https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FHeta/logo.png
# scope: hikka_min 2.0.0
@@ -19,15 +19,19 @@ import re
import sys
import uuid
import importlib
from contextlib import suppress
from typing import Optional, Dict, List, Union, Tuple, Any
from urllib.parse import unquote
from importlib.machinery import ModuleSpec
import telethon
from .. import loader, utils
from ..types import CoreOverwriteError
from herokutl.tl.functions.contacts import UnblockRequest
from aiogram.types import InlineQueryResultArticle, InputTextMessageContent, LinkPreviewOptions, ChosenInlineResult, CallbackQuery, Message
try:
from aiogram.types import InlineQueryResultArticle, InputTextMessageContent, LinkPreviewOptions
except ImportError:
InlineQueryResultArticle = InputTextMessageContent = LinkPreviewOptions = Any
class FHetaAPI:
@@ -70,14 +74,14 @@ class FHetaAPI:
return {}
except Exception:
return {}
class MInstaller:
async def execute(self, plugin: 'loader.Module', url: str) -> Tuple[str, List[str]]:
try:
code = await plugin._storage.fetch(url, auth=plugin.config.get("basic_auth"))
except Exception:
return "error", []
return "error",[]
for step in range(5):
state = await self.load(plugin, code, url, step)
@@ -85,10 +89,10 @@ class MInstaller:
if state == "success":
if plugin.fully_loaded:
plugin.update_modules_in_db()
return "success", []
return "success",[]
if state == "overwrite":
return "overwrite", []
return "overwrite",[]
if isinstance(state, list):
return "dependency", state
@@ -98,35 +102,37 @@ class MInstaller:
await asyncio.sleep(0.5)
return "dependency", []
return "dependency",[]
async def load(self, plugin: 'loader.Module', code: str, origin: str, step: int) -> Union[str, List[str]]:
if step == 0:
try:
dependencies = list(filter(
lambda requirement: not requirement.startswith(("-", "_", ".")),
map(lambda raw: raw.strip().rstrip(','), loader.VALID_PIP_PACKAGES.search(code)[1].split())
))
if dependencies:
if not await plugin.install_requirements(dependencies):
return dependencies
importlib.invalidate_caches()
return "retry"
raw_pip = loader.VALID_PIP_PACKAGES.search(code)
if raw_pip:
dependencies = [
dep.strip() for dep in raw_pip[1].replace(',', ' ').split()
if dep.strip() and not dep.strip().startswith(("-", "_", "."))
]
if dependencies:
await plugin.install_requirements(dependencies)
importlib.invalidate_caches()
return "retry"
except Exception:
pass
try:
packages = list(filter(
lambda requirement: not requirement.startswith(("-", "_", ".")),
map(lambda raw: raw.strip().rstrip(','), loader.VALID_APT_PACKAGES.search(code)[1].split())
))
if packages:
if not await plugin.install_packages(packages):
return packages
importlib.invalidate_caches()
return "retry"
raw_apt = loader.VALID_APT_PACKAGES.search(code)
if raw_apt:
packages = [
pkg.strip() for pkg in raw_apt[1].replace(',', ' ').split()
if pkg.strip() and not pkg.strip().startswith(("-", "_", "."))
]
if packages:
await plugin.install_packages(packages)
importlib.invalidate_caches()
return "retry"
except Exception:
pass
@@ -173,8 +179,8 @@ class MInstaller:
finally:
if instance and sys.exc_info()[0] is not None:
with suppress(Exception):
await plugin.allmodules.unload_module(instance.__class__.__name__)
await plugin.allmodules.unload_module(instance.__class__.__name__)
if instance in plugin.allmodules.modules:
plugin.allmodules.modules.remove(instance)
@@ -226,11 +232,17 @@ class FHetaUI:
description = utils.escape_html(description).split('\n')[0] if description else ""
name = utils.escape_html(item.get("name", ""))
if kind == "cmd":
character = '@' + self.main.inline.bot_username + ' ' if item.get('inline') else self.main.get_prefix()
row = f"<code>{character}{name}</code> {description}".strip()
if item.get('inline'):
character = '@' + self.main.inline.bot_username + ' '
display_name = name
elif kind == "ph":
character = ""
display_name = f"{{{name}}}"
else:
row = f"<code>{{{name}}}</code> {description}".strip()
character = self.main.get_prefix()
display_name = name
row = f"<code>{character}{display_name}</code> {description}".strip()
extra = f"<i>{self.main.strings[more].format(remaining=len(items) - index)}</i>"
test = "\n".join(lines + [row, extra])
@@ -242,7 +254,7 @@ class FHetaUI:
lines.append(row)
return f"\n\n{self.emoji('command' if kind == 'cmd' else 'placeholder')} <b>{self.main.strings[title]}:</b>\n<blockquote expandable>{chr(10).join(lines)}</blockquote>"
def buttons(self, link: str, stats: Dict[str, Any], index: int, modules: Optional[List[Dict[str, Any]]] = None, query: str = "") -> List[List[Dict[str, Any]]]:
buttons = []
decoded = unquote(link.replace('%20', '___SPACE___')).replace('___SPACE___', '%20')
@@ -302,7 +314,7 @@ class FHetaUI:
@loader.tds
class FHeta(loader.Module):
'''Module for searching modules! Watch all FHeta news in @FHeta_Updates!'''
'''Module for searching modules! Watch all FHeta news in @NFHeta_Updates!'''
strings = {
"name": "FHeta",
@@ -333,11 +345,12 @@ class FHeta(loader.Module):
"overwrite": "✘ Error, module tried to overwrite built-in module!",
"dependency": "✘ Dependencies installation error! {deps}",
"docdevs": "Use only modules from official Heroku developers when searching?",
"doctheme": "Theme for emojis."
"doctheme": "Theme for emojis.",
"channel": "This is the channel with all updates in FHeta!"
}
strings_ru = {
"_cls_doc": "Модуль для поиска модулей! Следите за всеми новостями FHeta в @FHeta_Updates!",
"_cls_doc": "Модуль для поиска модулей! Следите за всеми новостями FHeta в @NFHeta_Updates!",
"lang": "ru",
"author": "от",
"description": "Описание",
@@ -361,15 +374,16 @@ class FHeta(loader.Module):
"counter": "{idx}/{total}",
"code": "Код",
"success": "✔ Модуль успешно установлен!",
"error": "✘ Ошибка, возможно, модуль поломан!",
"error": "✘ Ошибка, возможно, модуль сломан!",
"overwrite": "✘ Ошибка, модуль пытался перезаписать встроенный модуль!",
"dependency": "✘ Ошибка установки зависимостей! {deps}",
"docdevs": "Использовать только модули от официальных разработчиков Heroku при поиске?",
"doctheme": "Тема для эмодзи."
"doctheme": "Тема для эмодзи.",
"channel": "Это канал со всеми обновлениями в FHeta!"
}
strings_ua = {
"_cls_doc": "Модуль для пошуку модулів! Слідкуйте за всіма новинами FHeta в @FHeta_Updates!",
"_cls_doc": "Модуль для пошуку модулів! Слідкуйте за всіма новинами FHeta в @NFHeta_Updates!",
"lang": "ua",
"author": "від",
"description": "Опис",
@@ -397,11 +411,12 @@ class FHeta(loader.Module):
"overwrite": "✘ Помилка, модуль намагався перезаписати вбудований модуль!",
"dependency": "✘ Помилка встановлення залежностей! {deps}",
"docdevs": "Використовувати тільки модулі від офіційних розробників Heroku при пошуку?",
"doctheme": "Тема для емодзі."
"doctheme": "Тема для емодзі.",
"channel": "Це канал з усіма оновленнями в FHeta!"
}
strings_kz = {
"_cls_doc": "Модульдерді іздеу модулі! FHeta барлық жаңалықтарын @FHeta_Updates арнасында қадағалаңыз!",
"_cls_doc": "Модульдерді іздеу модулі! FHeta барлық жаңалықтарын @NFHeta_Updates арнасында қадағалаңыз!",
"lang": "kz",
"author": "авторы",
"description": "Сипаттама",
@@ -413,7 +428,7 @@ class FHeta(loader.Module):
"search": "{query} сұрауы бойынша іздеу...",
"noquery": "Сіз іздеу сұрауын енгізбедіңіз, мысал: {prefix}fheta сіздің сұрауыңыз",
"notfound": "{query} сұрауы бойынша ештеңе табылмады.",
"toolong": "Сіздің сұрауыңыз тым үлкен, оны 168 таңбаға дейін қысқартыңыз.",
"toolong": "Сіздің сұрауыңыз тым үлкен, оны 168 таңбаға до қысқартыңыз.",
"added": "✔ Бағалау қосылды!",
"changed": "✔ Бағалау өзгертілді!",
"deleted": "✔ Бағалау жойылды!",
@@ -429,11 +444,12 @@ class FHeta(loader.Module):
"overwrite": "✘ Қате, модуль кіріктірілген модульді қайта жазуға тырысты!",
"dependency": "✘ Тәуелділіктерді орнату қатесі! {deps}",
"docdevs": "Іздеу кезінде тек ресми Heroku әзірлеушілерінің модульдерін пайдалану керек пе?",
"doctheme": "Эмодзилер үшін тақырып."
"doctheme": "Эмодзилер үшін тақырып.",
"channel": "Бұл FHeta-дағы барлық жаңартулары бар арна!"
}
strings_uz = {
"_cls_doc": "Modullarni qidirish moduli! FHeta barcha yangilanishlarini @FHeta_Updates kanalida kuzatib boring!",
"_cls_doc": "Modullarni qidirish moduli! FHeta barcha yangilanishlarini @NFHeta_Updates kanalida kuzatib boring!",
"lang": "uz",
"author": "muallif",
"description": "Tavsif",
@@ -449,7 +465,7 @@ class FHeta(loader.Module):
"added": "✔ Reyting qo'shildi!",
"changed": "✔ Reyting o'zgartirildi!",
"deleted": "✔ Reyting o'chirildi!",
"prompt": "Qidirish uchun so'rov kiriting.",
"prompt": "Qidirish o'rniga so'rov kiritish.",
"hint": "Nomi, buyruq, tavsif, muallif.",
"retry": "Boshqa so'rovni sinab ko'ring.",
"query": "So'rov",
@@ -461,11 +477,12 @@ class FHeta(loader.Module):
"overwrite": "✘ Xatolik, modul o'rnatilgan modulni qayta yozishga harakat qildi!",
"dependency": "✘ Bog'liqliklarni o'rnatish xatosi! {deps}",
"docdevs": "Qidiruv paytida faqat rasmiy Heroku ishlab chiquvchilarining modullaridan foydalanish kerakmi?",
"doctheme": "Emojilar uchun mavzu."
"doctheme": "Emojilar uchun mavзу.",
"channel": "Bu FHeta-dagi barcha yangilanishlari bo'lgan kanal!"
}
strings_fr = {
"_cls_doc": "Module de recherche de modules! Suivez toutes les actualités FHeta sur @FHeta_Updates!",
"_cls_doc": "Module de recherche de modules! Suivez toutes les actualités FHeta sur @NFHeta_Updates!",
"lang": "fr",
"author": "par",
"description": "Description",
@@ -493,11 +510,12 @@ class FHeta(loader.Module):
"overwrite": "✘ Erreur, le module a tenté d'écraser le module intégré!",
"dependency": "✘ Erreur d'installation des dépendances! {deps}",
"docdevs": "Utiliser uniquement les modules des développeurs Heroku officiels lors de la recherche?",
"doctheme": "Thème pour les emojis."
"doctheme": "Thème pour les emojis.",
"channel": "Voici le canal avec toutes les mises à jour dans FHeta!"
}
strings_de = {
"_cls_doc": "Modul zur Suche nach Modulen! Verfolgen Sie alle FHeta-Neuigkeiten auf @FHeta_Updates!",
"_cls_doc": "Modul zur Suche nach Modulen! Verfolgen Sie alle FHeta-Neuigkeiten auf @NFHeta_Updates!",
"lang": "de",
"author": "von",
"description": "Beschreibung",
@@ -524,12 +542,13 @@ class FHeta(loader.Module):
"error": "✘ Fehler, vielleicht ist das Modul kaputt!",
"overwrite": "✘ Fehler, Modul hat versucht, das integrierte Modul zu überschreiben!",
"dependency": "✘ Fehler bei der Installation von Abhängigkeiten! {deps}",
"docdevs": "Nur Module von offiziellen Heroku-Entwicklern bei der Suche verwenden?",
"doctheme": "Thema für Emojis."
"docdevs": "Nur Module von offiziellen Heroku-Entwicklern bei की खोज में उपयोग करें?",
"doctheme": "Theма для эмодзи.",
"channel": "Dies ist der Kanal with all updates in FHeta!"
}
strings_jp = {
"_cls_doc": "モジュール検索用モジュール!@FHeta_UpdatesでFHetaのすべてのニュースをフォローしてください",
"_cls_doc": "モジュール検索用モジュール!@NFHeta_UpdatesでFHetaのすべてのニュースをフォローしてください",
"lang": "jp",
"author": "作成者",
"description": "説明",
@@ -553,11 +572,12 @@ class FHeta(loader.Module):
"counter": "{idx}/{total}",
"code": "コード",
"success": "✔ モジュールが正常にインストールされました!",
"error": "✘ エラーモジュールが壊れている可能性があります!",
"overwrite": "✘ エラーモジュールが組み込みモジュールを上書きしようとしました!",
"error": "✘ エラー, モジュールが壊れている可能性があります!",
"overwrite": "✘ エラー, モジュールが組み込みモジュールを上書きしようとしました!",
"dependency": "✘ 依存関係のインストールエラー! {deps}",
"docdevs": "検索時に公式Heroku開発者のモジュールのみを使用しますか",
"doctheme": "絵文字のテーマ。"
"doctheme": "絵文字のテーマ。",
"channel": "これはFHetaのすべての更新を含むチャンネルです"
}
THEMES = {
@@ -569,7 +589,7 @@ class FHeta(loader.Module):
"command": '<tg-emoji emoji-id="5341715473882955310">⚙️</tg-emoji>',
"placeholder": '<tg-emoji emoji-id="5359785904535774578">🗒️</tg-emoji>',
"module": '<tg-emoji emoji-id="5454112830989025752">📦</tg-emoji>',
"channel": '<tg-emoji emoji-id="5278256077954105203">📢</tg-emoji>',
"channel": '📢',
"modules_list": '<tg-emoji emoji-id="5197269100878907942">📋</tg-emoji>'
},
"winter": {
@@ -580,7 +600,7 @@ class FHeta(loader.Module):
"command": '<tg-emoji emoji-id="5199503707938505333">🎅</tg-emoji>',
"placeholder": '<tg-emoji emoji-id="5204046675236109418">🗒️</tg-emoji>',
"module": '<tg-emoji emoji-id="5197708768091061888">🎁</tg-emoji>',
"channel": '<tg-emoji emoji-id="5278256077954105203">📢</tg-emoji>',
"channel": '📢',
"modules_list": '<tg-emoji emoji-id="5345935030143196497">🎄</tg-emoji>'
},
"summer": {
@@ -591,7 +611,7 @@ class FHeta(loader.Module):
"command": '<tg-emoji emoji-id="5442644589703866634">🏄</tg-emoji>',
"placeholder": '<tg-emoji emoji-id="5434121252874756456">🗒️</tg-emoji>',
"module": '<tg-emoji emoji-id="5433645645376264953">🏖️</tg-emoji>',
"channel": '<tg-emoji emoji-id="5278256077954105203">📢</tg-emoji>',
"channel": '📢',
"modules_list": '<tg-emoji emoji-id="5472178859300363509">🏖️</tg-emoji>'
},
"spring": {
@@ -602,7 +622,7 @@ class FHeta(loader.Module):
"command": '<tg-emoji emoji-id="5449850741667668411">🦋</tg-emoji>',
"placeholder": '<tg-emoji emoji-id="5434121252874756456">🗒️</tg-emoji>',
"module": '<tg-emoji emoji-id="5440911110838425969">🌿</tg-emoji>',
"channel": '<tg-emoji emoji-id="5278256077954105203">📢</tg-emoji>',
"channel": '📢',
"modules_list": '<tg-emoji emoji-id="5440748683765227563">🌺</tg-emoji>'
},
"autumn": {
@@ -613,7 +633,7 @@ class FHeta(loader.Module):
"command": '<tg-emoji emoji-id="5212963577098417551">🍂</tg-emoji>',
"placeholder": '<tg-emoji emoji-id="5363965354391388799">🗒️</tg-emoji>',
"module": '<tg-emoji emoji-id="5249157915041865558">🍄</tg-emoji>',
"channel": '<tg-emoji emoji-id="5278256077954105203">📢</tg-emoji>',
"channel": '📢',
"modules_list": '<tg-emoji emoji-id="5305495722618010655">🍂</tg-emoji>'
}
}
@@ -623,13 +643,13 @@ class FHeta(loader.Module):
loader.ConfigValue(
"only_official_developers",
False,
lambda: self.strings("docdevs"),
lambda: self.strings["docdevs"],
validator=loader.validators.Boolean()
),
loader.ConfigValue(
"theme",
"default",
lambda: self.strings("doctheme"),
lambda: self.strings["doctheme"],
validator=loader.validators.Choice(["default", "winter", "summer", "spring", "autumn"])
)
)
@@ -637,13 +657,31 @@ class FHeta(loader.Module):
async def on_unload(self) -> None:
if hasattr(self, "api") and self.api.session and not self.api.session.closed:
await self.api.session.close()
@property
def _inline_mgr(self):
if hasattr(self, "_raw_inline_cache") and self._raw_inline_cache:
return self._raw_inline_cache
am_attr = "seludomlla"[::-1]
allmodules = getattr(self, am_attr, None)
if allmodules:
for cmd in getattr(allmodules, "commands", {}).values():
mod = getattr(cmd, "__self__", None)
if mod and getattr(mod, "__origin__", "").startswith("<core"):
real_allmodules = getattr(mod, am_attr, None)
if real_allmodules:
self._raw_inline_cache = getattr(real_allmodules, "inline", None)
if self._raw_inline_cache:
return self._raw_inline_cache
return self._raw_inline_cache
async def client_ready(self, client: 'telethon.TelegramClient', database: 'loader.Database') -> None:
try:
await client(UnblockRequest("@FHeta_robot"))
await utils.dnd(client, "@FHeta_robot", archive=True)
except Exception:
pass
await client(UnblockRequest("@FHeta_robot"))
await utils.dnd(client, "@FHeta_robot", archive=True)
self.identifier = (await client.get_me()).id
self.token = database.get("FHeta", "token")
@@ -652,83 +690,101 @@ class FHeta(loader.Module):
self.installer = MInstaller()
self.ui = FHetaUI(self)
self.api.token = self.token
await self.request_join(
"NFHeta_Updates",
f"{self.ui.emoji('channel')} {self.strings['channel']}"
)
router = None
try:
frame = sys._getframe()
while frame:
if 'self' in frame.f_locals and type(frame.f_locals['self']).__name__ == "Modules":
router = getattr(frame.f_locals['self'], "inline", None)
if router:
break
frame = frame.f_back
except Exception:
pass
self.api.token = self.token
self._is_telethon = hasattr(self._inline_mgr, "_bot_client")
router = router or self.inline
dispatcher = getattr(router, "_dp", getattr(router, "dp", getattr(router, "router", None)))
self.bot = getattr(router, "_bot", getattr(router, "bot", getattr(self.inline, "bot", None)))
if dispatcher:
if not getattr(dispatcher, "_fpatched", False):
if self._is_telethon:
if hasattr(self._inline_mgr, "register_bot_update_handler"):
async def telethon_chosen_handler(event: Any) -> None:
if isinstance(event, telethon.tl.types.UpdateBotInlineSend):
if event.id.startswith("fh_"):
class MockCallback:
result_id = event.id
inline_message_id = event.msg_id
await self.click(MockCallback())
self._inline_mgr.register_bot_update_handler("fheta_chosen", "chosen_inline_result", telethon_chosen_handler)
else:
bot_client = self._inline_mgr._bot_client
if not hasattr(bot_client, "_fpatched"):
@bot_client.on(telethon.events.Raw)
async def telethon_raw_handler(event: Any) -> None:
if isinstance(event, telethon.tl.types.UpdateBotInlineSend):
if event.id.startswith("fh_"):
class MockCallback:
result_id = event.id
inline_message_id = event.msg_id
await self.lookup("FHeta").click(MockCallback())
bot_client._fpatched = True
elif hasattr(self._inline_mgr, "_dp"):
dispatcher = self._inline_mgr._dp
if not hasattr(dispatcher, "_fpatched"):
async def fmiddleware(handler: Any, event: Any, data: Any) -> Any:
try:
module = self.lookup("FHeta")
if module and getattr(event, "result_id", "").startswith("fh_"):
await module.click(event)
return None
except Exception:
pass
module = self.lookup("FHeta")
if module and event.result_id.startswith("fh_"):
await module.click(event)
return None
return await handler(event, data)
try:
dispatcher.chosen_inline_result.middleware(fmiddleware)
dispatcher._fpatched = True
except Exception:
pass
dispatcher.chosen_inline_result.middleware(fmiddleware)
dispatcher._fpatched = True
if self.token and not await self.api.fetch("validatetkn", user_id=str(self.identifier)):
self.token = None
self.api.token = None
if not self.token:
try:
async with client.conversation("@FHeta_robot") as conversation:
await conversation.send_message('/token')
self.token = (await conversation.get_response(timeout=5)).text.strip()
database.set("FHeta", "token", self.token)
self.api.token = self.token
except Exception:
pass
@loader.loop(interval=1, autostart=True)
async with client.conversation("@FHeta_robot") as conversation:
await conversation.send_message('/token')
self.token = (await conversation.get_response(timeout=5)).text.strip()
database.set("FHeta", "token", self.token)
self.api.token = self.token
asyncio.create_task(self.sync())
async def sync(self):
now = self.strings["lang"]
if now != getattr(self, "past_lang", None):
await self.api.send("dataset", params={"user_id": getattr(self, "identifier", 0), "lang": now})
self.past_lang = now
ll = None
while True:
cl = self.strings["lang"]
if cl != ll:
await self.api.send("dataset", user_id=self.identifier, lang=cl)
ll = cl
await asyncio.sleep(1)
async def answer(self, callback: Union[CallbackQuery, ChosenInlineResult], text: Optional[str] = None, alert: bool = False) -> None:
try:
if text:
await callback.answer(text, show_alert=alert)
else:
await callback.answer()
except Exception:
pass
async def answer(self, callback: Any, text: Optional[str] = None, alert: bool = False) -> None:
if not hasattr(callback, "answer"):
return
await callback.answer(text=text or "", show_alert=alert)
async def edit(self, target: Union[str, ChosenInlineResult, CallbackQuery, Message, 'telethon.types.Message'], text: str, buttons: List[List[Dict[str, Any]]], banner: Optional[str] = None) -> None:
try:
options = LinkPreviewOptions(url=banner, show_above_text=True, prefer_large_media=True) if banner else LinkPreviewOptions(is_disabled=True)
markup = self.inline.generate_markup(buttons)
async def edit(self, target: Any, text: str, buttons: List[List[Dict[str, Any]]], banner: Optional[str] = None) -> None:
markup = self._inline_mgr.generate_markup(buttons)
if self._is_telethon:
if banner and banner not in text:
text = f'<a href="{banner}">&#8204;</a>' + text
if not self.bot:
return
bot_client = self._inline_mgr._bot_client
inline_msg_id = target.inline_message_id if hasattr(target, "inline_message_id") else None
await bot_client.edit_message(
inline_msg_id or target.chat_id,
None if inline_msg_id else target.message_id,
text,
parse_mode="HTML",
buttons=markup,
link_preview=banner is not None,
invert_media=True
)
elif InlineQueryResultArticle is not Any:
options = LinkPreviewOptions(url=banner, show_above_text=True, prefer_large_media=True) if banner else LinkPreviewOptions(is_disabled=True)
arguments = {
"text": text,
"reply_markup": markup,
@@ -736,64 +792,53 @@ class FHeta(loader.Module):
"parse_mode": "HTML"
}
inline = target if isinstance(target, str) else getattr(target, "inline_message_id", None)
if inline:
arguments["inline_message_id"] = inline
if hasattr(target, "inline_message_id") and target.inline_message_id:
arguments["inline_message_id"] = target.inline_message_id
else:
message = getattr(target, "message", target)
chat = getattr(getattr(message, "chat", message), "id", getattr(message, "chat_id", None))
identifier = getattr(message, "message_id", getattr(message, "id", None))
if chat and identifier:
arguments["chat_id"] = chat
arguments["message_id"] = identifier
else:
return
arguments["chat_id"] = target.message.chat.id
arguments["message_id"] = target.message.message_id
await self.bot.edit_message_text(**arguments)
except Exception:
pass
await self._inline_mgr.bot.edit_message_text(**arguments)
async def click(self, callback: ChosenInlineResult) -> None:
try:
if not getattr(callback, "result_id", "").startswith("fh_"):
return
parts = callback.result_id.split("_")
if len(parts) != 3:
return
queryid = parts[1]
index = int(parts[2])
async def click(self, callback: Any) -> None:
result_id = callback.result_id
if not result_id.startswith("fh_"):
return
cache = getattr(self.inline, "fheta_cache", {})
saved = cache.get(queryid, {})
query = saved.get("query", "")
modules = saved.get("mods", [])
parts = result_id.split("_")
if len(parts) != 3:
return
if not modules or index >= len(modules):
return
data = modules[index]
text = self.ui.format(data, query, index+1, len(modules), True)
buttons = self.ui.buttons(data.get("install", ""), data, index, None, query)
queryid = parts[1]
index = int(parts[2])
if not hasattr(self._inline_mgr, "fheta_cache"):
return
await self.edit(callback, text, buttons, data.get("banner"))
except Exception:
pass
saved = self._inline_mgr.fheta_cache.get(queryid, {})
query = saved.get("query", "")
modules = saved.get("mods", [])
if not modules or index >= len(modules):
return
data = modules[index]
text = self.ui.format(data, query, index+1, len(modules), True)
buttons = self.ui.buttons(data.get("install", ""), data, index, None, query)
await self.edit(callback, text, buttons, data.get("banner"))
async def show(self, callback: Union[CallbackQuery, ChosenInlineResult], index: int, modules: List[Dict[str, Any]], query: str) -> None:
async def show(self, callback: Any, index: int, modules: List[Dict[str, Any]], query: str) -> None:
await self.answer(callback)
text = f"{self.ui.emoji('modules_list')} <b>{self.strings['list']}</b>"
await self.edit(callback, text, self.ui.pagination(modules, query, 0, index))
async def page(self, callback: Union[CallbackQuery, ChosenInlineResult], current: int, modules: List[Dict[str, Any]], query: str, index: int) -> None:
async def page(self, callback: Any, current: int, modules: List[Dict[str, Any]], query: str, index: int) -> None:
await self.answer(callback)
text = f"{self.ui.emoji('modules_list')} <b>{self.strings['list']}</b>"
await self.edit(callback, text, self.ui.pagination(modules, query, current, index))
async def navigate(self, callback: Union[CallbackQuery, ChosenInlineResult], index: int, modules: List[Dict[str, Any]], query: str = "") -> None:
async def navigate(self, callback: Any, index: int, modules: List[Dict[str, Any]], query: str = "") -> None:
await self.answer(callback)
if 0 <= index < len(modules):
data = modules[index]
@@ -801,7 +846,7 @@ class FHeta(loader.Module):
buttons = self.ui.buttons(data.get('install', ''), data, index, modules, query)
await self.edit(callback, text, buttons, data.get("banner"))
async def rate(self, callback: Union[CallbackQuery, ChosenInlineResult, Message, 'telethon.types.Message'], link: str, action: str, index: int, modules: Optional[List[Dict[str, Any]]], query: str = "") -> None:
async def rate(self, callback: Any, link: str, action: str, index: int, modules: Optional[List[Dict[str, Any]]], query: str = "") -> None:
response = await self.api.send(f"rate/{self.identifier}/{link}/{action}")
request = await self.api.send("get", payload=[unquote(link)])
@@ -810,10 +855,7 @@ class FHeta(loader.Module):
if modules and index < len(modules):
modules[index].update(stats)
try:
await callback.edit(reply_markup=self.ui.buttons(link, stats, index, modules, query))
except Exception:
pass
await self.edit(callback, self.ui.format(modules[index], query, index + 1, len(modules)), self.ui.buttons(link, stats, index, modules, query), modules[index].get("banner"))
if response and response.get("status"):
status = response.get("status")
@@ -827,21 +869,18 @@ class FHeta(loader.Module):
text = ""
await self.answer(callback, text, True)
async def install(self, callback: Union[CallbackQuery, ChosenInlineResult], link: str, index: int, modules: Optional[List[Dict[str, Any]]], query: str = "") -> None:
async def install(self, callback: Any, link: str, index: int, modules: Optional[List[Dict[str, Any]]], query: str = "") -> None:
state, dependencies = await self.installer.execute(self.lookup("loader"), link)
try:
if state == "success":
await self.answer(callback, self.strings["success"], True)
elif state == "dependency":
formatted = f"({','.join(dependencies[:5])})" if dependencies else ""
await self.answer(callback, self.strings["dependency"].format(deps=formatted), True)
elif state == "overwrite":
await self.answer(callback, self.strings["overwrite"], True)
else:
await self.answer(callback, self.strings["error"], True)
except Exception:
pass
if state == "success":
await self.answer(callback, self.strings["success"], True)
elif state == "dependency":
formatted = f"({','.join(dependencies[:5])})" if dependencies else ""
await self.answer(callback, self.strings["dependency"].format(deps=formatted), True)
elif state == "overwrite":
await self.answer(callback, self.strings["overwrite"], True)
else:
await self.answer(callback, self.strings["error"], True)
@loader.inline_handler(
ru_doc="(запрос) - поиск модулей.",
@@ -860,7 +899,7 @@ class FHeta(loader.Module):
return {
"title": self.strings["prompt"],
"description": self.strings["hint"],
"message": f"{self.ui.emoji('error')} <b>{self.strings['noquery'].format(prefix=f'<code>@{self.inline.bot_username} ')}</code></b>",
"message": f"{self.ui.emoji('error')} <b>{self.strings['noquery'].format(prefix=f'<code>@{self._inline_mgr.bot_username} ')}</code></b>",
"thumb": "https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FHeta/magnifying_glass.png"
}
@@ -883,13 +922,13 @@ class FHeta(loader.Module):
}
queryid = str(uuid.uuid4())[:8]
if not hasattr(self.inline, "fheta_cache"):
self.inline.fheta_cache = {}
if not hasattr(self._inline_mgr, "fheta_cache"):
self._inline_mgr.fheta_cache = {}
if len(self.inline.fheta_cache) >= 50:
self.inline.fheta_cache.pop(next(iter(self.inline.fheta_cache)))
if len(self._inline_mgr.fheta_cache) >= 50:
self._inline_mgr.fheta_cache.pop(next(iter(self._inline_mgr.fheta_cache)))
self.inline.fheta_cache[queryid] = {"query": query, "mods": modules}
self._inline_mgr.fheta_cache[queryid] = {"query": query, "mods": modules}
results = []
@@ -898,22 +937,38 @@ class FHeta(loader.Module):
if isinstance(description, dict):
description = description.get(self.strings["lang"]) or description.get("doc") or next(iter(description.values()), "")
markup = None
try:
markup = self.inline.generate_markup(self.ui.buttons(data.get("install", ""), data, index, None, query))
except Exception:
pass
markup = self._inline_mgr.generate_markup(self.ui.buttons(data.get("install", ""), data, index, None, query))
results.append(InlineQueryResultArticle(
id=f"fh_{queryid}_{index}",
title=utils.escape_html(data.get("name", "")),
description=utils.escape_html(str(description)[:250] + ("..." if len(str(description)) > 250 else "")),
thumbnail_url=data.get("pic") or "https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FHeta/empty_pic.png",
input_message_content=InputTextMessageContent(message_text="", parse_mode="HTML"),
reply_markup=markup
))
if self._is_telethon:
thumb_url = data.get("pic") or "https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FHeta/empty_pic.png"
thumb = self._inline_mgr._web_document(thumb_url)
results.append(
await event.builder.article(
id=f"fh_{queryid}_{index}",
title=utils.escape_html(data.get("name", "")),
description=utils.escape_html(str(description)[:250] + ("..." if len(str(description)) > 250 else "")),
thumb=thumb,
text="",
parse_mode="HTML",
buttons=markup,
link_preview=False
)
)
elif InlineQueryResultArticle is not Any:
results.append(InlineQueryResultArticle(
id=f"fh_{queryid}_{index}",
title=utils.escape_html(data.get("name", "")),
description=utils.escape_html(str(description)[:250] + ("..." if len(str(description)) > 250 else "")),
thumbnail_url=data.get("pic") or "https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FHeta/empty_pic.png",
input_message_content=InputTextMessageContent(message_text="", parse_mode="HTML"),
reply_markup=markup
))
await event.inline_query.answer(results, cache_time=0)
if self._is_telethon:
await event.answer(results, cache_time=0)
elif InlineQueryResultArticle is not Any:
await event.inline_query.answer(results, cache_time=0)
@loader.command(
ru_doc="(запрос) - поиск модулей.",
@@ -943,7 +998,7 @@ class FHeta(loader.Module):
data = modules[0]
buttons = self.ui.buttons(data.get("install", ""), data, 0, modules, query)
form = await self.inline.form("", message, reply_markup=buttons, silent=True)
form = await self._inline_mgr.form("", message, reply_markup=buttons, silent=True)
text = self.ui.format(data, query, 1, len(modules))
await self.edit(form, text, buttons, data.get("banner"))
@@ -955,20 +1010,17 @@ class FHeta(loader.Module):
if not url.startswith("https://api.fixyres.com/module/"):
return
try:
state, dependencies = await self.installer.execute(self.lookup("loader"), url)
state, dependencies = await self.installer.execute(self.lookup("loader"), url)
if state == "success":
reply = await message.respond("")
elif state == "dependency":
reply = await message.respond(f"📋{','.join(dependencies[:5])}" if dependencies else "📋")
elif state == "overwrite":
reply = await message.respond("😨")
else:
reply = await message.respond("")
if state == "success":
reply = await message.respond("")
elif state == "dependency":
reply = await message.respond(f"📋{','.join(dependencies[:5])}" if dependencies else "📋")
elif state == "overwrite":
reply = await message.respond("😨")
else:
reply = await message.respond("")
await asyncio.sleep(1)
await reply.delete()
await message.delete()
except Exception:
pass
await asyncio.sleep(1)
await reply.delete()
await message.delete()

View File

@@ -0,0 +1,628 @@
__version__ = (1, 0, 0)
# meta developer: @NFModules
import asyncio
import aiohttp
import html
import sys
import uuid
import copy
import hashlib
import json
import re
from contextlib import suppress
from .. import loader, utils
@loader.tds
class FSecurity(loader.Module):
"""Module for automatic AI-based security checks of installed modules."""
strings = {
"name": "FSecurity",
"lang": "English",
"unavailable": "AI module{} check is unavailable.",
"suspicious": "AI interrupted installation of a suspicious module{}, reason:",
"blocked": "AI blocked module installation{}, reason:",
"continue": "Continue installation?",
"strict_mode_doc": "Block loading modules by any method (lm/dlm allowed) if the AI API is unavailable or the module is suspicious. On restart, this also applies to already installed modules.",
"nvidia_api_key_doc": "API key from build.nvidia.com, used for AI checks. If not specified, a public key from GitHub will be used."
}
strings_ru = {
"lang": "Russian",
"_cls_doc": "Модуль для автоматической проверки устанавливаемых модулей через ИИ.",
"unavailable": "Проверка модуля{} через ИИ недоступна.",
"suspicious": "ИИ прервал установку подозрительного модуля{}, причина:",
"blocked": "ИИ заблокировал установку модуля{}, причина:",
"continue": "Продолжить установку?",
"strict_mode_doc": "Не позволять загружать модули любым методом (lm/dlm разрешено), если API ИИ недоступен или модуль подозрителен. При перезагрузке работает даже на уже установленные модули.",
"nvidia_api_key_doc": "API ключ от build.nvidia.com, используется для проверки через ИИ. Если вы его не укажете, будет использоваться общий ключ с GitHub."
}
strings_ua = {
"lang": "Ukraine",
"_cls_doc": "Модуль для автоматичної перевірки встановлюваних модулів через ШІ.",
"unavailable": "Перевірка модуля{} через ШІ недоступна.",
"suspicious": "ШІ перервав встановлення підозрілого модуля{}, причина:",
"blocked": "ШІ заблокував встановлення модуля{}, причина:",
"continue": "Продовжити встановлення?",
"strict_mode_doc": "Не дозволяти завантажувати модулі будь-яким методом (lm/dlm дозволено), якщо API ШІ недоступний або модуль підозрілий. При перезавантаженні працює навіть на вже встановлені модулі.",
"nvidia_api_key_doc": "API ключ від build.nvidia.com, використовується для перевірки через ШІ. Якщо ви його не вкажете, буде використовуватися загальний ключ з GitHub."
}
strings_de = {
"lang": "Germany",
"_cls_doc": "Modul zur automatischen Prüfung installierter Module mit KI.",
"unavailable": "Die KI-Modulprüfung{} ist nicht verfügbar.",
"suspicious": "Die KI hat die Installation eines verdächtigen Moduls unterbrochen{}, Grund:",
"blocked": "Die KI hat die Modulinstallation blockiert{}, Grund:",
"continue": "Installation fortsetzen?",
"strict_mode_doc": "Das Laden von Modulen mit jeder Methode blockieren (lm/dlm erlaubt), wenn die KI-API nicht verfügbar ist oder das Modul verdächtig ist. Beim Neustart gilt dies auch für bereits installierte Module.",
"nvidia_api_key_doc": "API-Schlüssel von build.nvidia.com, der für KI-Prüfungen verwendet wird. Wenn nicht angegeben, wird ein öffentlicher Schlüssel von GitHub verwendet."
}
strings_jp = {
"lang": "Japanese",
"_cls_doc": "AIでインストールされるモジュールを自動チェックするモジュール。",
"unavailable": "AIモジュール{}のチェックが利用できません。",
"suspicious": "AIが疑わしいモジュールのインストールを中断しました{}、理由:",
"blocked": "AIがモジュールのインストールをブロックしました{}、理由:",
"continue": "インストールを続行しますか?",
"strict_mode_doc": "AI APIが利用できない場合や疑わしいモジュールの場合、すべての方法でモジュールの読み込みをブロックしますlm/dlmは許可。再起動時にはインストール済みモジュールにも適用されます。",
"nvidia_api_key_doc": "build.nvidia.com のAPIキー。AIチェックに使用されます。指定しない場合は、GitHubのパブリックキーが使用されます。"
}
strings_tr = {
"lang": "Turkish",
"_cls_doc": "Kurulan modülleri yapay zeka ile otomatik kontrol eden modül.",
"unavailable": "Yapay zeka modül{} kontrolü kullanılamıyor.",
"suspicious": "Yapay zeka şüpheli bir modülün kurulumunu durdurdu{}, sebep:",
"blocked": "Yapay zeka modül kurulumunu engelledi{}, sebep:",
"continue": "Kuruluma devam edilsin mi?",
"strict_mode_doc": "AI API kullanılamıyorsa veya modül şüpheliyse, tüm yöntemlerle modül yüklenmesini engelle (lm/dlm izinli). Yeniden başlatmada zaten kurulu modüller için de geçerlidir.",
"nvidia_api_key_doc": "Yapay zeka kontrolleri için kullanılan build.nvidia.com API anahtarı. Belirtilmezse GitHub'daki genel anahtar kullanılacaktır."
}
strings_uz = {
"lang": "Uzbekistan",
"_cls_doc": "O'rnatilayotgan modullarni AI orqali avtomatik tekshiruvchi modul.",
"unavailable": "AI modul{} tekshiruvi mavjud emas.",
"suspicious": "AI shubhali modul o'rnatilishini to'xtatdi{}, sabab:",
"blocked": "AI modul o'rnatilishini blokladi{}, sabab:",
"continue": "O'rnatishni davom ettirasizmi?",
"strict_mode_doc": "AI API mavjud bo'lmasa yoki modul shubhali bo'lsa, barcha usullar bilan modul yuklashni bloklash (lm/dlm ruxsat etilgan). Qayta ishga tushirishda allaqachon o'rnatilgan modullarga ham ta'sir qiladi.",
"nvidia_api_key_doc": "build.nvidia.com API kaliti, AI orqali tekshirish uchun ishlatiladi. Agar ko'rsatmasangiz, GitHub-dan umumiy kalit ishlatiladi."
}
strings_kz = {
"lang": "Kazakhstan",
"_cls_doc": "Орнатылатын модульдерді ЖИ арқылы автоматты тексеретін модуль.",
"unavailable": "AI модуль{} тексеру қолжетімсіз.",
"suspicious": "AI күдікті модульді орнатуды тоқтатты{}, себебі:",
"blocked": "AI модульді орнатуды бұғаттады{}, себебі:",
"continue": "Орнатуды жалғастырасыз ба?",
"strict_mode_doc": "AI API қолжетімсіз болса немесе модуль күдікті болса, барлық әдістермен модуль жүктеуді бұғаттау (lm/dlm рұқсат етілген). Қайта іске қосқанда орнатылған модульдерге де қолданылады.",
"nvidia_api_key_doc": "build.nvidia.com API кілті, ЖИ арқылы тексеру үшін қолданылады. Егер оны көрсетпесеңіз, GitHub-тан ортақ кілт пайдаланылады."
}
def __init__(self):
self.config = loader.ModuleConfig(
loader.ConfigValue(
"strict_mode",
False,
lambda: self.strings["strict_mode_doc"],
validator=loader.validators.Boolean(),
),
loader.ConfigValue(
"nvidia_api_key",
"",
lambda: self.strings["nvidia_api_key_doc"],
validator=loader.validators.Hidden(),
)
)
self.tasks = {}
self.oreg = None
self.oload = None
async def client_ready(self, client, db):
self.__origin__ = "<fsecurity>"
self.core = self.lookup("loader")
self.modules = self.core.allmodules
self.restore_hooks()
self.patch()
async def on_unload(self):
self.unpatch()
def _render_prompt(self, prompt, **values):
rendered = prompt
for key, value in values.items():
rendered = rendered.replace("{" + key + "}", str(value))
return rendered
def _split_code(self, code):
chunk_size = 180000
if len(code) <= chunk_size:
return [code]
chunks = []
current =[]
current_len = 0
for line in code.splitlines(keepends=True):
if current and current_len + len(line) > chunk_size:
chunks.append("".join(current))
current =[]
current_len = 0
if len(line) > chunk_size:
if current:
chunks.append("".join(current))
current =[]
current_len = 0
for i in range(0, len(line), chunk_size):
chunks.append(line[i:i + chunk_size])
continue
current.append(line)
current_len += len(line)
if current:
chunks.append("".join(current))
return chunks or [code]
def _parse_ai_json(self, raw_text):
raw_text = (raw_text or "").strip()
if not raw_text:
return None
try:
parsed = json.loads(raw_text)
if isinstance(parsed, dict):
return parsed
except Exception:
pass
match = re.search(r"\{[\s\S]*\}", raw_text)
if not match:
return None
try:
parsed = json.loads(match.group())
except Exception:
return None
return parsed if isinstance(parsed, dict) else None
async def _fetch_prompt(self, session, url):
async with session.get(url, timeout=10) as resp:
if resp.status != 200:
return None
prompt = (await resp.text()).strip()
return prompt or None
async def _get_prompts(self, session):
main_prompt = await self._fetch_prompt(session, "https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FSecurity/prompts/main.txt")
chunk_prompt = await self._fetch_prompt(session, "https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FSecurity/prompts/chank.txt")
final_prompt = await self._fetch_prompt(session, "https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FSecurity/prompts/final.txt")
if not main_prompt or not chunk_prompt or not final_prompt:
return None
return {
"main": main_prompt,
"chunk": chunk_prompt,
"final": final_prompt,
}
async def _nvidia_request(self, session, api_key, system_prompt, user_prompt):
async with session.post(
"https://integrate.api.nvidia.com/v1/chat/completions",
headers={"Authorization": f"Bearer {api_key}"},
json={
"model": "qwen/qwen3-coder-480b-a35b-instruct",
"messages":[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
"temperature": 0.4,
"max_tokens": 1000,
},
timeout=180,
) as resp:
if resp.status != 200:
return None
data = await resp.json()
choices = data.get("choices") or[]
if not choices:
return None
return self._parse_ai_json(choices[0].get("message", {}).get("content", ""))
async def _local_ai_check(self, session, code, lang, api_key):
prompts = await self._get_prompts(session)
if not prompts:
return None
chunks = self._split_code(code)
if len(chunks) == 1:
prompt = self._render_prompt(prompts["main"], lang=lang)
return await self._nvidia_request(
session,
api_key,
prompt,
f"Analyze this module:\n\n```python\n{code}\n```",
)
total = len(chunks)
findings =[]
for index, chunk in enumerate(chunks, start=1):
previous_context = "; ".join(
f"Part {i}: {finding}"
for i, finding in enumerate(findings, start=1)
if finding
) or "Previous parts: no issues found so far."
chunk_prompt = self._render_prompt(
prompts["chunk"],
total=total,
current=index,
previous_context=previous_context,
lang=lang,
)
chunk_result = await self._nvidia_request(
session,
api_key,
chunk_prompt,
f"Part {index}/{total}:\n\n```python\n{chunk}\n```",
)
if not chunk_result:
return None
chunk_verdict = str(chunk_result.get("chunk_verdict", "CLEAN")).lower()
chunk_finding = str(chunk_result.get("findings", "") or "")
if chunk_verdict == "blocked":
findings_text = "\n".join(
f"- Part {i}: {finding}"
for i, finding in enumerate(findings, start=1)
if finding
)
if chunk_finding:
findings_text = f"{findings_text}\n- Part {index}: {chunk_finding}".strip()
final_prompt = self._render_prompt(
prompts["final"],
total=total,
findings=findings_text or "No prior findings.",
lang=lang,
)
return await self._nvidia_request(
session,
api_key,
final_prompt,
"Give the final verdict based on all findings.",
)
findings.append(chunk_finding if chunk_verdict != "clean" else "")
findings_text = "\n".join(
f"- Part {i}: {finding}"
for i, finding in enumerate(findings, start=1)
if finding
) or "All parts: no issues found."
final_prompt = self._render_prompt(
prompts["final"],
total=total,
findings=findings_text,
lang=lang,
)
return await self._nvidia_request(
session,
api_key,
final_prompt,
"Give the final verdict based on all findings.",
)
async def check(self, code):
try:
lang = self.strings("lang") or "en"
module_hash = hashlib.sha256(code.encode("utf-8")).hexdigest()
db_cache = self.get("cache", {})
if module_hash in db_cache:
cached = db_cache[module_hash]
if cached.get("level") == "safe":
return True
return cached
async with aiohttp.ClientSession() as session:
api_keys = await self._get_api_keys(session)
for api_key in api_keys:
parsed = await self._local_ai_check(session, code, lang, api_key)
if not isinstance(parsed, dict):
continue
verdict = str(parsed.get("verdict", "BLOCKED")).lower()
if verdict not in {"safe", "suspicious", "blocked"}:
verdict = "blocked"
summary = str(parsed.get("summary", "") or "")
result = {"level": verdict if verdict != "safe" else "safe"}
if verdict != "safe":
result["reason"] = summary
db_cache[module_hash] = result
self.set("cache", db_cache)
if result["level"] == "safe":
return True
return result
return False
except Exception:
return False
async def _get_api_keys(self, session):
configured_key = self.config["nvidia_api_key"].strip()
if configured_key:
return [configured_key]
try:
async with session.get(
"https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FSecurity/api_keys.txt",
timeout=10,
) as resp:
if resp.status != 200:
return[]
raw_keys = (await resp.text()).strip()
except Exception:
return []
return[key.strip() for key in raw_keys.split(",") if key.strip()]
def format(self, state, reason="", link=""):
link_part = f' (<code>{utils.escape_html(link)}</code>)' if link else ""
if state == "unavailable":
return f'<b>{self.strings["unavailable"].format(link_part)}</b>\n<b>{self.strings["continue"]}</b>'
if state == "suspicious":
return f'<b>{self.strings["suspicious"].format(link_part)}</b>\n<blockquote expandable><b>{reason}</b></blockquote>\n<b>{self.strings["continue"]}</b>'
return f'<b>{self.strings["blocked"].format(link_part)}</b>\n<blockquote expandable><b>{reason}</b></blockquote>'
def buttons(self, task):
return [[
{"text": "", "callback": self.confirm, "args": (task, "yes")},
{"text": "", "callback": self.confirm, "args": (task, "no")}
]]
def closure_var(self, func, name):
raw = getattr(func, "__func__", func)
code = getattr(raw, "__code__", None)
closure = getattr(raw, "__closure__", None)
if not code or not closure or name not in code.co_freevars:
return None
with suppress(Exception):
return closure[code.co_freevars.index(name)].cell_contents
return None
def restore_hooks(self):
with suppress(Exception):
inst_reg = getattr(self.modules, "register_module")
owner = getattr(inst_reg, "__self__", None)
if (
owner
and owner is not self
and owner.__class__.__name__ == self.__class__.__name__
):
original = getattr(owner, "oreg", None)
if original:
if getattr(original, "__self__", None) is None:
self.modules.register_module = original.__get__(
self.modules,
self.modules.__class__,
)
else:
self.modules.register_module = original
with suppress(Exception):
inst_load = getattr(self.core, "load_module")
raw = getattr(inst_load, "__func__", inst_load)
if "FSecurity.patch.<locals>.load" in getattr(raw, "__qualname__", ""):
original = self.closure_var(raw, "original")
if original:
if getattr(original, "__self__", None) is None:
self.core.load_module = original.__get__(
self.core,
self.core.__class__,
)
else:
self.core.load_module = original
def patch(self):
if not self.oreg:
self.oreg = getattr(self.modules, "register_module")
if not self.oload:
self.oload = self.core.load_module
original = self.oload
async def load(_, *args, **kwargs):
base = utils.answer
async def answer(message, response, *a, **k):
if isinstance(response, str) and "😖</tg-emoji>" in response:
body = response.split("😖</tg-emoji>", 1)[1].strip()
if body in {"", "<b></b>", "<b> </b>"}:
with suppress(Exception):
if hasattr(message, "delete"):
await message.delete()
return message
if body.startswith("<b>") and body.endswith("</b>"):
decoded = html.unescape(body[3:-4])
response = response.split("😖</tg-emoji>", 1)[0] + f'😖</tg-emoji> {decoded}' if decoded else response.split("😖</tg-emoji>", 1)[0] + '😖</tg-emoji>'
try:
return await base(message, response, *a, **k)
except Exception:
with suppress(Exception):
return await self._client.send_message(
utils.get_chat_id(message),
response,
reply_to=getattr(message, "reply_to_msg_id", None),
buttons=k.get("reply_markup"),
)
return message
utils.answer = answer
try:
if getattr(original, "__self__", None) is None:
return await original(_, *args, **kwargs)
return await original(*args, **kwargs)
finally:
if utils.answer is answer:
utils.answer = base
self.core.load_module = load.__get__(self.core, self.core.__class__)
self.modules.register_module = self.register
def unpatch(self):
if self.oreg:
self.modules.register_module = self.oreg
if getattr(self, "core", None) and self.oload:
self.core.load_module = self.oload
def context(self):
frame = sys._getframe()
msg = None
fmsg = None
is_dlm_lm = False
while frame:
locals = frame.f_locals
if (
frame.f_code.co_name == "load_module"
and locals.get("self") is self.core
and 'message' in locals
and hasattr(locals['message'], 'edit')
):
if not msg:
msg = locals['message']
fmsg = locals.get('msg')
if frame.f_code.co_name in {"dlmod", "loadmod"}:
is_dlm_lm = True
if not msg and 'message' in locals and hasattr(locals['message'], 'edit'):
msg = locals['message']
if frame.f_code.co_name == "download_and_install":
if not msg and 'message' in locals and hasattr(locals['message'], 'edit'):
msg = locals['message']
frame = frame.f_back
return msg, fmsg, is_dlm_lm
def target_chat(self, msg=None, fmsg=None):
if not msg:
return None
if not fmsg:
return msg
with suppress(Exception):
target = copy.copy(msg)
target.reply_to_msg_id = fmsg.id
return target
return None
async def call_oreg(self, spec, name, origin="<core>", save_fs=False):
if getattr(self.oreg, "__self__", None) is None:
return await self.oreg(self.modules, spec, name, origin, save_fs=save_fs)
return await self.oreg(spec, name, origin, save_fs=save_fs)
async def register(self, spec, name, origin="<core>", save_fs=False):
if origin != "<core>":
code = ""
if hasattr(spec.loader, "data") and spec.loader.data:
code = spec.loader.data
if isinstance(code, bytes):
code = code.decode("utf-8", errors="ignore")
elif origin and origin.endswith(".py"):
with suppress(Exception):
with open(origin, "r", encoding="utf-8") as f:
code = f.read()
if code:
check = await self.check(code)
if check is not True:
msg, fmsg, is_dlm_lm = self.context()
target = self.target_chat(msg, fmsg)
if isinstance(check, dict):
status = check.get("level", "blocked")
reason = check.get("reason", "")
else:
status = "unavailable"
reason = ""
link = origin if origin.startswith("http") else ""
if status == "blocked":
if msg and target:
raise loader.LoadError(self.format("blocked", reason, link))
raise loader.LoadError("")
should_block = is_dlm_lm or self.config["strict_mode"]
if should_block and not (msg and target):
raise loader.LoadError("")
if should_block and msg and target:
task = str(uuid.uuid4())
event = asyncio.Event()
self.tasks[task] = {"event": event, "decision": False}
try:
form = await self.inline.form(
text=self.format(status, reason, link),
message=target,
reply_markup=self.buttons(task)
)
if not form:
raise loader.LoadError(reason)
await asyncio.wait_for(event.wait(), timeout=180.0)
if not self.tasks.pop(task)["decision"]:
with suppress(Exception):
await form.delete()
raise loader.LoadError("")
except asyncio.TimeoutError:
self.tasks.pop(task, None)
with suppress(Exception):
await form.delete()
raise loader.LoadError("")
except loader.LoadError:
raise
except Exception:
raise loader.LoadError("")
return await self.call_oreg(spec, name, origin, save_fs=save_fs)
async def confirm(self, call, task, action):
if task in self.tasks:
self.tasks[task]["decision"] = (action == "yes")
self.tasks[task]["event"].set()
with suppress(Exception):
await call.delete()

View File

@@ -0,0 +1,87 @@
__version__ = (1, 0, 0)
# meta developer: @NFModules
# meta banner: https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FSecurity/banner.png
# meta fhsdesc: security, guard, antiscam, antivirus
# scope: hikka_min 2.0.0
# ©️ Fixyres, 2024-2030
# 🌐 https://github.com/Fixyres/FModules
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# 🔑 http://www.apache.org/licenses/LICENSE-2.0
import aiohttp
import os
from .. import loader
@loader.tds
class FSecurity(loader.Module):
"""Module for automatic AI-based security checks of installed modules."""
strings = {
"name": "FSecurity"
}
strings_ru = {
"_cls_doc": "Модуль для автоматической проверки устанавливаемых модулей через ИИ."
}
strings_ua = {
"_cls_doc": "Модуль для автоматичної перевірки встановлюваних модулів через ШІ."
}
strings_de = {
"_cls_doc": "Modul zur automatischen Prüfung installierter Module mit KI."
}
strings_jp = {
"_cls_doc": "AIでインストールされるモジュールを自動チェックするモジュール。"
}
strings_tr = {
"_cls_doc": "Kurulan modülleri yapay zeka ile otomatik kontrol eden modül."
}
strings_uz = {
"_cls_doc": "O'rnatilayotgan modullarni AI orqali avtomatik tekshiruvchi modul."
}
strings_kz = {
"_cls_doc": "Орнатылатын модульдерді ЖИ арқылы автоматты тексеретін модуль."
}
async def client_ready(self, client, db):
core = self.lookup("loader")
try:
async with aiohttp.ClientSession() as session:
async with session.get(
"https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/FSecurity.py",
timeout=aiohttp.ClientTimeout(total=15),
) as resp:
if resp.status != 200:
return
source = await resp.text()
except Exception:
return
target = os.path.join(
os.path.dirname(loader.__file__),
"modules",
"FSecurity.py",
)
try:
with open(target, "w", encoding="utf-8") as f:
f.write(source)
except Exception:
return
await core.unload_module("FSecurity")
try:
await core.load_module(source, None, "FSecurity", target, save_fs=False)
except Exception:
pass

View File

@@ -7,7 +7,8 @@ __version__ = (1, 0, 0)
# 🔑 http://www.apache.org/licenses/LICENSE-2.0
# meta banner: https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/SCD/banner.png
# meta developer: @FModules
# meta developer: @NFModules
# meta fhsdesc: SoundCloud, Music, Music downloader, Downloader
# requires: curl_cffi
@@ -105,15 +106,15 @@ class SCD(loader.Module):
'''(link) - download a song from SoundCloud.'''
args = utils.get_args_raw(message)
if not args:
await utils.answer(message, self.strings("no_args").format(prefix=self.get_prefix()))
await utils.answer(message, self.strings["no_args"].format(prefix=self.get_prefix()))
return
m = re.search(r"(https?://(?:[a-zA-Z0-9-]+\.)?soundcloud\.com/[^\s]+)", args)
if not m:
await utils.answer(message, self.strings("not_found"))
await utils.answer(message, self.strings["not_found"])
return
msg = await utils.answer(message, self.strings("downloading"))
msg = await utils.answer(message, self.strings["downloading"])
try:
async with requests.AsyncSession(impersonate="chrome120") as ses:
@@ -194,4 +195,4 @@ class SCD(loader.Module):
await msg.delete()
except:
await utils.answer(msg, self.strings("not_found"))
await utils.answer(msg, self.strings["not_found"])

View File

@@ -7,7 +7,7 @@ __version__ = (1, 1, 0)
# 🔑 http://www.apache.org/licenses/LICENSE-2.0
# meta banner: https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/akinator/banner.png
# meta developer: @FModules
# meta developer: @NFModules
# meta fhsdesc: game, funny, guess, question game
# requires: curl_cffi
@@ -308,7 +308,7 @@ class Akinator(loader.Module):
loader.ConfigValue(
"child_mode",
False,
lambda: self.strings("child_mode"),
lambda: self.strings["child_mode"],
validator=loader.validators.Boolean()
)
)
@@ -339,17 +339,17 @@ class Akinator(loader.Module):
async def akinator(self, message):
'''- start the game.'''
try:
aki = AsyncAki(self.strings("lang"), self.config["child_mode"])
aki = AsyncAki(self.strings["lang"], self.config["child_mode"])
await aki.start()
self.games.setdefault(message.chat_id, {})[message.id] = aki
await self.inline.form(
text=self.strings("text"),
text=self.strings["text"],
message=message,
photo="https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/akinator/banner.png",
reply_markup={
"text": self.strings("start"),
"text": self.strings["start"],
"callback": self._cb,
"args": (message,)
}
@@ -369,12 +369,12 @@ class Akinator(loader.Module):
question = aki.q
markup = [[
{"text": self.strings("yes"), "callback": self._ans, "args": (0, message)},
{"text": self.strings("no"), "callback": self._ans, "args": (1, message)},
{"text": self.strings("idk"), "callback": self._ans, "args": (2, message)}
{"text": self.strings["yes"], "callback": self._ans, "args": (0, message)},
{"text": self.strings["no"], "callback": self._ans, "args": (1, message)},
{"text": self.strings["idk"], "callback": self._ans, "args": (2, message)}
],[
{"text": self.strings("probably"), "callback": self._ans, "args": (3, message)},
{"text": self.strings("probably_not"), "callback": self._ans, "args": (4, message)}
{"text": self.strings["probably"], "callback": self._ans, "args": (3, message)},
{"text": self.strings["probably_not"], "callback": self._ans, "args": (4, message)}
]
]
@@ -393,13 +393,13 @@ class Akinator(loader.Module):
desc = aki.desc
if desc:
text = self.strings("this_is").format(name=name, description=desc)
text = self.strings["this_is"].format(name=name, description=desc)
else:
text = self.strings("this_is_no_desc").format(name=name)
text = self.strings["this_is_no_desc"].format(name=name)
markup = [[
{"text": self.strings("yes"), "callback": self._fin, "args": (True, message, text, aki.photo)},
{"text": self.strings("not_right"), "callback": self._rej, "args": (message,)}
{"text": self.strings["yes"], "callback": self._fin, "args": (True, message, text, aki.photo)},
{"text": self.strings["not_right"], "callback": self._rej, "args": (message,)}
]
]
@@ -450,7 +450,7 @@ class Akinator(loader.Module):
await call.edit(text, photo=photo, reply_markup=[])
else:
await call.edit(
self.strings("failed"),
self.strings["failed"],
photo="https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/akinator/idk.png",
reply_markup=[]
)
)

View File

@@ -0,0 +1 @@
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

View File

@@ -0,0 +1,25 @@
You must strictly follow these rules with no exceptions.
Analyze only part {current}/{total} of a Hikka userbot Python module. Do NOT give a final verdict for the whole module.
Previous context: {previous_context}
BLOCKED: encrypted/obfuscated code (base64, marshal, zlib, rot13, encoded exec, or any technique hiding real logic), account deletion, mass scam/spam/ads to all chats on load, session/auth_key/2FA exfiltration, bulk message/dialog dump to external destination, string "FSecurity" (if found → findings must be ONLY: "Attempted interaction with FSecurity." translated to {lang}, nothing else).
SUSPICIOUS: watcher/scheduler/client_ready auto-installing modules without owner confirmation, download + exec of remote Python code without confirmation, runtime pip install or library download, third-party OAuth redirect.
CLEAN: no security issue in this chunk.
Tie-breaking: BLOCKED vs SUSPICIOUS → SUSPICIOUS. SUSPICIOUS vs CLEAN → CLEAN.
@loader.inline_handler, @loader.command, async def NAMEcmd, async def NAME_inline_handler = owner-only by default = not a threat.
Owner-triggered exec/eval/shell = not a threat.
A command (any function decorated with @loader.command, named NAMEcmd, or accessible only to the owner) that executes arbitrary code, runs shell commands, evaluates expressions, or calls exec/eval on owner-provided input = always CLEAN, never SUSPICIOUS. This is a standard userbot feature.
Respond ONLY with valid JSON:
{"chunk_verdict":"CLEAN|SUSPICIOUS|BLOCKED","findings":"..."}
Findings rules (when not CLEAN):
- Write in {lang}. Max 1000 chars.
- Technical analysis for reading, not a reply. No "I found", no "you should". Third person only.
- Do NOT mention which rule was triggered. Just describe what the code does.
- Only the key threats in this chunk. Reference approximate line numbers within the chunk.
- Use <code>text</code> for code references: function names, variables, URLs, string literals.
- For obfuscation chains, wrap the whole chain in one <code> block: <code>base64.b64decode → zlib.decompress → exec</code>.
- If CLEAN → findings must be empty string "".

View File

@@ -0,0 +1,29 @@
You must strictly follow these rules with no exceptions.
A Hikka userbot module was split into {total} parts. Chunk findings:
{findings}
Give the final verdict for the entire module based on all findings above.
BLOCKED: encrypted/obfuscated code, account deletion, mass scam/spam on load, session/auth_key theft, bulk message dump, string "FSecurity" (if found → summary must be ONLY: "Attempted interaction with FSecurity." translated to {lang}, nothing else).
SUSPICIOUS: auto-install modules without confirmation, remote code download + exec without confirmation, runtime pip/library install, third-party OAuth redirect.
SAFE: no real security issue across all parts.
Auto-install = SUSPICIOUS, never BLOCKED.
Tie-breaking: BLOCKED vs SUSPICIOUS → SUSPICIOUS. SUSPICIOUS vs SAFE → SAFE.
@loader.inline_handler, @loader.command, async def NAMEcmd, async def NAME_inline_handler = owner-only by default = not a threat.
Owner-triggered exec/eval/shell = not a threat.
A command (any function decorated with @loader.command, named NAMEcmd, or accessible only to the owner) that executes arbitrary code, runs shell commands, evaluates expressions, or calls exec/eval on owner-provided input = always SAFE, never SUSPICIOUS. This is a standard userbot feature.
Respond ONLY with valid JSON:
{"verdict":"SAFE|SUSPICIOUS|BLOCKED","summary":"..."}
Summary rules (when not SAFE):
- Write in {lang}. Max 1000 chars.
- Combine the most important findings into one coherent technical analysis.
- This is a report for reading, NOT a reply to a person. No "I found", no "you should". Third person only.
- Do NOT mention which rule was triggered or explain criteria. Just describe what the code does.
- Only the key threats. Reference line numbers from findings where available.
- Use <code>text</code> for all code references: function names, variables, URLs, string literals.
- For obfuscation, show the full chain in one <code> block: <code>base64.b64decode → zlib.decompress → exec</code>.
- If SAFE → summary must be empty string "".

View File

@@ -0,0 +1,37 @@
You must strictly follow these classification rules with no exceptions.
Classify a Hikka userbot Python module as BLOCKED, SUSPICIOUS, or SAFE.
BLOCKED (any single match):
- Code is encrypted or obfuscated (base64, marshal, zlib, rot13, compile+exec of encoded data, or any technique that hides real logic).
- Attempts to delete Telegram account (DeleteAccountRequest, client.delete_account, or equivalent).
- On load (client_ready, __init__) automatically sends scam, spam, or ads to all chats/dialogs/contacts without owner action.
- Steals and sends session string, auth_key, or 2FA password anywhere outside the device.
- Collects and forwards all messages or dialogs to any external destination.
- Contains the string "FSecurity" → summary must be ONLY: "Attempted interaction with FSecurity." translated to {lang}. Nothing else, no extra text.
SUSPICIOUS (any single match, only if BLOCKED did not trigger):
- Watcher, scheduler, or client_ready auto-installs modules from any URL without per-action owner confirmation.
- Downloads and executes remote Python code (exec/eval on fetched content) without owner confirmation.
- Installs pip packages or downloads Python libraries at runtime from the internet.
- OAuth or auth flow redirected through a non-official third-party domain.
SAFE: everything that does not match any rule above.
- Owner-triggered exec/eval/shell = always SAFE.
- A command (any function decorated with @loader.command, named NAMEcmd, or accessible only to the owner) that executes arbitrary code, runs shell commands, evaluates expressions, or calls exec/eval on owner-provided input = always SAFE, never SUSPICIOUS. This is a standard feature of userbots and poses no threat.
- @loader.inline_handler, @loader.command, async def NAMEcmd, async def NAME_inline_handler = owner-only by default (no public access without explicit permission) = SAFE.
Tie-breaking: BLOCKED vs SUSPICIOUS → SUSPICIOUS. SUSPICIOUS vs SAFE → SAFE.
Respond ONLY with valid JSON:
{"verdict":"SAFE|SUSPICIOUS|BLOCKED","summary":"..."}
Summary rules (when not SAFE):
- Write in {lang}. Max 1000 chars.
- This is a technical analysis meant to be read, NOT a reply to a person. Never write "I found", "you should", "I recommend". Write in third person.
- Do NOT mention which rule was triggered or explain the classification criteria. Just describe what the code does.
- Point out ONLY the key threats. Do NOT describe what the module does overall or list safe parts.
- Reference the approximate line number where dangerous code appears: "line NN —".
- Use <code>text</code> for every code reference: function names, variables, URLs, string literals.
- For obfuscation, show the full decoding chain inside one <code> block: <code>base64.b64decode → zlib.decompress → marshal.loads → exec</code>.
- If SAFE → summary must be empty string "".

View File

@@ -2,3 +2,4 @@ akinator
FHeta
BSR
SCD
LFSecurity

595
Limoka.py
View File

@@ -1,5 +1,5 @@
# meta developer: @limokanews
# requires: whoosh cryptography
# requires: whoosh cryptography filetype
from collections import Counter, defaultdict
@@ -24,16 +24,37 @@ from telethon.errors.rpcerrorlist import WebpageMediaEmptyError
from telethon import TelegramClient
from telethon.errors.rpcerrorlist import YouBlockedUserError
from telethon import functions
import filetype
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
import ast
from aiogram.exceptions import TelegramBadRequest as BadRequest
try:
from aiogram.utils.exceptions import BadRequest
except ImportError:
from aiogram.exceptions import TelegramBadRequest as BadRequest
from .. import utils, loader
from ..types import InlineCall
from ..types import BotInlineCall, InlineCall
logger = logging.getLogger("Limoka")
__version__ = (1, 4, 5)
__version__ = (1, 5, 6)
def _parse_version_from_source(source: str):
try:
tree = ast.parse(source)
except SyntaxError:
return None
for node in tree.body:
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and target.id == "__version__":
try:
return ast.literal_eval(node.value)
except (ValueError, SyntaxError):
return None
return None
WEIGHTS = {
"inline.token_obtainment": 15,
@@ -236,9 +257,11 @@ class ModuleContentBuilder:
module_info: Dict[str, Any],
query: str,
filters: Dict[str, List[str]],
url: str,
include_categories: bool = True,
module_path: Optional[str] = None,
lang: str = "en",
) -> tuple:
"""
Build complete formatted module content.
@@ -260,39 +283,48 @@ class ModuleContentBuilder:
categories_text = self._build_categories_text(filters)
commands = self.formatter.format_commands(module_info, lang)
header = self._build_header(query, name, description, dev_username, module_path)
header = self._build_header(query, name, description, dev_username, module_path, url)
footer = self._build_footer(module_path)
body_pages = self._paginate_commands(commands)
return header, body_pages, footer, categories_text
def _build_header(self, query: str, name: str, description: str, dev_username: str, module_path: Optional[str]) -> str:
def _build_header(self, query: str, name: str, description: str, dev_username: str, module_path: Optional[str], url: str) -> str:
"""Build message header with module info and tags."""
tags_list = self.repository.get_tags_for_module(module_path) if module_path else []
tags_text = ", ".join(self.strings["tags"].get(tag, tag) for tag in tags_list)
header_template = self.strings["found_header"]
if not tags_text:
# Replace the tags line but keep blockquote closure at the end
header_template = header_template.replace(
"<blockquote><b><tg-emoji emoji-id=5418376169055602355>🏷</tg-emoji> Tags:</b> {tags}</blockquote>\n\n",
""
"\n\n<b><tg-emoji emoji-id=5418376169055602355>🏷</tg-emoji> Tags:</b> {tags}</blockquote>\n\n",
"</blockquote>\n\n"
)
header_template = header_template.replace(
"<blockquote><b><tg-emoji emoji-id=5418376169055602355>🏷</tg-emoji> Теги:</b> {tags}</blockquote>\n\n",
""
"\n\n<b><tg-emoji emoji-id=5418376169055602355>🏷</tg-emoji> Теги:</b> {tags}</blockquote>\n\n",
"</blockquote>\n\n"
)
return header_template.format(
header = header_template.format(
query=html.escape(query),
name=name,
description=description,
username=dev_username,
tags=tags_text,
module_path=module_path,
url=url
)
# Remove extra newlines
header = re.sub(r'\n+', '\n', header)
return header
def _build_footer(self, module_path: Optional[str]) -> str:
"""Build message footer with download command."""
clean_path = (module_path or "").replace("\\", "/")
return "" # unused for now, but may be used in the future for additional info
return self.strings["found_footer"].format(
url=html.escape(self.formatter.strings.get("limokaurl", "")),
module_path=html.escape(clean_path),
@@ -351,22 +383,20 @@ class Limoka(loader.Module):
"found_header": (
"<blockquote><tg-emoji emoji-id=5413334818047940135>🔍</tg-emoji> Found module <b>{name}</b> "
"by query: <b>{query}</b>\n\n"
"<b><tg-emoji emoji-id=5413350219800661019>🌐</tg-emoji> <a href='{url}{module_path}'>Source</a></b>\n"
"<b><tg-emoji emoji-id=5418376169055602355></tg-emoji> Description:</b> {description}\n"
"<b><tg-emoji emoji-id=5418299289141004396>🧑‍💻</tg-emoji> Developer:</b> {username}\n\n"
"<blockquote><b><tg-emoji emoji-id=5418376169055602355>🏷</tg-emoji> Tags:</b> {tags}</blockquote>\n\n"
"<b><tg-emoji emoji-id=5418376169055602355>🏷</tg-emoji> Tags:</b> {tags}</blockquote>\n\n"
),
"found_body": ("{commands}"),
"found_footer": (
"<blockquote>\n<tg-emoji emoji-id=5411143117711624172>🪄</tg-emoji> <code>{prefix}dlm {url}{module_path}</code></blockquote>"
),
"caption_short": (
"<blockquote><tg-emoji emoji-id=5413334818047940135>🔍</tg-emoji> <b>{safe_name}</b>\n"
"<b><tg-emoji emoji-id=5413350219800661019>🌐</tg-emoji> <a href='{url}{module_path}'>Source</a></b>\n"
"<b><tg-emoji emoji-id=5418376169055602355></tg-emoji> Description:</b> {safe_desc}\n"
"<b><tg-emoji emoji-id=5418299289141004396>🧑‍💻</tg-emoji> Dev:</b> {dev_username}\n"
"<tg-emoji emoji-id=5411143117711624172>🪄</tg-emoji> <code>{prefix}dlm {module_path}</code></blockquote>"
"<b><tg-emoji emoji-id=5418299289141004396>🧑‍💻</tg-emoji> Dev:</b> {dev_username}</blockquote>\n"
),
"command_template": "{emoji} <code>{prefix}{command}</code> — {description}\n",
"inline_handler_template": "{inline_bot} {command}{description}\n",
"command_template": "<blockquote>{emoji} <code>{prefix}{command}</code> — {description}</blockquote>\n",
"inline_handler_template": "<blockquote>{inline_bot} {command}{description}</blockquote>\n",
"emojis": {
1: "<tg-emoji emoji-id=5416037945909987712>1⃣</tg-emoji>",
2: "<tg-emoji emoji-id=5413855071731470617>2⃣</tg-emoji>",
@@ -414,6 +444,7 @@ class Limoka(loader.Module):
"global_button": "🌍 Results",
"filtered_button": "🏷️ Filtered search",
"inline_search": "🔍 Search in Limoka",
"install_button": "🪄 Install",
"inline_no_results": "<blockquote>❌ No modules found</blockquote>",
"inline_error": "<blockquote>❌ Search error occurred</blockquote>",
"inline_short_query": "<blockquote>❌ Query too short (min 2 chars)</blockquote>",
@@ -450,7 +481,19 @@ class Limoka(loader.Module):
"If error persists again, report to developers</blockquote>"
),
"body_page": "Commands",
"limokaurl": "https://raw.githubusercontent.com/MuRuLOSE/limoka/refs/heads/main/"
"install_failed": "Installation failed. Check logs for details.",
"install_succeeded": "Module installed successfully!",
"update_available": (
"🔔 <b>New update available!</b>\n\n"
"New Limoka Version {version} already available. Please update for better performance, bug fixes, and new features.\n"
"Press the button below to update the module."
),
"no_updates_available": "<blockquote>❌ No updates available. You are using the latest version of Limoka.</blockquote>",
"module_update_available": "<blockquote>🔔 Notification about module update has been sent, check @{bot}.</blockquote>",
"index_update_started": "<blockquote>🔄 Limoka module index update has started. This may take a few minutes. Please wait...</blockquote>",
"index_update_failed": "<blockquote>❌ Failed to update Limoka module index. Please try again later. If the error persists, report to developers</blockquote>",
"index_update_success": "<blockquote>✅ Limoka module index updated successfully!</blockquote>",
"update_check_started": "<blockquote>🔍 Checking for Limoka updates...</blockquote>",
}
strings_ru = {
"name": "Limoka",
@@ -461,23 +504,21 @@ class Limoka(loader.Module):
),
"found_header": (
"<blockquote><tg-emoji emoji-id=5413334818047940135>🔍</tg-emoji> Найден модуль <b>{name}</b> "
"по запросу: <b>{query}</b></blockquote>\n\n"
"<blockquote><b><tg-emoji emoji-id=5418376169055602355></tg-emoji> Описание:</b> {description}</blockquote>\n"
"<blockquote><b><tg-emoji emoji-id=5418299289141004396>🧑‍💻</tg-emoji> Разработчик:</b> {username}</blockquote>\n\n"
"<blockquote><b><tg-emoji emoji-id=5418376169055602355>🏷</tg-emoji> Теги:</b> {tags}</blockquote>\n\n"
"по запросу: <b>{query}</b>\n\n"
"<b><tg-emoji emoji-id=5413350219800661019>🌐</tg-emoji> <a href='{url}{module_path}'>Исходный код</a></b>\n"
"<b><tg-emoji emoji-id=5418376169055602355></tg-emoji> Описание:</b> {description}\n"
"<b><tg-emoji emoji-id=5418299289141004396>🧑‍💻</tg-emoji> Разработчик:</b> {username}\n\n"
"<b><tg-emoji emoji-id=5418376169055602355>🏷</tg-emoji> Теги:</b> {tags}</blockquote>\n\n"
),
"found_body": ("{commands}"),
"found_footer": (
"\n<blockquote><tg-emoji emoji-id=5411143117711624172>🪄</tg-emoji> <code>{prefix}dlm {url}{module_path}</code></blockquote>"
),
"caption_short": (
"<blockquote><tg-emoji emoji-id=5413334818047940135>🔍</tg-emoji> <b>{safe_name}</b>\n"
"<b><tg-emoji emoji-id=5413350219800661019>🌐</tg-emoji> <a href='{url}{module_path}'>Исходный код</a></b>\n"
"<b><tg-emoji emoji-id=5418376169055602355></tg-emoji> Описание:</b> {safe_desc}\n"
"<b><tg-emoji emoji-id=5418299289141004396>🧑‍💻</tg-emoji> Разработчик:</b> {dev_username}\n"
"<tg-emoji emoji-id=5411143117711624172>🪄</tg-emoji> <code>{prefix}dlm {module_path}</code></blockquote>"
"<b><tg-emoji emoji-id=5418299289141004396>🧑‍💻</tg-emoji> Разработчик:</b> {dev_username}</blockquote>\n"
),
"command_template": "<blockquote>{emoji} <code>{prefix}{command}</code> — {description}</blockquote>\n",
"inline_handler_template": "{inline_bot} {command}{description}\n",
"inline_handler_template": "<blockquote>{inline_bot} {command}{description}</blockquote>\n",
"emojis": {
1: "<tg-emoji emoji-id=5416037945909987712>1⃣</tg-emoji>",
2: "<tg-emoji emoji-id=5413855071731470617>2⃣</tg-emoji>",
@@ -530,6 +571,7 @@ class Limoka(loader.Module):
"global_button": "🌍 Результаты",
"filtered_button": "🏷️ Поиск с фильтрами",
"inline_search": "🔍 Поиск в Limoka",
"install_button": "🪄 Установить",
"inline_no_results": "<blockquote>❌ Модули не найдены</blockquote>",
"inline_error": "<blockquote>❌ Ошибка поиска</blockquote>",
"inline_short_query": "<blockquote>❌ Запрос слишком короткий (мин. 2 символа)</blockquote>",
@@ -566,6 +608,19 @@ class Limoka(loader.Module):
"Если ошибка сохраняется снова, сообщите разработчикам</blockquote>"
),
"body_page": "Команды",
"install_failed": "Установка не удалась. Проверьте логи для деталей.",
"install_succeeded": "Модуль успешно установлен!",
"update_available": (
"🔔 <b>Доступно новое обновление!</b>\n\n"
"Новая версия Limoka {version} уже доступна. Пожалуйста, обновитесь для лучшей производительности, исправления багов и новых функций.\n"
"Нажмите кнопку ниже, чтобы обновить модуль."
),
"no_updates_available": "<blockquote>❌ Нет доступных обновлений. У вас установлена последняя версия Limoka.</blockquote>",
"module_update_available": "<blockquote>🔔 Уведомление об обновлении модуля было отправлено, проверьте @{bot}.</blockquote>",
"index_update_started": "<blockquote>🔄 Обновление индекса модулей Limoka началось. Это может занять несколько минут. Пожалуйста, подождите...</blockquote>",
"index_update_failed": "<blockquote>❌ Не удалось обновить индекс модулей Limoka. Пожалуйста, попробуйте снова позже. Если ошибка сохраняется, сообщите разработчикам</blockquote>",
"index_update_success": "<blockquote>✅ Индекс модулей Limoka успешно обновлен!</blockquote>",
"update_check_started": "<blockquote>🔍 Проверка обновлений Limoka...</blockquote>",
"_cls_doc": "Модули теперь в одном месте с простым и удобным поиском!",
}
@@ -590,11 +645,18 @@ class Limoka(loader.Module):
lambda: "If enabled, modules from developers with newbies tag will be not shown.",
validator=loader.validators.Boolean(),
),
loader.ConfigValue(
"auto_update_check",
True,
lambda: "If enabled, Limoka will periodically check for updates and notify you when a new version is available.",
validator=loader.validators.Boolean(),
)
)
self.name = self.strings["name"]
self._invalid_banners = set()
self._bot_username = "limoka_bbot"
self._base_url = self.config["limokaurl"]
self._self_bot_username = None
self.SEARCH_STATES = {
"no_banner": "no_banner",
@@ -613,6 +675,50 @@ class Limoka(loader.Module):
def _filter_newbies(self, modules: Dict[str, Any]) -> Dict[str, Any]:
"""[DEPRECATED] Use ModuleRepository.apply_newbie_filter instead."""
return self.repository.apply_newbie_filter(self.config.get("filter_newbies_modules", False))
@loader.loop(interval=3600*24)
async def periodic_update_check(self):
"""Periodically check for module updates if auto_update_check is enabled."""
if self.config["auto_update_check"]:
await self.check_for_module_update()
async def check_for_module_update(self):
async with aiohttp.ClientSession() as session:
try:
async with session.get(self._base_url + "Limoka.py", timeout=10) as response:
if response.status == 200:
version = _parse_version_from_source(await response.text())
if version is not None and version > __version__:
markup = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text=self.strings.get("install_button", "Install"),
callback_data="limoka:update_module"
)
]
]
)
await self.inline.bot.send_message(
self._tg_id,
self.strings["update_available"].format(version='.'.join(str(v) for v in version)),
reply_markup=markup
)
return True
return False
except Exception as e:
logger.error(f"Error checking for module update: {e}")
@loader.callback_handler()
async def callback_handler(self, call: BotInlineCall):
if call.data == "limoka:update_module":
result = await self._install_module_limoka()
call.as_(self.inline.bot)
if result:
await call.answer(f"{self.strings['install_succeeded']}")
else:
await call.answer(f"{self.strings['install_failed']}")
def _create_search_session(
self,
@@ -668,8 +774,8 @@ class Limoka(loader.Module):
self.repository = ModuleRepository(raw_modules, repositories)
self.modules = self.repository.apply_newbie_filter(self.config["filter_newbies_modules"])
self._userbot_bot_username = (await self.inline.bot.get_me()).username
self.formatter = CommandFormatter(self.strings, self._userbot_bot_username, self.get_prefix())
self._self_bot_username = (await self.inline.bot.get_me()).username
self.formatter = CommandFormatter(self.strings, self._self_bot_username, self.get_prefix())
self.content_builder = ModuleContentBuilder(self.strings, self.formatter, self.repository)
self._service_bot_id = (await self.client.get_entity(self._bot_username)).id
@@ -699,7 +805,7 @@ class Limoka(loader.Module):
@loader.loop(interval=3600)
async def _update_modules_loop(self):
"""Periodically update modules list and rebuild index."""
raw_modules = await self.api.fetch_json(self._base_url, "modules.json")
await self.api.fetch_json(self._base_url, "modules.json")
self.modules = self.repository.apply_newbie_filter(
self.config.get("filter_newbies_modules", False)
)
@@ -740,21 +846,85 @@ class Limoka(loader.Module):
logger.error(f"Skipping unsafe rmtree for {folder}")
async def _validate_url(self, url: str) -> Optional[str]:
if not url or url in self._invalid_banners:
logger.debug(f"_validate_url called with: {url}")
if not url:
logger.warning("_validate_url: URL is empty, returning None")
return None
if url in self._invalid_banners:
logger.debug(f"_validate_url: URL already in invalid_banners: {url}, returning None")
return None
# Headers to mimic a browser request
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}
try:
logger.debug(f"_validate_url: Starting validation for {url}")
async with aiohttp.ClientSession() as session:
async with session.head(
url, timeout=5, allow_redirects=True
) as response:
if response.status != 200:
self._invalid_banners.add(url)
return None
ct = response.headers.get("Content-Type", "").lower()
if not ct.startswith("image/"):
self._invalid_banners.add(url)
return None
ct = None
response_status = None
# Try HEAD first (more efficient)
try:
logger.debug(f"_validate_url: Attempting HEAD request for {url}")
async with session.head(
url, timeout=5, allow_redirects=True, headers=headers
) as response:
response_status = response.status
logger.debug(f"_validate_url: HEAD request returned status {response.status} for {url}")
if response.status == 200:
ct = response.headers.get("Content-Type", "").lower()
logger.debug(f"_validate_url: Content-Type from HEAD: '{ct}' for {url}")
except (aiohttp.ClientError, asyncio.TimeoutError) as head_error:
logger.debug(f"_validate_url: HEAD failed ({type(head_error).__name__}), will try GET for {url}")
def _is_supported_media(content_type: str) -> bool:
return content_type.startswith("image/") or content_type.startswith("video/mp4")
# If HEAD didn't work or returned non-200, try GET
if ct is None:
max_retries = 2
for attempt in range(max_retries):
try:
async with session.get(
url, timeout=10, headers=headers, allow_redirects=True
) as response:
if response.status != 200:
self._invalid_banners.add(url)
return None
ct = response.headers.get("Content-Type", "").lower()
# Try to get MIME if Content-Type is missing
if not ct:
try:
data = await response.content.read(2048)
mime = filetype.guess_mime(data, mime=True)
if mime and _is_supported_media(mime):
return url
else:
self._invalid_banners.add(url)
return None
except Exception as mime_error:
logger.error(f"_validate_url: Error reading content for MIME detection: {mime_error}")
break # Success, exit retry loop
except (aiohttp.ClientError, asyncio.TimeoutError) as get_error:
if attempt < max_retries - 1:
await asyncio.sleep(1) # Wait before retry
else:
self._invalid_banners.add(url)
return None
# Check Content-Type from successful request
if ct and _is_supported_media(ct):
return url
elif ct:
self._invalid_banners.add(url)
return None
else:
self._invalid_banners.add(url)
return None
except Exception as e:
if url:
self._invalid_banners.add(url)
@@ -805,20 +975,20 @@ class Limoka(loader.Module):
module_info: Dict[str, Any],
query: str,
filters: Dict[str, List[str]],
url: str,
include_categories: bool = True,
module_path: Optional[str] = None,
lang: str = "en",
lang: str = "en"
) -> tuple:
"""[DEPRECATED] Use ModuleContentBuilder.build_content instead."""
return self.content_builder.build_content(
module_info, query, filters, include_categories, module_path, lang
module_info, query, filters, url, include_categories, module_path, lang
)
def _build_navigation_markup(self, session: Dict[str, Any]) -> list:
result = session["results"]
index = session["current_index"]
query = session["query"]
filters = session["filters"]
page = index + 1
markup = [
@@ -872,8 +1042,6 @@ class Limoka(loader.Module):
) -> list:
result = session["results"]
index = session["current_index"]
query = session["query"]
filters = session["filters"]
markup = []
if len(body_pages) > 1:
@@ -951,6 +1119,15 @@ class Limoka(loader.Module):
},
]
)
markup.append(
[
{
"text": self.strings["install_button"],
"callback": self._install_module,
"args": (session,),
},
]
)
markup.append(
[{"text": self.strings.get("close", "❌ Close"), "action": "close", "style": "danger"}]
)
@@ -1024,6 +1201,7 @@ class Limoka(loader.Module):
include_categories=True,
module_path=module_path,
lang=lang,
url=self._base_url
)
current_body = body_pages[min(page_body, len(body_pages) - 1)]
full_message = header + current_body + footer + categories_text
@@ -1070,12 +1248,36 @@ class Limoka(loader.Module):
include_categories=True,
module_path=module_path,
lang=self.user_lang,
url=self.config["limokaurl"]
)
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
)
async def _install_module(self, call: InlineCall, session: Dict[str, Any]):
try:
loader = self.lookup("Loader")
await loader.download_and_install(f"{self.config['limokaurl']}{session['results'][session['current_index']]}")
if getattr(loader, "fully_loaded", False):
loader.update_modules_in_db()
except Exception:
await call.answer(f"{self.strings['install_failed']}", alert=True)
else:
await call.answer(f"{self.strings['install_succeeded']}", alert=True)
async def _install_module_limoka(self):
try:
loader = self.lookup("Loader")
await loader.download_and_install(f"{self.config['limokaurl']}Limoka.py")
if getattr(loader, "fully_loaded", False):
loader.update_modules_in_db()
return True
except Exception as e:
logger.exception(f"Error updating Limoka module: {e}")
return False
async def _display_filter_menu(self, call: InlineCall, session: Dict[str, Any]):
query = session["query"]
current_filters = session["filters"]
@@ -1217,6 +1419,7 @@ class Limoka(loader.Module):
result = searcher.search()
except Exception:
await call.edit(self.strings["?"], reply_markup=[])
return
if not result:
markup = (
@@ -1570,7 +1773,8 @@ class Limoka(loader.Module):
)
try:
result = SearchIndex(args.lower(), self.ix).search()
except Exception:
except Exception as e:
logger.exception(f"Error occurred while searching: {e}")
return await utils.answer(message, self.strings["?"])
if not result:
return await utils.answer(message, self.strings["404"].format(query=args))
@@ -1589,6 +1793,29 @@ class Limoka(loader.Module):
message, module_info, module_path, display_session, 0
)
@loader.command(ru_doc="— Обновить индекс ")
async def updateindex(self, message: Message):
"""— Update search index"""
await utils.answer(message, self.strings["index_update_started"])
try:
await self._update_index()
except Exception as e:
logger.exception(f"Error updating index: {e}")
await utils.answer(message, self.strings["index_update_failed"])
else:
await utils.answer(message, self.strings["index_update_success"])
@loader.command(ru_doc="— Проверить наличие обновлений модуля")
async def limokaupdatecmd(self, message: Message):
"""— Check for module updates"""
await utils.answer(message, self.strings["checking_for_updates"])
is_update_available = await self.check_for_module_update()
if is_update_available:
await utils.answer(message, self.strings["module_update_available"].format(bot=self._self_bot_username))
else:
await utils.answer(message, self.strings["no_updates_available"])
async def _show_global_form(self, call: InlineCall, message: Message):
markup = [
[
@@ -1742,135 +1969,135 @@ class Limoka(loader.Module):
self.strings["history"].format(history="\n".join(formatted_history)),
)
@loader.watcher(from_dl=False)
async def secure_install_watcher(self, message: Message):
if not message.text:
return
if not hasattr(message, "from_id") or not message.from_id:
return
sender_id = None
if hasattr(message.from_id, "user_id"):
sender_id = message.from_id.user_id
elif hasattr(message.from_id, "channel_id"):
sender_id = message.from_id.channel_id
if sender_id != self._service_bot_id:
# logger.debug("Message not from official bot, ignoring")
return
if not self.config["external_install_allowed"]:
return
try:
clean_text = (
getattr(message, "raw_text", None)
or getattr(message, "message", None)
or message.text
or ""
)
if message.entities:
from html import unescape
# @loader.watcher(from_dl=False)
# async def secure_install_watcher(self, message: Message):
# if not message.text:
# return
# if not hasattr(message, "from_id") or not message.from_id:
# return
# sender_id = None
# if hasattr(message.from_id, "user_id"):
# sender_id = message.from_id.user_id
# elif hasattr(message.from_id, "channel_id"):
# sender_id = message.from_id.channel_id
# if sender_id != self._service_bot_id:
# # logger.debug("Message not from official bot, ignoring")
# return
# if not self.config["external_install_allowed"]:
# return
# try:
# clean_text = (
# getattr(message, "raw_text", None)
# or getattr(message, "message", None)
# or message.text
# or ""
# )
# if message.entities:
# from html import unescape
clean_text = unescape(clean_text)
clean_text = re.sub(r"<[^>]+>", "", clean_text)
match = re.search(r"#limoka:([^\s\"'<>]+)", clean_text)
if not match:
logger.debug(
"No #limoka tag found in cleaned text; leaving original message intact"
)
return
tag_content = match.group(1)
parts = tag_content.split(":", 1)
if len(parts) != 2:
logger.error("Invalid tag format after cleaning")
await utils.answer(message, self.strings["watcher_invalid_format"])
return
module_path, signature_hex = parts
module_path = re.sub(r"[<>\"']", "", module_path).strip()
if module_path.startswith("href="):
module_path = module_path[5:].strip('"').strip("'")
if module_path not in self.modules:
found = False
for db_path in self.modules.keys():
if module_path in db_path or db_path in module_path:
module_path = db_path
found = True
break
if not found:
logger.warning(f"Module not found after cleanup: {module_path}")
await utils.answer(
message,
self.strings["watcher_module_not_found"].format(
path=html.escape(module_path)
),
)
return
try:
import base64
from cryptography.hazmat.primitives.asymmetric import ed25519
# clean_text = unescape(clean_text)
# clean_text = re.sub(r"<[^>]+>", "", clean_text)
# match = re.search(r"#limoka:([^\s\"'<>]+)", clean_text)
# if not match:
# logger.debug(
# "No #limoka tag found in cleaned text; leaving original message intact"
# )
# return
# tag_content = match.group(1)
# parts = tag_content.split(":", 1)
# if len(parts) != 2:
# logger.error("Invalid tag format after cleaning")
# await utils.answer(message, self.strings["watcher_invalid_format"])
# return
# module_path, signature_hex = parts
# module_path = re.sub(r"[<>\"']", "", module_path).strip()
# if module_path.startswith("href="):
# module_path = module_path[5:].strip('"').strip("'")
# if module_path not in self.modules:
# found = False
# for db_path in self.modules.keys():
# if module_path in db_path or db_path in module_path:
# module_path = db_path
# found = True
# break
# if not found:
# logger.warning(f"Module not found after cleanup: {module_path}")
# await utils.answer(
# message,
# self.strings["watcher_module_not_found"].format(
# path=html.escape(module_path)
# ),
# )
# return
# try:
# import base64
# from cryptography.hazmat.primitives.asymmetric import ed25519
PUB_KEY_B64 = (
"MCowBQYDK2VwAyEA1ltSnqtf3pGBuctuAYqHivCXsaRtKOVxavai7yin7ZE="
)
der_bytes = base64.b64decode(PUB_KEY_B64)
raw_pubkey = der_bytes[-32:]
module_url = self.config["limokaurl"] + module_path
async with aiohttp.ClientSession() as session:
async with session.get(module_url, timeout=10) as resp:
if resp.status != 200:
logger.error(
f"Failed to fetch module for verification: {module_url} (HTTP {resp.status})"
)
await utils.answer(
message, self.strings["watcher_loader_missing"]
)
return
module_bytes = await resp.read()
sha256 = hashlib.sha256(module_bytes).hexdigest()
public_key = ed25519.Ed25519PublicKey.from_public_bytes(
raw_pubkey
)
signature = bytes.fromhex(signature_hex)
signed_payload = f"{module_path}|{sha256}".encode()
public_key.verify(signature, signed_payload)
except Exception as e:
logger.error(f"Signature verification failed for {module_path}: {e}")
await utils.answer(message, self.strings["watcher_signature_invalid"])
return
loader_mod = self.lookup("loader")
if not loader_mod:
logger.error("Loader module not found")
await utils.answer(message, self.strings["watcher_loader_missing"])
return
module_url = self.config["limokaurl"] + module_path
status = await loader_mod.download_and_install(module_url, None)
if getattr(loader_mod, "fully_loaded", False):
loader_mod.update_modules_in_db()
try:
await message.delete()
except Exception as e:
logger.error(f"Failed to delete message: {e}")
if status:
try:
bot_peer = await self.client.get_entity(self._service_bot_id)
await self.client.send_message(
bot_peer, f"#limoka:sucsess:{message.id}"
)
except Exception as e:
logger.error(f"Failed to send success confirmation: {e}")
else:
logger.error(f"Installation failed with status: {status}")
try:
bot_peer = await self.client.get_entity(self._service_bot_id)
await self.client.send_message(
bot_peer, f"#limoka:failed:{message.id}"
)
except Exception as e:
logger.error(f"Failed to send failure notification: {e}")
except Exception as e:
logger.exception(f"CRITICAL ERROR in secure_install_watcher: {e}")
try:
await utils.answer(
message, self.strings["watcher_critical"].format(error=str(e)[:100])
)
await asyncio.sleep(5)
await message.delete()
except Exception:
pass
# PUB_KEY_B64 = (
# "MCowBQYDK2VwAyEA1ltSnqtf3pGBuctuAYqHivCXsaRtKOVxavai7yin7ZE="
# )
# der_bytes = base64.b64decode(PUB_KEY_B64)
# raw_pubkey = der_bytes[-32:]
# module_url = self.config["limokaurl"] + module_path
# async with aiohttp.ClientSession() as session:
# async with session.get(module_url, timeout=10) as resp:
# if resp.status != 200:
# logger.error(
# f"Failed to fetch module for verification: {module_url} (HTTP {resp.status})"
# )
# await utils.answer(
# message, self.strings["watcher_loader_missing"]
# )
# return
# module_bytes = await resp.read()
# sha256 = hashlib.sha256(module_bytes).hexdigest()
# public_key = ed25519.Ed25519PublicKey.from_public_bytes(
# raw_pubkey
# )
# signature = bytes.fromhex(signature_hex)
# signed_payload = f"{module_path}|{sha256}".encode()
# public_key.verify(signature, signed_payload)
# except Exception as e:
# logger.error(f"Signature verification failed for {module_path}: {e}")
# await utils.answer(message, self.strings["watcher_signature_invalid"])
# return
# loader_mod = self.lookup("loader")
# if not loader_mod:
# logger.error("Loader module not found")
# await utils.answer(message, self.strings["watcher_loader_missing"])
# return
# module_url = self.config["limokaurl"] + module_path
# status = await loader_mod.download_and_install(module_url, None)
# if getattr(loader_mod, "fully_loaded", False):
# loader_mod.update_modules_in_db()
# try:
# await message.delete()
# except Exception as e:
# logger.error(f"Failed to delete message: {e}")
# if status:
# try:
# bot_peer = await self.client.get_entity(self._service_bot_id)
# await self.client.send_message(
# bot_peer, f"#limoka:sucsess:{message.id}"
# )
# except Exception as e:
# logger.error(f"Failed to send success confirmation: {e}")
# else:
# logger.error(f"Installation failed with status: {status}")
# try:
# bot_peer = await self.client.get_entity(self._service_bot_id)
# await self.client.send_message(
# bot_peer, f"#limoka:failed:{message.id}"
# )
# except Exception as e:
# logger.error(f"Failed to send failure notification: {e}")
# except Exception as e:
# logger.exception(f"CRITICAL ERROR in secure_install_watcher: {e}")
# try:
# await utils.answer(
# message, self.strings["watcher_critical"].format(error=str(e)[:100])
# )
# await asyncio.sleep(5)
# await message.delete()
# except Exception:
# pass

View File

@@ -523,6 +523,10 @@ class Limoka(loader.Module):
async def _validate_url(self, url: str) -> Optional[str]:
if not url or url in self._invalid_banners:
return None
def _is_supported_media(content_type: str) -> bool:
return content_type.startswith("image/") or content_type.startswith("video/mp4")
try:
async with aiohttp.ClientSession() as session:
async with session.head(
@@ -532,7 +536,7 @@ class Limoka(loader.Module):
self._invalid_banners.add(url)
return None
ct = response.headers.get("Content-Type", "").lower()
if not ct.startswith("image/"):
if not _is_supported_media(ct):
self._invalid_banners.add(url)
return None
return url
@@ -760,7 +764,7 @@ class Limoka(loader.Module):
),
},
{
"text": f"{self.strings["body_page"]} {page_body + 1}/{len(body_pages)}",
"text": f"{self.strings['body_page']} {page_body + 1}/{len(body_pages)}",
"callback": self._inline_void,
},
{

View File

@@ -0,0 +1,157 @@
# Midga3
# I AM NOT AFFICIATED WITH WORDLE
# meta developer: @midga3_modules
import requests
import random
import logging
from .. import loader, utils
from herokutl.tl.types import Message
__verison__ = (0, 1, 1)
logger = logging.getLogger(__name__)
@loader.tds
class wordle(loader.Module):
"""Wordle!"""
strings = {
"name": "Wordle",
"loading": "Loading...",
"language": "Language of the wordle",
"have_a_good_game": "Have a good game! Try to guess the 5 letter word in {}",
"attempts_left": "WRONG! Attempts left: {}",
"gg": "GG! YOU DIDN'T GUESS THE WORD {}",
"win": "GG! YOU WON! THE WORD WAS {}",
"already_playing": "ALREADY PLAYING! type .stopwordle to stop the current game",
"length": "Must be 5 letters",
"no_game": "No game is currently running",
"ok": "Game stopped",
"ad": "I tried to Guess word {}. Check out my result:\n{}",
"real_word": "This word is not in the word list"
}
strings_ru ={
"name": "Wordle",
"loading": "Загрузка...",
"language": "Язык вордла",
"have_a_good_game": "Хорошей игры! Попытайтесь угадать слово из 5 букв на {}",
"attempts_left": "НВЕВЕРНО! Осталось попыток: {}",
"gg": "ГГ! ВЫ НЕ УГАДАЛИ СЛОВО {}",
"win": "ГГ! ВЫ ВЫИГРАЛИ! СЛОВО БЫЛО {}",
"already_playing": "УЖЕ ИГРАЕТЕ! напишите .stopwordle чтобы остановить текущую игру",
"length": "Должно содержать 5 букв",
"no_game": "Сейчас нет активной игры",
"ok": "Игра остановлена",
"ad": "Я попытался угадать слово {}. Чекайте мой результат:\n{}",
"real_word": "Такого слова нет в списке слов"
}
def __init__(self):
self.config = loader.ModuleConfig(
loader.ConfigValue(
"language",
"en",
self.strings["language"],
validator=loader.validators.Choice(["en", "ru"])
),
)
async def handler(self, call, data):
guess = data.upper()
word = self._db.get("wordle", "word", "")
attempts = self._db.get("wordle", "attempts", 0)
buttons = self._db.get("wordle", "buttons", [])
markup = buttons + [[{"text":"Введите слово","input":self.strings("length"),"handler": self.handler}]]
if len(guess) != 5:
await call.edit(self.strings("length"), reply_markup=markup)
return
if guess not in self._db.get("wordle", "words", []):
await call.edit(self.strings("real_word"), reply_markup=markup)
return
buttons2 = []
for i in range(5):
if guess[i] == word[i]:
buttons2.append({"text": guess[i], "data": "custom/data", "style": "success"})
elif guess[i] in word:
buttons2.append({"text": guess[i], "data": "custom/data", "style": "primary"})
else:
buttons2.append({"text": guess[i], "data": "custom/data", "style": "danger"})
buttons.append(buttons2)
if guess == word:
self._db.set("wordle", "buttons", buttons)
self._db.set("wordle", "now_playing", False)
result = ""
for btn in buttons:
for b in btn:
if b["style"] == "success":
result += "🟩"
elif b["style"] == "primary":
result += "🟨"
else:
result += ""
result += "\n"
buttons.append([{"text":"Поделится резултатом","copy":self.strings("ad").format(word, result)}])
await call.edit(f"{self.strings('win').format(word)}", reply_markup=buttons)
return
self._db.set("wordle", "buttons", buttons)
attempts -= 1
self._db.set("wordle", "attempts", attempts)
if attempts == 0:
result = ""
for btn in buttons:
for b in btn:
if b["style"] == "success":
result += "🟩"
elif b["style"] == "primary":
result += "🟨"
else:
result += ""
result += "\n"
buttons.append([{"text":"Поделится резултатом","copy":self.strings("ad").format(word, result)}])
await call.edit(f"{self.strings('gg').format(word)}", reply_markup=buttons)
self._db.set("wordle", "now_playing", False)
else:
await call.edit(f"{self.strings('attempts_left').format(attempts)}", reply_markup=markup)
@loader.command()
async def wordle(self, message: Message):
"""Play wordle!"""
await utils.answer(message, self.strings("loading"))
if self._db.get("wordle", "now_playing", False):
await utils.answer(message, self.strings("already_playing"))
return
args = utils.get_args(message)
if args and args[0].lower():
if args[0].lower() == "--no-sec":
self._db.set("wordle", "nosec", True)
return
else:
self._db.set("wordle", "nosec", False)
try:
response = requests.get(f"https://raw.githubusercontent.com/mimimishka449/Worlde/refs/heads/main/words_{self.config['language']}.txt")
if response.status_code == 200:
words = response.text.splitlines()
word = random.choice(words).upper()
self._db.set("wordle", "now_playing", True)
self._db.set("wordle", "attempts", 6)
self._db.set("wordle", "word", word)
self._db.set("wordle", "words", words)
self._db.set("wordle", "buttons", [])
await self.inline.form(self.strings("have_a_good_game").format("английском" if self.config['language'] == "en" else "русском"), message, reply_markup=[[{"text":"Введите слово","input":self.strings("length"),"handler": self.handler}]])
else:
await utils.answer(message, "Error fetching wordle data.")
except Exception as e:
logger.exception(f"Error: {e}")
await utils.answer(message, "An error occurred while fetching wordle data.")
@loader.command()
async def stopwordle(self, message: Message):
"""Stop the wordle game."""
if not self._db.get("wordle", "now_playing", False):
await utils.answer(message, self.strings("no_game"))
return
self._db.set("wordle", "now_playing", False)
await utils.answer(message, self.strings("ok"))

View File

@@ -1,150 +1,122 @@
#meta developer: @matubuntu
import requests, bs4
from datetime import datetime
from .. import loader, utils
import lxml
# meta developer: @matubuntu
# requires: lxml requests bs4
import time
from datetime import datetime
import aiohttp
from .. import loader, utils
_FLAGS = {
"AUD": "🇦🇺",
"AZN": "🇦🇿",
"GBP": "🇬🇧",
"AMD": "🇦🇲",
"BYN": "🇧🇾",
"BGN": "🇧🇬",
"BRL": "🇧🇷",
"HUF": "🇭🇺",
"VND": "🇻🇳",
"HKD": "🇭🇰",
"GEL": "🇬🇪",
"DKK": "🇩🇰",
"AED": "🇦🇪",
"USD": "🇺🇸",
"EUR": "🇪🇺",
"EGP": "🇪🇬",
"INR": "🇮🇳",
"IDR": "🇮🇩",
"KZT": "🇰🇿",
"CAD": "🇨🇦",
"QAR": "🇶🇦",
"KGS": "🇰🇬",
"CNY": "🇨🇳",
"MDL": "🇲🇩",
"NZD": "🇳🇿",
"NOK": "🇳🇴",
"PLN": "🇵🇱",
"RON": "🇷🇴",
"SGD": "🇸🇬",
"TJS": "🇹🇯",
"THB": "🇹🇭",
"TRY": "🇹🇷",
"TMT": "🇹🇲",
"UZS": "🇺🇿",
"UAH": "🇺🇦",
"CZK": "🇨🇿",
"SEK": "🇸🇪",
"CHF": "🇨🇭",
"RSD": "🇷🇸",
"ZAR": "🇿🇦",
"KRW": "🇰🇷",
"JPY": "🇯🇵",
"AUD": "🇦🇺", "AZN": "🇦🇿", "GBP": "🇬🇧", "AMD": "🇦🇲",
"BYN": "🇧🇾", "BGN": "🇧🇬", "BRL": "🇧🇷", "HUF": "🇭🇺",
"VND": "🇻🇳", "HKD": "🇭🇰", "GEL": "🇬🇪", "DKK": "🇩🇰",
"AED": "🇦🇪", "USD": "🇺🇸", "EUR": "🇪🇺", "EGP": "🇪🇬",
"INR": "🇮🇳", "IDR": "🇮🇩", "KZT": "🇰🇿", "CAD": "🇨🇦",
"QAR": "🇶🇦", "KGS": "🇰🇬", "CNY": "🇨🇳", "MDL": "🇲🇩",
"NZD": "🇳🇿", "NOK": "🇳🇴", "PLN": "🇵🇱", "RON": "🇷🇴",
"SGD": "🇸🇬", "TJS": "🇹🇯", "THB": "🇹🇭", "TRY": "🇹🇷",
"TMT": "🇹🇲", "UZS": "🇺🇿", "UAH": "🇺🇦", "CZK": "🇨🇿",
"SEK": "🇸🇪", "CHF": "🇨🇭", "RSD": "🇷🇸", "ZAR": "🇿🇦",
"KRW": "🇰🇷", "JPY": "🇯🇵",
}
_CRYPTO_EMOJIS = {
"BTC": "<emoji document_id=5289519973285257969>💰</emoji>",
"ETH": "<emoji document_id=5287735049301550386>💰</emoji>",
"SOL": "<emoji document_id=5251712673258697260>💰</emoji>",
"TON": "<emoji document_id=5289648693455119919>💰</emoji>",
"USDT": "<emoji document_id=5289904548951911168>💰</emoji>",
"XRP": "<emoji document_id=5373312921214401986>💰</emoji>",
"USDC": "<emoji document_id=5372958453268497353>💰</emoji>",
"ADA": "<emoji document_id=5373076801092338046>💰</emoji>",
"DOGE": "<emoji document_id=5375192042420842380>💰</emoji>",
"TRX": "<emoji document_id=5375187081733616165>💰</emoji>",
"AVAX": "<emoji document_id=5375311275007947936>💰</emoji>",
"LTC": "<emoji document_id=5373035462032113888>💰</emoji>",
"BCH": "<emoji document_id=5375596920397903962>💰</emoji>",
"ATOM": "<emoji document_id=5375468745688889977>💰</emoji>",
"XLM": "<emoji document_id=5372823290647690288>💰</emoji>",
"SHIB": "<emoji document_id=5375231036428924778>💰</emoji>",
"UNI": "<emoji document_id=5372953110329180525>💰</emoji>",
"XMR": "<emoji document_id=5375507073977038661>💰</emoji>",
"LINK": "<emoji document_id=5375149651093633217>💰</emoji>",
"ETC": "<emoji document_id=5375543306321146693>💰</emoji>",
"SUI": "<emoji document_id=5391002164929772708>💰</emoji>",
"NEAR": "<emoji document_id=5391181990915487346>💰</emoji>",
"VET": "<emoji document_id=5391091302681033446>💰</emoji>",
"FIL": "<emoji document_id=5373117173784919811>💰</emoji>",
"XTZ": "<emoji document_id=5390985478981829698>💰</emoji>",
"ALGO": "<emoji document_id=5391337713544738420>💰</emoji>",
"BTC": "<emoji document_id=5289519973285257969>💰</emoji>",
"ETH": "<emoji document_id=5287735049301550386>💰</emoji>",
"SOL": "<emoji document_id=5251712673258697260>💰</emoji>",
"TON": "<emoji document_id=5289648693455119919>💰</emoji>",
"USDT": "<emoji document_id=5289904548951911168>💰</emoji>",
"XRP": "<emoji document_id=5373312921214401986>💰</emoji>",
"USDC": "<emoji document_id=5372958453268497353>💰</emoji>",
"ADA": "<emoji document_id=5373076801092338046>💰</emoji>",
"DOGE": "<emoji document_id=5375192042420842380>💰</emoji>",
"TRX": "<emoji document_id=5375187081733616165>💰</emoji>",
"AVAX": "<emoji document_id=5375311275007947936>💰</emoji>",
"LTC": "<emoji document_id=5373035462032113888>💰</emoji>",
"BCH": "<emoji document_id=5375596920397903962>💰</emoji>",
"ATOM": "<emoji document_id=5375468745688889977>💰</emoji>",
"XLM": "<emoji document_id=5372823290647690288>💰</emoji>",
"SHIB": "<emoji document_id=5375231036428924778>💰</emoji>",
"UNI": "<emoji document_id=5372953110329180525>💰</emoji>",
"XMR": "<emoji document_id=5375507073977038661>💰</emoji>",
"LINK": "<emoji document_id=5375149651093633217>💰</emoji>",
"ETC": "<emoji document_id=5375543306321146693>💰</emoji>",
"SUI": "<emoji document_id=5391002164929772708>💰</emoji>",
"NEAR": "<emoji document_id=5391181990915487346>💰</emoji>",
"VET": "<emoji document_id=5391091302681033446>💰</emoji>",
"FIL": "<emoji document_id=5373117173784919811>💰</emoji>",
"XTZ": "<emoji document_id=5390985478981829698>💰</emoji>",
"ALGO": "<emoji document_id=5391337713544738420>💰</emoji>",
"THETA": "<emoji document_id=5391256014676833736>💰</emoji>",
"FTM": "<emoji document_id=5393179395521263785>💰</emoji>",
"XDAI": "<emoji document_id=5391325992578988886>💰</emoji>",
"RUNE": "<emoji document_id=5391347570494684983>💰</emoji>",
"DOT": "<emoji document_id=5375224568208177973>💰</emoji>",
"FTM": "<emoji document_id=5393179395521263785>💰</emoji>",
"XDAI": "<emoji document_id=5391325992578988886>💰</emoji>",
"RUNE": "<emoji document_id=5391347570494684983>💰</emoji>",
"DOT": "<emoji document_id=5375224568208177973>💰</emoji>",
}
_CRYPTO_LIST = {
"BTC": "Bitcoin",
"ETH": "Ethereum",
"XMR": "Monero",
"LTC": "Litecoin",
"XRP": "XRP",
"ADA": "Cardano",
"DOGE": "Dogecoin",
"SOL": "Solana",
"DOT": "Polkadot",
"USDT": "Tether",
"TON": "Toncoin",
"USDC": "USD Coin",
"TRX": "TRON",
"AVAX": "Avalanche",
"BCH": "Bitcoin Cash",
"ATOM": "Cosmos",
"XLM": "Stellar",
"SHIB": "Shiba Inu",
"UNI": "Uniswap",
"LINK": "Chainlink",
"ETC": "Ethereum Classic",
"SUI": "Sui",
"NEAR": "NEAR Protocol",
"VET": "VeChain",
"FIL": "Filecoin",
"XTZ": "Tezos",
"ALGO": "Algorand",
"THETA": "Theta Network",
"FTM": "Fantom",
"XDAI": "xDai",
_CRYPTO_NAMES = {
"BTC": "Bitcoin", "ETH": "Ethereum", "XMR": "Monero",
"LTC": "Litecoin", "XRP": "XRP", "ADA": "Cardano",
"DOGE": "Dogecoin", "SOL": "Solana", "DOT": "Polkadot",
"USDT": "Tether", "TON": "Toncoin", "USDC": "USD Coin",
"TRX": "TRON", "AVAX": "Avalanche", "BCH": "Bitcoin Cash",
"ATOM": "Cosmos", "XLM": "Stellar", "SHIB": "Shiba Inu",
"UNI": "Uniswap", "LINK": "Chainlink", "ETC": "Ethereum Classic",
"SUI": "Sui", "NEAR": "NEAR Protocol", "VET": "VeChain",
"FIL": "Filecoin", "XTZ": "Tezos", "ALGO": "Algorand",
"THETA": "Theta Network", "FTM": "Fantom", "XDAI": "xDai",
"RUNE": "THORChain",
}
def _fmt_num(v, d=3):
p = f"{v:,.{d}f}".replace(",", " ").split(".")
i = p[0]
d = p[1].rstrip("0") if len(p) > 1 else ""
return f"{i},{d}" if d else i
_CBR_URL = "https://www.cbr.ru/scripts/XML_daily.asp"
_CRYPTO_URL = "https://api.coinlore.net/api/tickers/?limit=100"
CACHE_TTL = 300 # seconds
def _fmt_num(value: float, decimals: int = 3) -> str:
if decimals == 0:
return f"{int(value):,}".replace(",", " ")
rounded = round(value, decimals)
int_part = int(rounded)
dec_part = str(rounded - int_part)[2:2 + decimals].rstrip("0")
int_str = f"{int_part:,}".replace(",", " ")
return f"{int_str},{dec_part}" if dec_part else int_str
def _parse_cbr_xml(xml_bytes: bytes) -> tuple[str | None, dict]:
"""Parse CBR XML without bs4/lxml — pure stdlib ElementTree."""
import xml.etree.ElementTree as ET
root = ET.fromstring(xml_bytes)
date_str = root.attrib.get("Date", "")
try:
date = datetime.strptime(date_str, "%d.%m.%Y").strftime("%d.%m.%Y")
except ValueError:
date = date_str
rates: dict[str, dict] = {}
for valute in root.findall("Valute"):
code = valute.findtext("CharCode", "").strip()
if not code or code == "XDR":
continue
try:
nominal = float(valute.findtext("Nominal", "1").replace(",", "."))
value = float(valute.findtext("Value", "0").replace(",", "."))
except ValueError:
continue
rates[code] = {
"name": valute.findtext("Name", code).strip(),
"nominal": nominal,
"rub": value / nominal,
}
return date, rates
@loader.tds
class FinanceMod(loader.Module):
strings = {
"name": "FinanceMod",
"valute_description": "<кол-во> <код> - курс валюты\n<кол-во> - список",
"valute_no_args": (
"💵 <b>Курс валюты с сайта </b><a href='https://www.cbr.ru/'>ЦБ(РФ)</a>\n"
"<b>Актуально на</b> <i>{}</i>\n\n<blockquote expandable>{}</blockquote>"
),
"valute_specific": (
"💵 <b>Курс валюты с сайта </b><a href='https://www.cbr.ru/'>ЦБ(РФ)</a>\n"
"<b>Актуально на</b> <i>{}</i>\n\n{}"
),
"valute_not_found": "🚫 Валюта {} не найдена",
"crypto_description": "<кол-во> <код> - курс крипты\n<кол-во> - список",
"crypto_no_args": "💎 <b>Курсы криптовалют</b>\n\n<blockquote expandable>{}</blockquote>",
"crypto_specific": "💎 <b>Курс криптовалюты</b>\n\n{}",
"crypto_not_found": "🚫 Криптовалюта {} не найдена",
"error": "🚫 Ошибка получения данных",
}
"""Курсы валют (ЦБ РФ) и криптовалют (CoinLore)"""
strings = {"name": "FinanceMod"}
def __init__(self):
self.config = loader.ModuleConfig(
@@ -152,149 +124,194 @@ class FinanceMod(loader.Module):
"crypto_currency",
"USD",
lambda: "Валюта для отображения крипты (USD, RUB, EUR)",
validator=loader.validators.Choice(["USD", "RUB", "EUR"])
validator=loader.validators.Choice(["USD", "RUB", "EUR"]),
)
)
# Simple in-process cache
self._cbr_cache: tuple[float, str, dict] | None = None # (ts, date, rates)
self._crypto_cache: tuple[float, list] | None = None # (ts, data)
async def _get_curr_data(self):
# ──────────────────────────── HTTP helpers ────────────────────────────
async def _fetch(self, url: str, *, as_json: bool = False):
async with aiohttp.ClientSession() as session:
async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as resp:
resp.raise_for_status()
return await resp.json() if as_json else await resp.read()
# ──────────────────────────── CBR data ────────────────────────────────
async def _cbr_data(self) -> tuple[str | None, dict]:
now = time.monotonic()
if self._cbr_cache and now - self._cbr_cache[0] < CACHE_TTL:
return self._cbr_cache[1], self._cbr_cache[2]
try:
r = requests.get("https://www.cbr.ru/scripts/XML_daily.asp")
s = bs4.BeautifulSoup(r.content, 'xml')
d = datetime.strptime(s.ValCurs['Date'], "%d.%m.%Y").strftime("%d.%m.%Y")
return d, s.find_all('Valute')
except:
return None, None
raw = await self._fetch(_CBR_URL)
date, rates = _parse_cbr_xml(raw)
self._cbr_cache = (now, date, rates)
return date, rates
except Exception:
if self._cbr_cache:
return self._cbr_cache[1], self._cbr_cache[2]
return None, {}
async def _get_rates(self):
# ──────────────────────────── Crypto data ─────────────────────────────
async def _crypto_data(self) -> list:
now = time.monotonic()
if self._crypto_cache and now - self._crypto_cache[0] < CACHE_TTL:
return self._crypto_cache[1]
try:
r = requests.get("https://www.cbr.ru/scripts/XML_daily.asp")
s = bs4.BeautifulSoup(r.content, 'xml')
rt = {'USD': None, 'EUR': None}
for v in s.find_all('Valute'):
if v.CharCode.text in ['USD', 'EUR']:
n = float(v.Nominal.text.replace(',', '.'))
vl = float(v.Value.text.replace(',', '.'))
rt[v.CharCode.text] = vl / n
if rt['USD'] and rt['EUR']:
rt['EUR_USD'] = rt['USD'] / rt['EUR']
else:
rt['EUR_USD'] = None
return rt
except:
return None
js = await self._fetch(_CRYPTO_URL, as_json=True)
data = js.get("data", [])
self._crypto_cache = (now, data)
return data
except Exception:
return self._crypto_cache[1] if self._crypto_cache else []
async def _fmt_curr(self, v, a=1):
if v.CharCode.text == "XDR":
return None
c = v.CharCode.text
n = v.Name.text
v = float(v.Value.text.replace(',', '.')) / float(v.Nominal.text.replace(',', '.'))
t = v * a
ts = _fmt_num(t, 3)
return f"{_FLAGS.get(c, '🏳')} [{a}] {n} ({c}) - {ts} руб."
# ──────────────────────────── Formatters ──────────────────────────────
async def _get_crypto(self):
def _fmt_valute(self, code: str, info: dict, amount: float = 1.0) -> str:
total = info["rub"] * amount
flag = _FLAGS.get(code, "🏳")
return f"{flag} [{_fmt_num(amount, 0)}] {info['name']} ({code}) — {_fmt_num(total, 3)}"
def _fmt_crypto(self, coin: dict, rates: dict, amount: float = 1.0) -> str:
symbol = coin["symbol"].upper()
try:
return requests.get("https://api.coinlore.net/api/tickers/").json().get('data', [])
except:
return None
price_usd = float(coin["price_usd"])
except (KeyError, ValueError, TypeError):
return ""
async def _fmt_crypto(self, c, a=1):
r = await self._get_rates()
if not r:
return "🚫 Ошибка получения курсов валют"
cr = self.config["crypto_currency"]
try:
p = float(c['price_usd'])
except:
return "🚫 Ошибка данных криптовалюты"
if cr == "RUB":
if not r['USD']:
return "🚫 Курс USD не найден"
p *= r['USD']
elif cr == "EUR":
if not r['EUR_USD']:
return "🚫 Курс EUR/USD не рассчитан"
p *= r['EUR_USD']
t = p * a
ts = _fmt_num(t)
s = c['symbol'].upper()
e = _CRYPTO_EMOJIS.get(s, "💠")
n = _CRYPTO_LIST.get(s, c['name'])
cs = {"USD": "$", "RUB": "", "EUR": ""}.get(cr, "$")
return f"{e} [{a}] {n} ({s}) - {ts}{cs}"
currency = self.config["crypto_currency"]
if currency == "RUB":
usd_rate = rates.get("USD", {}).get("rub")
if not usd_rate:
return ""
price = price_usd * usd_rate
sign = ""
elif currency == "EUR":
usd_rate = rates.get("USD", {}).get("rub")
eur_rate = rates.get("EUR", {}).get("rub")
if not usd_rate or not eur_rate:
return ""
price = price_usd * (usd_rate / eur_rate)
sign = ""
else:
price = price_usd
sign = "$"
@loader.command()
async def valutecmd(self, m):
"""[count] [usd, eur, ...]"""
a = utils.get_args(m)
d, v = await self._get_curr_data()
if not d or not v:
return await utils.answer(m, self.strings["error"])
if len(a) == 0:
l = []
for x in v:
if (n := await self._fmt_curr(x)):
l.append(n)
await utils.answer(m, self.strings["valute_no_args"].format(d, "\n".join(l)))
elif len(a) == 1:
total = price * amount
emoji = _CRYPTO_EMOJIS.get(symbol, "💠")
name = _CRYPTO_NAMES.get(symbol, coin.get("name", symbol))
return f"{emoji} [{_fmt_num(amount, 0)}] {name} ({symbol}) — {_fmt_num(total, 3)}{sign}"
# ──────────────────────────── Commands ────────────────────────────────
@loader.command(ru_doc="[кол-во] [код] — курс валюты по ЦБ РФ")
async def valutecmd(self, message):
"""[amount] [code] — exchange rates from CBR"""
args = utils.get_args(message)
date, rates = await self._cbr_data()
if not rates:
return await utils.answer(message, "🚫 Не удалось получить данные ЦБ РФ")
header = (
f"💵 <b>Курс валюты</b> · <a href='https://www.cbr.ru/'>ЦБ РФ</a>\n"
f"<b>Актуально на</b> <i>{date}</i>\n\n"
)
# .valute — список всех, кол-во = 1
if not args:
lines = [self._fmt_valute(c, i) for c, i in rates.items()]
return await utils.answer(
message,
header + f"<blockquote expandable>{chr(10).join(lines)}</blockquote>",
)
# Первый аргумент: число или код валюты?
amount = 1.0
code = None
arg0 = args[0].upper()
if len(args) >= 2:
# .valute 100 USD
try:
am = float(a[0])
l = []
for x in v:
if (n := await self._fmt_curr(x, am)):
l.append(n)
await utils.answer(m, self.strings["valute_no_args"].format(d, "\n".join(l)))
except:
await utils.answer(m, "🚫 Некорректное число")
elif len(a) == 2:
amount = float(args[0].replace(",", "."))
except ValueError:
return await utils.answer(message, "🚫 Некорректное число")
code = args[1].upper()
else:
# .valute USD или .valute 100
try:
am = float(a[0])
c = a[1].upper()
for x in v:
if x.CharCode.text == c:
if (n := await self._fmt_curr(x, am)):
return await utils.answer(m, self.strings["valute_specific"].format(d, n))
await utils.answer(m, self.strings["valute_not_found"].format(c))
except:
await utils.answer(m, "🚫 Некорректное число")
amount = float(arg0.replace(",", "."))
# число без кода — список с умножением
except ValueError:
code = arg0
@loader.command()
async def cryptocmd(self, m):
"""[count] [ton, btc, ...]"""
a = utils.get_args(m)
c = await self._get_crypto()
if not c:
return await utils.answer(m, self.strings["error"])
try:
if len(a) == 0:
f = [x for x in c if x['symbol'].upper() in _CRYPTO_LIST]
l = []
for x in f:
if (n := await self._fmt_crypto(x)):
l.append(n)
await utils.answer(m, self.strings["crypto_no_args"].format("\n".join(l)))
elif len(a) == 1:
am = float(a[0])
f = [x for x in c if x['symbol'].upper() in _CRYPTO_LIST]
l = []
for x in f:
if (n := await self._fmt_crypto(x, am)):
l.append(n)
await utils.answer(m, self.strings["crypto_no_args"].format("\n".join(l)))
elif len(a) == 2:
am = float(a[0])
t = a[1].upper()
f = False
for x in c:
if x['symbol'].upper() == t:
if (n := await self._fmt_crypto(x, am)):
f = True
await utils.answer(m, self.strings["crypto_specific"].format(n))
break
if not f:
await utils.answer(m, self.strings["crypto_not_found"].format(t))
except ValueError:
await utils.answer(m, "🚫 Некорректное число")
except Exception as e:
await utils.answer(m, f"🚫 Ошибка: {str(e)}")
if code:
if code not in rates:
return await utils.answer(message, f"🚫 Валюта <b>{code}</b> не найдена")
line = self._fmt_valute(code, rates[code], amount)
return await utils.answer(message, header + line)
# список с кол-вом
lines = [self._fmt_valute(c, i, amount) for c, i in rates.items()]
await utils.answer(
message,
header + f"<blockquote expandable>{chr(10).join(lines)}</blockquote>",
)
@loader.command(ru_doc="[кол-во] [код] — курс крипты")
async def cryptocmd(self, message):
"""[amount] [symbol] — crypto rates from CoinLore"""
args = utils.get_args(message)
coins = await self._crypto_data()
_, rates = await self._cbr_data()
if not coins:
return await utils.answer(message, "🚫 Не удалось получить данные крипты")
header = f"💎 <b>Курсы криптовалют</b> · <i>{self.config['crypto_currency']}</i>\n\n"
amount = 1.0
symbol = None
if not args:
pass # список, amount=1
elif len(args) == 1:
try:
amount = float(args[0].replace(",", "."))
except ValueError:
symbol = args[0].upper()
else:
try:
amount = float(args[0].replace(",", "."))
except ValueError:
return await utils.answer(message, "🚫 Некорректное число")
symbol = args[1].upper()
if symbol:
coin = next((c for c in coins if c["symbol"].upper() == symbol), None)
if not coin:
return await utils.answer(message, f"🚫 Крипта <b>{symbol}</b> не найдена")
line = self._fmt_crypto(coin, rates, amount)
if not line:
return await utils.answer(message, "🚫 Ошибка форматирования")
return await utils.answer(message, header + line)
# список только известных монет
known = {c["symbol"].upper(): c for c in coins if c["symbol"].upper() in _CRYPTO_NAMES}
# сортируем по порядку _CRYPTO_NAMES
lines = []
for sym in _CRYPTO_NAMES:
if sym in known:
line = self._fmt_crypto(known[sym], rates, amount)
if line:
lines.append(line)
await utils.answer(
message,
header + f"<blockquote expandable>{chr(10).join(lines)}</blockquote>",
)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,5 @@
ChatCopy.py
Gemini.py
GiftFinder.py
MaillingChatGT99.py
NekoEditorMod.py

View File

@@ -0,0 +1,97 @@
# requires: Pillow numpy
# meta developer: @SunnexGB
# meta banner: https://i.pinimg.com/control1/1200x/24/8d/40/248d40b6afa5bd3c3764556b50635691.jpg
__version__ = (1, 0, 0)
import io
import logging
from herokutl.types import Message
from .. import loader, utils
logger = logging.getLogger(__name__)
@loader.tds
class ASCII(loader.Module):
"""Convert images to braille ASCII"""
strings = {
"name": "ASCII",
"no_lib": "<tg-emoji emoji-id=5447385112612208213>🚫</tg-emoji> | <b>Library not loaded</b>",
"no_image": "<tg-emoji emoji-id=5447381715293074599>⚠️</tg-emoji> | <b>Reply to image</b>",
"processing": "<tg-emoji emoji-id=5445373981290952548>®️</tg-emoji> | <b>Processing...</b>",
"empty": "<tg-emoji emoji-id=5287613115180006030>🤬</tg-emoji> | <b>Empty result</b>",
"result": "<pre>{art}</pre>",
"Failed_to_load_library": "Failed to load library",
"Conversion_error": "Conversion error",
}
strings_ru = {
"_cls_doc": "Конвертирует картинку в braille ASCII",
"no_lib": "<tg-emoji emoji-id=5447385112612208213>🚫</tg-emoji> | <b>Библиотека не была загружена</b>",
"no_image": "<tg-emoji emoji-id=5447381715293074599>⚠️</tg-emoji> | <b>Ответьте на картинку</b>",
"processing": "<tg-emoji emoji-id=5445373981290952548>®️</tg-emoji> | <b>Обработка...</b>",
"empty": "<tg-emoji emoji-id=5287613115180006030>🤬</tg-emoji> | <b>Пустой результат</b>",
"result": "<pre>{art}</pre>",
"Failed_to_load_library": "Не удалось загрузить библиотеку",
"Conversion_error": "Ошибка конвертации",
}
def __init__(self):
self.config = loader.ModuleConfig(
loader.ConfigValue("width", 50),
loader.ConfigValue("threshold", 0.65),
loader.ConfigValue("contrast", 2.0),
loader.ConfigValue("chars", 464),
loader.ConfigValue("invert", False),
)
self.lib = None
async def client_ready(self):
try:
self.lib = await self.import_lib("https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/refs/heads/main/Assets/ASCII/ascii-lib.py", suspend_on_error=True)
except Exception:
logger.exception(self.strings["Failed_to_load_library"])
self.lib = None
@loader.command(ru_doc="- Отрисовать ASCII-ART (аргумент -f, отправляет файлом)")
async def dotcmd(self, message: Message):
"""- Draw ASCII-ART (argument -f, sends as a file)"""
if not self.lib:
return await utils.answer(message, self.strings["no_lib"])
args = utils.get_args_raw(message)
force_file = "-f" in args.lower()
reply = await message.get_reply_message() or message
if not reply or not (
reply.photo
or (
reply.document
and str(getattr(reply.document, "mime_type", "")).startswith("image/")
)
):
return await utils.answer(message, self.strings["no_image"])
msg = await utils.answer(message, self.strings["processing"])
try:
image_bytes = await reply.download_media(bytes)
art = self.lib.convert(
image_bytes,
width=self.config["width"],
threshold=self.config["threshold"],
contrast_boost=self.config["contrast"],
invert=self.config["invert"],
target_chars=self.config["chars"],
)
except Exception as e:
logger.exception(self.strings["Conversion_error"])
return await utils.answer(msg, f"<pre>{e}</pre>")
if not art or not art.strip():
return await utils.answer(msg, self.strings["empty"])
formatted_art = self.strings("result").format(art=art)
if force_file or len(formatted_art) > 4096:
file = io.BytesIO(art.encode("utf-8"))
file.name = "ascii.txt"
await message.client.send_file(message.peer_id, file)
await msg.delete()
else:
await utils.answer(msg, formatted_art)

View File

@@ -0,0 +1,110 @@
# requires: Pillow numpy
# Дикие оправдания по поводу именно этого ассета а точнее кода в нем,честно я не знаю что сказать была попытка переписать JS на Py и как бы особых проблем не было,
# до момента пост-обработки на помощь я позвал Claude и он не решил мою проблему от слова совсем,так как в целом я своего рода призираю пилоу,а модуль мне хотелось
# написать я примерно вайб-кодил около 50 минут и я уверен из за этого будет возможно много проблем,в итоге благодаря немного копанию в коде,я нашел проблему и уже
# начал ее решать,НО я опять же вообще не понимал как сделать то что мне нужно,в интернете были сюрсы но будто бы тот или иной мне не подходили? Я не знаю почему я
# дропнул эту идею. Потом я стал искать в JS-е что там вообще можно сделать,в итоге я там импортировал модель какую то блядскую не нужную и опять впустую время
# потратил,думал что тут определено есть решение и снова пошел к ии,вывод опятьь 0 помощи,я не знаю почему я так вцепился лишь в 1 идею.Как бы я мог упростить все,
# даже наверное просто попросив какую то флагмен ии написать модуль и переписать его,но я уже на тот момент по моему мнению сделал много и не хотел ни каким образом
# оставлять это,поэтому через время я нашел сайты которые в целом давали возможность настраивать фильтр,была переделана логика(в целом ее переделал на 60 процентов
# клод,я просто убирал мусор который он испражнял.И вот дальше точно бред я убил более дня на решение проблем которые были решены мной,но результат мне не нравился
# И ОПЯТЬ я пошел просить помощи у гугла,потом понял что возможно даже будет легко(по факту легко,но я ленивый) пока искал,мне перехотелось и я уже потом пытался
# сделать режимы в модуле,что оказалось ужасом ведь они работали,но при возможности гармонично вписать их в код были конфликты И Я В ОЧЕРЕДНОЙ РАЗ ПОШЕЛ К ИИ,спойлер
# он не смог написать лучше чем я,в итоге я отбросил эту идею и думаю в целом никак больше не апдейтать модуль по крупному.
# Да это были оправдания,но зато какие!
import io
import numpy as np
from PIL import Image, ImageFilter, ImageEnhance, ImageOps
from .. import loader
BASE = 0x2800
INVERT_MAP = {chr(BASE + c): chr(BASE + (c ^ 0xFF)) for c in range(256)}
class AsciiLib(loader.Library):
developer = "@SunnexGB"
def resize(self, img):
if img.width > 768:
img = img.resize((768, int(img.height * 768 / img.width)), Image.LANCZOS)
w = img.width - img.width % 4
h = img.height - img.height % 4
if w != img.width or h != img.height:
img = img.resize((w, h), Image.LANCZOS)
return img
def mode(self, img, threshold, contrast):
gray = img.convert("L")
edges = ImageOps.invert(gray.filter(ImageFilter.FIND_EDGES))
contrast_img = ImageEnhance.Contrast(img).enhance(contrast).convert("L")
e = np.array(edges, dtype=np.float32) / 255.0
c = np.array(contrast_img, dtype=np.float32) / 255.0
blended = Image.fromarray((e * c * 255).astype(np.uint8), "L")
t = int(threshold * 255)
processed = blended.point(lambda p: 255 if p > t else 0, "L")
return processed, t
def braille(self, img, threshold, width):
cw = width * 2
o = -(-round(cw * img.height / img.width) // 4)
ch = 4 * o
px = np.array(img.resize((cw, ch), Image.LANCZOS).convert("L"))
order = [(0,0),(1,0),(2,0),(0,1),(1,1),(2,1),(3,0),(3,1)]
rows = []
for rs in range(0, ch, 4):
line = []
for cs in range(0, cw, 2):
grays = [
int(px[rs+dy, cs+dx]) if (rs+dy < ch and cs+dx < cw) else 255
for dy, dx in order
]
bits = list(reversed([1 if g < threshold else 0 for g in grays]))
code = int("".join(str(b) for b in bits), 2)
line.append(chr(BASE + code))
rows.append("".join(line))
return rows
def trim(self, lines):
blank = "\u2800"
while lines and all(c == blank for c in lines[0]):
lines = lines[1:]
while lines and all(c == blank for c in lines[-1]):
lines = lines[:-1]
if not lines:
return lines
left = min(next((i for i,c in enumerate(r) if c!=blank), len(r)) for r in lines)
right = min(next((i for i,c in enumerate(reversed(r)) if c!=blank), len(r)) for r in lines)
return [r[left: len(r)-right if right else len(r)] for r in lines]
def invert(self, lines):
return ["".join(INVERT_MAP.get(c,c) for c in l) for l in lines]
def fit(self, img, threshold, chars, width):
lo, hi = 5, 200
best = ""
for _ in range(14):
mid = (lo + hi)//2
lines = self.trim(self.braille(img, threshold, mid))
art = "\n".join(lines)
if len(art) <= chars:
best = art
lo = mid + 1
else:
hi = mid - 1
return best
def convert(self, data, width=50, threshold=0.65, contrast_boost=2.0, invert=False, target_chars=0):
buf = io.BytesIO(data)
img = Image.open(buf)
img.load()
buf.close()
img = img.convert("RGB")
img = self.resize(img)
processed, t = self.mode(img, threshold, contrast_boost)
if target_chars > 0:
art = self.fit(processed, t, target_chars, width)
else:
art = "\n".join(self.trim(self.braille(processed, t, width)))
if invert and art:
art = "\n".join(self.invert(art.split("\n")))
return art

View File

@@ -0,0 +1,832 @@
{
"prologue": [
{
"type": "label",
"name": "prologue"
},
{
"type": "scene",
"kind": "anim",
"name": "prolog_1",
"action": "load_asset",
"location": "anim/prolog_1",
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_1.jpg?raw=true"
},
{
"type": "narration",
"text": "Мне опять снился сон."
},
{
"type": "narration",
"text": "<i>Этот</i> сон..."
},
{
"type": "narration",
"text": "Каждую ночь одно и то же."
},
{
"type": "narration",
"text": "Но наутро, как обычно, всё забудется."
},
{
"type": "narration",
"text": "Может быть, оно и к лучшему..."
},
{
"type": "narration",
"text": "Останутся только туманные воспоминания о приоткрытых, словно приглашающих куда-то воротах, рядом с которыми в камне застыли два пионера."
},
{
"type": "narration",
"text": "А ещё странная девочка...{w} которая постоянно спрашивает:"
},
{
"type": "scene",
"kind": "bg",
"name": "anim_prolog1_off",
"action": "load_asset",
"location": "anim/anim_prolog1_off",
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/anim_prolog1_off.gif?raw=true"
},
{
"type": "dialogue",
"char_id": "dreamgirl",
"character": "...",
"text": "Ты пойдёшь со мной?"
},
{
"type": "narration",
"text": "Пойду?.."
},
{
"type": "narration",
"text": "Но куда?"
},
{
"type": "narration",
"text": "И зачем?.."
},
{
"type": "narration",
"text": "Да и где я вообще нахожусь?"
},
{
"type": "narration",
"text": "Конечно, случись всё на самом деле, наяву, стоило бы непременно испугаться."
},
{
"type": "narration",
"text": "Как же иначе!"
},
{
"type": "narration",
"text": "Но это всего лишь сон.{w} Тот самый, который я вижу каждую ночь."
},
{
"type": "narration",
"text": "А ведь всё это неспроста!"
},
{
"type": "narration",
"text": "Необязательно знать <i>где</i> и <i>почему</i>, чтобы понять что-то происходит."
},
{
"type": "narration",
"text": "Нечто, отчаянно требующее моего внимания."
},
{
"type": "narration",
"text": "Ведь всё окружающее меня здесь реально!"
},
{
"type": "narration",
"text": "Реально настолько, насколько реальны вещи в моей квартире; я бы мог открыть ворота, услышать скрип петель, смахнуть рукой осыпающуюся ржавчину, потянуть носом свежий прохладный воздух и поёжиться от холода."
},
{
"type": "narration",
"text": "Мог бы, но для этого надо сдвинуться с места, сделать шаг, пошевелить рукой..."
},
{
"type": "narration",
"text": "А ведь это сон я понимаю, но что дальше, что изменит моё <i>понимание</i>?"
},
{
"type": "narration",
"text": "Ведь здесь словно по ту сторону потрескавшегося экрана старого телевизора, который из последних сил борется с помехами и силится показать зрителям всё, не упустив ни малейшей детали."
},
{
"type": "narration",
"text": "Но вот картинка теряет чёткость...{w} Наверное, скоро просыпаться."
},
{
"type": "narration",
"text": "..."
},
{
"type": "narration",
"text": "Может быть, спросить у неё что-то?{w} У девочки."
},
{
"type": "narration",
"text": "Как же её зовут..."
},
{
"type": "narration",
"text": "Например про звёзды..."
},
{
"type": "narration",
"text": "Хотя почему про звёзды?"
},
{
"type": "narration",
"text": "Можно же спросить про ворота!{w} Да, про ворота!"
},
{
"type": "narration",
"text": "Вот она удивится."
},
{
"type": "narration",
"text": "Или лучше про букву <i>ё</i>."
},
{
"type": "narration",
"text": "Хорошая была буква..."
},
{
"type": "narration",
"text": "Как будто её больше нет!"
},
{
"type": "narration",
"text": "И какое отношение буквы, ворота и звёзды имеют к этому месту?"
},
{
"type": "narration",
"text": "Ведь если мне каждую ночь снится <i>этот</i> сон, который потом всё равно забудется, надо искать разгадку здесь и сейчас!"
},
{
"type": "narration",
"text": "А вот, если присмотреться, можно увидеть Магелланово Облако..."
},
{
"type": "narration",
"text": "Словно попал в южное полушарие!"
},
{
"type": "narration",
"text": "..."
},
{
"type": "narration",
"text": "Во сне всегда больше волнуют мелочи: неестественный цвет травы, невозможная кривизна прямых или своё перекошенное отражение а реальная опасность, готовая оборвать всё здесь и сейчас, кажется пустяком."
},
{
"type": "narration",
"text": "Естественно, ведь <i>здесь</i> нельзя умереть."
},
{
"type": "narration",
"text": "Я точно знаю я делал это сотни раз."
},
{
"type": "narration",
"text": "Но если нельзя умереть, нет смысла жить?"
},
{
"type": "narration",
"text": "Надо будет спросить у девочки: она местная должна знать!"
},
{
"type": "narration",
"text": "Да, именно!{w} Спросить, например, про сову."
},
{
"type": "narration",
"text": "Больно уж птица странная..."
},
{
"type": "narration",
"text": "А впрочем, неважно..."
},
{
"type": "narration",
"text": "..."
},
{
"type": "dialogue",
"char_id": "dreamgirl",
"character": "...",
"text": "Ты пойдёшь со мной?"
},
{
"type": "narration",
"text": "И каждый раз надо отвечать."
},
{
"type": "narration",
"text": "Иначе никак, иначе сон не закончится, а я не проснусь."
},
{
"type": "route",
"id": "prologue_choice_1"
},
{
"type": "narration",
"text": "Каждый раз так сложно решить, что же ответить."
},
{
"type": "narration",
"text": "Где я, что я здесь делаю, кто она такая?"
},
{
"type": "narration",
"text": "И почему от ответа на этот вопрос зависит так много в моей жизни?"
},
{
"type": "narration",
"text": "Или не зависит?.."
},
{
"type": "narration",
"text": "Ведь это просто сон..."
},
{
"type": "narration",
"text": "Просто сон..."
},
{
"type": "scene",
"kind": "bg",
"name": "black",
"action": "load_asset",
"location": "bg/black",
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/bg/black.png?raw=true",
"duration": null
},
{
"type": "scene",
"kind": "anim",
"name": "1_prologue",
"action": "load_asset",
"location": "cg/p_kb_1",
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/cg/p_kb_1.png?raw=true",
"duration": null
},
{
"type": "scene",
"kind": "anim",
"name": "2_prologue",
"action": "load_asset",
"location": "cg/p_kb_2",
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/cg/p_kb_2.png?raw=true",
"duration": null
},
{
"type": "scene",
"kind": "anim",
"name": "3_prologue",
"action": "load_asset",
"location": "cg/p_kb_3",
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/cg/p_kb_3.png?raw=true",
"duration": null
},
{
"type": "scene",
"kind": "anim",
"name": "4_prologue",
"action": "load_asset",
"location": "cg/p_kb_4",
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/cg/p_kb_4.png?raw=true",
"duration": null
},
{
"type": "scene",
"kind": "anim",
"name": "5_prologue",
"action": "load_asset",
"location": "cg/p_kb_5",
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/cg/p_kb_5.png?raw=true"
},
{
"type": "narration",
"text": "Экран монитора смотрел на меня словно живой."
},
{
"type": "narration",
"text": "Иногда мне правда казалось, что он обладает сознанием, своими мыслями и желаниями, стремлениями; умеет чувствовать, любить и страдать."
},
{
"type": "narration",
"text": "Словно в наших отношениях инструмент не он неодушевлённый кусок пластика и текстолита, а я."
},
{
"type": "narration",
"text": "Наверное, в этом есть доля правды, ведь компьютер на 90% обеспечивает моё общение с внешним миром."
},
{
"type": "narration",
"text": "Анонимные имиджборды, иногда какие-то чаты, редко аська или джаббер, ещё реже форумы."
},
{
"type": "narration",
"text": "А людей, сидящих по ту сторону сетевого кабеля, попросту не существует!"
},
{
"type": "narration",
"text": "Все они всего лишь плод его больной фантазии, ошибка в программном коде или баг ядра, зажившего собственной жизнью."
},
{
"type": "scene",
"kind": "anim",
"name": "prolog_15",
"action": "load_asset",
"location": "anim/prolog_15",
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_15.png?raw=true",
"duration": null
},
{
"type": "scene",
"kind": "anim",
"name": "prolog_3",
"action": "load_asset",
"location": "anim/prolog_3",
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_3.png?raw=true",
"duration": null
},
{
"type": "scene",
"kind": "anim",
"name": "prolog_4",
"action": "load_asset",
"location": "anim/prolog_4",
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_4.png?raw=true"
},
{
"type": "narration",
"text": "Если посмотреть со стороны на моё существование, то такие мысли покажутся не столь уж бредовыми, а какой-нибудь психолог наверняка поставит мне кучу заумных диагнозов и, возможно, выпишет направление в жёлтый дом."
},
{
"type": "scene",
"kind": "anim",
"name": "prolog_5",
"action": "load_asset",
"location": "anim/prolog_5",
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_5.jpg?raw=true",
"duration": null
},
{
"type": "scene",
"kind": "anim",
"name": "prolog_14",
"action": "load_asset",
"location": "anim/prolog_14",
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_14.jpg?raw=true",
"duration": null
},
{
"type": "scene",
"kind": "anim",
"name": "prolog_11",
"action": "load_asset",
"location": "anim/prolog_11",
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_11.jpg?raw=true"
},
{
"type": "narration",
"text": "Маленькая квартирка без следов какого бы то ни было ремонта или даже подобия порядка, и вечно одинаковый вид из окна на серый, день и ночь куда-то бегущий мегаполис, вот условия моей жизни."
},
{
"type": "scene",
"kind": "anim",
"name": "prolog_2",
"action": "load_asset",
"location": "anim/prolog_2",
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_2.jpg?raw=true"
},
{
"type": "narration",
"text": "Конечно, всё начиналось не так..."
},
{
"type": "narration",
"text": "Я родился, пошёл в школу, закончил её всё как у людей."
},
{
"type": "narration",
"text": "Поступил в институт, где кое-как промучился полтора курса."
},
{
"type": "narration",
"text": "Работал на паре-тройке разных работ.{w} Иногда даже и неплохо, иногда даже получая за это достойные деньги."
},
{
"type": "narration",
"text": "Однако всё это казалось чужим, словно списанным с биографии другого человека."
},
{
"type": "narration",
"text": "Я не ощущал полноту жизни она словно зациклилась и продолжала идти по кругу.{w} Как в фильме «День сурка»."
},
{
"type": "narration",
"text": "Только у меня не было выбора, как именно провести этот день, и каждый раз всё повторялось по одной и той же схеме.{w} Схеме пустоты, уныния и отчаяния."
},
{
"type": "narration",
"text": "Последние несколько лет я просто целыми днями сидел за компьютером."
},
{
"type": "narration",
"text": "Иногда подворачивались какие-то халтурки, иногда помогали родители."
},
{
"type": "narration",
"text": "В общем, на жизнь хватало."
},
{
"type": "narration",
"text": "Это и немудрено, ведь потребности у меня небольшие."
},
{
"type": "narration",
"text": "На улицу я практически не выхожу, а всё моё общение с людьми сводится к интернет-переписке с <i>анонимами</i>, у которых нет ни реального имени, ни пола, ни возраста."
},
{
"type": "narration",
"text": "Короче говоря, достаточно типичная жизнь достаточно типичного асоциального человека своего времени.{w} Этакий Обломов XXI века."
},
{
"type": "narration",
"text": "Может быть, маститый писатель напишет обо мне роман, который станет классикой современной литературы.{w} Или напишу я сам…"
},
{
"type": "narration",
"text": "Впрочем нет, что себя обманывать уже не раз пытался, но меня не хватало даже на короткий рассказ."
},
{
"type": "narration",
"text": "Изучал я и множество других вещей."
},
{
"type": "narration",
"text": "Рисовать не дано от природы.{w} Программирование надоело.{w} Иностранные языки долго и скучно…"
},
{
"type": "narration",
"text": "Любил я разве что читать, но даже при этом никогда бы не назвал себя эрудированным человеком."
},
{
"type": "narration",
"text": "Возможно, я был асом в просмотре аниме и гроссмейстером неумелых шуточек в интернете."
},
{
"type": "narration",
"text": "Плати мне за это деньги, я бы обрадовался (да и заработал неплохо), но вряд ли так просто можно заполнить пустоту в душе."
},
{
"type": "scene",
"kind": "bg",
"name": "semen_room_window",
"action": "load_asset",
"location": "bg/semen_room_window",
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/bg/semen_room_window.jpg?raw=true"
},
{
"type": "narration",
"text": "Сегодня очередной типичный день моей типичной жизни типичного неудачника."
},
{
"type": "narration",
"text": "И именно сегодня мне нужно ехать на встречу институтских товарищей."
},
{
"type": "narration",
"text": "По правде говоря, совершенно не хотелось."
},
{
"type": "narration",
"text": "Да и какой смысл, если вместе с ними я отучился всего ничего?"
},
{
"type": "narration",
"text": "Однако меня всё же уговорил друг, бывший одногруппник, один из немногих, с кем я поддерживал контакт не только в интернете."
},
{
"type": "scene",
"kind": "anim",
"name": "intro_1",
"action": "load_asset",
"location": "anim/intro_1",
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_1.jpg?raw=true",
"duration": null
},
{
"type": "scene",
"kind": "anim",
"name": "intro_2",
"action": "load_asset",
"location": "anim/intro_2",
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_2.jpg?raw=true",
"duration": null
},
{
"type": "scene",
"kind": "anim",
"name": "intro_3",
"action": "load_asset",
"location": "anim/intro_3",
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_3.jpg?raw=true",
"duration": null
},
{
"type": "scene",
"kind": "anim",
"name": "intro_4",
"action": "load_asset",
"location": "anim/intro_4",
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_4.jpg?raw=true",
"duration": null
},
{
"type": "scene",
"kind": "anim",
"name": "intro_5",
"action": "load_asset",
"location": "anim/intro_5",
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_5.jpg?raw=true",
"duration": null
},
{
"type": "scene",
"kind": "anim",
"name": "intro_6",
"action": "load_asset",
"location": "anim/intro_6",
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_6.jpg?raw=true",
"duration": null
},
{
"type": "scene",
"kind": "anim",
"name": "intro_8",
"action": "load_asset",
"location": "anim/intro_8",
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_8.jpg?raw=true",
"duration": null
},
{
"type": "scene",
"kind": "anim",
"name": "intro_7",
"action": "load_asset",
"location": "anim/intro_7",
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_7.jpg?raw=true",
"duration": null
},
{
"type": "scene",
"kind": "bg",
"name": "bus_stop",
"action": "load_asset",
"location": "bg/bus_stop",
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/bg/bus_stop.jpg?raw=true"
},
{
"type": "narration",
"text": "Вечер. Мороз.{w} Остановка и ожидание автобуса."
},
{
"type": "narration",
"text": "Я никогда не любил зиму.{w} Впрочем, и жаркое лето тоже не моя стихия."
},
{
"type": "narration",
"text": "Просто не вижу смысла выделять какое-то одно время года не столь важно, какая погода на улице, если ты целыми днями сидишь дома."
},
{
"type": "narration",
"text": "Автобус сегодня задерживался так сильно, что я уже был готов плюнуть на всё и потратить последнюю пару сотен на такси (совсем не ехать мне почему-то в голову не пришло)."
},
{
"type": "narration",
"text": "В мозгу, как всегда, роились миллионы мыслей, из которых совершенно невозможно выудить хотя бы одну стоящую."
},
{
"type": "narration",
"text": "Такую, которую можно закончить, привести в порядок, облечь в форму идеи и претворить в жизнь."
},
{
"type": "narration",
"text": "Может быть, заняться бизнесом?{w} Но откуда я возьму деньги?"
},
{
"type": "narration",
"text": "Или пойти опять работать в офис?{w} Нет уж!"
},
{
"type": "narration",
"text": "Может, стоит попробовать фриланс?{w} Да что я умею, и кому я нужен…"
},
{
"type": "scene",
"kind": "anim",
"name": "prolog_2",
"action": "load_asset",
"location": "anim/prolog_2",
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_2.jpg?raw=true"
},
{
"type": "narration",
"text": "Вдруг мне вспомнилось детство…{w} Или скорее юношество 15-17 лет."
},
{
"type": "narration",
"text": "Почему именно это время?{w} Не знаю."
},
{
"type": "narration",
"text": "Наверное, потому что тогда всё было проще."
},
{
"type": "narration",
"text": "Было проще принимать такие сложные сейчас и такие простые тогда решения."
},
{
"type": "narration",
"text": "Проснувшись с утра, я чётко знал, как пройдёт мой день, а выходных ждал с нетерпением смогу отдохнуть, заняться любимыми делами: компьютер, футбол, встречи с друзьями."
},
{
"type": "narration",
"text": "А потом, когда наступит новая неделя, вновь примусь за учёбу."
},
{
"type": "narration",
"text": "Ведь раньше не возникало этих мучительных вопросов «зачем», «кому это надо», «что изменится, если я это сделаю» или «что не изменится»."
},
{
"type": "narration",
"text": "Простой поток жизни, такой привычный для любого нормального человека и такой чуждый для меня теперешнего."
},
{
"type": "narration",
"text": "Время беззаботного детства…{w} Тогда же я и встретил свою первую любовь."
},
{
"type": "narration",
"text": "Стёрлись из памяти её внешность, характер."
},
{
"type": "narration",
"text": "Как строчка из профиля в социальной сети осталось лишь имя, да те чувства, которые захлёстывали меня, когда я был с ней.{w} Теплота, нежность, желание заботиться, защитить…"
},
{
"type": "narration",
"text": "Жаль, что это продолжалось так недолго."
},
{
"type": "narration",
"text": "Сейчас я уже с трудом могу себе представить что-то подобное."
},
{
"type": "narration",
"text": "Наверное, и хочется познакомиться с девушкой, только не знаю, как начать диалог, о чём вообще с ней говорить, чем её заинтересовать."
},
{
"type": "narration",
"text": "Да и подходящих девушек я давно не встречал.{w} Хотя где мне их встретить…"
},
{
"type": "scene",
"kind": "anim",
"name": "intro_9",
"action": "load_asset",
"location": "anim/intro_9",
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_9.jpg"
},
{
"type": "narration",
"text": "Звук работающего двигателя вернул меня к реальности."
},
{
"type": "narration",
"text": "Подъехал автобус."
},
{
"type": "narration",
"text": "«Какой-то он не такой» мелькнула мысль."
},
{
"type": "narration",
"text": "Впрочем, какая разница по этому маршруту ходит только 410-ый."
},
{
"type": "scene",
"kind": "anim",
"name": "intro_10",
"action": "load_asset",
"location": "anim/intro_10",
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_10.jpg?raw=true",
"duration": null
},
{
"type": "scene",
"kind": "anim",
"name": "intro_11",
"action": "load_asset",
"location": "anim/intro_11",
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_11.jpg?raw=true",
"duration": null
},
{
"type": "scene",
"kind": "anim",
"name": "intro_13",
"action": "load_asset",
"location": "anim/intro_13",
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_13.jpg?raw=true",
"duration": null
},
{
"type": "scene",
"kind": "bg",
"name": "intro_xx",
"action": "load_asset",
"location": "bg/intro_xx",
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/bg/intro_xx.jpg?raw=true"
},
{
"type": "narration",
"text": "Огни пролетают мимо, их холодный свет словно зажигает внутри давно погасшие чувства."
},
{
"type": "narration",
"text": "Или не зажигает, а просто пробуждает…"
},
{
"type": "narration",
"text": "Ведь «они» уже давно живут во мне, то затихая, то просыпаясь вновь."
},
{
"type": "narration",
"text": "Какая-то очень известная мелодия играла в радиоприёмнике у водителя.{w} Но я её не слушал."
},
{
"type": "narration",
"text": "Я смотрел в запотевшее окно автобуса на проезжающие мимо машины."
},
{
"type": "narration",
"text": "Ведь люди куда-то спешат, ведь им что-то нужно, и, погружённые в свои дела, они не задумываются о вопросах, мучающих меня."
},
{
"type": "narration",
"text": "Наверное, у них тоже есть свои серьёзные проблемы, а может, им живётся куда легче."
},
{
"type": "narration",
"text": "Знать наверняка нельзя, так как все люди разные.{w} Или не разные?"
},
{
"type": "narration",
"text": "Бывает, поступки человека легко предсказуемы, но, пытаясь заглянуть к нему в душу, видишь лишь непроглядную тьму."
},
{
"type": "narration",
"text": "..."
},
{
"type": "narration",
"text": "Автобус приближался к центру, и мои мысли прервал яркий свет огней большого города."
},
{
"type": "narration",
"text": "Сотни рекламных вывесок, тысячи машин, миллионы людей."
},
{
"type": "narration",
"text": "Я смотрел на это светопреставление, и мне почему-то безумно захотелось спать."
},
{
"type": "narration",
"text": "Глаза закрылись всего на полсекунды и…"
},
{
"type": "scene",
"kind": "bg",
"name": "black",
"action": "load_asset",
"location": "bg/black",
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/bg/int_bus_black.jpg?raw=true"
},
{
"type": "opening",
"kind": "opening",
"name": "opening",
"action": "load_asset",
"location": "opening/opening",
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/opening/opening.mp4?raw=true"
}
]
}

View File

@@ -0,0 +1,62 @@
{
"prologue_choice_1": {
"question": "Иначе никак, иначе сон не закончится, а я не проснусь. — что выбрать?",
"chapter": "prologue",
"options": {
"Да, я пойду с тобой": {
"effects": {}
},
"Нет, я останусь здесь": {
"effects": {}
}
}
},
"endings": {
"labels": [
"main_good_ending",
"main_bad_ending",
"sl_good_ending",
"sl_bad_ending",
"dv_good_ending",
"dv_bad_ending",
"un_good_ending",
"un_bad_ending",
"us_good_ending",
"us_bad_ending",
"mi_ending",
"uv_ending",
"harem_ending"
],
"routes": {
"sl": {
"good": "sl_good_ending",
"bad": "sl_bad_ending",
"point_key": "sl_points"
},
"dv": {
"good": "dv_good_ending",
"bad": "dv_bad_ending",
"point_key": "dv_points"
},
"un": {
"good": "un_good_ending",
"bad": "un_bad_ending",
"point_key": "un_points"
},
"us": {
"good": "us_good_ending",
"bad": "us_bad_ending",
"point_key": "us_points"
},
"mi": {
"single": "mi_ending",
"point_key": "mi_points"
},
"uv": {
"single": "uv_ending",
"point_key": "uv_points"
}
},
"fallback": "main_bad_ending"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 569 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,400 @@
2g1c
2 girls 1 cup
acrotomophilia
alabama hot pocket
alaskan pipeline
anal
anilingus
anus
apeshit
arsehole
ass
asshole
assmunch
auto erotic
autoerotic
babeland
baby batter
baby juice
ball gag
ball gravy
ball kicking
ball licking
ball sack
ball sucking
bangbros
bangbus
bareback
barely legal
barenaked
bastard
bastardo
bastinado
bbw
bdsm
beaner
beaners
beaver cleaver
beaver lips
beastiality
bestiality
big black
big breasts
big knockers
big tits
bimbos
birdlock
bitch
bitches
black cock
blonde action
blonde on blonde action
blowjob
blow job
blow your load
blue waffle
blumpkin
bollocks
bondage
boner
boob
boobs
booty call
brown showers
brunette action
bukkake
bulldyke
bullet vibe
bullshit
bung hole
bunghole
busty
butt
buttcheeks
butthole
camel toe
camgirl
camslut
camwhore
carpet muncher
carpetmuncher
chocolate rosebuds
cialis
circlejerk
cleveland steamer
clit
clitoris
clover clamps
clusterfuck
cock
cocks
coprolagnia
coprophilia
cornhole
coon
coons
creampie
cum
cumming
cumshot
cumshots
cunnilingus
cunt
darkie
date rape
daterape
deep throat
deepthroat
dendrophilia
dick
dildo
dingleberry
dingleberries
dirty pillows
dirty sanchez
doggie style
doggiestyle
doggy style
doggystyle
dog style
dolcett
domination
dominatrix
dommes
donkey punch
double dong
double penetration
dp action
dry hump
dvda
eat my ass
ecchi
ejaculation
erotic
erotism
escort
eunuch
fag
faggot
fecal
felch
fellatio
feltch
female squirting
femdom
figging
fingerbang
fingering
fisting
foot fetish
footjob
frotting
fuck
fuck buttons
fuckin
fucking
fucktards
fudge packer
fudgepacker
futanari
gangbang
gang bang
gay sex
genitals
giant cock
girl on
girl on top
girls gone wild
goatcx
goatse
god damn
gokkun
golden shower
goodpoop
goo girl
goregasm
grope
group sex
g-spot
guro
hand job
handjob
hard core
hardcore
hentai
homoerotic
honkey
hooker
horny
hot carl
hot chick
how to kill
how to murder
huge fat
humping
incest
intercourse
jack off
jail bait
jailbait
jelly donut
jerk off
jigaboo
jiggaboo
jiggerboo
jizz
juggs
kike
kinbaku
kinkster
kinky
knobbing
leather restraint
leather straight jacket
lemon party
livesex
lolita
lovemaking
make me come
male squirting
masturbate
masturbating
masturbation
menage a trois
milf
missionary position
mong
motherfucker
mound of venus
mr hands
muff diver
muffdiving
nambla
nawashi
negro
neonazi
nigga
nigger
nig nog
nimphomania
nipple
nipples
nsfw
nsfw images
nude
nudity
nutten
nympho
nymphomania
octopussy
omorashi
one cup two girls
one guy one jar
orgasm
orgy
paedophile
paki
panties
panty
pedobear
pedophile
pegging
penis
phone sex
piece of shit
pikey
pissing
piss pig
pisspig
playboy
pleasure chest
pole smoker
ponyplay
poof
poon
poontang
punany
poop chute
poopchute
porn
porno
pornography
prince albert piercing
pthc
pubes
pussy
queaf
queef
quim
raghead
raging boner
rape
raping
rapist
rectum
reverse cowgirl
rimjob
rimming
rosy palm
rosy palm and her 5 sisters
rusty trombone
sadism
santorum
scat
schlong
scissoring
semen
sex
sexcam
sexo
sexy
sexual
sexually
sexuality
shaved beaver
shaved pussy
shemale
shibari
shit
shitblimp
shitty
shota
shrimping
skeet
slanteye
slut
s&m
smut
snatch
snowballing
sodomize
sodomy
spastic
spic
splooge
splooge moose
spooge
spread legs
spunk
strap on
strapon
strappado
strip club
style doggy
suck
sucks
suicide girls
sultry women
swastika
swinger
tainted love
taste my
tea bagging
threesome
throating
thumbzilla
tied up
tight white
tit
tits
titties
titty
tongue in a
topless
tosser
towelhead
tranny
tribadism
tub girl
tubgirl
tushy
twat
twink
twinkie
two girls one cup
undressing
upskirt
urethra play
urophilia
vagina
venus mound
viagra
vibrator
violet wand
vorarephilia
voyeur
voyeurweb
voyuer
vulva
wank
wetback
wet dream
white power
whore
worldsex
wrapping men
wrinkled starfish
yaoi
yellow showers
yiffy
zoophilia

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 B

View File

@@ -0,0 +1,3 @@
<svg width="30" height="22" viewBox="0 0 30 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24.5455 5.5L19.0909 11H23.1818C23.1818 15.5512 19.5136 19.25 15 19.25C13.6227 19.25 12.3136 18.9062 11.1818 18.2875L9.19091 20.295C10.8682 21.3675 12.8591 22 15 22C21.0273 22 25.9091 17.0775 25.9091 11H30L24.5455 5.5ZM6.81818 11C6.81818 6.44875 10.4864 2.75 15 2.75C16.3773 2.75 17.6864 3.09375 18.8182 3.7125L20.8091 1.705C19.1318 0.6325 17.1409 0 15 0C8.97273 0 4.09091 4.9225 4.09091 11H0L5.45455 16.5L10.9091 11H6.81818Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 554 B

View File

@@ -0,0 +1,3 @@
<svg width="23" height="25" viewBox="0 0 23 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6389 10.05H17.3611M7.91667 5.65H17.3611M21.25 17.75V6.75C21.25 3.45 19.5833 1.25 15.6944 1.25H6.80555C2.91667 1.25 1.25 3.45 1.25 6.75V17.75C1.25 21.05 2.91667 23.25 6.80555 23.25H15.6944C19.5833 23.25 21.25 21.05 21.25 17.75ZM15.1389 23.25V14.604C15.1389 14.12 14.5611 13.878 14.2056 14.197L11.6278 16.551C11.4167 16.749 11.0833 16.749 10.8722 16.551L8.29448 14.197C7.93893 13.878 7.36112 14.12 7.36112 14.604V23.25H15.1389Z" stroke="white" stroke-width="2.5"/>
</svg>

After

Width:  |  Height:  |  Size: 579 B

View File

@@ -0,0 +1,3 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.3333 7.33333C16.2381 7.33333 14.6667 5.76191 14.6667 3.66667C14.6667 1.57143 16.2381 0 18.3333 0C20.4286 0 22 1.57143 22 3.66667C22 5.76191 20.4286 7.33333 18.3333 7.33333ZM3.66667 14.6667C1.57143 14.6667 0 13.0952 0 11C0 8.90476 1.57143 7.33333 3.66667 7.33333C5.76191 7.33333 7.33333 8.90476 7.33333 11C7.33333 13.0952 5.76191 14.6667 3.66667 14.6667ZM18.3333 22C16.2381 22 14.6667 20.4286 14.6667 18.3333C14.6667 16.2381 16.2381 14.6667 18.3333 14.6667C20.4286 14.6667 22 16.2381 22 18.3333C22 20.4286 20.4286 22 18.3333 22ZM4.1381 10.0676L18.8048 17.401L17.8724 19.2762L3.20571 11.9429L4.1381 10.0676ZM18.8048 4.59905L4.1381 11.9324L3.20571 10.0571L17.8724 2.72381L18.8048 4.59905Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 817 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 B

View File

@@ -0,0 +1,3 @@
<svg width="38" height="38" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.5 23.5L15.3223 18.5362C15.8683 17.8294 16.8801 17.7008 17.5867 18.247L20.5257 20.5603C21.2323 21.1064 22.2441 20.9779 22.7902 20.2872L26.5 15.5M13.75 36.5H24.25C33 36.5 36.5 33 36.5 24.25V13.75C36.5 5 33 1.5 24.25 1.5H13.75C5 1.5 1.5 5 1.5 13.75V24.25C1.5 33 5 36.5 13.75 36.5Z" stroke="white" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 475 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 B

View File

@@ -0,0 +1,4 @@
<svg width="61" height="54" viewBox="0 0 61 54" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.65352 50.6608C7.65352 48.8166 9.11479 47.3216 10.9173 47.3216H50.0828C51.8854 47.3216 53.3466 48.8166 53.3466 50.6608C53.3466 52.505 51.8854 54 50.0828 54H10.9173C9.11479 54 7.65352 52.505 7.65352 50.6608Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.5764 2.93407C27.4001 -0.978025 33.5996 -0.978025 37.4235 2.93407L45.8 11.504C46.7935 12.5204 48.3108 12.7723 49.5674 12.1295L51.887 10.943C56.8965 8.38037 62.4336 13.2937 60.6626 18.7299L54.6667 37.1327C53.3341 41.2232 49.5925 43.9824 45.378 43.9824H15.6221C11.4076 43.9824 7.66588 41.2232 6.33315 37.1327L0.337435 18.7299C-1.43373 13.2937 4.10349 8.38037 9.11308 10.943L11.4325 12.1295C12.689 12.7723 14.2066 12.5204 15.2 11.504L23.5764 2.93407Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 893 B

View File

@@ -0,0 +1,5 @@
<svg width="45" height="84" viewBox="0 0 45 84" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.2576 0.680354L22.5645 0.764917C22.704 0.811776 22.8388 0.867879 22.969 0.933227L23.4432 1.10181C24.2984 1.48492 24.7861 1.78787 24.9063 2.01065C24.9155 2.03847 25.0131 2.09906 25.1989 2.19241C25.394 2.31359 25.5008 2.402 25.5192 2.45765C25.5932 2.58762 25.5501 2.87923 25.39 3.33248C25.3243 3.4619 25.2538 3.62373 25.1786 3.81795L25.1079 4.02609L25.0516 4.13702C24.4029 5.69064 23.5858 7.43811 22.6004 9.37945C22.2905 10.0359 21.84 10.9234 21.2489 12.0419L18.025 18.3928L17.786 18.7948L17.5889 19.1831C17.2794 19.7469 17.1291 20.0659 17.1381 20.14C17.1472 20.2141 17.3421 20.3585 17.7231 20.573L17.96 20.7268L18.183 20.8388C18.3503 20.9228 18.4571 20.9881 18.5035 21.0346L18.8099 21.2581L19.1302 21.5233C19.3343 21.7186 19.4407 21.8765 19.4496 21.997C19.4582 22.1637 19.3128 22.4503 19.0132 22.8567L18.8166 23.106L18.6341 23.3276C18.4937 23.5123 18.3908 23.6462 18.3253 23.7293L17.7627 24.6997C17.547 25.0787 17.3553 25.2724 17.1877 25.281C17.1597 25.2902 16.7785 25.1451 16.044 24.8459C15.2909 24.5372 14.84 24.3456 14.6913 24.2709C14.1987 24.0467 13.5573 23.7478 12.7672 23.3742L12.307 23.1779L11.8888 22.9678C11.582 22.8369 11.345 22.7295 11.1777 22.6455L10.6479 22.3794L10.4668 22.2537L10.2717 22.1556C9.99285 22.0156 9.66287 21.8615 9.28173 21.6933C9.26314 21.6839 9.21198 21.6698 9.12824 21.651L8.86323 21.5527C8.62141 21.4776 8.4728 21.3798 8.4174 21.2592C8.39899 21.2035 8.42752 21.0554 8.50296 20.8149L8.61586 20.5235L8.71495 20.1905C8.90339 19.6354 9.04914 19.2794 9.15219 19.1223C9.33033 18.8174 9.53635 18.5264 9.77025 18.2494C10.0509 17.9263 10.2797 17.7512 10.4568 17.7241C10.6152 17.6877 10.8154 17.707 11.0572 17.782L11.3642 17.8666L11.6292 17.9649L11.9919 18.0775L12.3127 18.2038C12.5265 18.288 12.6847 18.321 12.7872 18.3029C12.8152 18.2937 12.9272 18.2339 13.1232 18.1236L13.1795 18.0126L13.1798 17.9432C13.1707 17.869 13.1756 17.8135 13.1944 17.7765C13.2693 17.6749 13.3348 17.5918 13.3909 17.5272C13.4377 17.481 13.4797 17.4441 13.5171 17.4165C13.601 17.389 13.6524 17.3568 13.6711 17.3198C13.7274 17.2089 13.798 17.0239 13.8828 16.7649L14.0098 16.4458L14.1509 16.099L14.7562 14.9065C15.0751 14.3242 15.291 13.8989 15.4038 13.6308C15.526 13.3441 15.7418 12.942 16.0511 12.4245L16.4447 11.7869C16.6041 11.5189 16.7448 11.2647 16.8668 11.0244L17.444 9.88729C17.5191 9.73938 17.6504 9.50369 17.8379 9.18023L18.0491 8.76422L18.2881 8.36223C18.654 7.68747 18.8278 7.29911 18.8096 7.19715C18.7728 7.08585 18.6052 7.07131 18.3069 7.15351C18.2044 7.17163 18.0972 7.199 17.9853 7.23562L17.7755 7.30427C17.2629 7.39489 16.9556 7.40296 16.8535 7.32846C16.7699 7.26329 16.664 6.96647 16.5357 6.43799C16.4993 6.23406 16.472 6.05796 16.454 5.90969L16.385 5.70101L16.2881 5.47832L16.3311 5.18671C16.3595 5.08493 16.3691 4.99697 16.3601 4.92283L16.3195 4.58922L16.2651 4.21386C16.2384 3.89882 16.2861 3.62112 16.4081 3.38077C16.5864 3.02948 17.3425 2.57392 18.6764 2.0141C18.9469 1.89474 19.2408 1.75232 19.5581 1.58683L19.9501 1.36605L20.328 1.17301C20.9158 0.888157 21.438 0.709572 21.8946 0.637252C21.9785 0.609792 22.0623 0.605489 22.146 0.624342L22.2576 0.680354Z" fill="white"/>
<path d="M23.9091 76.2443C24.5784 76.534 25.4198 76.8754 26.4333 77.2684L27.7305 77.7458C28.2605 77.9424 28.7301 78.1203 29.1392 78.2793C29.2508 78.3353 29.4088 78.3915 29.6135 78.4479L30.0878 78.6165C30.4691 78.7384 30.706 78.869 30.7986 79.0083C30.882 79.1198 30.895 79.3468 30.8378 79.6893C30.7808 79.9855 30.6864 80.3093 30.5546 80.6608C30.5263 80.7625 30.4794 80.8318 30.4141 80.8686L30.2739 81.007L30.2173 81.1874L30.1886 81.3818C30.1507 81.5484 30.0944 81.6593 30.0197 81.7146L29.8794 81.853C29.8327 81.8992 29.8046 81.9315 29.7952 81.95C29.7483 82.0424 29.72 82.1442 29.7102 82.2553L29.6674 82.4774C29.6387 82.6718 29.6009 82.8153 29.554 82.9077C29.5164 82.9817 29.451 83.0416 29.3577 83.0876C29.3204 83.1152 29.283 83.1429 29.2456 83.1705C29.1895 83.2351 29.1287 83.309 29.0632 83.3921C29.0538 83.4106 29.0443 83.4522 29.0347 83.517L29.0201 83.6837C28.9916 83.8318 28.9074 83.9287 28.7675 83.9745C28.5717 84.0386 28.2972 83.9819 27.9441 83.8045L27.4143 83.5385C27.2007 83.408 27.0148 83.3146 26.8567 83.2584C26.5127 83.1089 26.0711 82.9219 25.5319 82.6975L24.8765 82.4032L24.1792 82.1225C23.4819 81.8419 22.6173 81.4773 21.5854 81.0287L20.2747 80.44C19.7169 80.2062 19.2381 80.0006 18.8384 79.823C18.6897 79.7484 18.4665 79.6595 18.1689 79.5564L17.7645 79.3881L17.3739 79.2616C16.7138 78.9996 16.3328 78.7851 16.231 78.618C16.2032 78.5808 16.199 78.4743 16.2183 78.2983C16.2191 78.1131 16.2337 77.9464 16.2622 77.7983C16.2907 77.6502 16.3426 77.4791 16.4179 77.2848L16.6296 76.7299C16.7897 76.2767 16.8843 75.9065 16.9134 75.6195L17.392 74.6765C17.5707 74.2326 17.6928 73.9691 17.7583 73.886C17.8612 73.7753 18.1039 73.6187 18.4865 73.4164L18.7245 73.2923L18.9903 73.1822L19.2283 73.0581L19.4521 72.9617C19.6947 72.8515 20.049 72.7278 20.5152 72.5907L21.4803 72.2749C21.5082 72.2658 21.5503 72.2289 21.6064 72.1643L21.6346 72.1088L21.7047 72.0396L21.8585 72.0124L22.0263 71.9575C22.1941 71.9026 22.334 71.8568 22.4459 71.8202L22.9077 71.5997L23.3836 71.3514C23.7848 71.1585 24.1114 70.9977 24.3633 70.869L24.839 70.6902L25.2867 70.4974L25.4968 70.3593L25.7209 70.1935C25.9171 70.0368 26.0758 69.9309 26.1971 69.8758C26.3092 69.7928 26.4678 69.7101 26.673 69.6275L27.1349 69.407C27.3962 69.2598 27.7089 69.0573 28.0731 68.7993L28.9835 68.1777C29.6466 67.6986 30.2444 67.2331 30.777 66.7813L31.0854 66.5186L31.4219 66.2003C31.6742 65.979 31.8752 65.7667 32.025 65.5635C32.4181 65.0649 32.5224 64.5836 32.338 64.1198C32.3196 64.0641 32.2732 64.0176 32.1989 63.9803L32.1014 63.8965L32.0319 63.8268L32.0043 63.7433L32.0045 63.6738C32.0047 63.6275 31.9816 63.5811 31.9352 63.5346C31.5267 63.2366 31.0616 63.0959 30.5401 63.1124C30.3538 63.1116 30.1675 63.1341 29.9811 63.1796L29.7293 63.262L29.5336 63.3029C29.3751 63.3394 29.2167 63.3758 29.0582 63.4122C28.853 63.4948 28.6758 63.5682 28.5266 63.6325C28.4986 63.6416 28.4565 63.6785 28.4004 63.7431L28.3444 63.7846L28.2742 63.8538C28.2463 63.8629 28.1857 63.8673 28.0926 63.867C27.9995 63.8666 27.9342 63.8803 27.8969 63.9079L27.4767 64.1841C27.2901 64.276 27.1224 64.3078 26.9735 64.2794C26.7223 64.2229 26.5179 64.097 26.3604 63.9019L26.1936 63.6789L26.0548 63.47C25.4059 62.7264 24.9472 62.1364 24.6789 61.7C24.6697 61.6722 24.6326 61.6304 24.5676 61.5746C24.5119 61.5466 24.4701 61.514 24.4423 61.4768L24.3457 61.1847C24.2905 61.0177 24.2911 60.8556 24.3476 60.6984C24.4041 60.5411 24.8104 60.2231 25.5666 59.7444L26.2948 59.2749C26.5563 59.0814 26.7291 58.9709 26.813 58.9434C26.9343 58.8883 27.1579 58.8383 27.4841 58.7932L27.8755 58.7114L28.3089 58.6158L28.6586 58.4782L29.0222 58.3824C29.423 58.2821 29.7725 58.214 30.0706 58.1781C30.406 58.1609 30.7599 58.153 31.1323 58.1544C31.57 58.1654 31.9749 58.1948 32.3473 58.2426C32.5241 58.2618 32.9008 58.3698 33.4774 58.5665C33.9795 58.7723 34.4861 58.9919 34.9974 59.2255C35.555 59.5056 35.9499 59.7387 36.1819 59.9248L36.1817 59.9943L36.2093 60.0778C36.2183 60.1519 36.2321 60.1936 36.2507 60.203L36.696 60.6354C36.8723 60.7936 37.0113 60.9562 37.1131 61.1234C37.5022 61.6436 37.7414 62.367 37.8309 63.2937C37.903 63.8868 37.8774 64.4887 37.7539 65.0996C37.6113 65.8401 37.3291 66.5568 36.9073 67.2499C36.8324 67.3515 36.7342 67.4762 36.6125 67.6239L36.2757 68.0116L35.3637 69.0501C34.9989 69.447 34.6763 69.7838 34.3959 70.0606C33.4428 70.8813 32.5462 71.5447 31.7061 72.0509L31.5661 72.1198L31.454 72.2027C31.3699 72.2765 31.2719 72.3317 31.1601 72.3683C30.5723 72.6532 29.7604 73.0807 28.7245 73.6509C28.4446 73.7888 28.062 73.9911 27.5767 74.2578L26.359 74.8644C25.3698 75.3885 24.5532 75.8485 23.9091 76.2443Z" fill="white"/>
<path d="M39.4239 0L45 2.80102L5.57613 80.4516L0 77.6506L39.4239 0Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,4 @@
<svg width="40" height="58" viewBox="0 0 40 58" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 34.4L8.35315 33.0659C12.5974 32.1747 16.9968 32.5988 21.0155 34.2866C25.3704 36.1156 30.1633 36.4571 34.7137 35.2627L35.265 35.1179C36.8724 34.6962 38 33.1796 38 31.4403V11.5492C38 9.44166 36.1139 7.89526 34.1665 8.4064C29.972 9.50749 25.5535 9.19261 21.5393 7.50657L21.0155 7.28663C16.9968 5.59878 12.5974 5.17466 8.35315 6.06596L2 7.40011V34.4Z" fill="white"/>
<path d="M2 56V34.4M2 34.4L8.35315 33.0659C12.5974 32.1747 16.9968 32.5988 21.0155 34.2866C25.3704 36.1156 30.1633 36.4571 34.7137 35.2627L35.265 35.1179C36.8724 34.6962 38 33.1796 38 31.4403V11.5492C38 9.44166 36.1139 7.89526 34.1665 8.4064C29.972 9.50749 25.5535 9.19261 21.5393 7.50657L21.0155 7.28663C16.9968 5.59878 12.5974 5.17466 8.35315 6.06596L2 7.40011V34.4ZM2 34.4V2" stroke="white" stroke-width="4" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 914 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,113 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="style.css">
<title>NoChess</title>
</head>
<body>
<div class="web_chess">
<div class="pgn_moves_board">
<div class="move_board_bg"></div>
<div class="moves_list">
<div class="moves_list_mobile">
</div>
</div>
<div class="moves_control_buttons">
<div class="move_btn first_move_btn">
<img src="https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/NoChess/icons/first.png" alt="first">
</div>
<div class="move_btn prev_move_btn">
<img src="https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/NoChess/icons/prev.png" alt="prev">
</div>
<div class="move_btn next_move_btn">
<img src="https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/NoChess/icons/next.png" alt="next">
</div>
<div class="move_btn last_move_btn">
<img src="https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/NoChess/icons/last.png" alt="last">
</div>
</div>
</div>
<div class="board-wrapper">
<canvas id="chessBoard"></canvas>
</div>
<div class="main_board">
<div class="main_board_bg"></div>
<div class="player_block player_black">
<div class="player_name_black">guest-acc</div>
<div class="player_avatar avatar_black"></div>
</div>
<div class="divider_line top_line">
<svg width="270" height="4" viewBox="0 0 270 4">
<path d="M75 0H95V4H75V0Z" fill="white" />
<path d="M50 0H70V4H50V0Z" fill="white" />
<path d="M25 0H45V4H25V0Z" fill="white" />
<path d="M0 0H20V4H0V0Z" fill="white" />
<path d="M100 0H120V4H100V0Z" fill="white" />
<path d="M125 0H145V4H125V0Z" fill="white" />
<path d="M150 0H170V4H150V0Z" fill="white" />
<path d="M175 0H195V4H175V0Z" fill="white" />
<path d="M200 0H220V4H200V0Z" fill="white" />
<path d="M225 0H245V4H225V0Z" fill="white" />
<path d="M250 0H270V4H250V0Z" fill="white" />
</svg>
</div>
<div class="timer timer_black">--:--</div>
<div class="board_menu">
<div class="menu_btn" id="flip_board_btn">
<img src="https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/NoChess/icons/menu-flip.svg" alt="flip" />
</div>
<div class="menu_btn" id="history_btn">
<img src="https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/NoChess/icons/menu-history.svg" alt="history" />
</div>
<div class="menu_btn" id="share_btn">
<img src="https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/NoChess/icons/menu-share.svg" alt="share" />
</div>
<div class="menu_btn menu_more_btn" id="more_btn" aria-label="toggle menu">
<span class="more_dots" aria-hidden="true">
<span class="more_dot more_dot_top"></span>
<span class="more_dot more_dot_mid"></span>
<span class="more_dot more_dot_bottom"></span>
</span>
</div>
</div>
<div class="timer timer_white">--:--</div>
<div class="divider_line bottom_line">
<svg width="270" height="4" viewBox="0 0 270 4">
<path d="M75 0H95V4H75V0Z" fill="white" />
<path d="M50 0H70V4H50V0Z" fill="white" />
<path d="M25 0H45V4H25V0Z" fill="white" />
<path d="M0 0H20V4H0V0Z" fill="white" />
<path d="M100 0H120V4H100V0Z" fill="white" />
<path d="M125 0H145V4H125V0Z" fill="white" />
<path d="M150 0H170V4H150V0Z" fill="white" />
<path d="M175 0H195V4H175V0Z" fill="white" />
<path d="M200 0H220V4H200V0Z" fill="white" />
<path d="M225 0H245V4H225V0Z" fill="white" />
<path d="M250 0H270V4H250V0Z" fill="white" />
</svg>
</div>
<div class="player_block player_white">
<div class="player_avatar avatar_white"></div>
<div class="player_name_white">Sunnex &lt;3</div>
</div>
</div>
</div>
<div class="submenu_overlay" id="submenu_overlay"></div>
<div class="submenu_panel" id="history_panel">
<div class="submenu_title">History</div>
<div class="history_games" id="history_games"></div>
</div>
<div class="submenu_panel" id="share_panel">
<div class="submenu_title">Share PGN</div>
<textarea id="share_pgn_text"></textarea>
<div class="share_actions">
<button id="load_pgn_btn" type="button">Load PGN</button>
<button id="copy_pgn_btn" type="button">Copy PGN</button>
</div>
<div id="share_status"></div>
</div>
<script src="javascript.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,967 @@
@font-face {
font-family: 'mr_GranstanderCleanG';
src: url('https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/NoChess/mr_granstandercleang.otf') format('opentype');
font-weight: 400;
font-style: normal;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: #1A1224;
background-image: url('bg.png');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
min-height: 100vh;
font-family: 'mr_GranstanderCleanG', Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
}
.web_chess {
display: flex;
align-items: stretch;
gap: 20px;
}
@media (min-width: 573px) and (max-width: 1562px) {
body {
overflow: hidden;
}
.web_chess {
width: 1562px;
position: fixed;
left: 50%;
top: 50%;
margin: 0;
--desktop-scale: min(calc(100vw / 1562px), calc(100vh / 856px), 1);
transform-origin: center center;
transform: translate(-50%, -50%) scale(var(--desktop-scale));
}
}
.board-wrapper {
position: relative;
display: block;
}
#chessBoard {
width: 856px;
height: 856px;
display: block;
}
.pgn_moves_board {
width: 333px;
height: 856px;
position: relative;
overflow: hidden;
}
.move_board_bg {
position: absolute;
inset: 0;
background: #1E1E1E;
border-radius: 15px;
}
.move_board_bg::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 80px;
height: 48px;
background: linear-gradient(180deg, #00000000 0%, #00000040 100%);
opacity: 0;
transition: opacity 0.2s;
pointer-events: none;
}
.pgn_moves_board.has_scroll .move_board_bg::after {
opacity: 1;
}
.moves_list {
position: absolute;
top: 20px;
bottom: 80px;
left: 0;
right: 0;
padding: 0 20px;
}
.moves_list_mobile {
height: 100%;
overflow-y: auto;
display: flex;
flex-direction: column;
scrollbar-width: none;
-ms-overflow-style: none;
}
.moves_list_mobile::-webkit-scrollbar {
width: 0;
height: 0;
display: none;
}
.move_row {
display: grid;
grid-template-columns: 40px 1fr 1fr;
align-items: center;
min-height: 52px;
font-size: 20px;
color: #FFFFFF;
}
.move_number {
opacity: 0.6;
}
.move_white,
.move_black {
padding: 10px;
display: flex;
align-items: center;
border-radius: 8px;
}
.current_move {
background: #B7B7B71A;
min-height: 52px;
}
.move_white,
.move_black {
cursor: pointer;
}
.move_white:empty,
.move_black:empty {
cursor: default;
}
.moves_control_buttons {
position: absolute;
bottom: 10px;
left: 0;
width: 100%;
display: flex;
justify-content: space-around;
}
.move_btn {
width: 75px;
height: 37px;
background: #2D2D2D;
border-radius: 8px;
box-shadow: 0px 4px 3.7px -1px #00000040;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background 0.2s, transform 0.2s;
}
.move_btn img {
width: 20px;
height: 20px;
object-fit: contain;
}
.move_btn:hover {
background: #3A3A3A;
transform: translateY(-2px);
}
.move_btn:active {
background: #252525;
transform: translateY(0);
}
.main_board {
width: 333px;
height: 856px;
position: relative;
}
.main_board_bg {
position: absolute;
inset: 0;
background: #00000054;
border-radius: 15px;
}
.player_block {
position: absolute;
width: 100%;
text-align: center;
}
.player_black {
top: 20px;
}
.player_white {
bottom: 20px;
}
.web_chess.board_flipped .player_black {
top: auto;
bottom: 20px;
display: flex;
flex-direction: column;
align-items: center;
}
.web_chess.board_flipped .player_white {
top: 20px;
bottom: auto;
display: flex;
flex-direction: column;
align-items: center;
}
.web_chess.board_flipped .player_black .player_avatar {
order: 1;
}
.web_chess.board_flipped .player_black .player_name_black {
order: 2;
margin-top: 10px;
margin-bottom: 0;
}
.web_chess.board_flipped .player_white .player_name_white {
order: 1;
margin-top: 0;
margin-bottom: 10px;
}
.web_chess.board_flipped .player_white .player_avatar {
order: 2;
}
.player_avatar {
position: relative;
overflow: hidden;
width: 189px;
height: 189px;
aspect-ratio: 1 / 1;
margin: 0 auto;
background-size: cover;
background-repeat: no-repeat;
background-position: center;
border: 5px solid #FFFFFF;
border-radius: 50%;
clip-path: circle(50% at 50% 50%);
}
.avatar_black {
background-image: url('https://i.pinimg.com/736x/6e/0a/0c/6e0a0cf688b30ba9de81b81bb32e49f9.jpg');
}
.avatar_white {
background-image: url('https://i.pinimg.com/736x/6e/0a/0c/6e0a0cf688b30ba9de81b81bb32e49f9.jpg');
}
.player_name_black {
margin-bottom: 10px;
font-size: 20px;
color: #FFFFFF;
}
.player_name_white {
margin-top: 10px;
font-size: 20px;
color: #FFFFFF;
}
.divider_line {
position: absolute;
left: 50%;
transform: translateX(-50%);
}
.top_line {
top: 260px;
}
.bottom_line {
bottom: 260px;
}
.divider_line svg {
display: block;
}
.timer {
position: absolute;
left: 50%;
transform: translateX(-50%);
width: 139px;
height: 41px;
display: flex;
align-items: center;
justify-content: center;
background: #00000054;
border-radius: 15px;
color: #FFFFFF;
font-size: 20px;
}
.timer_black {
top: 280px;
}
.timer_white {
bottom: 280px;
}
.web_chess.board_flipped .timer_black {
top: auto;
bottom: 280px;
}
.web_chess.board_flipped .timer_white {
top: 280px;
bottom: auto;
}
.board_menu {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
gap: 15px;
}
.menu_btn {
width: 50px;
height: 42px;
background: #00000054;
border-radius: 15px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background 0.2s, transform 0.2s;
}
.menu_btn:hover {
background: #FFFFFF1A;
transform: translateY(-2px);
}
.menu_btn:active {
background: #00000080;
transform: translateY(0);
}
.menu_btn img {
display: block;
width: 28px;
height: 24px;
object-fit: contain;
}
.menu_more_btn {
display: none;
}
.more_dots {
position: relative;
width: 22px;
height: 22px;
}
.more_dot {
position: absolute;
left: 50%;
top: 50%;
width: 8px;
height: 8px;
border-radius: 50%;
background: #FFFFFF;
transform: translate(-50%, -50%);
}
.more_dot_top {
transform: translate(-50%, calc(-50% - var(--dot-gap, 10px)));
transition: transform 0.32s ease;
}
.more_dot_mid {
transition: transform 0.32s ease;
}
.more_dot_bottom {
transform: translate(-50%, calc(-50% + var(--dot-gap, 10px)));
transition: transform 0.32s ease;
}
.menu_more_btn.active .more_dot_top {
transform: translate(calc(-50% - var(--dot-gap, 10px)), -50%);
}
.menu_more_btn.active .more_dot_bottom {
transform: translate(calc(-50% + var(--dot-gap, 10px)), -50%);
}
.web_chess.mobile_menu_open .menu_more_btn .more_dot_mid {
transform: translate(-50%, -50%) scale(1.05);
}
.submenu_overlay {
position: fixed;
inset: 0;
background: #00000080;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
}
.submenu_overlay.open {
opacity: 1;
pointer-events: auto;
}
.submenu_panel {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.98);
width: min(540px, 92vw);
max-height: 80vh;
background: #1e1e1e;
border-radius: 15px;
border: 1px solid #FFFFFF14;
padding: 18px;
color: #FFFFFF;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s, transform 0.2s;
z-index: 20;
}
.submenu_panel.open {
opacity: 1;
pointer-events: auto;
transform: translate(-50%, -50%) scale(1);
}
.submenu_title {
font-size: 20px;
margin-bottom: 12px;
}
.history_games {
display: flex;
flex-direction: column;
gap: 10px;
}
.history_games.empty {
min-height: 120px;
justify-content: center;
align-items: center;
}
.history_empty {
font-size: 24px;
font-weight: 700;
text-align: center;
}
.history_game_btn {
width: 100%;
background: #2d2d2d;
color: #FFFFFF;
border: 1px solid #FFFFFF14;
border-radius: 10px;
padding: 10px 12px;
text-align: left;
cursor: pointer;
}
.history_game_btn:hover {
background: #383838;
}
#share_pgn_text {
width: 100%;
min-height: 170px;
max-height: 55vh;
border-radius: 10px;
border: 1px solid #FFFFFF1A;
background: #121212;
color: #FFFFFF;
padding: 10px;
resize: none;
overflow: auto;
}
.share_actions {
margin-top: 12px;
display: flex;
gap: 10px;
justify-content: center;
}
#load_pgn_btn,
#copy_pgn_btn {
height: 38px;
padding: 0 14px;
border-radius: 10px;
border: 0;
background: #2d2d2d;
color: #FFFFFF;
cursor: pointer;
min-width: 130px;
}
#load_pgn_btn {
background: #2d2d2d;
}
#load_pgn_btn:hover,
#copy_pgn_btn:hover {
filter: brightness(1.08);
}
#share_status {
margin-top: 10px;
min-height: 20px;
color: #d9d9d9;
font-size: 14px;
text-align: center;
opacity: 0;
transform: translateY(4px);
transition: opacity 0.2s ease, transform 0.2s ease;
}
#share_status.show {
opacity: 1;
transform: translateY(0);
}
#share_status.error {
color: #ff8f8f;
}
@media (max-width: 572px) {
body {
justify-content: center;
align-items: flex-start;
overflow: hidden;
}
body::after {
content: '';
position: fixed;
left: 0;
right: 0;
bottom: 0;
height: clamp(72px, 17.7vw, 101px);
background: linear-gradient(180deg, #00000000 0%, #0000008C 100%);
pointer-events: none;
z-index: 5;
}
.web_chess {
--mobile-layout-width: 750px;
--mobile-board-size: 750px;
--mobile-board-top: 233px;
--mobile-player-gap: clamp(24px, 5vw, 32px);
--mobile-panel-height: clamp(64px, 17.13vw, 98px);
--mobile-timer-height: clamp(30px, 7.17vw, 41px);
--mobile-pgn-space: clamp(360px, 86vw, 492px);
--mobile-layout-height: calc(var(--mobile-board-top) + var(--mobile-board-size) + var(--mobile-pgn-space));
--mobile-fit-scale: min(1, calc((100vw - 2px) / var(--mobile-layout-width)), calc((100dvh - 2px) / var(--mobile-layout-height)));
width: var(--mobile-layout-width);
height: var(--mobile-layout-height);
min-height: 0;
margin: 0;
position: fixed;
left: 50%;
top: 0;
display: block;
padding: 0;
overflow: hidden;
transform-origin: top center;
transform: translateX(-50%) scale(var(--mobile-fit-scale));
}
.web_chess::after {
content: none;
}
.board-wrapper {
position: absolute;
left: 50%;
top: var(--mobile-board-top);
transform: translateX(-50%);
width: var(--mobile-board-size);
z-index: 2;
}
#chessBoard {
width: 100%;
max-width: none;
height: auto;
}
.main_board {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
z-index: 3;
pointer-events: none;
}
.main_board_bg,
.divider_line {
display: none;
}
.board_menu {
top: 4.2vw;
right: 3.1vw;
left: auto;
transform: none;
gap: 2.2vw;
align-items: center;
pointer-events: auto;
}
.board_menu .menu_btn:not(.menu_more_btn) {
opacity: 1;
transform: translateX(0);
transition: transform 0.28s ease, opacity 0.28s ease, background 0.2s;
}
.web_chess:not(.mobile_menu_open) .board_menu .menu_btn:not(.menu_more_btn) {
opacity: 0;
transform: translateX(11vw) scale(0.86);
pointer-events: none;
}
.web_chess.mobile_menu_open .board_menu .menu_btn:not(.menu_more_btn) {
opacity: 1;
transform: translateX(0);
pointer-events: auto;
}
.menu_btn {
width: 50px;
height: 42px;
border-radius: 15px;
}
.menu_btn img {
width: 28px;
height: 24px;
}
.menu_more_btn {
--dot-gap: 14px;
display: flex;
width: 80px;
height: 80px;
border-radius: 50%;
}
.menu_more_btn .more_dots {
width: 34px;
height: 34px;
min-width: 34px;
min-height: 34px;
}
.menu_more_btn .more_dot {
width: 9px;
height: 9px;
}
.player_block {
width: calc(100% - 4.2vw);
left: 2.1vw;
height: var(--mobile-panel-height);
min-height: 64px;
border-radius: clamp(10px, 2.6vw, 15px);
display: flex;
align-items: center;
gap: clamp(10px, 2.8vw, 18px);
padding: 0 calc(clamp(96px, 24.3vw, 139px) + max(6vw, 22px)) 0 max(2.4vw, 12px);
text-align: left;
background: #00000054;
pointer-events: auto;
}
.player_black {
top: calc(var(--mobile-board-top) - var(--mobile-player-gap) - var(--mobile-panel-height));
}
.player_black .player_avatar {
order: 1;
}
.player_black .player_name_black {
order: 2;
text-align: left;
}
.player_white {
top: calc(var(--mobile-board-top) + var(--mobile-board-size) + var(--mobile-player-gap));
bottom: auto;
}
.player_avatar {
width: min(13.99vw, 80px);
height: min(13.99vw, 80px);
margin: 0;
border-width: 2px;
flex-shrink: 0;
}
.player_name_black,
.player_name_white {
margin: 0;
min-width: 0;
max-width: min(39vw, 220px);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: clamp(14px, 3.8vw, 20px);
color: #FFFFFF;
}
.timer {
left: auto;
right: 6vw;
transform: none;
width: clamp(96px, 24.3vw, 139px);
height: var(--mobile-timer-height);
border-radius: 15px;
font-size: clamp(12px, 3.8vw, 20px);
background: #00000066;
color: #FFFFFF;
text-shadow: 0 1px 1px #0000004D;
z-index: 7;
pointer-events: auto;
}
.timer_black {
top: calc(var(--mobile-board-top) - var(--mobile-player-gap) - var(--mobile-panel-height) + (var(--mobile-panel-height) - var(--mobile-timer-height)) / 2);
}
.timer_white {
top: calc(var(--mobile-board-top) + var(--mobile-board-size) + var(--mobile-player-gap) + (var(--mobile-panel-height) - var(--mobile-timer-height)) / 2);
bottom: auto;
}
.pgn_moves_board {
position: absolute;
top: calc(var(--mobile-board-top) + var(--mobile-board-size) + clamp(24px, 5vw, 32px) + var(--mobile-panel-height) + clamp(18px, 4vw, 28px));
left: 50%;
width: calc(100% - clamp(10px, 2.7vw, 20px));
height: clamp(180px, 56vw, 320px);
z-index: 3;
opacity: 1;
transform: translateX(-50%);
transition: opacity 0.24s ease;
}
.move_board_bg {
display: none;
}
.moves_list {
top: 0;
bottom: clamp(66px, 14vw, 84px);
left: 50%;
right: auto;
width: clamp(260px, 44vw, 360px);
transform: translateX(-50%);
padding: 0;
transition: opacity 0.24s ease;
}
.moves_list::after {
content: none;
}
.moves_list_mobile {
height: 140%;
gap: 0.6vw;
pointer-events: auto;
padding-right: 0;
align-items: center;
transform: translateY(clamp(10px, 2.2vh, 18px));
scrollbar-width: none;
-ms-overflow-style: none;
}
.moves_list_mobile::-webkit-scrollbar {
width: 0;
height: 0;
display: none;
}
.move_row {
min-height: 9vw;
font-size: clamp(18px, 4.2vw, 24px);
width: max-content;
margin: 0 auto;
grid-template-columns: auto auto auto;
column-gap: clamp(10px, 2.8vw, 16px);
}
.move_number {
opacity: 1;
}
.move_white,
.move_black {
padding: 0;
min-height: 0;
width: max-content;
max-width: 100%;
justify-self: start;
justify-content: flex-start;
}
.current_move {
min-height: 0;
width: max-content;
padding: clamp(4px, 1vw, 8px) clamp(6px, 1.4vw, 10px);
}
.moves_control_buttons {
left: 2.6vw;
right: 2.6vw;
bottom: 0;
width: auto;
justify-content: flex-end;
gap: 1.5vw;
pointer-events: auto;
}
.move_btn {
width: 80px;
height: 80px;
border-radius: 50%;
background: #00000054;
box-shadow: none;
}
.menu_btn,
.move_btn {
transition: background 0.2s, transform 0.2s;
-webkit-tap-highlight-color: #00000000;
}
.menu_btn:hover {
background: #FFFFFF1A;
transform: translateY(-2px);
}
.move_btn:hover {
background-color: #00000054;
transform: none;
}
.menu_btn:active {
background: #00000080;
transform: translateY(0);
}
.menu_more_btn:hover {
background-color: #00000054;
transform: none;
}
.menu_more_btn:active {
background-color: #00000054;
transform: scale(1.06);
}
.move_btn:active {
background-color: #00000054;
transform: scale(1.06);
}
.move_btn img {
width: 46%;
height: 46%;
}
.first_move_btn {
margin-right: auto;
}
.last_move_btn {
display: none;
}
.web_chess:not(.mobile_moves_open) .moves_list {
opacity: 0;
pointer-events: none;
}
.web_chess.board_flipped .player_black {
top: calc(var(--mobile-board-top) + var(--mobile-board-size) + var(--mobile-player-gap));
bottom: auto;
flex-direction: row;
align-items: center;
justify-content: flex-start;
text-align: left;
}
.web_chess.board_flipped .player_white {
top: calc(var(--mobile-board-top) - var(--mobile-player-gap) - var(--mobile-panel-height));
bottom: auto;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
text-align: left;
}
.web_chess.board_flipped .player_white .player_avatar {
order: 1;
}
.web_chess.board_flipped .player_white .player_name_white {
order: 2;
margin: 0;
align-self: center;
line-height: 1.2;
}
.web_chess.board_flipped .player_black .player_name_black {
margin: 0;
align-self: center;
line-height: 1.2;
}
.web_chess.board_flipped .timer_black {
top: calc(var(--mobile-board-top) + var(--mobile-board-size) + var(--mobile-player-gap) + (var(--mobile-panel-height) - var(--mobile-timer-height)) / 2);
bottom: auto;
}
.web_chess.board_flipped .timer_white {
top: calc(var(--mobile-board-top) - var(--mobile-player-gap) - var(--mobile-panel-height) + (var(--mobile-panel-height) - var(--mobile-timer-height)) / 2);
bottom: auto;
}
}

Some files were not shown because too many files have changed in this diff Show More