diff --git a/.env b/.env new file mode 100644 index 0000000..d47128e --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +BOT_TOKEN= Your token +WEBAPP_URL= diff --git a/README.md b/README.md index 09d0352..ed2e335 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,42 @@ # CollapseBot-Telegram -Our telegram bot + +A simple and efficient Telegram bot built with Python. + +## How to Create & Run + +### 1. Create a Bot via BotFather +1. Open Telegram and search for **@BotFather**. +2. Send the command `/newbot`. +3. Enter a name for your bot (e.g., `CollapseBot`). +4. Create a username for your bot (must end in `bot`, e.g., `collapse_test_bot`). +5. BotFather will send you an **HTTP API Token**. Copy it. + +### 2. Configure Settings +1. In BotFather, type `/mybots`. +2. Select your bot. +3. Go to **Bot Settings** > **Inline Mode** and turn it **ON** (if required). + +### 3. Setup Local Environment +1. Clone this repository. +2. Create a file named `.env` in the root directory. +3. Paste your API Token into the `.env` file: + ```env + BOT_TOKEN=your_copied_api_token_here + +### CLI Design main menu + +
+
+
+{tag_l} (GitHub)\n"
+ f"{get_msg('pre', lang)} {tag_p} (GitHub)",
+ parse_mode="HTML",
+ disable_web_page_preview=True
+ )
+
+@dp.message(F.text == "/clients")
+async def cmd_clients(message: types.Message):
+ lang = message.from_user.language_code
+ clients_text = await get_cached_clients(lang)
+ await message.answer(
+ f"{get_msg('clients_title', lang)}\n\n{clients_text}",
+ parse_mode="HTML"
+ )
+
+@dp.message(F.text == "/changelog")
+async def cmd_changelog(message: types.Message):
+ v = await get_cached_versions()
+ tag_l, link_l, body_l = v["latest"]
+
+ if body_l:
+ safe_body = body_l[:3500].replace("<", "<").replace(">", ">") + ("..." if len(body_l) > 3500 else "")
+ else:
+ safe_body = "Нет описания."
+
await message.answer(
- "Hello! I am an inline bot. Type @CollapseLoader_bot to see my snippets."
+ f"Changelog: {tag_l}\n\n{safe_body}\n\nGitHub",
+ parse_mode="HTML",
+ disable_web_page_preview=True
)
+from aiogram.filters import Command
+@dp.message(Command("client"))
+async def cmd_client(message: types.Message):
+ parts = message.text.split(maxsplit=1)
+ if len(parts) < 2:
+ await message.answer("Укажите ID или название клиента. Пример: /client 47 или /client Vanilla", parse_mode="HTML")
+ return
+
+ query = parts[1]
+ client_data = await get_client_info(query)
+
+ if not client_data:
+ await message.answer(f"Клиент {query} не найден.", parse_mode="HTML")
+ return
+
+ name = client_data.get("name", "Unknown")
+ version = client_data.get("version", "N/A")
+ c_id = client_data.get("id", "N/A")
+ file_name = client_data.get("filename", "N/A")
+ launches = client_data.get("launches", 0)
+ downloads = client_data.get("downloads", 0)
+ working = "Да" if client_data.get("working") else "Нет"
+
+ text = (
+ f"Клиент: {name}\n"
+ f"Версия: {version}\n"
+ f"ID: {c_id}\n"
+ f"Файл: {file_name}\n"
+ f"Работает: {working}\n\n"
+ f"Запусков: {launches}\n"
+ f"Скачиваний: {downloads}"
+ )
+ await message.answer(text, parse_mode="HTML")
+
+@dp.message(F.text == "/subscribe")
+async def cmd_subscribe(message: types.Message):
+ add_subscriber(message.from_user.id)
+ await message.answer(get_msg("sub_ok", message.from_user.language_code))
+
+@dp.message(F.text == "/unsubscribe")
+async def cmd_unsubscribe(message: types.Message):
+ remove_subscriber(message.from_user.id)
+ await message.answer(get_msg("unsub_ok", message.from_user.language_code))
@dp.inline_query()
async def inline_query_handler(query: types.InlineQuery):
+ increment_stat("snippet_searches")
query_text = query.query.lower().strip()
+ lang = query.from_user.language_code
results = []
- for key, data in snippets.items():
- title = data.get("title", key)
- content = data.get("content", "")
+ status_val = await get_cached_status(lang)
+ v = await get_cached_versions()
+ clients_val = await get_cached_clients(lang)
+ tag_l, link_l, _ = v["latest"]
+ tag_p, link_p, _ = v["pre"]
- if (
- query_text in key.lower()
- or query_text in title.lower()
- or query_text in content.lower()
- ):
+ all_ok = all("Online" in s for s in status_val.split("\n") if s)
+ status_summary = get_msg("online" if all_ok else "error", lang)
- description = content.split("\n")[0] if content else "No content"
- if len(description) > 50:
- description = description[:47] + "..."
+ dynamic_items = [
+ {
+ "id": "dynamic_status",
+ "title": f"{get_msg('status_title', lang).replace('', '').replace('', '')} {status_summary}",
+ "description": "Atlas, Auth, API",
+ "msg": f"{get_msg('status_title', lang)}\n\n{status_val}",
+ "keywords": ["status", "статус", "сервер", "server", "атлас", "atlas", "auth", "api"]
+ },
+ {
+ "id": "dynamic_versions",
+ "title": f"{tag_l} | {tag_p}",
+ "description": get_msg('version_title', lang).replace('', '').replace('', ''),
+ "msg": (
+ f"{get_msg('version_title', lang)}\n\n"
+ f"{get_msg('stable', lang)} {tag_l} (GitHub)\n"
+ f"{get_msg('pre', lang)} {tag_p} (GitHub)"
+ ),
+ "keywords": ["version", "версия", "update", "обнова", "pre", "пре", "релиз"]
+ },
+ {
+ "id": "dynamic_clients",
+ "title": f"{get_msg('clients_title', lang).replace('', '').replace('', '')}",
+ "description": "Список клиентов",
+ "msg": f"{get_msg('clients_title', lang)}\n\n{clients_val}",
+ "keywords": ["clients", "клиенты", "список"]
+ }
+ ]
+ if not query_text:
+ for item in dynamic_items:
results.append(
InlineQueryResultArticle(
- id=key,
- title=title,
- description=description,
+ id=item["id"],
+ title=item["title"],
+ description=item["description"],
input_message_content=InputTextMessageContent(
- message_text=content, parse_mode="Markdown"
- ),
+ message_text=item["msg"], parse_mode="HTML", disable_web_page_preview=True
+ )
)
)
+ else:
+ for item in dynamic_items:
+ if any(k in query_text for k in item["keywords"]):
+ results.append(
+ InlineQueryResultArticle(
+ id=item["id"],
+ title=item["title"],
+ description=item["description"],
+ input_message_content=InputTextMessageContent(
+ message_text=item["msg"], parse_mode="HTML", disable_web_page_preview=True
+ )
+ )
+ )
+
+ choices = []
+ for key, data in snippets.items():
+ if key in ["status", "version", "clients"]:
+ continue
+ search_text = f"{key} {data.get('title', '')} {data.get('content', '')}".lower()
+ choices.append((key, search_text))
+
+ if query_text:
+ matches = process.extract(
+ query_text,
+ {k: s for k, s in choices},
+ limit=15,
+ scorer=fuzz.partial_token_set_ratio
+ )
+ matched_keys = [m[2] for m in matches if m[1] > 40]
+ else:
+ matched_keys = [k for k in snippets.keys() if k not in ["status", "version", "clients"]]
+
+ for key in matched_keys:
+ data = snippets.get(key)
+ if not data: continue
+
+ title = data.get("title", key)
+ content = data.get("content", "")
+ description = content.split("\n")[0] if content else "No content"
+
+ if len(description) > 50:
+ description = description[:47] + "..."
+
+ formatted_content = safe_format(content)
+
+ results.append(
+ InlineQueryResultArticle(
+ id=key,
+ title=title,
+ description=description,
+ input_message_content=InputTextMessageContent(
+ message_text=formatted_content, parse_mode="HTML"
+ ),
+ )
+ )
+
+ try:
+ await query.answer(results[:50], cache_time=5)
+ except Exception as e:
+ logger.debug(f"Could not answer inline query: {e}")
+
+async def check_updates_task():
+ last_tag = None
+ while True:
+ try:
+ v = await get_cached_versions()
+ tag = v["latest"][0]
+ url = v["latest"][1]
+
+ if last_tag is None:
+ last_tag = tag
+
+ if tag != last_tag and tag != "N/A":
+ last_tag = tag
+ logger.info(f"New version detected: {tag}")
+ subs = get_subscribers()
+ for user_id in subs:
+ try:
+ await bot.send_message(
+ user_id,
+ get_msg("new_update", "ru", tag=tag, url=url),
+ parse_mode="HTML"
+ )
+ except Exception as e:
+ logger.error(f"Failed to notify {user_id}: {e}")
+ except Exception as e:
+ logger.error(f"Error in check_updates_task: {e}")
+
+ await asyncio.sleep(1800)
+
+async def server_monitor_task():
+ last_status = "online"
+ offline_start_time = 0
+
+ while True:
+ try:
+ client = await get_client()
+ url = "https://huggingface.co/datasets/Collapsecdn/collapsecdn"
+
+ try:
+ resp = await client.get(url, timeout=10.0)
+ is_online = resp.status_code < 400
+ except Exception:
+ is_online = False
+
+ current_status = "online" if is_online else "offline"
+
+ if current_status == "offline" and last_status == "online":
+ offline_start_time = time.time()
+ last_status = "offline"
+ logger.warning("CDN/API Server went offline! Notifying subscribers.")
+ subs = get_subscribers()
+ for user_id in subs:
+ try:
+ await bot.send_message(
+ chat_id=user_id,
+ text="CRITICAL: Сервер CDN/API временно недоступен!\n\nСлужба не отвечает на запросы, возможны перебои в работе лоадера.",
+ parse_mode="HTML"
+ )
+ except Exception as e:
+ logger.error(f"Failed to notify {user_id}: {e}")
+
+ elif current_status == "online" and last_status == "offline":
+ downtime = int((time.time() - offline_start_time) / 60)
+ last_status = "online"
+ logger.info("CDN/API Server is back online! Notifying subscribers.")
+ subs = get_subscribers()
+ for user_id in subs:
+ try:
+ await bot.send_message(
+ chat_id=user_id,
+ text=f"Сервер восстановлен!\n\nСистемы CDN/API снова работают стабильно. Примерное время простоя: {downtime} мин.",
+ parse_mode="HTML"
+ )
+ except Exception as e:
+ logger.error(f"Failed to notify {user_id}: {e}")
+
+ except Exception as e:
+ logger.error(f"Error in server_monitor_task: {e}")
+
+ await asyncio.sleep(60)
- await query.answer(results[:50], cache_time=1)
+async def api_status(request):
+ status_text = await get_cached_status("ru")
+ return web.json_response({"status": status_text})
+async def api_versions(request):
+ v = await get_cached_versions()
+ return web.json_response(v)
+
+async def api_clients(request):
+ clients_text = await get_cached_clients("ru")
+ return web.json_response({"clients": clients_text})
+
+async def handle_index(request):
+ return web.FileResponse('webapp/index.html')
+
+async def start_webapp_server():
+ app = web.Application()
+ app.router.add_get('/api/status', api_status)
+ app.router.add_get('/api/versions', api_versions)
+ app.router.add_get('/api/clients', api_clients)
+
+ if os.path.exists("webapp"):
+ app.router.add_get('/', handle_index)
+ app.router.add_static('/', 'webapp')
+ logger.info("Serving static WebApp files from 'webapp' directory")
+
+ runner = web.AppRunner(app)
+ await runner.setup()
+ site = web.TCPSite(runner, '127.0.0.1', 8085)
+ await site.start()
+ logger.info("WebApp server started locally on http://127.0.0.1:8085")
async def main():
- await dp.start_polling(bot)
+ asyncio.create_task(start_webapp_server())
+ bot_info = await bot.get_me()
+ logger.info(f"Starting bot @{bot_info.username}")
+ asyncio.create_task(get_cached_status("ru"))
+ asyncio.create_task(get_cached_status("en"))
+ asyncio.create_task(get_cached_clients("ru"))
+ asyncio.create_task(get_cached_clients("en"))
+ asyncio.create_task(get_cached_versions())
+ asyncio.create_task(check_updates_task())
+ asyncio.create_task(server_monitor_task())
+ await dp.start_polling(bot)
if __name__ == "__main__":
- asyncio.run(main())
+ import sys
+
+ if "--worker" in sys.argv or os.name != 'nt' or os.environ.get("DOCKER_ENV"):
+ try:
+ if os.name == 'nt':
+ os.system(f"title CollapseBot Logs")
+
+ asyncio.run(main())
+ except (KeyboardInterrupt, SystemExit):
+ logger.info("Bot stopped!")
+ else:
+ try:
+ subprocess.Popen(
+ [sys.executable, "manager.py"],
+ creationflags=subprocess.CREATE_NEW_CONSOLE
+ )
+ except Exception as e:
+ asyncio.run(main())
diff --git a/manager.py b/manager.py
new file mode 100644
index 0000000..bbedbe0
--- /dev/null
+++ b/manager.py
@@ -0,0 +1,356 @@
+import subprocess
+import sys
+import os
+import time
+import ctypes
+import ctypes.wintypes
+import threading
+from collections import deque
+import json
+import atexit
+import signal
+
+try:
+ from rich.console import Console
+ from rich.panel import Panel
+ from rich.text import Text
+ from rich.table import Table
+ from rich import box
+ from rich.align import Align
+ from rich.style import Style
+ from rich.markup import escape
+except ImportError:
+ print("Installing required UI packages...")
+ subprocess.call([sys.executable, "-m", "pip", "install", "rich"])
+ from rich.console import Console
+ from rich.panel import Panel
+ from rich.text import Text
+ from rich.table import Table
+ from rich import box
+ from rich.align import Align
+ from rich.style import Style
+ from rich.markup import escape
+
+console = Console()
+log_buffer = deque(maxlen=20)
+full_logs = deque(maxlen=500)
+log_counter = 0
+stats = {"start_time": time.time(), "engines_spawned": 0, "errors": 0}
+
+def log_reader(pipe):
+ global log_counter
+ try:
+ for line in iter(pipe.readline, ''):
+ if not line: break
+ decoded = line.strip()
+ log_buffer.append(decoded)
+ full_logs.append(decoded)
+ log_counter += 1
+ except Exception:
+ pass
+ pipe.close()
+
+def clear_screen():
+ os.system('cls' if os.name == 'nt' else 'clear')
+
+_current_bot_process = None
+_current_tunnel_process = None
+_tunnel_url = "Not Started"
+
+def update_env_url(url):
+ try:
+ if os.path.exists(".env"):
+ with open(".env", "r", encoding="utf-8") as f:
+ content = f.read()
+ import re
+ new_content = re.sub(r'(?m)^WEBAPP_URL=.*$', '', content).strip()
+ new_content += f'\nWEBAPP_URL={url}\n'
+ with open(".env", "w", encoding="utf-8") as f:
+ f.write(new_content)
+ else:
+ with open(".env", "w", encoding="utf-8") as f:
+ f.write(f'WEBAPP_URL={url}\n')
+ except Exception as e:
+ full_logs.append(f"Failed to update .env: {e}")
+
+def tunnel_reader(pipe):
+ global log_counter, _tunnel_url
+ import re
+ try:
+ for line in iter(pipe.readline, ''):
+ if not line: break
+ decoded = line.strip()
+ full_logs.append("[TUNNEL] " + decoded)
+ log_counter += 1
+
+ match = re.search(r'(https://[a-zA-Z0-9-]+\.lhr\.life|https://[a-zA-Z0-9-]+\.serveo\.net|https://[a-zA-Z0-9-]+\.serveousercontent\.com)', decoded)
+ if match:
+ _tunnel_url = match.group(1)
+ update_env_url(_tunnel_url)
+ except Exception:
+ pass
+ pipe.close()
+
+def start_tunnel():
+ global _current_tunnel_process, _tunnel_url
+ if _current_tunnel_process and _current_tunnel_process.poll() is None:
+ try:
+ _current_tunnel_process.terminate()
+ except:
+ pass
+
+ _tunnel_url = "Connecting..."
+ cmd = ["ssh", "-o", "StrictHostKeyChecking=no", "-R", "80:127.0.0.1:8085", "nokey@localhost.run"]
+ process = subprocess.Popen(
+ cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ bufsize=1,
+ stdin=subprocess.DEVNULL,
+ creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0x08000000) if os.name == 'nt' else 0
+ )
+ _current_tunnel_process = process
+ threading.Thread(target=tunnel_reader, args=(process.stdout,), daemon=True).start()
+
+def cleanup():
+ global _current_bot_process, _current_tunnel_process
+ if _current_bot_process and _current_bot_process.poll() is None:
+ try:
+ _current_bot_process.terminate()
+ _current_bot_process.wait(timeout=2)
+ except Exception:
+ try:
+ _current_bot_process.kill()
+ except Exception:
+ pass
+
+ if _current_tunnel_process and _current_tunnel_process.poll() is None:
+ try:
+ _current_tunnel_process.terminate()
+ _current_tunnel_process.wait(timeout=2)
+ except:
+ try:
+ _current_tunnel_process.kill()
+ except:
+ pass
+
+atexit.register(cleanup)
+
+def signal_handler(sig, frame):
+ cleanup()
+ sys.exit(0)
+
+signal.signal(signal.SIGINT, signal_handler)
+signal.signal(signal.SIGTERM, signal_handler)
+
+def start_bot():
+ global _current_bot_process
+ process = subprocess.Popen(
+ [sys.executable, "main.py", "--worker"],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ bufsize=1
+ )
+ _current_bot_process = process
+ threading.Thread(target=log_reader, args=(process.stdout,), daemon=True).start()
+ stats["engines_spawned"] += 1
+ return process
+
+def draw_interface(bot_process):
+ is_running = bot_process.poll() is None
+ status_color = "#00FF66" if is_running else "#FF3B30"
+ status_text = "● ONLINE" if is_running else "○ OFFLINE"
+
+ # Elegant minimalist header
+ header = Text("\n✦ COLLAPSE CONTROL PANEL ✦\n", style="bold #00E5FF", justify="center")
+
+ # Sleek status table with simple lines
+ status_table = Table(
+ box=box.SIMPLE,
+ show_header=False,
+ expand=True,
+ border_style="#333333",
+ padding=(0, 1)
+ )
+ status_table.add_column("Key", style="dim #A0A0A0", justify="right", width=20)
+ status_table.add_column("Value", style="white", justify="left")
+
+ status_table.add_row("Bot Status", f"[{status_color}]{status_text}[/]")
+ status_table.add_row("WebApp Tunnel", f"[bold #00FF66]{escape(_tunnel_url)}[/]")
+ status_table.add_row("Python", f"[dim #808080]{escape(sys.version.split()[0])}[/]")
+ status_table.add_row("Directory", f"[dim #808080]{escape(os.path.basename(os.getcwd()))}[/]")
+
+ # Minimalist control layout
+ controls = Table(box=box.SIMPLE, expand=True, show_header=False, border_style="#333333", padding=(0, 2))
+ controls.add_column("Col1", justify="center")
+ controls.add_column("Col2", justify="center")
+
+ controls.add_row(
+ "[bold #00FF66]1[/] Start Bot",
+ "[bold #9F7AEA]4[/] Live Logs"
+ )
+ controls.add_row(
+ "[bold #FF3B30]2[/] Stop Bot",
+ "[bold #FFA500]5[/] Statistics"
+ )
+ controls.add_row(
+ "[bold #FFCC00]3[/] Restart",
+ "[bold #E2E8F0]6[/] Exit"
+ )
+
+ # Combining into a single minimalist panel grid
+ grid = Table.grid(expand=True)
+ grid.add_row(header)
+ grid.add_row(Panel(status_table, border_style="#444444", title="[bold #A0A0A0] Status [/]", title_align="left"))
+ grid.add_row(Panel(controls, border_style="#444444", title="[bold #A0A0A0] Controls [/]", title_align="left"))
+
+ return Panel(
+ grid,
+ border_style="#00E5FF",
+ box=box.ROUNDED,
+ padding=(0, 2)
+ )
+
+def get_bot_stats():
+ try:
+ if os.path.exists("stats.json"):
+ with open("stats.json", "r", encoding="utf-8") as f:
+ return json.load(f)
+ except Exception:
+ pass
+ return {"start_count": 0, "snippet_searches": 0}
+
+def main_manager():
+ bot_process = start_bot()
+ start_tunnel()
+
+ import msvcrt
+ global _tunnel_url
+ last_tunnel_url = _tunnel_url
+
+ try:
+ while True:
+ last_bot_status = bot_process.poll() is None
+
+ clear_screen()
+ console.print(draw_interface(bot_process))
+ console.print(" [bold #00E5FF]❯ Select Operation (1-6):[/] ", end="")
+ sys.stdout.flush()
+
+ choice = None
+ while True:
+ if msvcrt.kbhit():
+ char = msvcrt.getch().decode('utf-8', errors='ignore')
+ if char in ['1', '2', '3', '4', '5', '6']:
+ choice = char
+ print(choice)
+ time.sleep(0.2)
+ break
+
+ current_running = bot_process.poll() is None
+ if _tunnel_url != last_tunnel_url or current_running != last_bot_status:
+ last_tunnel_url = _tunnel_url
+ break
+
+ time.sleep(0.1)
+
+ if choice is None:
+ continue
+
+ if choice == '1':
+ if bot_process.poll() is not None:
+ with console.status("[bold #00FF7F]Starting Bot...[/]"):
+ bot_process = start_bot()
+ time.sleep(1.5)
+ else:
+ console.print(" [bold #FFD700]Bot is already active![/]")
+ time.sleep(1)
+
+ elif choice == '2':
+ if bot_process.poll() is None:
+ with console.status("[bold #FF3366]Stopping Bot...[/]"):
+ bot_process.terminate()
+ time.sleep(1.5)
+ else:
+ console.print(" [bold #FFD700]Bot is already halted![/]")
+ time.sleep(1)
+
+ elif choice == '3':
+ with console.status("[bold #FFD700]Restarting Bot...[/]"):
+ if bot_process.poll() is None:
+ bot_process.terminate()
+ bot_process.wait()
+ bot_process = start_bot()
+ start_tunnel()
+ time.sleep(1.5)
+
+ elif choice == '4':
+ clear_screen()
+ console.print(Panel("[bold #9370DB]LIVE BOT LOGS[/]\n[dim]Press ANY KEY to return to the main menu...[/]", border_style="#9370DB"))
+
+ import msvcrt
+ snapshot = list(full_logs)
+ for line in snapshot[-30:]:
+ console.print(line)
+
+ last_seen_total = log_counter
+
+ # Очищаем буфер от случайных нажатий (например Enter)
+ while msvcrt.kbhit():
+ msvcrt.getch()
+
+ while True:
+ if msvcrt.kbhit():
+ msvcrt.getch()
+ break
+
+ if log_counter > last_seen_total:
+ new_lines_count = log_counter - last_seen_total
+ actual_to_print = min(new_lines_count, len(full_logs))
+ current_all_logs = list(full_logs)
+ for i in range(len(current_all_logs) - actual_to_print, len(current_all_logs)):
+ console.print(current_all_logs[i])
+ last_seen_total = log_counter
+
+ time.sleep(0.1)
+
+ elif choice == '5':
+ clear_screen()
+ uptime = time.strftime("%Hh %Mm %Ss", time.gmtime(time.time() - stats["start_time"]))
+ bot_stats = get_bot_stats()
+
+ stats_table = Table(box=box.ROUNDED, show_header=False, border_style="#FFA500")
+ stats_table.add_row("Session Uptime :", uptime)
+ stats_table.add_row("Bot Restarts :", str(stats["engines_spawned"]))
+ stats_table.add_row("Bot Starts (/start) :", str(bot_stats.get("start_count", 0)))
+ stats_table.add_row("Snippets Searched :", str(bot_stats.get("snippet_searches", 0)))
+ stats_table.add_row("Bot Health :", "[bold #00FF7F]OK[/]")
+
+ stats_panel = Panel(
+ Align.center(stats_table),
+ title="[bold #FFA500] STATISTICS [/]",
+ border_style="#FFA500",
+ padding=(1, 4)
+ )
+ console.print(stats_panel)
+ console.print("\n [dim]Press ANY KEY to return...[/]")
+ import msvcrt
+ while msvcrt.kbhit(): msvcrt.getch()
+ while not msvcrt.kbhit(): time.sleep(0.05)
+ msvcrt.getch()
+
+ elif choice == '6':
+ cleanup()
+ console.print("\n [bold #FF00FF]Session Terminated. Goodbye.[/]")
+ break
+ finally:
+ cleanup()
+
+if __name__ == "__main__":
+ try:
+ main_manager()
+ except (KeyboardInterrupt, SystemExit):
+ cleanup()
+ sys.exit(0)
diff --git a/requirements.txt b/requirements.txt
index a1bb535..47310eb 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,8 @@
-aiogram===3.22.0
+aiogram==3.25.0
python-dotenv==1.2.1
requests==2.32.5
httpx==0.28.1
-configparser===7.2.0
-PyYAML==6.0.3
\ No newline at end of file
+configparser==7.2.0
+PyYAML==6.0.3
+thefuzz==0.22.1
+rich==13.7.0
\ No newline at end of file
diff --git a/snippets.yaml b/snippets.yaml
index a4e4b8d..b310ff5 100644
--- a/snippets.yaml
+++ b/snippets.yaml
@@ -4,6 +4,7 @@ data-clear:
**Шаги для очистки данных CollapseLoader:**
1. **Закройте лоадер полностью**
+
2. **Перейдите в папку:**
- Windows: `%appdata%\CollapseLoader\`
@@ -21,10 +22,18 @@ crash-logs:
1. **Нажмите правой кнопкой мыши на клиент**
- Нажмите "Копировать логи"
+ 2. **Вставьте логи в текстовый редактор**
+ - Например, в Блокнот
+
+ 3. **Сохраните файл**
+
+ 4. **Отправьте файл чтоб получить помощь**
+ - Это поможет нам быстрее разобраться в проблеме
+
installation:
title: '📥 Установка CollapseLoader'
content: |
- **Пошаговая установка:**
+ **Пошаговая установка collapseloader:**
1. **Скачайте лоадер:**
- Официальный сайт: https://collapseloader.org
@@ -45,7 +54,7 @@ social-link:
- Telegram: https://t.me/collapseloader
- Telegram DevLogs: https://t.me/collapseloader_devlogs
- Discord: https://collapseloader.org/discord
- - Github: github.com/dest4590/collapseloader
+ - Github: https://github.com/dest4590/collapseloader
technical-support:
title: '🔧 Техническая поддержка'
@@ -62,12 +71,28 @@ technical-support:
3. Проверить, не удалил ли антивирус файлы клиента
**💬 Где получить помощь:**
- - <#1231330786481930347> - создание тикетов
- - Поиск по серверу (частые вопросы)
+ - Откройте тикет в нашем Discord сервере где вам помогут с проблемой
+ - Так же вы можете обратиться в наш Telegram чат вам помогут с проблемой
**⚡ Быстрая помощь:**
- - Приложите скриншоты
- - Укажите что уже пробовали
+ - Приложите файлы логов чтобы админи могли быстрее разобраться в проблеме
+ - Укажите что уже пробовал сделать для решения проблемы
+
+bug-report:
+ title: ' 🪲 Как сообщить о баге'
+ content: |
+ **Как сообщить о баге:**
+
+ 1. **Соберите информацию:**
+ - Версия CollapseLoader
+ - Подробное описание проблемы
+ - Приложите скриншоты или видео, если это возможно
+
+ 2. **Создайте публикацию в нашем дискорде в разделе bug-tracking**
+ - Укажите всю собранную информацию
+
+ **⚡ Быстрая помощь:**
+ - Чем больше информации вы предоставите, тем быстрее мы сможем разобраться в проблеме
clear-loader-files:
title: ' 🧹 Очистка файлов лоадера'
@@ -78,39 +103,29 @@ clear-loader-files:
2. Нажмите на кнопку "Сбросить файлы лоадера"
3. После сообщения об успешной очистке, попробуйте снова запустить клиент
-not-loading:
- title: '❗ Лоадер/Клиенты не загружается'
+not-loading-method-1:
+ title: '❗ Метод 1 Лоадер/Клиенты не загружается'
content: |
**Если лоадер не запускается (зависает в экране загрузки, пишет что сервера офлайн)**
1. Попробуйте включить DPI Bypass в настройках лоадера, после этого перезапустите лоадер **от администратора**
- 2. Проверьте, не блокирует ли ваш провайдер доступ к серверам лоадера: https://auth.collapseloader.org и https://cdn.collapseloader.org (Если у вас 0.2.7 НО если у вас новее версия используйте метод 2)
+ 2. Проверьте, не блокирует ли ваш провайдер доступ к серверам лоадера: https://atlas.collapseloader.org
3. Попробуйте подключиться через VPN
-not-loading-new:
+not-loading-method-2:
title: '❗ Метод 2 Лоадер/Клиенты не загружаеться'
content: |
**Если лоадер не запускается (зависает в экране загрузки, пишет что сервера офлайн)**
- 1. Добавьте в запрет-лист эти адреса: https://auth.collapseloader.org и https://cdn.collapseloader.org и https://api.collapseloader.org и https://atlas.collapseloader.org
+ 1. Добавьте в запрет-лист этот адреса: https://atlas.collapseloader.org
2. Если вам не помогло, попробуйте подключиться через VPN
-client-download-stuck:
- title: '⏳ Загрузка клиента зависла'
- content: |
- **Если загрузка клиента зависла на 0 процентов:**
-
- 1. Откройте настройки лоадера
- 2. Включите опцию DPI Bypass
- 3. Перезапустите лоадер от имени администратора
- 4. Попробуйте снова скачать клиент
-
-Ti-ceryezno?:
- title: 'Если пишет "Ти серьезно?" при попытке зайти в лоадер'
+Tu-ceryezno?:
+ title: ' 😔 Если пишет "Ты серьезно?" при попытке зайти в лоадер'
content: |
- Если пишет "Ти серьезно?" при попытке зайти в лоадер
+ Если пишет "Ты серьезно?" при попытке зайти в лоадер
**Советую вот что сделать:**
1. Закрой лоадер
2. Удали лоадер
- 3. Проверь сколько у тебя айкю
+ 3. Проверь сколько у тебя айкю
\ No newline at end of file
diff --git a/stats.json b/stats.json
new file mode 100644
index 0000000..78fde77
--- /dev/null
+++ b/stats.json
@@ -0,0 +1 @@
+{"start_count": 8}
\ No newline at end of file
diff --git a/utils.py b/utils.py
new file mode 100644
index 0000000..4413673
--- /dev/null
+++ b/utils.py
@@ -0,0 +1,292 @@
+import httpx
+import time
+import re
+import html
+import yaml
+import os
+import logging
+
+logger = logging.getLogger(__name__)
+
+cache = {
+ "status": {"data": None, "time": 0},
+ "version": {"data": None, "time": 0},
+ "clients": {"data": None, "time": 0},
+ "clients_raw": {"data": {}, "time": 0}
+}
+
+def load_snippets():
+ if not os.path.exists("snippets.yaml"):
+ logger.error("snippets.yaml not found!")
+ return {}
+ with open("snippets.yaml", "r", encoding="utf-8") as f:
+ try:
+ return yaml.safe_load(f) or {}
+ except yaml.YAMLError as e:
+ logger.error(f"Error parsing snippets.yaml: {e}")
+ return {}
+
+def safe_format(text):
+ text = html.escape(text)
+ text = re.sub(r'\*\*(.*?)\*\*', r'\1', text)
+ text = re.sub(r'\*(.*?)\*', r'\1', text)
+ return text
+
+import asyncio
+import json
+
+TRANSLATIONS = {
+ "ru": {
+ "start": "CollapseBot \n\nВведите @{username} в любом чате для поиска сниппетов.\n\nКоманды:\n/status - Состояние серверов\n/version - Версии лоадера\n/clients - Доступные клиенты\n/client <id> - Подробно о клиенте\n/changelog - Что нового\n/help - Справка",
+ "help": "Справка по CollapseBot:\n\nОсновные команды:\n/status - Текущее состояние серверов Atlas\n/version - Версии лоадера (stable / pre-release)\n/clients - Выборка доступных клиентов (Vanilla, Fabric, Forge)\n\nЛоадер и Клиенты:\n/changelog - Посмотреть, что нового в свежей версии лоадера\n/client <id/название> - Детальная статистика по клиенту (запуски, статус)\n\nУведомления:\n/subscribe - Получать пуши о новых обновлениях и статусе серверов\n/unsubscribe - Отписаться от рассылки\n\nПоиск параметров (Инлайн):\nНапишите @{username} запрос в любом чате, чтобы найти руководство или сниппет лоадера.",
+ "status_title": "Статус серверов Collapse:",
+ "version_title": "Версии CollapseLoader:",
+ "stable": "Стабильная:",
+ "pre": "Пре-релиз:",
+ "sub_ok": "Вы подписались на уведомления об обновлениях!",
+ "unsub_ok": "Вы отписались от уведомлений.",
+ "new_update": "Вышло обновление!\n\nВерсия: {tag}\nСсылка: GitHub",
+ "online": "Онлайн",
+ "error": "Ошибки",
+ "clients_title": "Доступные клиенты:",
+ "clients_empty": "Нет доступных клиентов."
+ },
+ "en": {
+ "start": "CollapseBot \n\nType @{username} in any chat to search snippets.\n\nCommands:\n/status - Server status\n/version - Loader versions\n/clients - Available clients\n/client <id> - Client details\n/changelog - What's new\n/help - Help message",
+ "help": "CollapseBot Help:\n\nCommands:\n/status - Check Atlas server status\n/version - View loader versions\n/clients - View clients list \n\nLoader & Clients:\n/changelog - Check what's new in the latest loader update\n/client <id/name> - View detailed info about a specific client (launches, status)\n\nNotifications:\n/subscribe - Get push notifications for updates & downtime\n/unsubscribe - Opt out of notifications\n\nInline Search:\nType @{username} [query] in any chat to search loader snippets.",
+ "status_title": "Collapse Server Status:",
+ "version_title": "CollapseLoader Versions:",
+ "stable": "Stable:",
+ "pre": "Pre-release:",
+ "sub_ok": "You have subscribed to update notifications!",
+ "unsub_ok": "You have unsubscribed from notifications.",
+ "new_update": "New update available!\n\nVersion: {tag}\nLink: GitHub",
+ "online": "Online",
+ "error": "Error",
+ "clients_title": "Available clients:",
+ "clients_empty": "No clients available."
+ }
+}
+
+def get_msg(key, lang="ru", **kwargs):
+ lang = lang if lang in TRANSLATIONS else "ru"
+ return TRANSLATIONS[lang].get(key, key).format(**kwargs)
+
+_client = None
+
+async def get_client():
+ global _client
+ if _client is None or _client.is_closed:
+ _client = httpx.AsyncClient(
+ timeout=5.0,
+ follow_redirects=True,
+ headers={"User-Agent": "CollapseBot/2.5"}
+ )
+ return _client
+
+_cache_locks = {
+ "status": asyncio.Lock(),
+ "version": asyncio.Lock(),
+ "clients": asyncio.Lock(),
+ "refresh_status": asyncio.Lock(),
+ "refresh_version": asyncio.Lock(),
+ "refresh_clients": asyncio.Lock()
+}
+
+async def get_cached_status(lang="ru"):
+ now = time.time()
+ cache_key = f"status_{lang}"
+
+ if cache_key in cache and now - cache[cache_key]["time"] < 60:
+ return cache[cache_key]["data"]
+
+ if cache_key in cache and cache[cache_key]["data"]:
+ # Background refresh if not already refreshing
+ if not _cache_locks["refresh_status"].locked():
+ asyncio.create_task(refresh_status_cache(lang))
+ return cache[cache_key]["data"]
+
+ async with _cache_locks["status"]:
+ if cache_key in cache and cache[cache_key]["data"]:
+ return cache[cache_key]["data"]
+ return await refresh_status_cache(lang)
+
+async def refresh_status_cache(lang="ru"):
+ if _cache_locks["refresh_status"].locked() and not _cache_locks["status"].locked():
+ # Avoid redundant concurrent refreshes
+ return cache.get(f"status_{lang}", {}).get("data", "Initializing...")
+
+ async with _cache_locks["refresh_status"]:
+ now = time.time()
+ cache_key = f"status_{lang}"
+
+ services = {
+ "CDN/API": "https://huggingface.co/datasets/Collapsecdn/collapsecdn"
+ }
+
+ async def check_service(name, url, client):
+ start = time.perf_counter()
+ try:
+ resp = await client.get(url)
+ elapsed = int((time.perf_counter() - start) * 1000)
+ if resp.status_code < 400:
+ return f"{name}: Online ({elapsed}ms)"
+ else:
+ return f"{name}: Error {resp.status_code} ({elapsed}ms)"
+ except Exception:
+ return f"{name}: Offline"
+
+ try:
+ client = await get_client()
+ tasks = [check_service(name, url, client) for name, url in services.items()]
+ results = await asyncio.gather(*tasks)
+
+ status = "\n".join(results)
+ cache[cache_key] = {"data": status, "time": now}
+ return status
+ except Exception as e:
+ logger.error(f"Error refreshing status cache: {e}")
+ return cache.get(cache_key, {}).get("data", "Error fetching status")
+
+async def get_cached_versions():
+ now = time.time()
+ if cache["version"]["data"]:
+ if now - cache["version"]["time"] > 300:
+ if not _cache_locks["refresh_version"].locked():
+ asyncio.create_task(refresh_version_cache())
+ return cache["version"]["data"]
+
+ async with _cache_locks["version"]:
+ if cache["version"]["data"]:
+ return cache["version"]["data"]
+ return await refresh_version_cache()
+
+async def refresh_version_cache():
+ if _cache_locks["refresh_version"].locked() and not _cache_locks["version"].locked():
+ return cache["version"]["data"]
+
+ async with _cache_locks["refresh_version"]:
+ now = time.time()
+ try:
+ url_latest = "https://api.github.com/repos/dest4590/collapseloader/releases/latest"
+ url_all = "https://api.github.com/repos/dest4590/collapseloader/releases"
+
+ client = await get_client()
+ resp_l = await client.get(url_latest)
+ data_l = resp_l.json() if resp_l.status_code == 200 else {}
+
+ resp_all = await client.get(url_all)
+ releases = resp_all.json() if resp_all.status_code == 200 else []
+ pre = next((r for r in releases if r.get("prerelease")), None)
+
+ result = {
+ "latest": (data_l.get("tag_name", "N/A"), data_l.get("html_url", ""), data_l.get("body", "Нет данных")),
+ "pre": (pre.get("tag_name", "N/A"), pre.get("html_url", ""), pre.get("body", "Нет данных")) if pre else ("N/A", "", "Нет данных")
+ }
+ cache["version"] = {"data": result, "time": now}
+ return result
+ except Exception as e:
+ logger.error(f"Error refreshing version cache: {e}")
+ return cache["version"]["data"] or {"latest": ("N/A", ""), "pre": ("N/A", "")}
+
+async def get_cached_clients(lang="ru"):
+ now = time.time()
+ cache_key = f"clients_{lang}"
+
+ if cache_key in cache and cache[cache_key].get("data"):
+ if now - cache[cache_key]["time"] > 300:
+ if not _cache_locks["refresh_clients"].locked():
+ asyncio.create_task(refresh_clients_cache(lang))
+ return cache[cache_key]["data"]
+
+ async with _cache_locks["clients"]:
+ if cache_key in cache and cache[cache_key].get("data"):
+ return cache[cache_key]["data"]
+ return await refresh_clients_cache(lang)
+
+async def refresh_clients_cache(lang="ru"):
+ if _cache_locks["refresh_clients"].locked() and not _cache_locks["clients"].locked():
+ return cache.get(f"clients_{lang}", {}).get("data", get_msg("clients_empty", lang))
+
+ async with _cache_locks["refresh_clients"]:
+ now = time.time()
+ cache_key = f"clients_{lang}"
+ try:
+ urls = {
+ "Vanilla / Custom": "https://huggingface.co/datasets/Collapsecdn/collapsecdn/raw/main/static/clients.json",
+ "Fabric": "https://huggingface.co/datasets/Collapsecdn/collapsecdn/raw/main/static/fabric-clients.json",
+ "Forge": "https://huggingface.co/datasets/Collapsecdn/collapsecdn/raw/main/static/forge-clients.json"
+ }
+ client = await get_client()
+
+ lines = []
+ raw_clients_map = {}
+ for category, url in urls.items():
+ resp = await client.get(url)
+ if resp.status_code == 200:
+ data = resp.json()
+ if isinstance(data, list):
+ clients_list = data
+ elif isinstance(data, dict):
+ clients_list = data.get("data", [])
+ else:
+ clients_list = []
+
+ if clients_list:
+ lines.append(f"\n{category}:")
+ for c in clients_list:
+ if not c.get("show", True):
+ continue
+ name = c.get("name", "Unknown")
+ version = c.get("version", "N/A")
+ client_id = c.get("id", "N/A")
+ lines.append(f"{name} (v{version}) - ID: {client_id}")
+ raw_clients_map[str(client_id)] = c
+ raw_clients_map[name.lower()] = c
+ else:
+ lines.append(f"\n{category}: Error {resp.status_code}")
+
+ if lines:
+ result = "\n".join(lines).strip()
+ else:
+ result = get_msg("clients_empty", lang)
+
+ cache[cache_key] = {"data": result, "time": now}
+ cache["clients_raw"] = {"data": raw_clients_map, "time": now}
+ return result
+ except Exception as e:
+ logger.error(f"Error refreshing clients cache: {e}")
+ return cache.get(cache_key, {}).get("data", get_msg("clients_empty", lang))
+
+
+SUBS_FILE = "subscribers.json"
+
+async def get_client_info(query):
+ now = time.time()
+ if not cache["clients_raw"]["data"] or now - cache["clients_raw"]["time"] > 300:
+ await refresh_clients_cache("ru")
+
+ raw_data = cache["clients_raw"]["data"]
+ query = str(query).lower()
+ return raw_data.get(query)
+
+def get_subscribers():
+ if not os.path.exists(SUBS_FILE):
+ return []
+ with open(SUBS_FILE, "r") as f:
+ try:
+ return json.load(f)
+ except:
+ return []
+
+def add_subscriber(user_id):
+ subs = set(get_subscribers())
+ subs.add(user_id)
+ with open(SUBS_FILE, "w") as f:
+ json.dump(list(subs), f)
+
+def remove_subscriber(user_id):
+ subs = set(get_subscribers())
+ subs.discard(user_id)
+ with open(SUBS_FILE, "w") as f:
+ json.dump(list(subs), f)
diff --git a/webapp/app.js b/webapp/app.js
new file mode 100644
index 0000000..ea128bb
--- /dev/null
+++ b/webapp/app.js
@@ -0,0 +1,183 @@
+const tg = window.Telegram.WebApp;
+const isTg = !!tg.initDataUnsafe?.user;
+
+document.addEventListener('DOMContentLoaded', () => {
+ tg.ready();
+ tg.expand();
+ tg.setHeaderColor('secondary_bg_color');
+ tg.setBackgroundColor('bg_color');
+
+ const userNameEl = document.getElementById('user-name');
+ if (isTg && tg.initDataUnsafe.user) {
+ userNameEl.textContent = tg.initDataUnsafe.user.first_name;
+ } else {
+ userNameEl.textContent = 'Preview';
+ }
+
+ document.getElementById('btn-close').addEventListener('click', () => {
+ if (isTg) tg.close();
+ });
+
+ document.getElementById('btn-refresh').addEventListener('click', () => {
+ updateAllData();
+ if (isTg) tg.HapticFeedback.impactOccurred('light');
+ });
+
+ updateAllData();
+});
+
+async function updateAllData() {
+ try {
+ await Promise.all([
+ fetchStatus(),
+ fetchVersions(),
+ fetchClients()
+ ]);
+ if (isTg) tg.HapticFeedback.notificationOccurred('success');
+ } catch (e) {
+ console.error('Update error:', e);
+ if (isTg) tg.HapticFeedback.notificationOccurred('error');
+ }
+}
+
+
+async function fetchStatus() {
+ const content = document.getElementById('status-content');
+ const badge = document.getElementById('main-status-badge');
+ const dot = document.getElementById('main-status-dot');
+ const text = document.getElementById('main-status-text');
+
+ try {
+ const res = await fetch('/api/status');
+ const data = await res.json();
+ const statusStr = data.status || '';
+
+ const lines = statusStr.split('\n').filter(l => l.trim());
+ let allOnline = true;
+ let html = '';
+
+ lines.forEach(line => {
+ const isOnline = line.toLowerCase().includes('online');
+ if (!isOnline) allOnline = false;
+
+ const parts = line.split(':');
+ const name = parts[0]?.trim() || 'Unknown';
+ const statusPart = parts.slice(1).join(':').trim();
+
+
+ const pingMatch = statusPart.match(/\((\d+ms)\)/);
+ const ping = pingMatch ? pingMatch[1] : '';
+
+ html += `
+