Added and updated repositories 2026-06-12 08:36:40

This commit is contained in:
github-actions[bot]
2026-06-12 08:36:40 +00:00
parent d33e49b696
commit ab03f6ed94
100 changed files with 12895 additions and 105 deletions

View File

@@ -0,0 +1,487 @@
# requires: aiohttp pyngrok
# meta developer: @H_SunMods
# meta banner: https://r2.fakecrime.bio/uploads/965a3206-4609-4dff-beb0-6831f8b90e12.jpg
# current ver
__version__ = (0, 1, 0)
import json
import socket
import asyncio
import secrets
import logging
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
from aiohttp import ClientSession, ClientTimeout, web
from herokutl.types import Message
from pyngrok import conf, ngrok
from .. import loader, utils
from ..inline.types import InlineCall
logging.getLogger("pyngrok").setLevel(logging.WARNING)
logging.getLogger("pyngrok.process").setLevel(logging.WARNING)
logging.getLogger("pyngrok.process.ngrok").setLevel(logging.WARNING)
html_raw = "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/refs/heads/main/Assets/NoChess/raw_assets/index.html"
css_raw = "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/refs/heads/main/Assets/NoChess/raw_assets/style.css"
js_raw = "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/refs/heads/main/Assets/NoChess/raw_assets/javascript.js"
asset_root_raw = "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/NoChess"
botfather_photo_url = "https://r2.fakecrime.bio/uploads/d3e16245-15a2-43f1-b176-493b4d9f1f21.jpg"
@loader.tds
class NoChess(loader.Module):
"""NoChess - web module that allows u to launch a web page either as a functional HTML page or as a Telegram Mini-App. This is an add-on for Chess module by @nullmod"""
# я пытался кароче сделать тут перевод делая реплейсы в зависимости от стрингов,но это не работает,поэтому да
strings = {
"name": "NoChess",
"starting": "( ノ・ェ・ )ノ <b>Starting NoChess...</b>",
"online": "(*˘︶˘*) <b>NoChess is running</b>",
"already_running": "ʕᵕᴥᵕʔ <b>NoChess is already running</b>",
"stopped": "・゚・(。>д<。)・゚・ NoChess stopped",
"not_running": "(✿╹◡╹) NoChess is not running",
"ngrok_missing": "Set a <code>ngrok_token</code>",
"ngrok_error": "Ngrok start error: <code>{}</code>",
"asset_read_error": "Failed to load web assets: <code>{}</code>",
"open_button": "Open mini-app",
"stop_button": "Stop",
"about_text": "<b>Important read:</b>\nSometimes the server won't lift cause there's enough processes running, for example on HikkaHost, for this I just rebooted the server\nNext is that <code>cma</code> setups the app by a template and it's rly crooked, so you'll have to set some web app config settings yourself\nAnd also:\n 1. First launch will start straight with a site link, not as a web app\n 2. Use <code>nochess</code>, and then <code>cma</code> to setup the web app\n 3. After that restart the process by typing <code>nochess -kill</code> and <code>nochess</code> again\nYeah it's hacky as hell, but I was so over doing stuff that I started dumping some routine like working with files on ai, which I didn't like so I decided to quick-release the module before it's too late\nWell and maybe soon I'll make an update, right now it's some pre-alpha version, that's why the version name is like this, later I'll change it to 1.0.0, if people actually dig the module as an idea",
"cma_start": "( ノ・ェ・ )ノ <b>Creating mini app in BotFather...</b>",
"cma_need_url": "Set mini app web URL first or run <code>.nochess</code> to get it.",
"cma_done": "(*˘︶˘*) <b>Done.</b>",
"cma_error": "Error: <code>{}</code>",
"RuntimeError": "inline bot username not found",
"not_supported_platform": "(┬┬_┬┬) Unfortunately, it is impossible to install this module on this platform.\n\n(〜^∇^)〜 This is not an error, please do not contact support."
}
strings_ru = {
"_cls_doc": "NoChess - Веб модуль который позволяет запускать веб-пейдж,как HTML страницу с функционалом,так же в виде Telegram Mini-App. Является дополнением к модулю Chess от @nullmod",
"starting": "( ノ・ェ・ )ノ <b>Запуск NoChess...</b>",
"online": "(*˘︶˘*) <b>NoChess запущен</b>",
"already_running": "ʕᵕᴥᵕʔ <b>NoChess уже запущен</b>",
"stopped": "・゚・(。>д<。)・゚・ NoChess остановлен",
"not_running": "(✿╹◡╹) NoChess не запущен",
"ngrok_missing": "Укажи <code>ngrok_token</code>",
"ngrok_error": "Ошибка запуска ngrok: <code>{}</code>",
"asset_read_error": "Не удалось загрузить веб-ассеты: <code>{}</code>",
"open_button": "Открыть мини-приложение",
"stop_button": "Остановить",
"about_text": "<b>Важно к прочтению:</b>\nИногда сервер не может подниматься из за того что запущено достаточно процессов, например на HikkaHost,для этого я просто перезагружал сервер.\nДалее это то что <code>cma</code> сетапает приложение по шаблону и оч криво, поэтому вам придется выставлять некоторые настройки конфигурации веб приложения самим.\nА еще:\n 1. Первый запуск будет запускаться сразу ссылкой на сайт, а не как веб приложение.\n 2. Используйте <code>nochess</code>, а потом <code>cma</code> чтобы настроить веб приложение.\n 3. После чего перезапустите процесс написав <code>nochess -kill</code> и повторно <code>nochess</code>.\nДа это костыли, но мне уже настолько было в падлу что то делать что я уже стал спихивать рутину по типу работы с файлами на ии, что мне не понравилось и я решил быстро релизать модуль пока не стало поздно.\nНу и может быть в скором времени я уже сделаю апдейт, на данный момент это какая то пре-альфа версия, поэтому и название версии такое, в дальнейшем изменю на 1.0.0, если модуль вообще понравиться людям как идея.",
"cma_start": "( ノ・ェ・ )ノ <b>Создаю эпку через BotFather...</b>",
"cma_need_url": "Сначала укажи URL мини-эпки или запусти <code>.nochess</code>, чтобы получить его",
"cma_done": "(*˘︶˘*) <b>Готово</b>",
"cma_error": "Ошибка: <code>{}</code>",
"RuntimeError": "юз инлайн бота не найден",
"not_supported_platform": "(┬┬_┬┬) К сожалению, на эту платформу невозможно установить этот модуль.\n\n(〜^∇^)〜 Это не ошибка, пожалуйста, не обращайтесь в поддержку."
}
async def client_ready(self):
platform = utils.get_named_platform()
if platform in ("HikkaHost"):
raise loader.LoadError(self.strings("not_supported_platform"))
def __init__(self):
self.config = loader.ModuleConfig(
loader.ConfigValue(
"ngrok_token",
None,
"Token from ngrok.com | Токен полученый на ngrok.com",
validator=loader.validators.Hidden(),
),
loader.ConfigValue(
"mini_app_url",
None,
"Mini app direct url | Директ ссылка на ваше мини приложение",
validator=loader.validators.String(),
),
loader.ConfigValue(
"block_light",
"#D8E3E7",
"Light board block color | Цвет светлых полей на доске",
validator=loader.validators.String()
),
loader.ConfigValue("block_dark",
"#7699AF",
"Dark board block color | Цвет тёмных полей на доске",
validator=loader.validators.String()
),
loader.ConfigValue(
"select_block",
"#FF5A5A",
"Selected block color | Цвет для выделения полей на доске",
validator=loader.validators.String()
),
loader.ConfigValue(
"move_pieces_color",
"#58B4FF",
"Move highlight color | Цвет подсвечиваниях перехода на другую позицию",
validator=loader.validators.String()
),
loader.ConfigValue(
"result_win",
"#00BE16",
"Winner color | Блок цвета победителя",
validator=loader.validators.String()
),
loader.ConfigValue(
"result_lose",
"#BE0000",
"Loser color | Блок цвета проигравшего",
validator=loader.validators.String()
),
loader.ConfigValue(
"result_draw",
"#434343",
"Draw color | Блок цвета при ничьей",
validator=loader.validators.String()
),
loader.ConfigValue(
"arrow_color",
"#BD3667",
"Arrow color | Цвет стрелки",
validator=loader.validators.String()
),
)
self.runner = None
self.tunnel_url = None
self.access_token = None
self.games_cache = []
self.games_dump = ""
def theme_config_dict(self):
return {
"block_light": self.config["block_light"],
"block_dark": self.config["block_dark"],
"select_block": self.config["select_block"],
"move_pieces_color": self.config["move_pieces_color"],
"result_win": self.config["result_win"],
"result_lose": self.config["result_lose"],
"result_draw": self.config["result_draw"],
"arrow_color": self.config["arrow_color"],
}
async def refresh_games_cache(self):
chess = self.lookup("chess")
if not chess or not getattr(chess, "games", None):
self.games_cache = []
self.games_dump = ""
return
chunks = []
items = list(chess.games.items())
def sort_key(item):
key = str(item[0])
return (0, int(key)) if key.isdigit() else (1, key)
for _, game in sorted(items, key=sort_key, reverse=True):
node = None
if isinstance(game, dict):
game_obj = game.get("game", {})
if isinstance(game_obj, dict):
node = game_obj.get("root_node") or game_obj.get("node")
if node is None:
node = game.get("root_node") or game.get("node")
if node is None and hasattr(game, "game"):
game_obj = getattr(game, "game", None)
if isinstance(game_obj, dict):
node = game_obj.get("root_node") or game_obj.get("node")
if node is None and hasattr(game, "root_node"):
node = getattr(game, "root_node", None)
if node is None and hasattr(game, "node"):
node = getattr(game, "node", None)
if node:
chunks.append(str(node).strip())
self.games_cache = [x for x in chunks if x]
self.games_dump = "\n\n".join(self.games_cache)
async def get_me_json(self):
me = await self.client.get_me()
fallback_photo = "https://i.pinimg.com/736x/6e/0a/0c/6e0a0cf688b30ba9de81b81bb32e49f9.jpg"
full_name = (getattr(me, "first_name", "") or "") + (
(" " + getattr(me, "last_name", "")) if getattr(me, "last_name", None) else ""
)
return {
"id": getattr(me, "id", None),
"username": getattr(me, "username", None),
"first_name": getattr(me, "first_name", None),
"last_name": getattr(me, "last_name", None),
"name": full_name.strip() or str(getattr(me, "id", "Unknown")),
"photo": fallback_photo,
"enemy_photo": fallback_photo,
}
def check_access(self, request):
token = request.query.get("token") or request.cookies.get("nochess_token")
return bool(self.access_token and token == self.access_token)
def ensure_access_token(self):
if self.access_token:
return self.access_token
self.access_token = self.get("access_token")
if not self.access_token:
self.access_token = secrets.token_urlsafe(32)
self.set("access_token", self.access_token)
return self.access_token
async def read_remote_asset(self, url):
timeout = ClientTimeout(total=15)
async with ClientSession(timeout=timeout) as session:
async with session.get(url) as response:
if response.status != 200:
raise RuntimeError(f"HTTP {response.status}: {url}")
return await response.text()
async def load_web_assets(self):
html = await self.read_remote_asset(html_raw)
css = await self.read_remote_asset(css_raw)
js = await self.read_remote_asset(js_raw)
return html, css, js
def localication_script(self):
return (
"<script>(async()=>{"
"try{const me=await fetch('/api/me').then(r=>r.json());window.nochess_profile=me;if(typeof setNoChessProfile==='function'){setNoChessProfile(me);}}catch(_e){}"
"let rawGames=[];"
"try{const d=await fetch('/api/games').then(r=>r.json());rawGames=Array.isArray(d.games)?d.games:[];}catch(_e){}"
"const apply=()=>{if(typeof parsePgnToGameState!=='function'||typeof buildHistoryList!=='function')return false;"
"parsed_games=(rawGames||[]).map(g=>parsePgnToGameState(g)).filter(Boolean);"
"buildHistoryList();if(parsed_games.length>0&&typeof loadGame==='function')loadGame(0);return true;};"
"if(apply())return;"
"let attempts=0;const iv=setInterval(()=>{attempts++;if(apply()||attempts>40)clearInterval(iv);},250);"
"})();</script>"
)
def inject_runtime_config(self, html, css, js):
asset_root = asset_root_raw.rstrip("/")
if asset_root:
css = css.replace("url('bg.png')", f"url('{asset_root}/other/bg.png')")
theme_json = json.dumps(self.theme_config_dict(), ensure_ascii=False)
bootstrap = (
"<script>"
f"window.nochess_theme={theme_json};"
f"window.nochess_asset_root={json.dumps(asset_root)};"
"</script>"
)
html = html.replace('<link rel="stylesheet" href="style.css">', f"<style>{css}</style>")
html = html.replace('<script src="javascript.js"></script>', bootstrap + f"<script>{js}</script>")
return html
async def handle_home(self, request):
try:
html, css, js = await self.load_web_assets()
except Exception as error:
return web.Response(
text=self.strings["asset_read_error"].format(utils.escape_html(str(error))),
status=500,
)
html = self.inject_runtime_config(html, css, js)
html = html.replace("</body>", self.localication_script() + "</body>")
response = web.Response(text=html, content_type="text/html")
response.set_cookie(
"nochess_token",
self.access_token,
max_age=86400,
httponly=True,
samesite="Lax",
)
return response
async def handle_games(self, request):
if not self.check_access(request):
return web.json_response({"error": "Unauthorized"}, status=401)
if not self.games_cache:
await self.refresh_games_cache()
return web.json_response({"games_dump": self.games_dump, "games": list(self.games_cache)})
async def handle_me(self, request):
if not self.check_access(request):
return web.json_response({"error": "Unauthorized"}, status=401)
return web.json_response(await self.get_me_json())
async def stop_server(self):
was_running = bool(self.runner)
try:
ngrok.kill()
except Exception:
pass
if self.runner:
await self.runner.cleanup()
self.runner = None
self.tunnel_url = None
return was_running
async def send_form(self, message, url):
await self.inline.form(
self.strings["online"],
message=message,
reply_markup=[
[{"text": self.strings["open_button"], "url": url}],
[{"text": self.strings["stop_button"], "callback": self.stop_callback}],
],
)
async def stop_callback(self, call: InlineCall):
was_running = await self.stop_server()
await call.answer(
self.strings["stopped"] if was_running else self.strings["not_running"],
show_alert=False,
)
try:
await call.delete()
except Exception:
try:
await call.edit(self.strings["stopped"] if was_running else self.strings["not_running"])
except Exception:
pass
@loader.command(ru_doc="[-kill] Вызываь веб интерфейс для просмотра партии")
async def nochess(self, message: Message):
"""[-kill] Call web interface to view chess game"""
try:
return await self.nochess_args(message)
except Exception as error:
await self.stop_server()
return await utils.answer(
message,
self.strings["ngrok_error"].format(utils.escape_html(str(error))),
)
async def nochess_args(self, message: Message):
args = (utils.get_args_raw(message) or "").strip().lower()
if args == "-kill":
was_running = await self.stop_server()
return await utils.answer(message, self.strings["stopped"] if was_running else self.strings["not_running"])
mini_url = (self.config["mini_app_url"] or "").strip().rstrip("/")
is_tg_direct = mini_url.startswith("https://t.me/")
if self.runner:
if is_tg_direct:
access = mini_url
else:
base = (self.tunnel_url or "").rstrip("/")
access = f"{base}/?token={self.access_token}" if base and self.access_token else base
await utils.answer(message, self.strings["already_running"])
if access:
await self.send_form(message, access)
return
if not self.config["ngrok_token"] and (not mini_url or is_tg_direct):
return await utils.answer(message, self.strings["ngrok_missing"])
await self.refresh_games_cache()
await utils.answer(message, self.strings["starting"])
self.ensure_access_token()
sock = socket.socket()
sock.bind(("", 0))
port = sock.getsockname()[1]
sock.close()
app = web.Application()
app.router.add_get("/", self.handle_home)
app.router.add_get("/api/games", self.handle_games)
app.router.add_get("/api/me", self.handle_me)
self.runner = web.AppRunner(app)
await self.runner.setup()
await web.TCPSite(self.runner, "127.0.0.1", port).start()
try:
if self.config["ngrok_token"]:
conf.get_default().auth_token = self.config["ngrok_token"]
tunnel = ngrok.connect(port)
self.tunnel_url = tunnel.public_url.rstrip("/")
else:
self.tunnel_url = mini_url
except Exception as error:
await self.stop_server()
return await utils.answer(
message,
self.strings["ngrok_error"].format(utils.escape_html(str(error))),
)
if is_tg_direct:
access_url = mini_url
else:
base = (self.tunnel_url or "").rstrip("/")
access_url = f"{base}/?token={self.access_token}" if base and self.access_token else base
await self.send_form(message, access_url)
@loader.command(ru_doc="Создает и настраивает эпку")
async def cma(self, message: Message):
"""Create and setup mini-app"""
raw_args = (utils.get_args_raw(message) or "").strip()
parts = raw_args.split()
web_url = ""
short_name = "NoChess"
if parts:
web_url = parts[0]
if len(parts) > 1:
short_name = parts[1]
if not web_url:
candidate = (self.tunnel_url or "").strip()
if not candidate:
candidate = (self.config["mini_app_url"] or "").strip()
if candidate.startswith("https://t.me/"):
candidate = ""
web_url = candidate
if not web_url:
return await utils.answer(message, self.strings["cma_need_url"])
self.ensure_access_token()
if web_url.startswith("http") and "t.me/" not in web_url:
parsed = urlsplit(web_url)
query = dict(parse_qsl(parsed.query, keep_blank_values=True))
query["token"] = self.access_token
web_url = urlunsplit((parsed.scheme, parsed.netloc, parsed.path, urlencode(query), parsed.fragment))
await utils.answer(message, self.strings["cma_start"])
try:
bot_username = (await self.inline.bot.get_me()).username
bot_username = (bot_username or "").strip().lstrip("@")
if not bot_username:
raise RuntimeError(self.strings["RuntimeError"])
await self.client.send_message("@BotFather", "/cancel")
await asyncio.sleep(0.9)
async with self.client.conversation("@BotFather", timeout=120) as conv:
await conv.send_message("/newapp")
await conv.get_response()
await asyncio.sleep(0.8)
await conv.send_message(f"@{bot_username}")
await conv.get_response()
await asyncio.sleep(0.8)
await conv.send_message("NoChessModule")
await conv.get_response()
await asyncio.sleep(0.8)
await conv.send_message("NoChess")
await conv.get_response()
await asyncio.sleep(0.8)
await conv.send_file(botfather_photo_url)
await conv.get_response()
await asyncio.sleep(0.8)
await conv.send_message("/empty")
await conv.get_response()
await asyncio.sleep(0.8)
await conv.send_message(web_url)
await conv.get_response()
await asyncio.sleep(0.8)
await conv.send_message(short_name)
await conv.get_response()
direct_link = f"https://t.me/{bot_username}/{short_name}"
module_ref = None
try:
module_ref = self.lookup("NoChess")
except Exception:
module_ref = None
if module_ref:
module_ref.config["mini_app_url"] = direct_link
else:
self.config["mini_app_url"] = direct_link
await utils.answer(message, self.strings["cma_done"])
except Exception as error:
await utils.answer(message, self.strings["cma_error"].format(utils.escape_html(str(error))))
@loader.command(ru_doc="ВАЖНО К ПРОЧТЕНИЮ")
async def about(self, message: Message):
"""IMPORTANT READING"""
await utils.answer(message, self.strings["about_text"])
async def on_unload(self):
await self.stop_server()