From 55e9c086d5f293878ffadbf4f0320786f9f817d6 Mon Sep 17 00:00:00 2001 From: Fyodor Sotnikov Date: Wed, 10 Sep 2025 17:44:52 +0300 Subject: [PATCH 1/2] Simplify bot commands --- src/bot.ts | 41 ++--- src/handlers/callback/clash.ts | 48 ++++++ src/handlers/callback/inbound.ts | 269 ------------------------------- src/handlers/callback/proto.ts | 58 ------- src/handlers/callback/sub.ts | 8 +- src/handlers/clash.ts | 29 ++++ src/handlers/connect.ts | 32 ---- src/handlers/index.ts | 2 +- src/handlers/start.ts | 46 ++++-- src/handlers/stat.ts | 10 +- src/handlers/sub.ts | 18 +-- src/index.ts | 1 - src/keyboards.ts | 39 +---- src/proto/index.ts | 3 - src/proto/vless.ts | 128 --------------- src/proto/vmess.ts | 91 ----------- src/proto/wireguard.ts | 58 ------- src/state.ts | 30 +--- src/types/conn.ts | 5 - src/types/stat.ts | 3 - 20 files changed, 150 insertions(+), 769 deletions(-) create mode 100644 src/handlers/callback/clash.ts delete mode 100644 src/handlers/callback/inbound.ts delete mode 100644 src/handlers/callback/proto.ts create mode 100644 src/handlers/clash.ts delete mode 100644 src/handlers/connect.ts delete mode 100644 src/proto/index.ts delete mode 100644 src/proto/vless.ts delete mode 100644 src/proto/vmess.ts delete mode 100644 src/proto/wireguard.ts diff --git a/src/bot.ts b/src/bot.ts index 7d88756..da65bfc 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,11 +1,15 @@ import * as dotenv from "dotenv"; import express from "express"; import { Telegraf, TelegramError } from "telegraf"; -import { awaitingMnemonic, siteHandler, statHandler } from "./handlers"; -import { handleInboundCallback } from "./handlers/callback/inbound"; -import { handleProtoCallback } from "./handlers/callback/proto"; +import { + awaitingMnemonic, + siteHandler, + statHandler, + subPlainHandler, + subClashHandler, +} from "./handlers"; import { handleSubscriptionCallback } from "./handlers/callback/sub"; -import { connectHandler } from "./handlers/connect"; +import { handleSubscriptionClashCallback } from "./handlers/callback/clash"; import { deleteHandler } from "./handlers/delete"; import { feedbackHandler, @@ -14,7 +18,6 @@ import { } from "./handlers/feedback"; import { scoreHandler } from "./handlers/score"; import { startHandler } from "./handlers/start"; -import { subHandler } from "./handlers/sub"; import { BotState } from "./state"; import { connectWithMnemonic } from "./site"; @@ -31,7 +34,7 @@ const missingVars = requiredEnvVars.filter((varName) => !process.env[varName]); if (missingVars.length > 0) { throw new Error( - `Missing required environment variables: ${missingVars.join(", ")}` + `Missing required environment variables: ${missingVars.join(", ")}`, ); } @@ -51,7 +54,7 @@ const botState = new BotState( API_AUTH_TOKEN, GOOGLE_SCRIPT_URL, TOKEN, - ADMIN_CHAT_ID + ADMIN_CHAT_ID, ); const app = express(); @@ -111,8 +114,8 @@ process.on("SIGTERM", () => gracefulShutdown("SIGTERM")); (async () => { // Command handlers bot.command("start", (ctx) => startHandler(ctx, botState)); - bot.command("connect", (ctx) => connectHandler(ctx, botState)); - bot.command("sub", (ctx) => subHandler(ctx, botState)); + bot.command("connect", (ctx) => subPlainHandler(ctx, botState)); + bot.command("clash", (ctx) => subClashHandler(ctx, botState)); bot.command("stat", (ctx) => statHandler(ctx, botState)); bot.command("site", (ctx) => siteHandler(ctx, botState)); bot.command("delete", (ctx) => deleteHandler(ctx, botState)); @@ -139,7 +142,7 @@ process.on("SIGTERM", () => gracefulShutdown("SIGTERM")); const words = message.trim().split(/\s+/); if (words.length !== 12) { return ctx.reply( - "Фраза должна содержать 12 слов. Попробуйте ещё раз командой /site" + "Фраза должна содержать 12 слов. Попробуйте ещё раз командой /site", ); } @@ -150,7 +153,7 @@ process.on("SIGTERM", () => gracefulShutdown("SIGTERM")); } else { await ctx.reply( `Ваша ссылка: ${data.subscription_url}`, - { parse_mode: "HTML" } + { parse_mode: "HTML" }, ); } } catch (err) { @@ -166,13 +169,13 @@ process.on("SIGTERM", () => gracefulShutdown("SIGTERM")); await ctx.reply( "Неизвестная команда 🫣\n\nДоступные команды:\n" + "• /start - Начать работу\n" + - "• /connect - Подключиться к VPN\n" + - "• /sub - Управление подпиской\n" + + "• /connect - Получить VPN ссылку\n" + + "• /clash - Получить VPN Clash ссылку\n" + "• /status - Проверить статус\n" + "• /stat - Статистика\n" + "• /site - Если оплачивали подписку на сайте\n" + "• /support - Поддержка и обратная связь\n" + - "• /delete - Удалить аккаунт" + "• /delete - Удалить аккаунт", ); } }); @@ -206,12 +209,10 @@ process.on("SIGTERM", () => gracefulShutdown("SIGTERM")); // Route callback data to appropriate handlers if (data.startsWith("feedback_")) { await handleFeedbackCallback(ctx, botState); - } else if (data.startsWith("proto_")) { - await handleProtoCallback(ctx, botState); - } else if (data.startsWith("inbound_")) { - await handleInboundCallback(ctx, botState); - } else if (data.startsWith("sub_")) { + } else if (data.startsWith("plain")) { await handleSubscriptionCallback(ctx, botState); + } else if (data.startsWith("clash")) { + await handleSubscriptionClashCallback(ctx, botState); } else { console.log(`[CALLBACK] Unknown callback data: ${data}`); await ctx.reply("Неизвестная команда."); @@ -250,7 +251,7 @@ process.on("SIGTERM", () => gracefulShutdown("SIGTERM")); ` Domain: ${DOMAIN}\n` + ` Port: ${PORT}\n` + ` Webhook path: ${WEBHOOK_PATH}\n` + - ` Environment: ${process.env.NODE_ENV || "development"}` + ` Environment: ${process.env.NODE_ENV || "development"}`, ); })().catch((err) => { console.error("[STARTUP] Fatal error during startup:", err); diff --git a/src/handlers/callback/clash.ts b/src/handlers/callback/clash.ts new file mode 100644 index 0000000..de20f8d --- /dev/null +++ b/src/handlers/callback/clash.ts @@ -0,0 +1,48 @@ +import QRCode from "qrcode"; +import { Context } from "telegraf"; +import { BotState } from "../../state"; + +export async function handleSubscriptionClashCallback( + ctx: Context, + botState: BotState, +) { + const callbackQuery = ctx.callbackQuery; + + if (!callbackQuery || !("data" in callbackQuery)) { + return await ctx.answerCbQuery(); + } + + const data = callbackQuery.data; + if (!data) { + return await ctx.answerCbQuery(); + } + + const user = ctx.from; + if (!user || !user.username) { + return ctx.answerCbQuery("Не удалось определить пользователя."); + } + + const userEntry = await botState.findUserByTelegramId(user.id); + if (!userEntry || userEntry.is_deleted) { + return ctx.answerCbQuery("Для начала используйте /start"); + } + + const subLink = botState.getSubLink(userEntry.id, "clash"); + + const qrBuffer = await QRCode.toBuffer(subLink, { + errorCorrectionLevel: "H", + type: "png", + margin: 8, + scale: 6, + }); + + await ctx.editMessageText( + `*Нажмите на ссылку, чтобы скопировать* \n\`\`\`\n${subLink}\n\`\`\``, + { parse_mode: "MarkdownV2" }, + ); + + await ctx.replyWithPhoto( + { source: qrBuffer }, + { caption: `QR-код для VPN Clash подписки 🧷` }, + ); +} diff --git a/src/handlers/callback/inbound.ts b/src/handlers/callback/inbound.ts deleted file mode 100644 index 9aa6d13..0000000 --- a/src/handlers/callback/inbound.ts +++ /dev/null @@ -1,269 +0,0 @@ -import QRCode from "qrcode"; -import { Context } from "telegraf"; -import { - getOrCreateVlessGrpcConnection, - getOrCreateVlessXtlsConnection, - getOrCreateVmessConnection, - getOrCreateWireguardConnection, - mapInboundToVless, - mapInboundToVmess, - vlessGrpcConn, - vlessXtlsConn, - vmessTcpConn, - wireguardConn, -} from "../../proto"; -import { BotState } from "../../state"; -import { CreateConnectionRequest } from "../../types"; - -export async function handleInboundCallback(ctx: Context, botState: BotState) { - const callbackQuery = ctx.callbackQuery; - - if (!callbackQuery || !("data" in callbackQuery)) { - return await ctx.answerCbQuery(); - } - - const data = callbackQuery.data; - if (!data) { - return await ctx.answerCbQuery(); - } - - const [_, tag, nodeId] = data.split("_"); - - console.log("NodeId - ", nodeId); - - const user = ctx.from; - if (!user || !user.username) { - return ctx.answerCbQuery("Не удалось определить пользователя."); - } - - const userEntry = await botState.findUserByTelegramId(user.id); - if (!userEntry || userEntry.is_deleted) { - return ctx.answerCbQuery("Для начала используйте /start"); - } - - const connectionData: CreateConnectionRequest = { - env: botState.getEnv(), - trial: false, - limit: botState.getDailyLimitMb(), - proto: tag, - user_id: userEntry.id, - }; - - if (tag === "Wireguard") { - connectionData.node_id = nodeId; - } - - try { - if (userEntry) { - const connections = await botState.getUserConnections(userEntry.id); - - console.log("connections", connections); - - if (tag === "VlessXtls") { - const connection = await getOrCreateVlessXtlsConnection( - connections, - async () => { - const res = - await botState.createConnection(connectionData); - if (res?.status === 200) { - return res.response; - } - throw new Error("Ошибка создания VlessXtls-соединения"); - }, - ); - - const node = await botState.getNode(nodeId); - const inbound = node?.inbounds.VlessXtls; - const conn_id = connection.id; - if (inbound) { - const vlessXtlsInb = mapInboundToVless(inbound); - - if (vlessXtlsInb && node?.address && conn_id) { - const vlessXtlsConfig = vlessXtlsConn( - conn_id, - node?.address, - vlessXtlsInb, - node?.label, - ); - console.log(vlessXtlsConfig); - await ctx.editMessageText( - `Ваша VlessXtls ссылка:\n\`\`\`\n${vlessXtlsConfig}\n\`\`\``, - { parse_mode: "MarkdownV2" }, - ); - const qrBuffer = await QRCode.toBuffer( - vlessXtlsConfig, - { - errorCorrectionLevel: "H", - type: "png", - margin: 4, - scale: 10, - }, - ); - await ctx.replyWithPhoto( - { source: qrBuffer }, - { caption: "QR-код для VlessXtls-ссылки 🧷" }, - ); - } - } - } else if (tag === "VlessGrpc") { - const connection = await getOrCreateVlessGrpcConnection( - connections, - async () => { - const res = - await botState.createConnection(connectionData); - if (res?.status === 200) { - return res.response; - } - throw new Error("Ошибка создания VlessGrpc-соединения"); - }, - ); - - const node = await botState.getNode(nodeId); - const inbound = node?.inbounds.VlessGrpc; - const conn_id = connection.id; - if (inbound && conn_id) { - const vlessGrpcInb = mapInboundToVless(inbound); - - if (vlessGrpcInb && node?.address && conn_id) { - console.log("connection:", connection); - - const vlessGrpcConfig = vlessGrpcConn( - conn_id, - node?.address, - vlessGrpcInb, - node?.label, - ); - console.log(vlessGrpcConfig); - const qrBuffer = await QRCode.toBuffer( - vlessGrpcConfig, - { - errorCorrectionLevel: "H", - type: "png", - margin: 4, - scale: 10, - }, - ); - await ctx.replyWithPhoto( - { source: qrBuffer }, - { caption: "QR-код для VlessGrpc ссылки 🧷" }, - ); - await ctx.editMessageText( - `Ваша VlessGrpc ссылка:\n\`\`\`\n${vlessGrpcConfig}\n\`\`\``, - { parse_mode: "MarkdownV2" }, - ); - } - } - } else if (tag === "Vmess") { - const connection = await getOrCreateVmessConnection( - connections, - async () => { - const res = - await botState.createConnection(connectionData); - if (res?.status === 200) { - return res.response; - } - throw new Error("Ошибка создания Vmess-соединения"); - }, - ); - - const node = await botState.getNode(nodeId); - const inbound = node?.inbounds.Vmess; - const conn_id = connection.id; - if (inbound && conn_id) { - const vmessInb = mapInboundToVmess(inbound); - - if (vmessInb && node?.address && conn_id) { - console.log("connection:", connection); - - const vmessConfig = vmessTcpConn( - conn_id, - node?.address, - vmessInb, - node?.label, - ); - console.log(vmessConfig); - - const qrBuffer = await QRCode.toBuffer(vmessConfig, { - errorCorrectionLevel: "H", - type: "png", - margin: 4, - scale: 10, - }); - await ctx.replyWithPhoto( - { source: qrBuffer }, - { caption: "QR-код для Vmess-ссылки 🧷" }, - ); - - await ctx.editMessageText( - `Ваша Vmess ссылка:\n\`\`\`\n${vmessConfig}\n\`\`\``, - { parse_mode: "MarkdownV2" }, - ); - } - } - } else if (tag === "Wireguard" && nodeId) { - console.log("Create Connection request", connectionData); - const connection = await getOrCreateWireguardConnection( - connections, - nodeId, - async () => { - const res = - await botState.createConnection(connectionData); - if (res?.status === 200) { - return res.response; - } - throw new Error("Error creating Wireguard-connection"); - }, - ); - - const connNodeId = connection.node_id; - if (connNodeId) { - const node = await botState.getNode(connNodeId); - const ipv4 = node?.address; - const pubkey = node?.inbounds.Wireguard.wg?.pubkey; - const port = node?.inbounds.Wireguard.wg?.port; - const dns = node?.inbounds.Wireguard.wg?.dns; - - if (ipv4 && pubkey && port && dns) { - const wgConfig = wireguardConn( - ipv4, - pubkey, - port, - connection, - node?.label, - dns, - ); - - console.log("WG CONFIG\n", wgConfig); - - if (wgConfig) { - await ctx.replyWithDocument({ - source: Buffer.from(wgConfig, "utf-8"), - filename: "wg0.conf", - }); - - const qrBuffer = await QRCode.toBuffer(wgConfig, { - errorCorrectionLevel: "H", - type: "png", - margin: 4, - scale: 10, - }); - - await ctx.replyWithPhoto( - { source: qrBuffer }, - { caption: "QR-код для WireGuard-конфига 🧷" }, - ); - await ctx.editMessageText(`Ваш Wireguard конфиг`, { - parse_mode: "MarkdownV2", - }); - } - } - } - } - } - } catch (err) { - console.error("Callback error:", err); - await ctx.reply( - "Произошла ошибка при создании соединения. Попробуйте позже.", - ); - } -} diff --git a/src/handlers/callback/proto.ts b/src/handlers/callback/proto.ts deleted file mode 100644 index 02470ec..0000000 --- a/src/handlers/callback/proto.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Context } from "telegraf"; -import { BotState } from "../../state"; - -export async function handleProtoCallback(ctx: Context, botState: BotState) { - const callbackQuery = ctx.callbackQuery; - - if (!callbackQuery || !("data" in callbackQuery)) { - return await ctx.answerCbQuery(); - } - - const data = callbackQuery.data; - if (!data) { - return await ctx.answerCbQuery(); - } - const proto = data!.slice("proto_".length); - const env = botState.getEnv(); - - const nodes = (await botState.getNodes(env)) ?? []; - - const inbounds = []; - - for (const node of nodes) { - for (const [tag, inbound] of Object.entries(node.inbounds)) { - if ( - (proto === "xray" && - ["VlessGrpc", "VlessXtls", "Vmess"].includes(tag)) || - (proto.toLowerCase() === "wireguard" && - tag.toLowerCase() === "wireguard") || - tag === proto - ) { - inbounds.push({ - nodeLabel: node.label || node.hostname || "Unknown", - nodeId: node.uuid, - tag, - port: inbound.port, - }); - } - } - } - - if (inbounds.length === 0) { - await ctx.editMessageText( - `Для протокола ${proto} нет доступных конфигураций.`, - ); - return ctx.answerCbQuery(); - } - - const buttons = inbounds.map((inb) => [ - { - text: `${inb.nodeLabel} (${inb.tag}:${inb.nodeId.slice(0, 8)})`, - callback_data: `inbound_${inb.tag}_${inb.nodeId}`, - }, - ]); - - await ctx.editMessageText(`Доступные сервера ${proto}:`, { - reply_markup: { inline_keyboard: buttons }, - }); -} diff --git a/src/handlers/callback/sub.ts b/src/handlers/callback/sub.ts index 7a1294e..bab4e73 100644 --- a/src/handlers/callback/sub.ts +++ b/src/handlers/callback/sub.ts @@ -17,8 +17,6 @@ export async function handleSubscriptionCallback( return await ctx.answerCbQuery(); } - const [_, linkFormat] = data.split("_"); - const user = ctx.from; if (!user || !user.username) { return ctx.answerCbQuery("Не удалось определить пользователя."); @@ -29,7 +27,7 @@ export async function handleSubscriptionCallback( return ctx.answerCbQuery("Для начала используйте /start"); } - const subLink = botState.getSubLink(userEntry.id, linkFormat); + const subLink = botState.getSubLink(userEntry.id, "plain"); const qrBuffer = await QRCode.toBuffer(subLink, { errorCorrectionLevel: "H", @@ -39,12 +37,12 @@ export async function handleSubscriptionCallback( }); await ctx.editMessageText( - `Ваша Subscription ссылка:\\(${linkFormat}\\)\n\`\`\`\n${subLink}\n\`\`\``, + `*Нажмите на ссылку, чтобы скопировать* \n\`\`\`\n${subLink}\n\`\`\``, { parse_mode: "MarkdownV2" }, ); await ctx.replyWithPhoto( { source: qrBuffer }, - { caption: `QR-код для ${linkFormat} подписки 🧷` }, + { caption: `QR-код для VPN подписки 🧷` }, ); } diff --git a/src/handlers/clash.ts b/src/handlers/clash.ts new file mode 100644 index 0000000..85b642c --- /dev/null +++ b/src/handlers/clash.ts @@ -0,0 +1,29 @@ +import { Context } from "telegraf"; +import { InlineKeyboardMarkup } from "telegraf/types"; +import { buildSubClashKeyboard } from "../keyboards"; +import { BotState } from "../state"; + +export const subClashHandler = async (ctx: Context, botState: BotState) => { + const user = ctx.from; + + if (!user) { + return ctx.reply("/start для начала"); + } + + const userEntry = await botState.findUserByTelegramId(user.id); + + if (!userEntry || userEntry.is_deleted) { + return ctx.reply("Для начала /start"); + } + console.log("User: ", userEntry); + const keyboard: InlineKeyboardMarkup = await buildSubClashKeyboard(); + + return ctx.reply( + "▫️ Подходит для: Clash Verge, Clash Meta, Stash \\(iOS\\), ShadowRocket и других" + + " \n\n*Нажмите на кнопку, чтобы получить ссылку*", + { + reply_markup: keyboard, + parse_mode: "MarkdownV2", + }, + ); +}; diff --git a/src/handlers/connect.ts b/src/handlers/connect.ts deleted file mode 100644 index f875d32..0000000 --- a/src/handlers/connect.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Context } from "telegraf"; -import { InlineKeyboardMarkup } from "telegraf/types"; -import { buildProtoKeyboard } from "../keyboards"; -import { BotState } from "../state"; - -export const connectHandler = async (ctx: Context, botState: BotState) => { - const user = ctx.from; - - if (!user) { - return ctx.reply( - "Не удалось определить пользователя. Используйте /start.", - ); - } - - console.log(user); - - let userEntry = await botState.findUserByTelegramId(user.id); - - if (!userEntry && user.username) { - userEntry = await botState.findUserByTelegramUsername(user.username); - } - - if (!userEntry || userEntry.is_deleted) { - return ctx.reply("connectHandler Для начала /start"); - } - - const keyboard: InlineKeyboardMarkup = await buildProtoKeyboard(); - - return ctx.reply("Выбери VPN протокол:", { - reply_markup: keyboard, - }); -}; diff --git a/src/handlers/index.ts b/src/handlers/index.ts index 823825a..78361bd 100644 --- a/src/handlers/index.ts +++ b/src/handlers/index.ts @@ -1,5 +1,5 @@ -export * from "./connect"; export * from "./start"; export * from "./site"; export * from "./stat"; export * from "./sub"; +export * from "./clash"; diff --git a/src/handlers/start.ts b/src/handlers/start.ts index 335045f..6218398 100644 --- a/src/handlers/start.ts +++ b/src/handlers/start.ts @@ -25,26 +25,44 @@ export const startHandler = async (ctx: Context, botState: BotState) => { "💡 [Полный список клиентов](https://github.com/XTLS/Xray-core?tab=readme-ov-file#gui-clients)", "", "⚡ *Быстрый старт:*", - "🔗 /connect \\- Получить VPN\\-ключ", - "📈 /status \\- Проверить статус серверов/нагрузка", - "💎 /sub \\- Подписочная ссылка", - "🌐 /site \\- Если оплачивали подписку на сайте", - "🆘 /support \\- Помощь и поддержка", - "💬 /feedback \\- Оставить отзыв", + + "🔗 /connect \— Ссылка на VPN", + "💎 /clash \ — Clash ссылка", + "🌐 /site \— Если оплачивали подписку на сайте", + "💚 /status \— Cтатус серверов", + "📊 /stat \ — Статистика использования", + "💙 /support \— Помощь и поддержка", + "✨ /feedback \— Оставить отзыв", ].join("\n"); const username = user.username ?? generateUsername(); const result = await botState.registerUser(user.id, username); switch (result.type) { - case "ok": - await ctx.telegram.sendMessage(ctx.chat.id, welcome_msg, { - parse_mode: "MarkdownV2", - ...({ disable_web_page_preview: true } as any), - }); - break; + case "ok": { + const userEntry = await botState.findUserByTelegramId(user.id); + if (userEntry) { + const conn = await botState.createConnection({ + env: botState.getEnv(), + proto: "VlessXtls", + user_id: userEntry.id, + }); + + if (conn) { + console.log( + `Created connection ${conn.response} for user ${userEntry.id}`, + ); + } + + await ctx.telegram.sendMessage(ctx.chat.id, welcome_msg, { + parse_mode: "MarkdownV2", + ...({ disable_web_page_preview: true } as any), + }); + break; + } + } - case "already_exists": + case "already_exists": { const userEntry = await botState.findUserByTelegramId(user.id); if (userEntry?.is_deleted) { await botState.undeleteUser(userEntry.id); @@ -60,7 +78,7 @@ export const startHandler = async (ctx: Context, botState: BotState) => { }); } break; - + } case "error": await ctx.reply("Упс, произошла ошибка. Попробуйте позже."); break; diff --git a/src/handlers/stat.ts b/src/handlers/stat.ts index bbdc318..6cd461d 100644 --- a/src/handlers/stat.ts +++ b/src/handlers/stat.ts @@ -28,13 +28,8 @@ function formatTrafficStats(stats: UserStat[]): string { const parts = stats.map((s) => { const lines = [`🔹 ${s.type}`, ` id: ${s.id}`]; - if (s.trial) { - lines.push(` limit: ${s.limit} MB`); - } - lines.push( ``, - ` Status: ${s.status}`, ``, ` • ↑ Upload: ${Math.round(s.stat.uplink / (1024 * 1024))} MB`, ` • ↓ Download: ${Math.round(s.stat.downlink / (1024 * 1024))} MB`, @@ -49,14 +44,11 @@ function formatTrafficStats(stats: UserStat[]): string { const totalDownload = Math.round( stats.reduce((sum, s) => sum + s.stat.downlink, 0) / (1024 * 1024), ); - const overallStatus = stats.some((s) => s.status === "Active") - ? "Active" - : "Expired"; + return [ `Статистика трафика за сегодня:\n`, parts.join("\n\n"), `\n🔻 Суммарно:`, - ` Status: ${overallStatus}`, ``, ` ↑ Upload: ${totalUpload} MB`, ` ↓ Download: ${totalDownload} MB`, diff --git a/src/handlers/sub.ts b/src/handlers/sub.ts index a63eb77..5db2f1c 100644 --- a/src/handlers/sub.ts +++ b/src/handlers/sub.ts @@ -1,9 +1,9 @@ import { Context } from "telegraf"; import { InlineKeyboardMarkup } from "telegraf/types"; -import { buildSubKeyboard } from "../keyboards"; +import { buildSubPlainKeyboard } from "../keyboards"; import { BotState } from "../state"; -export const subHandler = async (ctx: Context, botState: BotState) => { +export const subPlainHandler = async (ctx: Context, botState: BotState) => { const user = ctx.from; if (!user) { @@ -15,20 +15,12 @@ export const subHandler = async (ctx: Context, botState: BotState) => { if (!userEntry || userEntry.is_deleted) { return ctx.reply("Для начала /start"); } - console.log("User: ", userEntry); - - const keyboard: InlineKeyboardMarkup = await buildSubKeyboard(); + const keyboard: InlineKeyboardMarkup = await buildSubPlainKeyboard(); return ctx.reply( - "📋 *Выберите тип подписки:*\n\n" + - "📄 *Plain* \\- Обычная ссылка \\(рекомендуется\\)\n" + - "▫️ Подходит для: v2rayN \\(Windows\\), Shadowrocket, Streisand, FoxRay \\(iOS, MacOS\\), Nekoray, Hiddify и других\n\n" + - "⚙️ *Clash* \\- Конфигурационный файл\n" + - "▫️ Подходит для: Clash Verge, Clash Meta, Stash \\(iOS\\), Hiddify и других\n\n" + - "💡 Если сомневаетесь \\- выбирайте *Plain*\n\n" + - "⚠️ *Важно:* чтобы в подписке появились сервера, сначала выберите нужные в разделе /connect \\!\\!\\!" + - " \n\n Для проверки статуса серверов команда /status ", + "▫️ Подходит для: Streisand, Hiddify, FoxRay и других" + + " \n\n*Нажмите на кнопку, чтобы получить ссылку*", { reply_markup: keyboard, parse_mode: "MarkdownV2", diff --git a/src/index.ts b/src/index.ts index 1526598..fb6a0ee 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,3 @@ export * from "./handlers"; -export * from "./proto"; export * from "./state"; export * from "./types"; diff --git a/src/keyboards.ts b/src/keyboards.ts index 5efc080..6496ee5 100644 --- a/src/keyboards.ts +++ b/src/keyboards.ts @@ -1,45 +1,14 @@ import { InlineKeyboardButton } from "telegraf/typings/core/types/typegram"; import { ConnectionResponse } from "./types/conn"; -export function generateConnectionKeyboard(connections: ConnectionResponse[]) { - const keyboard: InlineKeyboardButton[][] = connections.map((conn) => { - const protoName = "Wireguard" in conn.proto ? "WG" : "XRay"; - const label = `${protoName} — ${conn.env}`; - - return [ - { - text: label, - callback_data: `conn:${conn.id}`, - }, - ]; - }); - - return { - reply_markup: { - inline_keyboard: keyboard, - }, - }; -} - -export async function buildProtoKeyboard(): Promise { +export async function buildSubPlainKeyboard(): Promise { return { - inline_keyboard: [ - [ - { text: "Xray", callback_data: "proto_xray" }, - { text: "Wireguard", callback_data: "proto_wireguard" }, - // { text: "Shadowsocks", callback_data: "proto_ss" }, - ], - ], + inline_keyboard: [[{ text: "Plain", callback_data: "plain" }]], }; } -export async function buildSubKeyboard(): Promise { +export async function buildSubClashKeyboard(): Promise { return { - inline_keyboard: [ - [ - { text: "Clash", callback_data: "sub_clash" }, - { text: "Plain", callback_data: "sub_plain" }, - ], - ], + inline_keyboard: [[{ text: "Clash", callback_data: "clash" }]], }; } diff --git a/src/proto/index.ts b/src/proto/index.ts deleted file mode 100644 index 13fd63c..0000000 --- a/src/proto/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./vless"; -export * from "./vmess"; -export * from "./wireguard"; diff --git a/src/proto/vless.ts b/src/proto/vless.ts deleted file mode 100644 index 59d5ebe..0000000 --- a/src/proto/vless.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { ConnectionResponse } from "../types/conn"; -import { Inbound, ProtoResponse } from "../types/node"; -import { isXrayResponse } from "../utils"; - -export function vlessXtlsConn( - connId: string, - ipv4: string, - vlessProto: ProtoResponse["Vless"], - label: string, -): string { - if (!vlessProto) { - throw new Error("vlessProto is undefined"); - } - - const port = vlessProto.port; - const streamSettings = vlessProto.stream_settings; - if (!streamSettings?.realitySettings) { - throw new Error("VLESS XTLS: reality settings error"); - } - - const realitySettings = streamSettings.realitySettings; - const pbk = realitySettings.publicKey; - const sid = realitySettings.shortIds[0]; - if (!sid) throw new Error("VLESS XTLS: reality settings SID error"); - const sni = realitySettings.serverNames[0]; - if (!sni) throw new Error("VLESS XTLS: reality settings SNI error"); - - return `vless://${connId}@${ipv4}:${port}?security=reality&flow=xtls-rprx-vision&type=tcp&sni=${sni}&fp=chrome&pbk=${pbk}&sid=${sid}#${label} XTLS`; -} - -export function vlessGrpcConn( - connId: string, - ip: string, - vlessProto: ProtoResponse["Vless"], - label: string, -): string { - if (!vlessProto) { - throw new Error("vlessProto is undefined"); - } - const port = vlessProto.port; - const stream = vlessProto.stream_settings; - - if (!stream) { - throw new Error("VLESS GRPC: stream settings error"); - } - - const reality = stream.realitySettings; - if (!reality) { - throw new Error("VLESS GRPC: reality settings error"); - } - - const grpc = stream.grpcSettings; - if (!grpc) { - throw new Error("VLESS GRPC: grpc settings error"); - } - - const serviceName = grpc.serviceName; - const pbk = reality.publicKey; - const sid = reality.shortIds?.find((id) => id && id.length > 0); - const sni = reality.serverNames?.[0]; - - if (!sid) throw new Error("VLESS GRPC: missing short_id"); - if (!sni) throw new Error("VLESS GRPC: missing SNI"); - - return `vless://${connId}@${ip}:${port}?security=reality&type=grpc&mode=gun&serviceName=${serviceName}&fp=chrome&sni=${sni}&pbk=${pbk}&sid=${sid}#${label} GRPC`; -} - -export async function getOrCreateVlessXtlsConnection( - response: ConnectionResponse[] | null | undefined, - createConnection: () => Promise, -): Promise { - if (!response || response.length === 0) { - return await createConnection(); - } - - const vlessXtlsConnections = response.filter((conn) => { - if (isXrayResponse(conn.proto)) { - return conn.proto.Xray === "VlessXtls"; - } - return false; - }); - - if (vlessXtlsConnections.length > 0) { - return vlessXtlsConnections[0]; - } - - return await createConnection(); -} - -export async function getOrCreateVlessGrpcConnection( - response: ConnectionResponse[] | null | undefined, - createConnection: () => Promise, -): Promise { - if (!response || response.length === 0) { - return await createConnection(); - } - - const vlessGrpcConnections = response.filter((conn) => { - if (isXrayResponse(conn.proto)) { - return conn.proto.Xray === "VlessGrpc"; - } - return false; - }); - - if (vlessGrpcConnections.length > 0) { - console.log("VLESS GRPC conn", vlessGrpcConnections[0]); - return vlessGrpcConnections[0]; - } - - return await createConnection(); -} - -export function mapInboundToVless(inbound: Inbound): ProtoResponse["Vless"] { - return { - tag: inbound.tag, - port: inbound.port, - stream_settings: inbound.stream_settings - ? { - tcpSettings: - inbound.stream_settings.tcpSettings ?? undefined, - realitySettings: - inbound.stream_settings.realitySettings ?? undefined, - grpcSettings: - inbound.stream_settings.grpcSettings ?? undefined, - } - : undefined, - }; -} diff --git a/src/proto/vmess.ts b/src/proto/vmess.ts deleted file mode 100644 index f3f2f40..0000000 --- a/src/proto/vmess.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { ConnectionResponse } from "../types/conn"; -import { Inbound, ProtoResponse } from "../types/node"; -import { isXrayResponse } from "../utils"; - -export function vmessTcpConn( - connId: string, - ipv4: string, - inbound: ProtoResponse["Vmess"], - label: string, -): string { - if (!inbound) { - throw new Error("vlessProto is undefined"); - } - if (!inbound.stream_settings) - throw new Error("VMESS: missing stream_settings"); - const tcpSettings = inbound.stream_settings.tcpSettings; - if (!tcpSettings) throw new Error("VMESS: missing tcpSettings"); - const header = tcpSettings.header; - if (!header) throw new Error("VMESS: missing header"); - const req = header.request; - if (!req) throw new Error("VMESS: missing request"); - const headers = req.headers; - if (!headers) throw new Error("VMESS: missing headers"); - - const hostArray = headers["Host"]; - if (!hostArray || hostArray.length === 0) - throw new Error("VMESS: missing Host header"); - const host = hostArray[0]; - - const path = req.path?.[0]; - if (!path) throw new Error("VMESS: missing path"); - - const port = inbound.port; - - const conn: Record = { - add: ipv4, - aid: "0", - host, - id: connId, - net: "tcp", - path, - port: port.toString(), - ps: `Vmess ${label}`, - scy: "auto", - tls: "none", - type: "http", - v: "2", - }; - - const jsonStr = JSON.stringify(conn); - const base64Str = Buffer.from(jsonStr).toString("base64"); - - return `vmess://${base64Str}#${label} ____`; -} - -export async function getOrCreateVmessConnection( - response: ConnectionResponse[] | null | undefined, - createConnection: () => Promise, -): Promise { - if (!response || response.length === 0) { - return await createConnection(); - } - - const vmessConnection = response.find( - (conn) => isXrayResponse(conn.proto) && conn.proto.Xray === "Vmess", - ); - - if (vmessConnection) { - console.log("VMESS", vmessConnection); - return vmessConnection; - } - - return await createConnection(); -} - -export function mapInboundToVmess(inbound: Inbound): ProtoResponse["Vmess"] { - return { - tag: inbound.tag, - port: inbound.port, - stream_settings: inbound.stream_settings - ? { - tcpSettings: - inbound.stream_settings.tcpSettings ?? undefined, - realitySettings: - inbound.stream_settings.realitySettings ?? undefined, - grpcSettings: - inbound.stream_settings.grpcSettings ?? undefined, - } - : undefined, - }; -} diff --git a/src/proto/wireguard.ts b/src/proto/wireguard.ts deleted file mode 100644 index 520858e..0000000 --- a/src/proto/wireguard.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { ConnectionResponse } from "../types/conn"; - -export function wireguardConn( - host: string, - pubkey: string, - port: number, - connection: ConnectionResponse, - label: string, - dns: string[], -): string | null { - if (!("Wireguard" in connection.proto)) { - console.error("Wrong proto", connection.proto); - return null; - } - - const connId = connection.id; - const wg = connection.proto.Wireguard; - const privkey = wg.param.keys.privkey; - const clientIp = `${wg.param.address.addr}/${wg.param.address.cidr}`; - - const config = `[Interface] -PrivateKey = ${privkey} -Address = ${clientIp} -DNS = ${dns} - -[Peer] -PublicKey = ${pubkey} -Endpoint = ${host}:${port} -AllowedIPs = 0.0.0.0/0, ::/0 -PersistentKeepalive = 25 - -# ${label} — conn_id: ${connId} -`; - - return config; -} - -export async function getOrCreateWireguardConnection( - response: ConnectionResponse[] | null | undefined, - nodeId: string, - createConnection: () => Promise, -): Promise { - if (!response || response.length === 0) { - return await createConnection(); - } - - const connection = response.find( - (conn) => "Wireguard" in conn.proto && conn.node_id === nodeId, - ); - - console.log("getOrCreateWireguardConnection: connection", connection); - - if (connection) { - return connection; - } - - return await createConnection(); -} diff --git a/src/state.ts b/src/state.ts index fa7ad10..5d1fa69 100644 --- a/src/state.ts +++ b/src/state.ts @@ -202,28 +202,13 @@ export class BotState { } } - async getUsers(): Promise { - try { - const prismaUsers = await this.prisma.user.findMany({ - orderBy: { createdAt: "asc" }, - }); - - return prismaUsers.map((prismaUser) => - this.mapPrismaUserToUser(prismaUser), - ); - } catch (error) { - console.error("Error fetching users from database:", error); - return []; - } - } - async getUserConnections(userId: string): Promise { try { const res = await this.api.get<{ status: number; message: string; response: ConnectionsResponseEntryRaw[]; - }>(`/user/connections?user_id=${userId}`); + }>(`/user/connections?id=${userId}`); if (res.data.status !== 200) return []; @@ -261,7 +246,7 @@ export class BotState { status: number; message: string; response: NodeResponse; - }>(`/node?node_id=${id}`); + }>(`/node?id=${id}`); return res.data.status === 200 ? res.data.response : null; } catch (e) { @@ -276,7 +261,7 @@ export class BotState { status: number; message: string; response: NodeScore | null; - }>(`/node/score?node_id=${uuid}`); + }>(`/node/score?id=${uuid}`); if (res.data.status === 200 && res.data.response) { return res.data.response; @@ -291,7 +276,7 @@ export class BotState { } getSubLink(userId: string, format: string): string { - const link = `${this.apiBaseUrl}/sub?user_id=${userId}&format=${format}`; + const link = `${this.apiBaseUrl}/sub?id=${userId}&format=${format}`; return link; } @@ -301,20 +286,17 @@ export class BotState { status: number; message: string; response: UserStatRaw[]; - }>(`/user/stat?user_id=${userId}`); + }>(`/user/stat?id=${userId}`); if (res.data.status !== 200 || !res.data.response) { return null; } const userStats: UserStat[] = res.data.response.map( - ([id, stat, type, status, limit, trial]) => ({ + ([id, stat, type]) => ({ id, stat, type, - status, - limit, - trial, }), ); diff --git a/src/types/conn.ts b/src/types/conn.ts index 80c1df6..34abfa6 100644 --- a/src/types/conn.ts +++ b/src/types/conn.ts @@ -1,7 +1,5 @@ export type CreateConnectionRequest = { env: string; - trial: boolean; - limit: number; proto: string; user_id: string; node_id?: string; @@ -29,11 +27,8 @@ export type XrayResponse = { export type ConnectionResponse = { id: string; - trial: boolean; - limit: number; env: string; proto: WireguardResponse | XrayResponse; - status: string; stat: { downlink: number; uplink: number; diff --git a/src/types/stat.ts b/src/types/stat.ts index c8f6427..c81544f 100644 --- a/src/types/stat.ts +++ b/src/types/stat.ts @@ -20,7 +20,4 @@ export interface UserStat { id: string; stat: TrafficStat; type: ProtoTag; - status: Status; - limit: number; - trial: boolean; } From 674ce0037afca95da14fe30e4b4ca972f7dddc63 Mon Sep 17 00:00:00 2001 From: Fyodor Sotnikov Date: Wed, 10 Sep 2025 19:25:54 +0300 Subject: [PATCH 2/2] Addin trial --- .../20250910151011_add_trial_fields/migration.sql | 10 ++++++++++ .../20250910151124_add_trial_fields/migration.sql | 2 ++ prisma/schema.prisma | 15 --------------- schema.prisma | 3 ++- src/state.ts | 8 ++------ src/types/user.ts | 2 ++ 6 files changed, 18 insertions(+), 22 deletions(-) create mode 100644 migrations/20250910151011_add_trial_fields/migration.sql create mode 100644 migrations/20250910151124_add_trial_fields/migration.sql delete mode 100644 prisma/schema.prisma diff --git a/migrations/20250910151011_add_trial_fields/migration.sql b/migrations/20250910151011_add_trial_fields/migration.sql new file mode 100644 index 0000000..5052078 --- /dev/null +++ b/migrations/20250910151011_add_trial_fields/migration.sql @@ -0,0 +1,10 @@ +/* + Warnings: + + - You are about to drop the column `daily_limit_mb` on the `users` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "public"."users" DROP COLUMN "daily_limit_mb", +ADD COLUMN "expired_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "is_trial" BOOLEAN NOT NULL DEFAULT false; diff --git a/migrations/20250910151124_add_trial_fields/migration.sql b/migrations/20250910151124_add_trial_fields/migration.sql new file mode 100644 index 0000000..db124db --- /dev/null +++ b/migrations/20250910151124_add_trial_fields/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "public"."users" ALTER COLUMN "expired_at" SET DEFAULT (now() + '3 days'::interval); diff --git a/prisma/schema.prisma b/prisma/schema.prisma deleted file mode 100644 index aaeee1b..0000000 --- a/prisma/schema.prisma +++ /dev/null @@ -1,15 +0,0 @@ -// This is your Prisma schema file, -// learn more about it in the docs: https://pris.ly/d/prisma-schema - -// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? -// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init - -generator client { - provider = "prisma-client-js" - output = "../src/generated/prisma" -} - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} diff --git a/schema.prisma b/schema.prisma index 9c1586f..2bbbf2b 100644 --- a/schema.prisma +++ b/schema.prisma @@ -16,10 +16,11 @@ model User { username String @unique password String env String @default("tg") - dailyLimitMb Int @default(1024) @map("daily_limit_mb") isDeleted Boolean @default(false) @map("is_deleted") createdAt DateTime @default(now()) @map("created_at") modifiedAt DateTime @updatedAt @map("modified_at") + expiredAt DateTime @default(dbgenerated("(now() + '3 days'::interval)")) @map("expired_at") + isTrial Boolean @default(false) @map("is_trial") @@index([telegramId]) @@index([username]) diff --git a/src/state.ts b/src/state.ts index 5d1fa69..316ec1f 100644 --- a/src/state.ts +++ b/src/state.ts @@ -14,7 +14,6 @@ import { generatePassword } from "./utils"; export class BotState { private prisma: PrismaClient; - private dailyLimitMb = parseInt(process.env.DAILY_LIMIT_MB || "1024"); private env = process.env.ENV || "tg"; private apiBaseUrl: string; @@ -62,10 +61,6 @@ export class BotState { return this.env; } - getDailyLimitMb(): number { - return this.dailyLimitMb; - } - getGoogleScriptUrl(): string { return this.googleScriptUrl; } @@ -86,6 +81,8 @@ export class BotState { : undefined, username: prismaUser.username, is_deleted: prismaUser.isDeleted, + trial: prismaUser.isTrial, + expired_at: prismaUser.expiredAt, }; } @@ -186,7 +183,6 @@ export class BotState { username: username, password: password, env: this.getEnv(), - dailyLimitMb: this.dailyLimitMb, }, }); diff --git a/src/types/user.ts b/src/types/user.ts index 48209b7..a821c71 100644 --- a/src/types/user.ts +++ b/src/types/user.ts @@ -3,6 +3,8 @@ export interface User { username: string; id: string; is_deleted: boolean; + trial: boolean; + expired_at: Date | null; } export type RegisterResult =