From c1716615e24d18dcc0d4a8a5e91b531ac6ae4a4d Mon Sep 17 00:00:00 2001 From: mtvpls Date: Mon, 29 Jun 2026 22:41:36 +0800 Subject: [PATCH] =?UTF-8?q?tg=E6=B3=A8=E5=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/admin/page.tsx | 10 ++- src/app/api/admin/telegram/route.ts | 1 + src/app/api/telegram/config/route.ts | 4 +- src/lib/admin.types.ts | 1 + src/lib/config.ts | 6 ++ src/lib/telegram.ts | 130 +++++++++++++++++++++++++-- 6 files changed, 145 insertions(+), 7 deletions(-) diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index d024ca4..a098cfd 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -13959,6 +13959,7 @@ const TelegramConfigComponent = ({ const [apiBaseUrl, setApiBaseUrl] = useState(''); const [loginEnabled, setLoginEnabled] = useState(true); const [bindingEnabled, setBindingEnabled] = useState(true); + const [registrationEnabled, setRegistrationEnabled] = useState(false); const [notificationsEnabled, setNotificationsEnabled] = useState(true); const [defaultNotifications, setDefaultNotifications] = useState(true); const [testChatId, setTestChatId] = useState(''); @@ -13974,6 +13975,7 @@ const TelegramConfigComponent = ({ setApiBaseUrl(telegram.apiBaseUrl || ''); setLoginEnabled(telegram.loginEnabled !== false); setBindingEnabled(telegram.bindingEnabled !== false); + setRegistrationEnabled(telegram.registrationEnabled === true); setNotificationsEnabled(telegram.notificationsEnabled !== false); setDefaultNotifications(telegram.defaultNotifications !== false); } @@ -13988,6 +13990,7 @@ const TelegramConfigComponent = ({ apiBaseUrl, loginEnabled, bindingEnabled, + registrationEnabled, notificationsEnabled, defaultNotifications, }); @@ -14014,7 +14017,10 @@ const TelegramConfigComponent = ({ const handleSetWebhook = async () => { await withLoading('setTelegramWebhook', async () => { try { - if (!enabled || !botToken.trim() || !botUsername.trim() || !webhookSecret.trim()) { + if (!enabled) { + throw new Error('请先开启 Telegram Bot'); + } + if (!botToken.trim() || !botUsername.trim() || !webhookSecret.trim()) { throw new Error('请先填写 Bot Token、Bot 用户名 和 Webhook Secret'); } @@ -14081,6 +14087,7 @@ const TelegramConfigComponent = ({

• 支持用户绑定 Telegram、快捷确认登录和站内通知推送

+

• 开启 Telegram 注册后,用户可在 Bot 中发送 /register 用户名 密码 注册账号

• Webhook 地址需在 Telegram Bot API 中手动设置

• Bot Token 和 Webhook Secret 仅服务端保存,不会暴露给前端

@@ -14136,6 +14143,7 @@ const TelegramConfigComponent = ({
{[ ['允许绑定', bindingEnabled, setBindingEnabled], + ['允许 Telegram 注册', registrationEnabled, setRegistrationEnabled], ['允许 Telegram 登录', loginEnabled, setLoginEnabled], ['启用 Telegram 通知', notificationsEnabled, setNotificationsEnabled], ['新绑定默认开启通知', defaultNotifications, setDefaultNotifications], diff --git a/src/app/api/admin/telegram/route.ts b/src/app/api/admin/telegram/route.ts index 9d45f72..b70a806 100644 --- a/src/app/api/admin/telegram/route.ts +++ b/src/app/api/admin/telegram/route.ts @@ -32,6 +32,7 @@ function maskTelegramConfig(config: AdminConfig['TelegramConfig']) { apiBaseUrl: config?.apiBaseUrl || '', loginEnabled: config?.loginEnabled !== false, bindingEnabled: config?.bindingEnabled !== false, + registrationEnabled: config?.registrationEnabled === true, notificationsEnabled: config?.notificationsEnabled !== false, defaultNotifications: config?.defaultNotifications !== false, }; diff --git a/src/app/api/telegram/config/route.ts b/src/app/api/telegram/config/route.ts index 4aaf499..583b521 100644 --- a/src/app/api/telegram/config/route.ts +++ b/src/app/api/telegram/config/route.ts @@ -8,12 +8,14 @@ export async function GET() { const config = await getTelegramConfig(); const loginProblems = getTelegramConfigProblems(config, 'login'); const bindingProblems = getTelegramConfigProblems(config, 'binding'); + const registrationProblems = getTelegramConfigProblems(config, 'registration'); return NextResponse.json({ enabled: config.enabled && Boolean(config.botToken), botUsername: config.botUsername.replace(/^@/, ''), loginEnabled: loginProblems.length === 0, bindingEnabled: bindingProblems.length === 0, + registrationEnabled: registrationProblems.length === 0, notificationsEnabled: config.notificationsEnabled, - problems: Array.from(new Set([...loginProblems, ...bindingProblems])), + problems: Array.from(new Set([...loginProblems, ...bindingProblems, ...registrationProblems])), }); } diff --git a/src/lib/admin.types.ts b/src/lib/admin.types.ts index 170c9ad..547b671 100644 --- a/src/lib/admin.types.ts +++ b/src/lib/admin.types.ts @@ -351,6 +351,7 @@ export interface AdminConfig { apiBaseUrl?: string; // Telegram Bot API 反代 Base URL loginEnabled?: boolean; // 是否启用 Telegram 登录 bindingEnabled?: boolean; // 是否启用用户绑定 + registrationEnabled?: boolean; // 是否启用 Telegram 注册 notificationsEnabled?: boolean; // 是否启用 Telegram 通知 defaultNotifications?: boolean; // 新绑定用户默认开启通知 }; diff --git a/src/lib/config.ts b/src/lib/config.ts index 1a292c0..f28e767 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -340,6 +340,7 @@ async function getInitConfig( apiBaseUrl: process.env.TELEGRAM_API_BASE_URL || '', loginEnabled: process.env.TELEGRAM_LOGIN_ENABLED !== 'false', bindingEnabled: process.env.TELEGRAM_BINDING_ENABLED !== 'false', + registrationEnabled: process.env.TELEGRAM_REGISTRATION_ENABLED === 'true', notificationsEnabled: process.env.TELEGRAM_NOTIFICATIONS_ENABLED !== 'false', defaultNotifications: process.env.TELEGRAM_DEFAULT_NOTIFICATIONS !== 'false', }, @@ -588,10 +589,15 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig { apiBaseUrl: process.env.TELEGRAM_API_BASE_URL || '', loginEnabled: process.env.TELEGRAM_LOGIN_ENABLED !== 'false', bindingEnabled: process.env.TELEGRAM_BINDING_ENABLED !== 'false', + registrationEnabled: process.env.TELEGRAM_REGISTRATION_ENABLED === 'true', notificationsEnabled: process.env.TELEGRAM_NOTIFICATIONS_ENABLED !== 'false', defaultNotifications: process.env.TELEGRAM_DEFAULT_NOTIFICATIONS !== 'false', }; } + if (adminConfig.TelegramConfig.registrationEnabled === undefined) { + adminConfig.TelegramConfig.registrationEnabled = + process.env.TELEGRAM_REGISTRATION_ENABLED === 'true'; + } if (adminConfig.SiteConfig.PansouKeywordBlocklist === undefined) { adminConfig.SiteConfig.PansouKeywordBlocklist = ''; } diff --git a/src/lib/telegram.ts b/src/lib/telegram.ts index 225b02a..5bed390 100644 --- a/src/lib/telegram.ts +++ b/src/lib/telegram.ts @@ -1,13 +1,14 @@ /* eslint-disable no-console,@typescript-eslint/no-explicit-any */ -import type { NextRequest } from 'next/server'; - import { HttpsProxyAgent } from 'https-proxy-agent'; +import type { NextRequest } from 'next/server'; import nodeFetch from 'node-fetch'; import type { AdminConfig } from './admin.types'; import { generateAuthCookieValue } from './auth-cookie'; +import { getConfig } from './config'; import { db, getStorage } from './db'; +import { lockManager } from './lock'; import type { IStorage, Notification } from './types'; import { getNotificationClickUrl } from './web-push'; @@ -20,6 +21,7 @@ export interface TelegramConfig { apiBaseUrl: string; loginEnabled: boolean; bindingEnabled: boolean; + registrationEnabled: boolean; notificationsEnabled: boolean; defaultNotifications: boolean; } @@ -107,6 +109,7 @@ function readEnvTelegramConfig(): TelegramConfig { apiBaseUrl: process.env.TELEGRAM_API_BASE_URL || '', loginEnabled: process.env.TELEGRAM_LOGIN_ENABLED !== 'false', bindingEnabled: process.env.TELEGRAM_BINDING_ENABLED !== 'false', + registrationEnabled: process.env.TELEGRAM_REGISTRATION_ENABLED === 'true', notificationsEnabled: process.env.TELEGRAM_NOTIFICATIONS_ENABLED !== 'false', defaultNotifications: process.env.TELEGRAM_DEFAULT_NOTIFICATIONS !== 'false', }; @@ -125,6 +128,7 @@ function mergeAdminTelegramConfig(base: TelegramConfig, admin?: AdminConfig | nu apiBaseUrl: cfg.apiBaseUrl || base.apiBaseUrl, loginEnabled: cfg.loginEnabled ?? base.loginEnabled, bindingEnabled: cfg.bindingEnabled ?? base.bindingEnabled, + registrationEnabled: cfg.registrationEnabled ?? base.registrationEnabled, notificationsEnabled: cfg.notificationsEnabled ?? base.notificationsEnabled, defaultNotifications: cfg.defaultNotifications ?? base.defaultNotifications, }; @@ -231,6 +235,89 @@ export async function unbindTelegramUser(telegramUserId: string): Promise { + const config = await getTelegramConfig(); + if (!config.registrationEnabled) { + throw new Error('Telegram 注册未开启,请联系管理员在后台开启。'); + } + + const storageType = + (process.env.NEXT_PUBLIC_STORAGE_TYPE as + | 'localstorage' + | 'redis' + | 'upstash' + | 'kvrocks' + | undefined) || 'localstorage'; + if (storageType === 'localstorage') { + throw new Error('localStorage 模式不支持注册功能'); + } + + const username = input.username.trim(); + const password = input.password; + + if (!/^[a-zA-Z0-9_]{3,20}$/.test(username)) { + throw new Error('用户名只能包含字母、数字、下划线,长度3-20位'); + } + if (password.length < 6) { + throw new Error('密码长度至少为6位'); + } + if (username === process.env.USERNAME) { + throw new Error('该用户名不可用'); + } + + const existingBinding = await getTelegramBindingByTelegramUser(input.telegramUserId); + if (existingBinding) { + throw new Error(`当前 Telegram 已绑定账号:${existingBinding.username}`); + } + + let releaseLock: (() => void) | null = null; + try { + releaseLock = await lockManager.acquire(`register:${username}`); + } catch { + throw new Error('服务器繁忙,请稍后重试'); + } + + try { + const userExists = await db.checkUserExistV2(username); + if (userExists) { + throw new Error('用户名已存在'); + } + + const siteConfig = (await getConfig()).SiteConfig; + const defaultTags = + siteConfig.DefaultUserTags && siteConfig.DefaultUserTags.length > 0 + ? siteConfig.DefaultUserTags + : undefined; + + await db.createUserV2(username, password, 'user', defaultTags); + + const binding: TelegramBinding = { + username, + telegramUserId: input.telegramUserId, + chatId: input.chatId, + telegramUsername: input.telegramUsername, + firstName: input.firstName, + lastName: input.lastName, + notificationsEnabled: config.defaultNotifications, + boundAt: now(), + updatedAt: now(), + }; + + await db.upsertTelegramBinding(binding); + return binding; + } finally { + releaseLock?.(); + } +} + export async function createTelegramLoginSession(): Promise { const session: TelegramLoginSession = { token: randomToken(), @@ -486,7 +573,7 @@ export function getTelegramDeepLink(botUsername: string, payload: string) { export function getTelegramConfigProblems( config: TelegramConfig, - feature?: 'login' | 'binding' | 'notifications' + feature?: 'login' | 'binding' | 'registration' | 'notifications' ): string[] { const problems: string[] = []; if (!config.enabled) problems.push('总开关未开启'); @@ -494,6 +581,7 @@ export function getTelegramConfigProblems( if ((feature === 'login' || feature === 'binding') && !config.botUsername) problems.push('Bot 用户名为空'); if (feature === 'login' && !config.loginEnabled) problems.push('Telegram 登录开关未开启'); if (feature === 'binding' && !config.bindingEnabled) problems.push('Telegram 绑定开关未开启'); + if (feature === 'registration' && !config.registrationEnabled) problems.push('Telegram 注册开关未开启'); if (feature === 'notifications' && !config.notificationsEnabled) problems.push('Telegram 通知开关未开启'); return problems; } @@ -511,11 +599,23 @@ function parseMessageText(update: any) { }; } +function buildTelegramHelpText(config: TelegramConfig) { + return [ + 'MoonTVPlus Telegram Bot 已连接。', + '', + '可用命令:', + config.registrationEnabled ? '/register 用户名 密码 - 注册并绑定账号' : '', + '/bind 绑定码 - 绑定账号', + '/status - 查看状态', + '/unbind - 解除绑定', + ].filter(Boolean).join('\n'); +} + export async function handleTelegramWebhookUpdate(update: any): Promise { const parsed = parseMessageText(update); if (parsed) { if (/^\/start$/i.test(parsed.text)) { - await sendTelegramMessage(parsed.chatId, 'MoonTVPlus Telegram Bot 已连接。\n\n可用命令:\n/bind 绑定码 - 绑定账号\n/status - 查看状态\n/unbind - 解除绑定'); + await sendTelegramMessage(parsed.chatId, buildTelegramHelpText(await getTelegramConfig())); return; } @@ -545,6 +645,26 @@ export async function handleTelegramWebhookUpdate(update: any): Promise { return; } + if (/^\/register$/i.test(parsed.text)) { + await sendTelegramMessage(parsed.chatId, '请发送:\n/register 用户名 密码\n\n用户名只能包含字母、数字、下划线,长度3-20位;密码至少6位。'); + return; + } + + const registerMatch = parsed.text.match(/^\/register\s+(\S+)\s+(\S+)$/i); + if (registerMatch) { + try { + const binding = await registerTelegramUser({ + ...parsed, + username: registerMatch[1], + password: registerMatch[2], + }); + await sendTelegramMessage(parsed.chatId, `注册成功:${binding.username}\n当前 Telegram 已自动绑定该账号。`); + } catch (error) { + await sendTelegramMessage(parsed.chatId, error instanceof Error ? error.message : '注册失败'); + } + return; + } + if (/^\/unbind$/i.test(parsed.text)) { const ok = await unbindTelegramUser(parsed.telegramUserId); await sendTelegramMessage(parsed.chatId, ok ? '已解除 Telegram 绑定。' : '当前 Telegram 账号尚未绑定。'); @@ -557,7 +677,7 @@ export async function handleTelegramWebhookUpdate(update: any): Promise { return; } - await sendTelegramMessage(parsed.chatId, '可用命令:\n/bind 绑定码 - 绑定账号\n/status - 查看状态\n/unbind - 解除绑定'); + await sendTelegramMessage(parsed.chatId, buildTelegramHelpText(await getTelegramConfig())); return; }