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/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..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,
},
});
@@ -202,28 +198,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 +242,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 +257,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 +272,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 +282,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;
}
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 =