mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-08-04 04:47:00 +08:00
feat: add i18n support for backend API and Telegram bot (#797)
* feat: add i18n support for backend API and Telegram bot - Add comprehensive i18n support for all backend API error messages (zh/en) - Add /lang command for Telegram bot to set language preference - Add bilingual command descriptions for Telegram bot - Support per-user language preference stored in KV - Global push uses DEFAULT_LANG, user push uses saved preference 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: improve Telegram bot language preference feature - Add internationalized message for disabled language feature - Fix hardcoded English message in /lang command - Optimize getTgMessages calls (reduce from 3 to 1 call) - Remove verbose comments for better code clarity - Add TgLangFeatureDisabledMsg to i18n (zh/en) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -3,9 +3,11 @@ import { Jwt } from "hono/utils/jwt";
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { getBooleanValue, getIntValue, getJsonSetting } from "../utils";
|
||||
import { deleteAddressWithData, newAddress, generateRandomName } from "../common";
|
||||
import { LocaleMessages } from "../i18n/type";
|
||||
|
||||
export const tgUserNewAddress = async (
|
||||
c: Context<HonoCustomType>, userId: string, address: string
|
||||
c: Context<HonoCustomType>, userId: string, address: string,
|
||||
msgs: LocaleMessages
|
||||
): Promise<{ address: string, jwt: string, password?: string | null }> => {
|
||||
if (c.env.RATE_LIMITER) {
|
||||
const { success } = await c.env.RATE_LIMITER.limit(
|
||||
@@ -23,7 +25,7 @@ export const tgUserNewAddress = async (
|
||||
const [name, domain] = trimmedAddress.includes("@") ? trimmedAddress.split("@") : [trimmedAddress, null];
|
||||
const jwtList = await c.env.KV.get<string[]>(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || [];
|
||||
if (jwtList.length >= getIntValue(c.env.TG_MAX_ADDRESS, 5)) {
|
||||
throw Error("绑定地址数量已达上限");
|
||||
throw Error(msgs.TgMaxAddressReachedMsg);
|
||||
}
|
||||
// Generate name if disabled or not provided
|
||||
const finalName = (!name || disableCustomAddressName) ? generateRandomName(c) : name;
|
||||
@@ -48,7 +50,8 @@ export const tgUserNewAddress = async (
|
||||
}
|
||||
|
||||
export const jwtListToAddressData = async (
|
||||
c: Context<HonoCustomType>, jwtList: string[]
|
||||
c: Context<HonoCustomType>, jwtList: string[],
|
||||
msgs: LocaleMessages
|
||||
): Promise<{
|
||||
addressList: string[], addressIdMap: Record<string, number>,
|
||||
invalidJwtList: string[]
|
||||
@@ -63,35 +66,36 @@ export const jwtListToAddressData = async (
|
||||
`SELECT name FROM address WHERE id = ? `
|
||||
).bind(address_id).first("name");
|
||||
if (!name) {
|
||||
addressList.push("无效地址");
|
||||
addressList.push(msgs.TgInvalidAddressMsg);
|
||||
invalidJwtList.push(jwt);
|
||||
continue;
|
||||
}
|
||||
addressList.push(address as string);
|
||||
addressIdMap[address as string] = address_id as number;
|
||||
} catch (e) {
|
||||
addressList.push("无效凭证");
|
||||
addressList.push(msgs.TgInvalidCredentialMsg);
|
||||
invalidJwtList.push(jwt);
|
||||
console.log(`获取地址列表失败: ${(e as Error).message}`);
|
||||
console.log(`Failed to get address list: ${(e as Error).message}`);
|
||||
}
|
||||
}
|
||||
return { addressList, addressIdMap, invalidJwtList };
|
||||
}
|
||||
|
||||
export const bindTelegramAddress = async (
|
||||
c: Context<HonoCustomType>, userId: string, jwt: string
|
||||
c: Context<HonoCustomType>, userId: string, jwt: string,
|
||||
msgs: LocaleMessages
|
||||
): Promise<string> => {
|
||||
const { address } = await Jwt.verify(jwt, c.env.JWT_SECRET, "HS256");
|
||||
if (!address) {
|
||||
throw Error("无效凭证");
|
||||
throw Error(msgs.TgInvalidCredentialMsg);
|
||||
}
|
||||
const jwtList = await c.env.KV.get<string[]>(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || [];
|
||||
const { addressIdMap } = await jwtListToAddressData(c, jwtList);
|
||||
const { addressIdMap } = await jwtListToAddressData(c, jwtList, msgs);
|
||||
if (address as string in addressIdMap) {
|
||||
return address as string;
|
||||
}
|
||||
if (jwtList.length >= getIntValue(c.env.TG_MAX_ADDRESS, 5)) {
|
||||
throw Error("绑定地址数量已达上限, 请先 /cleaninvalidaddress");
|
||||
throw Error(msgs.TgMaxAddressReachedCleanMsg);
|
||||
}
|
||||
await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, JSON.stringify([...jwtList, jwt]));
|
||||
// for mail push to telegram
|
||||
@@ -133,12 +137,13 @@ export const unbindTelegramByAddress = async (
|
||||
|
||||
|
||||
export const deleteTelegramAddress = async (
|
||||
c: Context<HonoCustomType>, userId: string, address: string
|
||||
c: Context<HonoCustomType>, userId: string, address: string,
|
||||
msgs: LocaleMessages
|
||||
): Promise<boolean> => {
|
||||
const jwtList = await c.env.KV.get<string[]>(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || [];
|
||||
const { addressIdMap } = await jwtListToAddressData(c, jwtList);
|
||||
const { addressIdMap } = await jwtListToAddressData(c, jwtList, msgs);
|
||||
if (!(address in addressIdMap)) {
|
||||
throw Error("此地址不属于您");
|
||||
throw Error(msgs.TgAddressNotYoursMsg);
|
||||
}
|
||||
await deleteAddressWithData(c, null, addressIdMap[address])
|
||||
return true;
|
||||
|
||||
@@ -5,26 +5,29 @@ import { Writable } from 'node:stream'
|
||||
import { newTelegramBot, initTelegramBotCommands, sendMailToTelegram } from './telegram'
|
||||
import settings from './settings'
|
||||
import miniapp from './miniapp'
|
||||
import i18n from '../i18n'
|
||||
|
||||
export const api = new Hono<HonoCustomType>();
|
||||
export { sendMailToTelegram }
|
||||
|
||||
api.use("/telegram/*", async (c, next) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!c.env.TELEGRAM_BOT_TOKEN) {
|
||||
return c.text("TELEGRAM_BOT_TOKEN is required", 400);
|
||||
return c.text(msgs.TgBotTokenRequiredMsg, 400);
|
||||
}
|
||||
if (!c.env.KV) {
|
||||
return c.text("KV is required", 400);
|
||||
return c.text(msgs.KVNotAvailableMsg, 400);
|
||||
}
|
||||
return await next();
|
||||
});
|
||||
|
||||
api.use("/admin/telegram/*", async (c, next) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!c.env.TELEGRAM_BOT_TOKEN) {
|
||||
return c.text("TELEGRAM_BOT_TOKEN is required", 400);
|
||||
return c.text(msgs.TgBotTokenRequiredMsg, 400);
|
||||
}
|
||||
if (!c.env.KV) {
|
||||
return c.text("KV is required", 400);
|
||||
return c.text(msgs.KVNotAvailableMsg, 400);
|
||||
}
|
||||
return await next();
|
||||
});
|
||||
@@ -51,7 +54,7 @@ api.post("/admin/telegram/init", async (c) => {
|
||||
console.log(`setting webhook to ${webhookUrl}`);
|
||||
const bot = newTelegramBot(c, token);
|
||||
await bot.telegram.setWebhook(webhookUrl)
|
||||
await initTelegramBotCommands(bot);
|
||||
await initTelegramBotCommands(c, bot);
|
||||
return c.json({
|
||||
message: "webhook set successfully",
|
||||
});
|
||||
|
||||
@@ -84,8 +84,7 @@ async function getTelegramBindAddress(c: Context<HonoCustomType>): Promise<Respo
|
||||
|
||||
async function newTelegramAddress(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const { initData, address, cf_token } = await c.req.json();
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
// check cf turnstile
|
||||
try {
|
||||
await checkCfTurnstile(c, cf_token);
|
||||
@@ -95,7 +94,7 @@ async function newTelegramAddress(c: Context<HonoCustomType>): Promise<Response>
|
||||
try {
|
||||
const userId = await checkTelegramAuth(c, initData);
|
||||
// get the address list from the KV
|
||||
const res = await tgUserNewAddress(c, userId, address)
|
||||
const res = await tgUserNewAddress(c, userId, address, msgs)
|
||||
return c.json(res);
|
||||
}
|
||||
catch (e) {
|
||||
@@ -105,9 +104,10 @@ async function newTelegramAddress(c: Context<HonoCustomType>): Promise<Response>
|
||||
|
||||
async function bindAddress(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const { initData, jwt } = await c.req.json();
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
try {
|
||||
const userId = await checkTelegramAuth(c, initData);
|
||||
await bindTelegramAddress(c, userId, jwt);
|
||||
await bindTelegramAddress(c, userId, jwt, msgs);
|
||||
return c.json({ success: true });
|
||||
}
|
||||
catch (e) {
|
||||
@@ -129,10 +129,11 @@ async function unbindAddress(c: Context<HonoCustomType>): Promise<Response> {
|
||||
|
||||
async function getMail(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const { initData, mailId } = await c.req.json();
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
try {
|
||||
const userId = await checkTelegramAuth(c, initData);
|
||||
const jwtList = await c.env.KV.get<string[]>(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || [];
|
||||
const { addressList, addressIdMap } = await jwtListToAddressData(c, jwtList);
|
||||
const { addressList, addressIdMap } = await jwtListToAddressData(c, jwtList, msgs);
|
||||
const result = await c.env.DB.prepare(
|
||||
`SELECT * FROM raw_mails where id = ?`
|
||||
).bind(mailId).first();
|
||||
@@ -140,14 +141,14 @@ async function getMail(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const superUser = settings?.enableGlobalMailPush && settings?.globalMailPushList.includes(userId);
|
||||
if (!superUser) {
|
||||
if (result?.address && !(result.address as string in addressIdMap)) {
|
||||
return c.text("无权查看此邮件", 403);
|
||||
return c.text(msgs.TgNoPermissionViewMailMsg, 403);
|
||||
}
|
||||
const address_id = addressIdMap[result?.address as string];
|
||||
const db_address_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM address where id = ? `
|
||||
).bind(address_id).first("id");
|
||||
if (!db_address_id) {
|
||||
return c.text("无权查看此邮件", 403);
|
||||
return c.text(msgs.TgNoPermissionViewMailMsg, 403);
|
||||
}
|
||||
}
|
||||
return c.json(result);
|
||||
|
||||
@@ -4,48 +4,79 @@ import { Telegraf, Context as TgContext, Markup } from "telegraf";
|
||||
import { callbackQuery } from "telegraf/filters";
|
||||
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { getDomains, getJsonObjectValue, getStringValue } from '../utils';
|
||||
import { getBooleanValue, getDomains, getJsonObjectValue, getStringValue } from '../utils';
|
||||
import { TelegramSettings } from "./settings";
|
||||
import { bindTelegramAddress, deleteTelegramAddress, jwtListToAddressData, tgUserNewAddress, unbindTelegramAddress, unbindTelegramByAddress } from "./common";
|
||||
import { commonParseMail } from "../common";
|
||||
import { UserFromGetMe } from "telegraf/types";
|
||||
import i18n from "../i18n";
|
||||
import { LocaleMessages } from "../i18n/type";
|
||||
|
||||
// Helper to get messages by userId
|
||||
const getTgMessages = async (
|
||||
c: Context<HonoCustomType>,
|
||||
ctx?: TgContext,
|
||||
userId?: string | null
|
||||
): Promise<LocaleMessages> => {
|
||||
// Check if user language config is enabled (default false)
|
||||
if (!getBooleanValue(c.env.TG_ALLOW_USER_LANG)) {
|
||||
return i18n.getMessages(c.env.DEFAULT_LANG || 'zh');
|
||||
}
|
||||
|
||||
const uid = userId || ctx?.message?.from?.id?.toString() || ctx?.callbackQuery?.from?.id?.toString();
|
||||
if (uid) {
|
||||
const savedLang = await c.env.KV.get(`${CONSTANTS.TG_KV_PREFIX}:lang:${uid}`);
|
||||
if (savedLang) { return i18n.getMessages(savedLang); }
|
||||
}
|
||||
return i18n.getMessages(c.env.DEFAULT_LANG || 'zh');
|
||||
};
|
||||
|
||||
// Bilingual command descriptions with full usage instructions
|
||||
const COMMANDS = [
|
||||
{
|
||||
command: "start",
|
||||
description: "开始使用"
|
||||
description: "开始使用 | Get started"
|
||||
},
|
||||
{
|
||||
command: "new",
|
||||
description: "新建邮箱地址, 如果要自定义邮箱地址, 请输入 /new, 通过 /new <name>@<domain> 可以指定, name [a-z0-9] 有效, name 为空则随机生成, @<domain> 可选"
|
||||
description: "新建邮箱, /new <name>@<domain>, name[a-z0-9]有效, 为空随机生成, @domain可选 | Create address, /new <name>@<domain>, name[a-z0-9] valid, empty=random, @domain optional"
|
||||
},
|
||||
{
|
||||
command: "address",
|
||||
description: "查看邮箱地址列表"
|
||||
description: "查看邮箱地址列表 | View address list"
|
||||
},
|
||||
{
|
||||
command: "bind",
|
||||
description: "绑定邮箱地址, 请输入 /bind <邮箱地址凭证>"
|
||||
description: "绑定邮箱, /bind <邮箱地址凭证> | Bind address, /bind <credential>"
|
||||
},
|
||||
{
|
||||
command: "unbind",
|
||||
description: "解绑邮箱地址, 请输入 /unbind <邮箱地址>"
|
||||
description: "解绑邮箱, /unbind <邮箱地址> | Unbind address, /unbind <address>"
|
||||
},
|
||||
{
|
||||
command: "delete",
|
||||
description: "删除邮箱地址, 请输入 /delete <邮箱地址>"
|
||||
description: "删除邮箱, /delete <邮箱地址> | Delete address, /delete <address>"
|
||||
},
|
||||
{
|
||||
command: "mails",
|
||||
description: "查看邮件, 请输入 /mails <邮箱地址>, 不输入地址默认查看第一个地址"
|
||||
description: "查看邮件, /mails <邮箱地址>, 不输入地址默认第一个 | View mails, /mails <address>, default first if empty"
|
||||
},
|
||||
{
|
||||
command: "cleaninvalidaddress",
|
||||
description: "清理无效地址, 请输入 /cleaninvalidaddress"
|
||||
description: "清理无效地址 | Clean invalid addresses"
|
||||
},
|
||||
{
|
||||
command: "lang",
|
||||
description: "设置语言 /lang <zh|en> | Set language /lang <zh|en>"
|
||||
},
|
||||
]
|
||||
|
||||
export const getTelegramCommands = (c: Context<HonoCustomType>) => {
|
||||
return getBooleanValue(c.env.TG_ALLOW_USER_LANG)
|
||||
? COMMANDS
|
||||
: COMMANDS.filter(cmd => cmd.command !== "lang");
|
||||
}
|
||||
|
||||
export function newTelegramBot(c: Context<HonoCustomType>, token: string): Telegraf {
|
||||
const bot = new Telegraf(token);
|
||||
const botInfo = getJsonObjectValue<UserFromGetMe>(c.env.TG_BOT_INFO);
|
||||
@@ -61,14 +92,16 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
|
||||
|
||||
const userId = ctx?.message?.from?.id || ctx.callbackQuery?.message?.chat?.id;
|
||||
if (!userId) {
|
||||
return await ctx.reply("无法获取用户信息");
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
return await ctx.reply(msgs.TgUnableGetUserInfoMsg);
|
||||
}
|
||||
|
||||
const settings = await c.env.KV.get<TelegramSettings>(CONSTANTS.TG_KV_SETTINGS_KEY, "json");
|
||||
if (settings?.enableAllowList && settings?.enableAllowList
|
||||
if (settings?.enableAllowList
|
||||
&& !settings.allowList.includes(userId.toString())
|
||||
) {
|
||||
return await ctx.reply("您没有权限使用此机器人");
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
return await ctx.reply(msgs.TgNoPermissionMsg);
|
||||
}
|
||||
try {
|
||||
await next();
|
||||
@@ -79,153 +112,192 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
|
||||
})
|
||||
|
||||
bot.command("start", async (ctx: TgContext) => {
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
const prefix = getStringValue(c.env.PREFIX)
|
||||
const domains = getDomains(c);
|
||||
const commands = getTelegramCommands(c);
|
||||
return await ctx.reply(
|
||||
"欢迎使用本机器人, 您可以打开 mini app \n\n"
|
||||
+ (prefix ? `当前已启用前缀: ${prefix}\n` : '')
|
||||
+ `当前可用域名: ${JSON.stringify(domains)}\n`
|
||||
+ "请使用以下命令:\n"
|
||||
+ COMMANDS.map(c => `/${c.command}: ${c.description}`).join("\n")
|
||||
`${msgs.TgWelcomeMsg}\n\n`
|
||||
+ (prefix ? `${msgs.TgCurrentPrefixMsg} ${prefix}\n` : '')
|
||||
+ `${msgs.TgCurrentDomainsMsg} ${JSON.stringify(domains)}\n`
|
||||
+ `${msgs.TgAvailableCommandsMsg}\n`
|
||||
+ commands.map(cmd => `/${cmd.command}: ${cmd.description}`).join("\n")
|
||||
);
|
||||
});
|
||||
|
||||
bot.command("new", async (ctx: TgContext) => {
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
const userId = ctx?.message?.from?.id;
|
||||
if (!userId) {
|
||||
return await ctx.reply("无法获取用户信息");
|
||||
return await ctx.reply(msgs.TgUnableGetUserInfoMsg);
|
||||
}
|
||||
try {
|
||||
// @ts-ignore
|
||||
const address = ctx?.message?.text.slice("/new".length).trim();
|
||||
const res = await tgUserNewAddress(c, userId.toString(), address);
|
||||
return await ctx.reply(`创建地址成功:\n`
|
||||
+ `地址: ${res.address}\n`
|
||||
+ (res.password ? `密码: \`${res.password}\`\n` : '')
|
||||
+ `凭证: \`${res.jwt}\`\n`,
|
||||
const res = await tgUserNewAddress(c, userId.toString(), address, msgs);
|
||||
return await ctx.reply(`${msgs.TgCreateSuccessMsg}\n`
|
||||
+ `${msgs.TgAddressMsg} ${res.address}\n`
|
||||
+ (res.password ? `${msgs.TgPasswordMsg} \`${res.password}\`\n` : '')
|
||||
+ `${msgs.TgCredentialMsg} \`${res.jwt}\`\n`,
|
||||
{
|
||||
parse_mode: "Markdown"
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
return await ctx.reply(`创建地址失败: ${(e as Error).message}`);
|
||||
return await ctx.reply(`${msgs.TgCreateFailedMsg} ${(e as Error).message}`);
|
||||
}
|
||||
});
|
||||
|
||||
bot.command("bind", async (ctx: TgContext) => {
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
const userId = ctx?.message?.from?.id;
|
||||
if (!userId) {
|
||||
return await ctx.reply("无法获取用户信息");
|
||||
return await ctx.reply(msgs.TgUnableGetUserInfoMsg);
|
||||
}
|
||||
try {
|
||||
// @ts-ignore
|
||||
const jwt = ctx?.message?.text.slice("/bind".length).trim();
|
||||
if (!jwt) {
|
||||
return await ctx.reply("请输入凭证");
|
||||
return await ctx.reply(msgs.TgPleaseInputCredentialMsg);
|
||||
}
|
||||
const address = await bindTelegramAddress(c, userId.toString(), jwt);
|
||||
return await ctx.reply(`绑定成功:\n`
|
||||
+ `地址: ${address}`
|
||||
const address = await bindTelegramAddress(c, userId.toString(), jwt, msgs);
|
||||
return await ctx.reply(`${msgs.TgBindSuccessMsg}\n`
|
||||
+ `${msgs.TgAddressMsg} ${address}`
|
||||
);
|
||||
}
|
||||
catch (e) {
|
||||
return await ctx.reply(`绑定失败: ${(e as Error).message}`);
|
||||
return await ctx.reply(`${msgs.TgBindFailedMsg} ${(e as Error).message}`);
|
||||
}
|
||||
});
|
||||
|
||||
bot.command("unbind", async (ctx: TgContext) => {
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
const userId = ctx?.message?.from?.id;
|
||||
if (!userId) {
|
||||
return await ctx.reply("无法获取用户信息");
|
||||
return await ctx.reply(msgs.TgUnableGetUserInfoMsg);
|
||||
}
|
||||
try {
|
||||
// @ts-ignore
|
||||
const address = ctx?.message?.text.slice("/unbind".length).trim();
|
||||
if (!address) {
|
||||
return await ctx.reply("请输入地址");
|
||||
return await ctx.reply(msgs.TgPleaseInputAddressMsg);
|
||||
}
|
||||
await unbindTelegramAddress(c, userId.toString(), address);
|
||||
return await ctx.reply(`解绑成功:\n地址: ${address}`
|
||||
return await ctx.reply(`${msgs.TgUnbindSuccessMsg}\n${msgs.TgAddressMsg} ${address}`
|
||||
);
|
||||
}
|
||||
catch (e) {
|
||||
return await ctx.reply(`解绑失败: ${(e as Error).message}`);
|
||||
return await ctx.reply(`${msgs.TgUnbindFailedMsg} ${(e as Error).message}`);
|
||||
}
|
||||
})
|
||||
|
||||
bot.command("delete", async (ctx: TgContext) => {
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
const userId = ctx?.message?.from?.id;
|
||||
if (!userId) {
|
||||
return await ctx.reply("无法获取用户信息");
|
||||
return await ctx.reply(msgs.TgUnableGetUserInfoMsg);
|
||||
}
|
||||
try {
|
||||
// @ts-ignore
|
||||
const address = ctx?.message?.text.slice("/delete".length).trim();
|
||||
if (!address) {
|
||||
return await ctx.reply("请输入地址");
|
||||
return await ctx.reply(msgs.TgPleaseInputAddressMsg);
|
||||
}
|
||||
await deleteTelegramAddress(c, userId.toString(), address);
|
||||
return await ctx.reply(`删除成功: ${address}`);
|
||||
await deleteTelegramAddress(c, userId.toString(), address, msgs);
|
||||
return await ctx.reply(`${msgs.TgDeleteSuccessMsg} ${address}`);
|
||||
} catch (e) {
|
||||
return await ctx.reply(`删除失败: ${(e as Error).message}`);
|
||||
return await ctx.reply(`${msgs.TgDeleteFailedMsg} ${(e as Error).message}`);
|
||||
}
|
||||
});
|
||||
|
||||
bot.command("address", async (ctx) => {
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
const userId = ctx?.message?.from?.id;
|
||||
if (!userId) {
|
||||
return await ctx.reply("无法获取用户信息");
|
||||
return await ctx.reply(msgs.TgUnableGetUserInfoMsg);
|
||||
}
|
||||
try {
|
||||
const jwtList = await c.env.KV.get<string[]>(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || [];
|
||||
const { addressList } = await jwtListToAddressData(c, jwtList);
|
||||
return await ctx.reply(`地址列表:\n\n`
|
||||
+ addressList.map(a => `地址: ${a}`).join("\n")
|
||||
const { addressList } = await jwtListToAddressData(c, jwtList, msgs);
|
||||
return await ctx.reply(`${msgs.TgAddressListMsg}\n\n`
|
||||
+ addressList.map(a => `${msgs.TgAddressMsg} ${a}`).join("\n")
|
||||
);
|
||||
} catch (e) {
|
||||
return await ctx.reply(`获取地址列表失败: ${(e as Error).message}`);
|
||||
return await ctx.reply(`${msgs.TgGetAddressFailedMsg} ${(e as Error).message}`);
|
||||
}
|
||||
});
|
||||
|
||||
bot.command("cleaninvalidaddress", async (ctx: TgContext) => {
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
const userId = ctx?.message?.from?.id;
|
||||
if (!userId) {
|
||||
return await ctx.reply("无法获取用户信息");
|
||||
return await ctx.reply(msgs.TgUnableGetUserInfoMsg);
|
||||
}
|
||||
try {
|
||||
const jwtList = await c.env.KV.get<string[]>(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || [];
|
||||
const { invalidJwtList } = await jwtListToAddressData(c, jwtList);
|
||||
const { invalidJwtList } = await jwtListToAddressData(c, jwtList, msgs);
|
||||
const newJwtList = jwtList.filter(jwt => !invalidJwtList.includes(jwt));
|
||||
await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, JSON.stringify(newJwtList));
|
||||
const { addressList } = await jwtListToAddressData(c, newJwtList);
|
||||
return await ctx.reply(`清理无效地址成功:\n\n`
|
||||
+ `当前地址列表:\n\n`
|
||||
+ addressList.map(a => `地址: ${a}`).join("\n")
|
||||
const { addressList } = await jwtListToAddressData(c, newJwtList, msgs);
|
||||
return await ctx.reply(`${msgs.TgCleanSuccessMsg}\n\n`
|
||||
+ `${msgs.TgCurrentAddressListMsg}\n\n`
|
||||
+ addressList.map(a => `${msgs.TgAddressMsg} ${a}`).join("\n")
|
||||
);
|
||||
} catch (e) {
|
||||
return await ctx.reply(`清理无效地址失败: ${(e as Error).message}`);
|
||||
return await ctx.reply(`${msgs.TgCleanFailedMsg} ${(e as Error).message}`);
|
||||
}
|
||||
});
|
||||
|
||||
bot.command("lang", async (ctx: TgContext) => {
|
||||
const userId = ctx?.message?.from?.id;
|
||||
if (!userId) {
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
return await ctx.reply(msgs.TgUnableGetUserInfoMsg);
|
||||
}
|
||||
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
|
||||
// Check if user language config is enabled
|
||||
if (!getBooleanValue(c.env.TG_ALLOW_USER_LANG)) {
|
||||
return await ctx.reply(msgs.TgLangFeatureDisabledMsg);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const lang = ctx?.message?.text.slice("/lang".length).trim().toLowerCase();
|
||||
if (lang === 'zh' || lang === 'en') {
|
||||
await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:lang:${userId}`, lang);
|
||||
return await ctx.reply(`${msgs.TgLangSetSuccessMsg} ${lang === 'zh' ? '中文' : 'English'}`);
|
||||
}
|
||||
|
||||
const currentLang = await c.env.KV.get(`${CONSTANTS.TG_KV_PREFIX}:lang:${userId}`);
|
||||
return await ctx.reply(
|
||||
`${msgs.TgCurrentLangMsg} ${currentLang || 'auto'}\n`
|
||||
+ `${msgs.TgSelectLangMsg}\n`
|
||||
+ `/lang zh - 中文\n`
|
||||
+ `/lang en - English`
|
||||
);
|
||||
});
|
||||
|
||||
const queryMail = async (ctx: TgContext, queryAddress: string, mailIndex: number, edit: boolean) => {
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
const userId = ctx?.message?.from?.id || ctx.callbackQuery?.message?.chat?.id;
|
||||
if (!userId) {
|
||||
return await ctx.reply("无法获取用户信息");
|
||||
return await ctx.reply(msgs.TgUnableGetUserInfoMsg);
|
||||
}
|
||||
const jwtList = await c.env.KV.get<string[]>(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || [];
|
||||
const { addressList, addressIdMap } = await jwtListToAddressData(c, jwtList);
|
||||
const { addressList, addressIdMap } = await jwtListToAddressData(c, jwtList, msgs);
|
||||
if (!queryAddress && addressList.length > 0) {
|
||||
queryAddress = addressList[0];
|
||||
}
|
||||
if (!(queryAddress in addressIdMap)) {
|
||||
return await ctx.reply(`未绑定此地址 ${queryAddress}`);
|
||||
return await ctx.reply(`${msgs.TgNotBoundAddressMsg} ${queryAddress}`);
|
||||
}
|
||||
const address_id = addressIdMap[queryAddress];
|
||||
const db_address_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM address where id = ? `
|
||||
).bind(address_id).first("id");
|
||||
if (!db_address_id) {
|
||||
return await ctx.reply("无效地址");
|
||||
return await ctx.reply(msgs.TgInvalidAddressMsg);
|
||||
}
|
||||
const { raw, id: mailId, created_at } = await c.env.DB.prepare(
|
||||
`SELECT * FROM raw_mails where address = ? `
|
||||
@@ -233,47 +305,49 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
|
||||
).bind(
|
||||
queryAddress, mailIndex
|
||||
).first<{ raw: string, id: string, created_at: string }>() || {};
|
||||
const { mail } = raw ? await parseMail({ rawEmail: raw }, queryAddress, created_at) : { mail: "已经没有邮件了" };
|
||||
const { mail } = raw ? await parseMail(msgs, { rawEmail: raw }, queryAddress, created_at) : { mail: msgs.TgNoMoreMailsMsg };
|
||||
const settings = await c.env.KV.get<TelegramSettings>(CONSTANTS.TG_KV_SETTINGS_KEY, "json");
|
||||
const miniAppButtons = []
|
||||
if (settings?.miniAppUrl && settings?.miniAppUrl?.length > 0 && mailId) {
|
||||
const url = new URL(settings.miniAppUrl);
|
||||
url.pathname = "/telegram_mail"
|
||||
url.searchParams.set("mail_id", mailId);
|
||||
miniAppButtons.push(Markup.button.webApp("查看邮件", url.toString()));
|
||||
miniAppButtons.push(Markup.button.webApp(msgs.TgViewMailBtnMsg, url.toString()));
|
||||
}
|
||||
if (edit) {
|
||||
return await ctx.editMessageText(mail || "无邮件",
|
||||
return await ctx.editMessageText(mail || msgs.TgNoMailMsg,
|
||||
{
|
||||
...Markup.inlineKeyboard([
|
||||
Markup.button.callback("上一条", `mail_${queryAddress}_${mailIndex - 1}`, mailIndex <= 0),
|
||||
Markup.button.callback(msgs.TgPrevBtnMsg, `mail_${queryAddress}_${mailIndex - 1}`, mailIndex <= 0),
|
||||
...miniAppButtons,
|
||||
Markup.button.callback("下一条", `mail_${queryAddress}_${mailIndex + 1}`, !raw),
|
||||
Markup.button.callback(msgs.TgNextBtnMsg, `mail_${queryAddress}_${mailIndex + 1}`, !raw),
|
||||
])
|
||||
},
|
||||
);
|
||||
}
|
||||
return await ctx.reply(mail || "无邮件",
|
||||
return await ctx.reply(mail || msgs.TgNoMailMsg,
|
||||
{
|
||||
...Markup.inlineKeyboard([
|
||||
Markup.button.callback("上一条", `mail_${queryAddress}_${mailIndex - 1}`, mailIndex <= 0),
|
||||
Markup.button.callback(msgs.TgPrevBtnMsg, `mail_${queryAddress}_${mailIndex - 1}`, mailIndex <= 0),
|
||||
...miniAppButtons,
|
||||
Markup.button.callback("下一条", `mail_${queryAddress}_${mailIndex + 1}`, !raw),
|
||||
Markup.button.callback(msgs.TgNextBtnMsg, `mail_${queryAddress}_${mailIndex + 1}`, !raw),
|
||||
])
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
bot.command("mails", async ctx => {
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
try {
|
||||
const queryAddress = ctx?.message?.text.slice("/mails".length).trim();
|
||||
return await queryMail(ctx, queryAddress, 0, false);
|
||||
} catch (e) {
|
||||
return await ctx.reply(`获取邮件失败: ${(e as Error).message}`);
|
||||
return await ctx.reply(`${msgs.TgGetMailFailedMsg} ${(e as Error).message}`);
|
||||
}
|
||||
});
|
||||
|
||||
bot.on(callbackQuery("data"), async ctx => {
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
// Use ctx.callbackQuery.data
|
||||
try {
|
||||
const data = ctx.callbackQuery.data;
|
||||
@@ -283,8 +357,8 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.log(`获取邮件失败: ${(e as Error).message}`, e);
|
||||
return await ctx.answerCbQuery(`获取邮件失败: ${(e as Error).message}`);
|
||||
console.log(`${msgs.TgGetMailFailedMsg} ${(e as Error).message}`, e);
|
||||
return await ctx.answerCbQuery(`${msgs.TgGetMailFailedMsg} ${(e as Error).message}`);
|
||||
}
|
||||
await ctx.answerCbQuery();
|
||||
});
|
||||
@@ -293,11 +367,12 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
|
||||
}
|
||||
|
||||
|
||||
export async function initTelegramBotCommands(bot: Telegraf) {
|
||||
await bot.telegram.setMyCommands(COMMANDS);
|
||||
export async function initTelegramBotCommands(c: Context<HonoCustomType>, bot: Telegraf) {
|
||||
await bot.telegram.setMyCommands(getTelegramCommands(c));
|
||||
}
|
||||
|
||||
const parseMail = async (
|
||||
msgs: LocaleMessages,
|
||||
parsedEmailContext: ParsedEmailContext,
|
||||
address: string, created_at: string | undefined | null
|
||||
) => {
|
||||
@@ -308,20 +383,20 @@ const parseMail = async (
|
||||
const parsedEmail = await commonParseMail(parsedEmailContext);
|
||||
let parsedText = parsedEmail?.text || "";
|
||||
if (parsedText.length && parsedText.length > 1000) {
|
||||
parsedText = parsedEmail?.text.substring(0, 1000) + "\n\n...\n消息过长请到miniapp查看";
|
||||
parsedText = parsedEmail?.text.substring(0, 1000) + `\n\n...\n${msgs.TgMsgTooLongMsg}`;
|
||||
}
|
||||
return {
|
||||
isHtml: false,
|
||||
mail: `From: ${parsedEmail?.sender || "无发件人"}\n`
|
||||
mail: `From: ${parsedEmail?.sender || msgs.TgNoSenderMsg}\n`
|
||||
+ `To: ${address}\n`
|
||||
+ (created_at ? `Date: ${created_at}\n` : "")
|
||||
+ `Subject: ${parsedEmail?.subject}\n`
|
||||
+ `Content:\n${parsedText || "解析失败,请打开 mini app 查看"}`
|
||||
+ `Content:\n${parsedText || msgs.TgParseFailedViewInAppMsg}`
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
isHtml: false,
|
||||
mail: `解析邮件失败: ${(e as Error).message}`
|
||||
mail: `${msgs.TgParseMailFailedMsg} ${(e as Error).message}`
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -336,10 +411,6 @@ export async function sendMailToTelegram(
|
||||
return;
|
||||
}
|
||||
const userId = await c.env.KV.get(`${CONSTANTS.TG_KV_PREFIX}:${address}`);
|
||||
const { mail } = await parseMail(parsedEmailContext, address, new Date().toUTCString());
|
||||
if (!mail) {
|
||||
return;
|
||||
}
|
||||
const settings = await c.env.KV.get<TelegramSettings>(CONSTANTS.TG_KV_SETTINGS_KEY, "json");
|
||||
const globalPush = settings?.enableGlobalMailPush && settings?.globalMailPushList;
|
||||
if (!userId && !globalPush) {
|
||||
@@ -349,28 +420,31 @@ export async function sendMailToTelegram(
|
||||
`SELECT id FROM raw_mails where address = ? and message_id = ?`
|
||||
).bind(address, message_id).first<string>("id");
|
||||
const bot = newTelegramBot(c, c.env.TELEGRAM_BOT_TOKEN);
|
||||
const miniAppButtons = []
|
||||
if (settings?.miniAppUrl && settings?.miniAppUrl?.length > 0 && mailId) {
|
||||
const url = new URL(settings.miniAppUrl);
|
||||
url.pathname = "/telegram_mail"
|
||||
url.searchParams.set("mail_id", mailId);
|
||||
miniAppButtons.push(Markup.button.webApp("查看邮件", url.toString()));
|
||||
}
|
||||
|
||||
const buildAndSend = async (targetUserId: string, msgs: LocaleMessages) => {
|
||||
const { mail } = await parseMail(msgs, parsedEmailContext, address, new Date().toUTCString());
|
||||
if (!mail) return;
|
||||
const buttons = [];
|
||||
if (settings?.miniAppUrl && mailId) {
|
||||
const url = new URL(settings.miniAppUrl);
|
||||
url.pathname = "/telegram_mail"
|
||||
url.searchParams.set("mail_id", mailId);
|
||||
buttons.push(Markup.button.webApp(msgs.TgViewMailBtnMsg, url.toString()));
|
||||
}
|
||||
await bot.telegram.sendMessage(targetUserId, mail, {
|
||||
...Markup.inlineKeyboard([...buttons])
|
||||
});
|
||||
};
|
||||
|
||||
if (globalPush) {
|
||||
const globalMsgs = i18n.getMessages(c.env.DEFAULT_LANG || 'zh');
|
||||
for (const pushId of settings.globalMailPushList) {
|
||||
await bot.telegram.sendMessage(pushId, mail, {
|
||||
...Markup.inlineKeyboard([
|
||||
...miniAppButtons,
|
||||
])
|
||||
});
|
||||
await buildAndSend(pushId, globalMsgs);
|
||||
}
|
||||
}
|
||||
if (!userId) {
|
||||
return;
|
||||
|
||||
if (userId) {
|
||||
const userMsgs = await getTgMessages(c, undefined, userId);
|
||||
await buildAndSend(userId, userMsgs);
|
||||
}
|
||||
await bot.telegram.sendMessage(userId, mail, {
|
||||
...Markup.inlineKeyboard([
|
||||
...miniAppButtons,
|
||||
])
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user