diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f9e88fd --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +BOT_TOKEN= +TG_WEBHOOK_SECRET= +AI_BASE_URL= +AI_API_KEY= +TURNSTILE_SECRET= \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index d582650..d2d27e1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,6 +31,7 @@ interface TelegramJoinRequest { first_name: string; last_name?: string; username?: string; + language_code?: string; }; user_chat_id: number; date: number; @@ -63,6 +64,7 @@ interface PendingJoinRecord { status: PendingStatus; createdAt: number; expiresAt: number; + locale: Locale; verificationMethod?: VerificationMethod; verifyMessageId?: number; } @@ -76,6 +78,7 @@ const JOIN_PREFIX = "join:"; const ACTIVE_PREFIX = "active:"; const CHALLENGE_TTL_MS = 10 * 60 * 1000; const RECORD_TTL_SECONDS = 24 * 60 * 60; +type Locale = "zh" | "en"; export default { async fetch(request, env, ctx): Promise { @@ -140,6 +143,7 @@ async function handleTelegramWebhook({ env, request }: RequestContext): Promise< const hasAvatar = await safelyFetchAvatarForUser(env, join.from.id); const hasBio = Boolean(join.bio?.trim()); const now = Date.now(); + const locale = detectLocale(join.from.language_code); if (!hasAvatar || !hasBio) { const localReasons = [ @@ -164,6 +168,7 @@ async function handleTelegramWebhook({ env, request }: RequestContext): Promise< status: "pending", createdAt: now, expiresAt: now + CHALLENGE_TTL_MS, + locale, }; const verifyMessageId = await sendVerificationPrompt(env, request, record); @@ -204,6 +209,7 @@ async function handleTelegramWebhook({ env, request }: RequestContext): Promise< status: "pending", createdAt: now, expiresAt: now + CHALLENGE_TTL_MS, + locale, }; const verifyMessageId = await sendVerificationPrompt(env, request, record); @@ -245,6 +251,7 @@ async function handleTelegramWebhook({ env, request }: RequestContext): Promise< status: "pending", createdAt: now, expiresAt: now + CHALLENGE_TTL_MS, + locale, }; const verifyMessageId = await sendVerificationPrompt(env, request, record); @@ -269,11 +276,11 @@ async function handleVerifyPage({ env, request }: RequestContext): Promise { const verifyUrl = `${getVerificationOrigin(env, request)}/verify?token=${encodeURIComponent(record.token)}`; + const copy = getCopy(record.locale); const result = await telegramCall<{ message_id: number }>(env, "sendMessage", { chat_id: record.userChatId, text: - `你申请加入《${record.chatTitle}》需要做一次验证。\n` + - `请在 10 分钟内点击下方按钮,在 Telegram 小程序里任选一种方式完成:\n` + - `1. Cloudflare Turnstile\n` + - `2. 系统生物识别\n\n` + - `验证完成后会自动放行入群申请。`, + copy.prompt(record.chatTitle), reply_markup: { - inline_keyboard: [[{ text: "打开验证中心", web_app: { url: verifyUrl } }]], + inline_keyboard: [[{ text: copy.openVerification, web_app: { url: verifyUrl } }]], }, disable_web_page_preview: true, }); @@ -607,16 +611,17 @@ async function telegramCall(env: Env, method: string, payload: Record - + -入群验证 +${escapeHtml(copy.pageTitle)}
${escapeHtml(message)}
`; +function renderStatusPage(message: string, locale: Locale): string { + const copy = getCopy(locale); + return `${escapeHtml(copy.statusPageTitle)}
${escapeHtml(message)}
`; +} + +function detectLocale(languageCode?: string): Locale { + return languageCode?.toLowerCase().startsWith("zh") ? "zh" : "en"; +} + +function t(locale: Locale, key: "invalidLink" | "expiredLink"): string { + return getCopy(locale)[key]; +} + +function getCopy(locale: Locale) { + if (locale === "zh") { + return { + pageTitle: "入群验证", + statusPageTitle: "验证状态", + invalidLink: "链接无效或已失效。", + expiredLink: "验证已超时,请重新申请入群。", + openVerification: "打开验证中心", + prompt: (chatTitle: string) => `你申请加入《${chatTitle}》需要做一次验证。\n请在 10 分钟内点击下方按钮,在 Telegram 小程序里任选一种方式完成:\n1. Cloudflare Turnstile\n2. 系统生物识别\n\n验证完成后会自动放行入群申请。`, + headerTitle: (displayName: string, chatTitle: string) => `${displayName} 申请加入 ${chatTitle}`, + headerSubtitle: "请完成一次简短验证后继续", + turnstileLabel: "方式一:Turnstile 人机验证", + biometricLabel: "方式二:系统生物识别", + startVerification: "开始验证", + success: "验证通过,5 秒后自动关闭", + close: "关闭", + invalidEnvironment: "当前验证环境无效,请回到 Telegram 重新打开", + verifying: "正在验证...", + verificationFailed: "验证失败", + networkErrorPrefix: "网络错误:", + biometricUnavailable: "当前环境不支持生物识别", + biometricInitFailed: "生物识别初始化失败", + faceId: "Face ID 验证", + fingerprint: "指纹验证", + biometricAccessReason: "用于入群身份验证", + biometricAccessDenied: "生物识别权限被拒绝", + biometricUseTurnstile: "当前环境不支持生物识别,请使用 Turnstile", + biometricAuthenticateReason: "验证身份以加入群组", + biometricFailed: "生物识别验证未通过", + submittingVerification: "正在提交验证...", + }; + } + + return { + pageTitle: "Join Verification", + statusPageTitle: "Verification Status", + invalidLink: "This link is invalid or has expired.", + expiredLink: "Verification timed out. Please submit your join request again.", + openVerification: "Open Verification", + prompt: (chatTitle: string) => `Your request to join \"${chatTitle}\" requires verification.\nPlease tap the button below and complete one of these methods in Telegram within 10 minutes:\n1. Cloudflare Turnstile\n2. Biometric verification\n\nYour join request will be approved automatically after verification.`, + headerTitle: (displayName: string, chatTitle: string) => `${displayName} requested to join ${chatTitle}`, + headerSubtitle: "Please complete a short verification to continue", + turnstileLabel: "Option 1: Turnstile verification", + biometricLabel: "Option 2: Biometric verification", + startVerification: "Start Verification", + success: "Verification complete. This page will close in 5 seconds.", + close: "Close", + invalidEnvironment: "Invalid verification environment. Please reopen this page from Telegram.", + verifying: "Verifying...", + verificationFailed: "Verification failed", + networkErrorPrefix: "Network error: ", + biometricUnavailable: "Biometric verification is not available in this environment", + biometricInitFailed: "Biometric initialization failed", + faceId: "Face ID Verification", + fingerprint: "Fingerprint Verification", + biometricAccessReason: "Used to verify your identity for group access", + biometricAccessDenied: "Biometric permission was denied", + biometricUseTurnstile: "Biometric verification is unavailable here. Please use Turnstile.", + biometricAuthenticateReason: "Verify your identity to join the group", + biometricFailed: "Biometric verification failed", + submittingVerification: "Submitting verification...", + }; } function getVerificationOrigin(env: Env, request: Request): string {