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:
Dream Hunter
2025-12-31 01:42:41 +08:00
committed by GitHub
parent 5e227d2b2d
commit 3ebe22115a
25 changed files with 762 additions and 276 deletions

View File

@@ -19,6 +19,8 @@
- feat: |邮件过滤| 移除后端 keyword 参数,改为前端过滤当前页邮件,优化查询性能
- feat: |数据库| 为 `message_id` 字段添加索引,优化邮件更新操作性能,需执行 `db/2025-12-15-message-id-index.sql` 更新数据库
- feat: |Admin| 维护页面增加自定义 SQL 清理功能,支持定时任务执行自定义清理语句
- feat: |国际化| 后端 API 错误消息全面支持中英文国际化
- feat: |Telegram| 机器人支持中英文切换,新增 `/lang` 命令设置语言偏好
## v1.1.0

View File

@@ -19,6 +19,8 @@
- feat: |Email Filtering| Remove backend keyword parameter, switch to frontend filtering of current page emails, optimize query performance
- feat: |Database| Add index for `message_id` field to optimize email update operations, need to execute `db/2025-12-15-message-id-index.sql` to update database
- feat: |Admin| Add custom SQL cleanup feature to maintenance page, support scheduled task execution of custom cleanup statements
- feat: |i18n| Backend API error messages now fully support Chinese and English internationalization
- feat: |Telegram| Bot supports Chinese/English switching, add `/lang` command to set language preference
## v1.1.0

View File

@@ -14,23 +14,24 @@ export default {
return c.json(settings)
},
saveSetting: async (c: Context<HonoCustomType>) => {
const msgs = i18n.getMessagesbyContext(c);
const value = await c.req.json();
const settings = new UserSettings(value);
if (settings.enableMailVerify && !c.env.KV) {
return c.text("Please enable KV first if you want to enable mail verify", 403)
return c.text(msgs.EnableKVForMailVerifyMsg, 403)
}
if (settings.enableMailVerify && !settings.verifyMailSender) {
return c.text("Please provide verifyMailSender", 400)
return c.text(msgs.VerifyMailSenderNotSetMsg, 400)
}
if (settings.enableMailVerify && settings.verifyMailSender) {
const mailDomain = settings.verifyMailSender.split("@")[1];
const domains = getDomains(c);
if (!domains.includes(mailDomain)) {
return c.text(`VerifyMailSender(${settings.verifyMailSender}) domain must in ${JSON.stringify(domains, null, 2)}`, 400)
return c.text(`${msgs.VerifyMailDomainInvalidMsg} ${JSON.stringify(domains, null, 2)}`, 400)
}
}
if (settings.maxAddressCount < 0) {
return c.text("Invalid maxAddressCount", 400)
return c.text(msgs.InvalidMaxAddressCountMsg, 400)
}
await saveSetting(c, CONSTANTS.USER_SETTINGS_KEY, JSON.stringify(settings));
return c.json({ success: true })
@@ -60,9 +61,10 @@ export default {
);
},
createUser: async (c: Context<HonoCustomType>) => {
const msgs = i18n.getMessagesbyContext(c);
const { email, password } = await c.req.json();
if (!email || !password) {
return c.text("Invalid email or password", 400)
return c.text(msgs.InvalidEmailOrPasswordMsg, 400)
}
// geo data
const reqIp = c.req.raw.headers.get("cf-connecting-ip")
@@ -77,14 +79,14 @@ export default {
email, password, JSON.stringify(userInfo)
).run();
if (!success) {
return c.text("Failed to register", 500)
return c.text(msgs.FailedToRegisterMsg, 500)
}
} catch (e) {
const errorMsg = (e as Error).message;
if (errorMsg && errorMsg.includes("UNIQUE")) {
return c.text("User already exists", 400)
return c.text(msgs.UserAlreadyExistsMsg, 400)
}
return c.text(`Failed to register: ${errorMsg}`, 500)
return c.text(`${msgs.FailedToRegisterMsg}: ${errorMsg}`, 500)
}
return c.json({ success: true })
},
@@ -99,7 +101,7 @@ export default {
`DELETE FROM users_address WHERE user_id = ?`
).bind(user_id).run();
if (!success || !addressSuccess) {
return c.text("Failed to delete user", 500)
return c.text(msgs.FailedDeleteUserMsg, 500)
}
return c.json({ success: true })
},
@@ -114,28 +116,29 @@ export default {
`UPDATE users SET password = ? WHERE id = ?`
).bind(password, user_id).run();
if (!success) {
return c.text("Failed to reset password", 500)
return c.text(msgs.FailedUpdatePasswordMsg, 500)
}
} catch (e) {
return c.text(`Failed to reset password: ${(e as Error).message}`, 500)
return c.text(`${msgs.FailedUpdatePasswordMsg}: ${(e as Error).message}`, 500)
}
return c.json({ success: true });
},
updateUserRoles: async (c: Context<HonoCustomType>) => {
const msgs = i18n.getMessagesbyContext(c);
const { user_id, role_text } = await c.req.json();
if (!user_id) return c.text("Invalid user_id", 400);
if (!user_id) return c.text(msgs.InvalidUserIdMsg, 400);
if (!role_text) {
const { success } = await c.env.DB.prepare(
`DELETE FROM user_roles WHERE user_id = ?`
).bind(user_id).run();
if (!success) {
return c.text("Failed to update user roles", 500)
return c.text(msgs.FailedUpdateUserDefaultRoleMsg, 500)
}
return c.json({ success: true })
}
const user_roles = getUserRoles(c);
if (!user_roles.find((r) => r.role === role_text)) {
return c.text("Invalid role_text", 400)
return c.text(msgs.InvalidRoleTextMsg, 400)
}
const { success } = await c.env.DB.prepare(
`INSERT INTO user_roles (user_id, role_text)`
@@ -143,7 +146,7 @@ export default {
+ ` ON CONFLICT(user_id) DO UPDATE SET role_text = ?, updated_at = datetime('now')`
).bind(user_id, role_text, role_text).run();
if (!success) {
return c.text("Failed to update user roles", 500)
return c.text(msgs.FailedUpdateUserDefaultRoleMsg, 500)
}
return c.json({ success: true })
},

View File

@@ -4,6 +4,11 @@ import { cleanup } from '../common';
import { CONSTANTS } from '../constants';
import { getJsonSetting, saveSetting } from '../utils';
import { CleanupSettings, CustomSqlCleanup } from '../models';
import i18n from '../i18n';
import { LocaleMessages } from '../i18n/type';
// SQL validation error types
type SqlValidationError = 'empty' | 'too_long' | 'not_delete' | 'multiple_statements' | 'has_comments';
// Normalize SQL: trim and remove trailing semicolon
const normalizeSql = (sql: string): string => {
@@ -14,34 +19,45 @@ const normalizeSql = (sql: string): string => {
return normalized;
};
// Get error message from error type
const getValidationErrorMsg = (errorType: SqlValidationError, msgs: LocaleMessages): string => {
switch (errorType) {
case 'empty': return msgs.SqlEmptyMsg;
case 'too_long': return msgs.SqlTooLongMsg;
case 'not_delete': return msgs.SqlOnlyDeleteMsg;
case 'multiple_statements': return msgs.SqlSingleStatementMsg;
case 'has_comments': return msgs.SqlNoCommentsMsg;
}
};
// Validate custom SQL cleanup statement
export const validateCustomSql = (sql: string): { valid: boolean; error?: string } => {
export const validateCustomSql = (sql: string): { valid: boolean; errorType?: SqlValidationError } => {
if (!sql || !sql.trim()) {
return { valid: false, error: "SQL statement is empty" };
return { valid: false, errorType: 'empty' };
}
const trimmedSql = normalizeSql(sql);
// Check SQL length (max 1000 characters)
if (trimmedSql.length > 1000) {
return { valid: false, error: "SQL statement is too long (max 1000 characters)" };
return { valid: false, errorType: 'too_long' };
}
const sqlUpper = trimmedSql.toUpperCase();
// Only allow DELETE statements
if (!sqlUpper.startsWith('DELETE ')) {
return { valid: false, error: "Only DELETE statements are allowed" };
return { valid: false, errorType: 'not_delete' };
}
// Only allow single statement (no semicolons after trimming)
if (trimmedSql.includes(';')) {
return { valid: false, error: "Only single SQL statement is allowed" };
return { valid: false, errorType: 'multiple_statements' };
}
// Forbid SQL comments
if (/--/.test(trimmedSql) || /\/\*/.test(trimmedSql)) {
return { valid: false, error: "SQL comments are not allowed" };
return { valid: false, errorType: 'has_comments' };
}
return { valid: true };
@@ -52,13 +68,14 @@ export const executeCustomSqlCleanup = async (
c: Context<HonoCustomType>,
customSql: CustomSqlCleanup
): Promise<{ success: boolean; rowsAffected?: number; error?: string }> => {
const msgs = i18n.getMessagesbyContext(c);
if (!customSql || !customSql.sql) {
return { success: false, error: "Invalid custom SQL cleanup config" };
return { success: false, error: msgs.InvalidCleanupConfigMsg };
}
const validation = validateCustomSql(customSql.sql);
if (!validation.valid) {
return { success: false, error: validation.error };
return { success: false, error: getValidationErrorMsg(validation.errorType!, msgs) };
}
const sql = normalizeSql(customSql.sql);
@@ -78,12 +95,13 @@ export const executeCustomSqlCleanup = async (
export default {
cleanup: async (c: Context<HonoCustomType>) => {
const msgs = i18n.getMessagesbyContext(c);
const { cleanType, cleanDays } = await c.req.json();
try {
await cleanup(c, cleanType, cleanDays);
} catch (error) {
console.error(error);
return c.text(`Failed to cleanup ${(error as Error).message}`, 500)
return c.text(`${msgs.OperationFailedMsg}: ${(error as Error).message}`, 500)
}
return c.json({ success: true })
},
@@ -92,6 +110,7 @@ export default {
return c.json(cleanupSetting)
},
saveCleanup: async (c: Context<HonoCustomType>) => {
const msgs = i18n.getMessagesbyContext(c);
const cleanupSetting = await c.req.json<CleanupSettings>();
// Validate custom SQL cleanup list
@@ -100,7 +119,8 @@ export default {
if (customSql.sql) {
const validation = validateCustomSql(customSql.sql);
if (!validation.valid) {
return c.text(`Invalid SQL [${customSql.name || 'unnamed'}]: ${validation.error}`, 400);
const errorMsg = getValidationErrorMsg(validation.errorType!, msgs);
return c.text(`[${customSql.name || 'unnamed'}]: ${errorMsg}`, 400);
}
}
}

View File

@@ -45,10 +45,9 @@ api.get('/admin/address', async (c) => {
api.post('/admin/new_address', async (c) => {
const { name, domain, enablePrefix } = await c.req.json();
const lang = c.get("lang") || c.env.DEFAULT_LANG;
const msgs = i18n.getMessages(lang);
const msgs = i18n.getMessagesbyContext(c);
if (!name) {
return c.text("Please provide a name", 400)
return c.text(msgs.RequiredFieldMsg, 400)
}
try {
const res = await newAddress(c, {
@@ -67,19 +66,20 @@ api.post('/admin/new_address', async (c) => {
})
api.delete('/admin/delete_address/:id', async (c) => {
const msgs = i18n.getMessagesbyContext(c);
const { id } = c.req.param();
const { success } = await c.env.DB.prepare(
`DELETE FROM address WHERE id = ? `
).bind(id).run();
if (!success) {
return c.text("Failed to delete address", 500)
return c.text(msgs.OperationFailedMsg, 500)
}
const { success: mailSuccess } = await c.env.DB.prepare(
`DELETE FROM raw_mails WHERE address IN`
+ ` (select name from address where id = ?) `
).bind(id).run();
if (!mailSuccess) {
return c.text("Failed to delete mails", 500)
return c.text(msgs.OperationFailedMsg, 500)
}
const { success: sendAccess } = await c.env.DB.prepare(
`DELETE FROM address_sender WHERE address IN`
@@ -94,13 +94,14 @@ api.delete('/admin/delete_address/:id', async (c) => {
})
api.delete('/admin/clear_inbox/:id', async (c) => {
const msgs = i18n.getMessagesbyContext(c);
const { id } = c.req.param();
const { success: mailSuccess } = await c.env.DB.prepare(
`DELETE FROM raw_mails WHERE address IN`
+ ` (select name from address where id = ?) `
).bind(id).run();
if (!mailSuccess) {
return c.text("Failed to clear inbox", 500)
return c.text(msgs.OperationFailedMsg, 500)
}
return c.json({
success: mailSuccess
@@ -108,13 +109,14 @@ api.delete('/admin/clear_inbox/:id', async (c) => {
})
api.delete('/admin/clear_sent_items/:id', async (c) => {
const msgs = i18n.getMessagesbyContext(c);
const { id } = c.req.param();
const { success: sendboxSuccess } = await c.env.DB.prepare(
`DELETE FROM sendbox WHERE address IN`
+ ` (select name from address where id = ?) `
).bind(id).run();
if (!sendboxSuccess) {
return c.text("Failed to clear sent items", 500)
return c.text(msgs.OperationFailedMsg, 500)
}
return c.json({
success: sendboxSuccess
@@ -136,15 +138,16 @@ api.get('/admin/show_password/:id', async (c) => {
})
api.post('/admin/address/:id/reset_password', async (c) => {
const msgs = i18n.getMessagesbyContext(c);
const { id } = c.req.param();
const { password } = await c.req.json();
// 检查功能是否启用
if (!getBooleanValue(c.env.ENABLE_ADDRESS_PASSWORD)) {
return c.text("Password management is disabled", 403);
return c.text(msgs.PasswordChangeDisabledMsg, 403);
}
if (!password) {
return c.text("Password is required", 400);
return c.text(msgs.NewPasswordRequiredMsg, 400);
}
const hashedPassword = await hashPassword(password);
@@ -153,7 +156,7 @@ api.post('/admin/address/:id/reset_password', async (c) => {
).bind(hashedPassword, id).run();
if (!success) {
return c.text("Failed to reset password", 500);
return c.text(msgs.FailedUpdatePasswordMsg, 500);
}
return c.json({ success: true });
@@ -181,18 +184,19 @@ api.get('/admin/address_sender', async (c) => {
})
api.post('/admin/address_sender', async (c) => {
const msgs = i18n.getMessagesbyContext(c);
/* eslint-disable prefer-const */
let { address, address_id, balance, enabled } = await c.req.json();
/* eslint-enable prefer-const */
if (!address_id) {
return c.text("Invalid address_id", 400)
return c.text(msgs.InvalidAddressIdMsg, 400)
}
enabled = enabled ? 1 : 0;
const { success } = await c.env.DB.prepare(
`UPDATE address_sender SET enabled = ?, balance = ? WHERE id = ? `
).bind(enabled, balance, address_id).run();
if (!success) {
return c.text("Failed to update address sender", 500)
return c.text(msgs.OperationFailedMsg, 500)
}
await sendAdminInternalMail(
c, address, "Account Send Access Updated",
@@ -291,16 +295,17 @@ api.get('/admin/account_settings', async (c) => {
})
api.post('/admin/account_settings', async (c) => {
const msgs = i18n.getMessagesbyContext(c);
/** @type {{ blockList: Array<string>, sendBlockList: Array<string> }} */
const {
blockList, sendBlockList, noLimitSendAddressList,
verifiedAddressList, fromBlockList, emailRuleSettings
} = await c.req.json();
if (!blockList || !sendBlockList || !verifiedAddressList) {
return c.text("Invalid blockList or sendBlockList", 400)
return c.text(msgs.InvalidInputMsg, 400)
}
if (!c.env.SEND_MAIL && verifiedAddressList.length > 0) {
return c.text("Please enable SEND_MAIL to use verifiedAddressList", 400)
return c.text(msgs.EnableSendMailMsg, 400)
}
await saveSetting(
c, CONSTANTS.ADDRESS_BLOCK_LIST_KEY,
@@ -315,7 +320,7 @@ api.post('/admin/account_settings', async (c) => {
JSON.stringify(verifiedAddressList)
)
if (fromBlockList?.length > 0 && !c.env.KV) {
return c.text("Please enable KV to use fromBlockList", 400)
return c.text(msgs.EnableKVMsg, 400)
}
if (fromBlockList) {
await c.env.KV.put(CONSTANTS.EMAIL_KV_BLACK_LIST, JSON.stringify(fromBlockList || []))

View File

@@ -2,6 +2,7 @@ import { Context } from "hono";
import { CONSTANTS } from "../constants";
import { getJsonSetting, saveSetting } from "../utils";
import { IpBlacklistSettings } from "../ip_blacklist";
import i18n from "../i18n";
/**
* Get IP blacklist settings from database
@@ -26,55 +27,47 @@ async function getIpBlacklistSettings(c: Context<HonoCustomType>): Promise<Respo
* Save IP blacklist settings to database
*/
async function saveIpBlacklistSettings(c: Context<HonoCustomType>): Promise<Response> {
const msgs = i18n.getMessagesbyContext(c);
const settings = await c.req.json<IpBlacklistSettings>();
// Validate settings
if (typeof settings.enabled !== 'boolean') {
return c.text("Invalid enabled value", 400);
return c.text(`${msgs.InvalidIpBlacklistSettingMsg}: enabled`, 400);
}
if (!Array.isArray(settings.blacklist)) {
return c.text("Invalid blacklist value", 400);
return c.text(`${msgs.InvalidIpBlacklistSettingMsg}: blacklist`, 400);
}
if (!Array.isArray(settings.asnBlacklist)) {
return c.text("Invalid asnBlacklist value", 400);
return c.text(`${msgs.InvalidIpBlacklistSettingMsg}: asnBlacklist`, 400);
}
if (!Array.isArray(settings.fingerprintBlacklist)) {
return c.text("Invalid fingerprintBlacklist value", 400);
return c.text(`${msgs.InvalidIpBlacklistSettingMsg}: fingerprintBlacklist`, 400);
}
if (typeof settings.enableDailyLimit !== 'boolean') {
return c.text("Invalid enableDailyLimit value", 400);
return c.text(`${msgs.InvalidIpBlacklistSettingMsg}: enableDailyLimit`, 400);
}
const limit = Number(settings.dailyRequestLimit);
if (isNaN(limit) || limit < 1 || limit > 1000000) {
return c.text("Invalid dailyRequestLimit value (must be between 1 and 1000000)", 400);
return c.text(`${msgs.InvalidIpBlacklistSettingMsg}: dailyRequestLimit (1-1000000)`, 400);
}
// Add size limit
const MAX_BLACKLIST_SIZE = 1000;
if (settings.blacklist.length > MAX_BLACKLIST_SIZE) {
return c.text(
`Blacklist exceeds maximum size (${MAX_BLACKLIST_SIZE} entries)`,
400
);
return c.text(`${msgs.BlacklistExceedsMaxSizeMsg}: blacklist (${MAX_BLACKLIST_SIZE})`, 400);
}
if (settings.asnBlacklist.length > MAX_BLACKLIST_SIZE) {
return c.text(
`ASN blacklist exceeds maximum size (${MAX_BLACKLIST_SIZE} entries)`,
400
);
return c.text(`${msgs.BlacklistExceedsMaxSizeMsg}: asnBlacklist (${MAX_BLACKLIST_SIZE})`, 400);
}
if (settings.fingerprintBlacklist.length > MAX_BLACKLIST_SIZE) {
return c.text(
`Fingerprint blacklist exceeds maximum size (${MAX_BLACKLIST_SIZE} entries)`,
400
);
return c.text(`${msgs.BlacklistExceedsMaxSizeMsg}: fingerprintBlacklist (${MAX_BLACKLIST_SIZE})`, 400);
}
// Sanitize patterns (trim and remove empty strings)

View File

@@ -5,6 +5,7 @@ import { getBooleanValue, getDomains, getStringValue, getIntValue, getUserRoles,
import { unbindTelegramByAddress } from './telegram_api/common';
import { CONSTANTS } from './constants';
import { AdminWebhookSettings, WebhookMail, WebhookSettings } from './models';
import i18n from './i18n';
const DEFAULT_NAME_REGEX = /[^a-z0-9]/g;
@@ -134,6 +135,7 @@ export const newAddress = async (
sourceMeta?: string | undefined | null,
}
): Promise<{ address: string, jwt: string, password?: string | null }> => {
const msgs = i18n.getMessagesbyContext(c);
// trim whitespace and remove special characters
name = name.trim().replace(getNameRegex(c), '')
// check name
@@ -153,10 +155,10 @@ export const newAddress = async (
);
// check name length
if (name.length < minAddressLength) {
throw new Error(`Name too short (min ${minAddressLength})`);
throw new Error(`${msgs.NameTooShortMsg} (min ${minAddressLength})`);
}
if (name.length > maxAddressLength) {
throw new Error(`Name too long (max ${maxAddressLength})`);
throw new Error(`${msgs.NameTooLongMsg} (max ${maxAddressLength})`);
}
// create address with prefix
if (typeof addressPrefix === "string") {
@@ -177,7 +179,7 @@ export const newAddress = async (
}
// check domain is valid
if (!domain || !allowDomains.includes(domain)) {
throw new Error("Invalid domain")
throw new Error(msgs.InvalidDomainMsg)
}
// create address
name = name + "@" + domain;
@@ -187,7 +189,7 @@ export const newAddress = async (
`INSERT INTO address(name, source_meta) VALUES(?, ?)`
).bind(name, sourceMeta).run();
if (!result.success) {
throw new Error("Failed to create address")
throw new Error(msgs.FailedCreateAddressMsg)
}
await updateAddressUpdatedAt(c, name);
} catch (e) {
@@ -198,13 +200,13 @@ export const newAddress = async (
`INSERT INTO address(name) VALUES(?)`
).bind(name).run();
if (!result.success) {
throw new Error("Failed to create address")
throw new Error(msgs.FailedCreateAddressMsg)
}
await updateAddressUpdatedAt(c, name);
} else if (message && message.includes("UNIQUE")) {
throw new Error("Address already exists")
throw new Error(msgs.AddressAlreadyExistsMsg)
} else {
throw new Error("Failed to create address")
throw new Error(msgs.FailedCreateAddressMsg)
}
}
const address_id = await c.env.DB.prepare(
@@ -247,8 +249,9 @@ export const cleanup = async (
cleanType: string | undefined | null,
cleanDays: number | undefined | null
): Promise<boolean> => {
const msgs = i18n.getMessagesbyContext(c);
if (!cleanType || typeof cleanDays !== 'number' || cleanDays < 0 || cleanDays > 1000) {
throw new Error("Invalid cleanType or cleanDays")
throw new Error(msgs.InvalidCleanupConfigMsg)
}
console.log(`Cleanup ${cleanType} before ${cleanDays} days`);
switch (cleanType) {
@@ -294,7 +297,7 @@ export const cleanup = async (
)
break;
default:
throw new Error("Invalid cleanType")
throw new Error(msgs.InvalidCleanTypeMsg)
}
return true;
}
@@ -336,11 +339,12 @@ export const deleteAddressWithData = async (
address: string | undefined | null,
address_id: number | undefined | null
): Promise<boolean> => {
const msgs = i18n.getMessagesbyContext(c);
if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) {
throw new Error("Delete email is disabled")
throw new Error(msgs.UserDeleteEmailDisabledMsg)
}
if (!address && !address_id) {
throw new Error("Address or address_id required")
throw new Error(msgs.RequiredFieldMsg)
}
// get address_id or address
if (!address_id) {
@@ -354,7 +358,7 @@ export const deleteAddressWithData = async (
}
// check address again
if (!address || !address_id) {
throw new Error("Can't find address");
throw new Error(msgs.AddressNotFoundMsg);
}
// unbind telegram
await unbindTelegramByAddress(c, address);
@@ -378,7 +382,7 @@ export const deleteAddressWithData = async (
`DELETE FROM address WHERE name = ? `
).bind(address).run();
if (!success || !mailSuccess || !sendboxSuccess || !addressSuccess || !sendAccess || !autoReplySuccess) {
throw new Error("Failed to delete address")
throw new Error(msgs.OperationFailedMsg)
}
return true;
}
@@ -389,6 +393,7 @@ export const handleListQuery = async (
limit: string | number | undefined | null,
offset: string | number | undefined | null
): Promise<Response> => {
const msgs = i18n.getMessagesbyContext(c);
if (typeof limit === "string") {
limit = parseInt(limit);
}
@@ -396,10 +401,10 @@ export const handleListQuery = async (
offset = parseInt(offset);
}
if (!limit || limit < 0 || limit > 100) {
return c.text("Invalid limit", 400)
return c.text(msgs.InvalidLimitMsg, 400)
}
if (offset == null || offset == undefined || offset < 0) {
return c.text("Invalid offset", 400)
return c.text(msgs.InvalidOffsetMsg, 400)
}
const resultsQuery = `${query} order by id desc limit ? offset ?`;
const { results } = await c.env.DB.prepare(resultsQuery).bind(

View File

@@ -46,6 +46,129 @@ const messages: LocaleMessages = {
PasswordLoginDisabledMsg: "Password login is disabled",
EmailPasswordRequiredMsg: "Email and password are required",
AddressNotFoundMsg: "Address not found",
// Common messages (merged similar ones)
OperationFailedMsg: "Operation failed",
RequiredFieldMsg: "Required field is missing",
InvalidInputMsg: "Invalid input",
// Address related
NameTooShortMsg: "Name is too short",
NameTooLongMsg: "Name is too long",
InvalidDomainMsg: "Invalid domain",
AddressAlreadyExistsMsg: "Address already exists",
MaxAddressCountReachedMsg: "Max address count reached",
AddressNotBindedMsg: "Address is not binded",
AddressAlreadyBindedMsg: "Address is already binded, please unbind first",
TargetUserNotFoundMsg: "Target user not found",
// Send mail related
NoBalanceMsg: "No balance",
AddressBlockedMsg: "Address is blocked",
SubjectEmptyMsg: "Subject is empty",
ContentEmptyMsg: "Content is empty",
AlreadyRequestedMsg: "Already requested",
EnableResendOrSmtpMsg: "Please enable resend or smtp for this domain",
EnableResendOrSmtpWithVerifiedMsg: "Please enable resend or smtp for this domain, or add recipient to verified address list",
InvalidToMailMsg: "Invalid recipient address",
// Admin related
InvalidAddressIdMsg: "Invalid address_id",
EnableKVMsg: "Please enable KV first",
EnableSendMailMsg: "Please enable SEND_MAIL first",
InvalidCleanupConfigMsg: "Invalid cleanType or cleanDays",
InvalidCleanTypeMsg: "Invalid cleanType",
EnableKVForMailVerifyMsg: "Please enable KV first if you want to enable mail verify",
VerifyMailDomainInvalidMsg: "VerifyMailSender domain must be in",
InvalidMaxAddressCountMsg: "Invalid maxAddressCount",
FailedDeleteUserMsg: "Failed to delete user",
InvalidUserIdMsg: "Invalid user_id",
InvalidRoleTextMsg: "Invalid role_text",
// SQL validation
SqlEmptyMsg: "SQL statement is empty",
SqlTooLongMsg: "SQL statement is too long (max 1000 characters)",
SqlOnlyDeleteMsg: "Only DELETE statements are allowed",
SqlSingleStatementMsg: "Only single SQL statement is allowed",
SqlNoCommentsMsg: "SQL comments are not allowed",
// Passkey related
InvalidPasskeyNameMsg: "Invalid passkey name",
PasskeyNotFoundMsg: "Passkey not found",
AuthenticationFailedMsg: "Authentication failed",
RegistrationFailedMsg: "Registration failed",
// Auto reply related
AutoReplyDisabledMsg: "Auto reply is disabled",
InvalidAutoReplyMsg: "Invalid subject or message",
SubjectOrMessageTooLongMsg: "Subject or message is too long",
// Bind address related
NoAddressOrUserTokenMsg: "No address or user token",
InvalidAddressOrUserTokenMsg: "Invalid address or user token",
// Pagination related
InvalidLimitMsg: "Invalid limit",
InvalidOffsetMsg: "Invalid offset",
// Clear inbox/sent items related
FailedClearInboxMsg: "Failed to clear inbox",
FailedClearSentItemsMsg: "Failed to clear sent items",
// Webhook related
WebhookNotAllowedForUserMsg: "Webhook settings is not allowed for this user",
// IP blacklist related
InvalidIpBlacklistSettingMsg: "Invalid IP blacklist setting",
BlacklistExceedsMaxSizeMsg: "Blacklist exceeds maximum size",
// Telegram bot messages
TgUnableGetUserInfoMsg: "Unable to get user info",
TgNoPermissionMsg: "You don't have permission to use this bot",
TgWelcomeMsg: "Welcome! You can open the mini app",
TgCurrentPrefixMsg: "Current prefix enabled:",
TgCurrentDomainsMsg: "Available domains:",
TgAvailableCommandsMsg: "Available commands:",
TgCreateSuccessMsg: "Address created successfully:",
TgCreateFailedMsg: "Failed to create address:",
TgBindSuccessMsg: "Binding successful:",
TgBindFailedMsg: "Binding failed:",
TgUnbindSuccessMsg: "Unbinding successful:",
TgUnbindFailedMsg: "Unbinding failed:",
TgDeleteSuccessMsg: "Deleted successfully:",
TgDeleteFailedMsg: "Delete failed:",
TgAddressListMsg: "Address list:",
TgGetAddressFailedMsg: "Failed to get address list:",
TgCleanSuccessMsg: "Invalid addresses cleaned:",
TgCurrentAddressListMsg: "Current address list:",
TgCleanFailedMsg: "Failed to clean invalid addresses:",
TgNotBoundAddressMsg: "This address is not bound:",
TgInvalidAddressMsg: "Invalid address",
TgNoMoreMailsMsg: "No more mails",
TgNoMailMsg: "No mail",
TgGetMailFailedMsg: "Failed to get mail:",
TgParseMailFailedMsg: "Failed to parse mail:",
TgViewMailBtnMsg: "View Mail",
TgPrevBtnMsg: "Prev",
TgNextBtnMsg: "Next",
TgPleaseInputCredentialMsg: "Please enter credential",
TgPleaseInputAddressMsg: "Please enter address",
TgAddressMsg: "Address:",
TgPasswordMsg: "Password:",
TgCredentialMsg: "Credential:",
TgNoSenderMsg: "No sender",
TgMsgTooLongMsg: "Message too long, please view in mini app",
TgParseFailedViewInAppMsg: "Parse failed, please view in mini app",
TgMaxAddressReachedMsg: "Maximum address limit reached",
TgMaxAddressReachedCleanMsg: "Maximum address limit reached, please /cleaninvalidaddress first",
TgInvalidCredentialMsg: "Invalid credential",
TgAddressNotYoursMsg: "This address does not belong to you",
TgLangSetSuccessMsg: "Language set successfully:",
TgCurrentLangMsg: "Current language:",
TgSelectLangMsg: "Please select language:",
TgNoPermissionViewMailMsg: "No permission to view this mail",
TgBotTokenRequiredMsg: "TELEGRAM_BOT_TOKEN is required",
TgLangFeatureDisabledMsg: "Language setting feature is disabled. System default language is used.",
}
export default messages;

View File

@@ -44,4 +44,127 @@ export type LocaleMessages = {
PasswordLoginDisabledMsg: string
EmailPasswordRequiredMsg: string
AddressNotFoundMsg: string
// Common messages (merged similar ones)
OperationFailedMsg: string
RequiredFieldMsg: string
InvalidInputMsg: string
// Address related
NameTooShortMsg: string
NameTooLongMsg: string
InvalidDomainMsg: string
AddressAlreadyExistsMsg: string
MaxAddressCountReachedMsg: string
AddressNotBindedMsg: string
AddressAlreadyBindedMsg: string
TargetUserNotFoundMsg: string
// Send mail related
NoBalanceMsg: string
AddressBlockedMsg: string
SubjectEmptyMsg: string
ContentEmptyMsg: string
AlreadyRequestedMsg: string
EnableResendOrSmtpMsg: string
EnableResendOrSmtpWithVerifiedMsg: string
InvalidToMailMsg: string
// Admin related
InvalidAddressIdMsg: string
EnableKVMsg: string
EnableSendMailMsg: string
InvalidCleanupConfigMsg: string
InvalidCleanTypeMsg: string
EnableKVForMailVerifyMsg: string
VerifyMailDomainInvalidMsg: string
InvalidMaxAddressCountMsg: string
FailedDeleteUserMsg: string
InvalidUserIdMsg: string
InvalidRoleTextMsg: string
// SQL validation
SqlEmptyMsg: string
SqlTooLongMsg: string
SqlOnlyDeleteMsg: string
SqlSingleStatementMsg: string
SqlNoCommentsMsg: string
// Passkey related
InvalidPasskeyNameMsg: string
PasskeyNotFoundMsg: string
AuthenticationFailedMsg: string
RegistrationFailedMsg: string
// Auto reply related
AutoReplyDisabledMsg: string
InvalidAutoReplyMsg: string
SubjectOrMessageTooLongMsg: string
// Bind address related
NoAddressOrUserTokenMsg: string
InvalidAddressOrUserTokenMsg: string
// Pagination related
InvalidLimitMsg: string
InvalidOffsetMsg: string
// Clear inbox/sent items related
FailedClearInboxMsg: string
FailedClearSentItemsMsg: string
// Webhook related
WebhookNotAllowedForUserMsg: string
// IP blacklist related
InvalidIpBlacklistSettingMsg: string
BlacklistExceedsMaxSizeMsg: string
// Telegram bot messages
TgUnableGetUserInfoMsg: string
TgNoPermissionMsg: string
TgWelcomeMsg: string
TgCurrentPrefixMsg: string
TgCurrentDomainsMsg: string
TgAvailableCommandsMsg: string
TgCreateSuccessMsg: string
TgCreateFailedMsg: string
TgBindSuccessMsg: string
TgBindFailedMsg: string
TgUnbindSuccessMsg: string
TgUnbindFailedMsg: string
TgDeleteSuccessMsg: string
TgDeleteFailedMsg: string
TgAddressListMsg: string
TgGetAddressFailedMsg: string
TgCleanSuccessMsg: string
TgCurrentAddressListMsg: string
TgCleanFailedMsg: string
TgNotBoundAddressMsg: string
TgInvalidAddressMsg: string
TgNoMoreMailsMsg: string
TgNoMailMsg: string
TgGetMailFailedMsg: string
TgParseMailFailedMsg: string
TgViewMailBtnMsg: string
TgPrevBtnMsg: string
TgNextBtnMsg: string
TgPleaseInputCredentialMsg: string
TgPleaseInputAddressMsg: string
TgAddressMsg: string
TgPasswordMsg: string
TgCredentialMsg: string
TgNoSenderMsg: string
TgMsgTooLongMsg: string
TgParseFailedViewInAppMsg: string
TgMaxAddressReachedMsg: string
TgMaxAddressReachedCleanMsg: string
TgInvalidCredentialMsg: string
TgAddressNotYoursMsg: string
TgLangSetSuccessMsg: string
TgCurrentLangMsg: string
TgSelectLangMsg: string
TgNoPermissionViewMailMsg: string
TgBotTokenRequiredMsg: string
TgLangFeatureDisabledMsg: string
}

View File

@@ -46,6 +46,129 @@ const messages: LocaleMessages = {
PasswordLoginDisabledMsg: "密码登录已禁用",
EmailPasswordRequiredMsg: "邮箱和密码不能为空",
AddressNotFoundMsg: "邮箱地址不存在",
// Common messages (merged similar ones)
OperationFailedMsg: "操作失败",
RequiredFieldMsg: "缺少必填字段",
InvalidInputMsg: "输入无效",
// Address related
NameTooShortMsg: "名称太短",
NameTooLongMsg: "名称太长",
InvalidDomainMsg: "无效的域名",
AddressAlreadyExistsMsg: "邮箱地址已存在",
MaxAddressCountReachedMsg: "已达到最大地址数量限制",
AddressNotBindedMsg: "邮箱地址未绑定",
AddressAlreadyBindedMsg: "邮箱地址已绑定, 请先解绑",
TargetUserNotFoundMsg: "目标用户不存在",
// Send mail related
NoBalanceMsg: "余额不足",
AddressBlockedMsg: "地址已被屏蔽",
SubjectEmptyMsg: "主题不能为空",
ContentEmptyMsg: "内容不能为空",
AlreadyRequestedMsg: "已经申请过了",
EnableResendOrSmtpMsg: "请先为此域名启用 resend 或 smtp",
EnableResendOrSmtpWithVerifiedMsg: "请先为此域名启用 resend 或 smtp或将收件人添加到已验证地址列表",
InvalidToMailMsg: "收件人地址无效",
// Admin related
InvalidAddressIdMsg: "无效的 address_id",
EnableKVMsg: "请先启用 KV",
EnableSendMailMsg: "请先启用 SEND_MAIL",
InvalidCleanupConfigMsg: "无效的 cleanType 或 cleanDays",
InvalidCleanTypeMsg: "无效的 cleanType",
EnableKVForMailVerifyMsg: "如果要启用邮件验证,请先启用 KV",
VerifyMailDomainInvalidMsg: "验证邮件发送者域名必须在",
InvalidMaxAddressCountMsg: "无效的 maxAddressCount",
FailedDeleteUserMsg: "删除用户失败",
InvalidUserIdMsg: "无效的 user_id",
InvalidRoleTextMsg: "无效的 role_text",
// SQL validation
SqlEmptyMsg: "SQL 语句为空",
SqlTooLongMsg: "SQL 语句过长 (最大 1000 字符)",
SqlOnlyDeleteMsg: "只允许 DELETE 语句",
SqlSingleStatementMsg: "只允许单条 SQL 语句",
SqlNoCommentsMsg: "不允许 SQL 注释",
// Passkey related
InvalidPasskeyNameMsg: "无效的 passkey 名称",
PasskeyNotFoundMsg: "Passkey 不存在",
AuthenticationFailedMsg: "认证失败",
RegistrationFailedMsg: "注册失败",
// Auto reply related
AutoReplyDisabledMsg: "自动回复已禁用",
InvalidAutoReplyMsg: "无效的主题或消息",
SubjectOrMessageTooLongMsg: "主题或消息太长",
// Bind address related
NoAddressOrUserTokenMsg: "缺少地址或用户令牌",
InvalidAddressOrUserTokenMsg: "无效的地址或用户令牌",
// Pagination related
InvalidLimitMsg: "无效的 limit 参数",
InvalidOffsetMsg: "无效的 offset 参数",
// Clear inbox/sent items related
FailedClearInboxMsg: "清空收件箱失败",
FailedClearSentItemsMsg: "清空已发送邮件失败",
// Webhook related
WebhookNotAllowedForUserMsg: "此用户不允许使用 Webhook 设置",
// IP blacklist related
InvalidIpBlacklistSettingMsg: "无效的 IP 黑名单设置",
BlacklistExceedsMaxSizeMsg: "黑名单超出最大条目限制",
// Telegram bot messages
TgUnableGetUserInfoMsg: "无法获取用户信息",
TgNoPermissionMsg: "您没有权限使用此机器人",
TgWelcomeMsg: "欢迎使用本机器人, 您可以打开 mini app",
TgCurrentPrefixMsg: "当前已启用前缀:",
TgCurrentDomainsMsg: "当前可用域名:",
TgAvailableCommandsMsg: "请使用以下命令:",
TgCreateSuccessMsg: "创建地址成功:",
TgCreateFailedMsg: "创建地址失败:",
TgBindSuccessMsg: "绑定成功:",
TgBindFailedMsg: "绑定失败:",
TgUnbindSuccessMsg: "解绑成功:",
TgUnbindFailedMsg: "解绑失败:",
TgDeleteSuccessMsg: "删除成功:",
TgDeleteFailedMsg: "删除失败:",
TgAddressListMsg: "地址列表:",
TgGetAddressFailedMsg: "获取地址列表失败:",
TgCleanSuccessMsg: "清理无效地址成功:",
TgCurrentAddressListMsg: "当前地址列表:",
TgCleanFailedMsg: "清理无效地址失败:",
TgNotBoundAddressMsg: "未绑定此地址:",
TgInvalidAddressMsg: "无效地址",
TgNoMoreMailsMsg: "已经没有邮件了",
TgNoMailMsg: "无邮件",
TgGetMailFailedMsg: "获取邮件失败:",
TgParseMailFailedMsg: "解析邮件失败:",
TgViewMailBtnMsg: "查看邮件",
TgPrevBtnMsg: "上一条",
TgNextBtnMsg: "下一条",
TgPleaseInputCredentialMsg: "请输入凭证",
TgPleaseInputAddressMsg: "请输入地址",
TgAddressMsg: "地址:",
TgPasswordMsg: "密码:",
TgCredentialMsg: "凭证:",
TgNoSenderMsg: "无发件人",
TgMsgTooLongMsg: "消息过长请到 mini app 查看",
TgParseFailedViewInAppMsg: "解析失败,请打开 mini app 查看",
TgMaxAddressReachedMsg: "绑定地址数量已达上限",
TgMaxAddressReachedCleanMsg: "绑定地址数量已达上限, 请先 /cleaninvalidaddress",
TgInvalidCredentialMsg: "无效凭证",
TgAddressNotYoursMsg: "此地址不属于您",
TgLangSetSuccessMsg: "语言设置成功:",
TgCurrentLangMsg: "当前语言:",
TgSelectLangMsg: "请选择语言:",
TgNoPermissionViewMailMsg: "无权查看此邮件",
TgBotTokenRequiredMsg: "需要设置 TELEGRAM_BOT_TOKEN",
TgLangFeatureDisabledMsg: "语言设置功能已禁用,使用系统默认语言",
}
export default messages;

View File

@@ -7,8 +7,7 @@ export default {
// 修改地址密码
changePassword: async (c: Context<HonoCustomType>) => {
const { new_password } = await c.req.json();
const lang = c.get("lang") || c.env.DEFAULT_LANG;
const msgs = i18n.getMessages(lang);
const msgs = i18n.getMessagesbyContext(c);
const { address, address_id } = c.get("jwtPayload");
// 检查功能是否启用
@@ -39,8 +38,7 @@ export default {
// 地址密码登录
login: async (c: Context<HonoCustomType>) => {
const { email, password, 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);
// 检查功能是否启用
if (!getBooleanValue(c.env.ENABLE_ADDRESS_PASSWORD)) {

View File

@@ -1,11 +1,13 @@
import { Context } from "hono";
import { getBooleanValue } from "../utils";
import i18n from "../i18n";
export default {
getAutoReply: async (c: Context<HonoCustomType>) => {
const msgs = i18n.getMessagesbyContext(c);
if (!getBooleanValue(c.env.ENABLE_AUTO_REPLY)) {
return c.text("Auto reply is disabled", 403)
return c.text(msgs.AutoReplyDisabledMsg, 403)
}
const { address } = c.get("jwtPayload")
const results = await c.env.DB.prepare(
@@ -23,17 +25,18 @@ export default {
})
},
saveAutoReply: async (c: Context<HonoCustomType>) => {
const msgs = i18n.getMessagesbyContext(c);
if (!getBooleanValue(c.env.ENABLE_AUTO_REPLY)) {
return c.text("Auto reply is disabled", 403)
return c.text(msgs.AutoReplyDisabledMsg, 403)
}
const { address } = c.get("jwtPayload");
const { auto_reply } = await c.req.json();
const { name, subject, source_prefix, message, enabled } = auto_reply;
if ((!subject || !message) && enabled) {
return c.text("Invalid subject or message", 400)
return c.text(msgs.InvalidAutoReplyMsg, 400)
}
else if (subject.length > 255 || message.length > 255) {
return c.text("Subject or message too long", 400)
return c.text(msgs.SubjectOrMessageTooLongMsg, 400)
}
const { success } = await c.env.DB.prepare(
`INSERT OR REPLACE INTO auto_reply_mails`
@@ -44,7 +47,7 @@ export default {
subject || '', message || '', enabled ? 1 : 0
).run();
if (!success) {
return c.text("Failed to auto_reply settings", 500)
return c.text(msgs.OperationFailedMsg, 500)
}
return c.json({
success: success

View File

@@ -45,8 +45,7 @@ api.get('/api/mail/:mail_id', async (c) => {
})
api.delete('/api/mails/:id', async (c) => {
const lang = c.get("lang") || c.env.DEFAULT_LANG;
const msgs = i18n.getMessages(lang);
const msgs = i18n.getMessagesbyContext(c);
if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) {
return c.text(msgs.UserDeleteEmailDisabledMsg, 403)
}
@@ -64,8 +63,7 @@ api.delete('/api/mails/:id', async (c) => {
api.get('/api/settings', async (c) => {
const { address, address_id } = c.get("jwtPayload")
const user_role = c.get("userRolePayload")
const lang = c.get("lang") || c.env.DEFAULT_LANG;
const msgs = i18n.getMessages(lang);
const msgs = i18n.getMessagesbyContext(c);
if (address_id && address_id > 0) {
try {
const db_address_id = await c.env.DB.prepare(
@@ -106,8 +104,7 @@ api.get('/api/settings', async (c) => {
})
api.post('/api/new_address', async (c) => {
const lang = c.get("lang") || c.env.DEFAULT_LANG;
const msgs = i18n.getMessages(lang);
const msgs = i18n.getMessagesbyContext(c);
if (getBooleanValue(c.env.DISABLE_ANONYMOUS_USER_CREATE_EMAIL)
&& !c.get("userPayload")
) {
@@ -171,8 +168,7 @@ api.delete('/api/delete_address', async (c) => {
})
api.delete('/api/clear_inbox', async (c) => {
const lang = c.get("lang") || c.env.DEFAULT_LANG;
const msgs = i18n.getMessages(lang);
const msgs = i18n.getMessagesbyContext(c);
if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) {
return c.text(msgs.UserDeleteEmailDisabledMsg, 403)
}
@@ -181,7 +177,7 @@ api.delete('/api/clear_inbox', async (c) => {
`DELETE FROM raw_mails WHERE address = ?`
).bind(address).run();
if (!success) {
return c.text("Failed to clear inbox", 500)
return c.text(msgs.FailedClearInboxMsg, 500)
}
return c.json({
success: success
@@ -189,8 +185,7 @@ api.delete('/api/clear_inbox', async (c) => {
})
api.delete('/api/clear_sent_items', async (c) => {
const lang = c.get("lang") || c.env.DEFAULT_LANG;
const msgs = i18n.getMessages(lang);
const msgs = i18n.getMessagesbyContext(c);
if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) {
return c.text(msgs.UserDeleteEmailDisabledMsg, 403)
}
@@ -199,7 +194,7 @@ api.delete('/api/clear_sent_items', async (c) => {
`DELETE FROM sendbox WHERE address = ?`
).bind(address).run();
if (!success) {
return c.text("Failed to clear sent items", 500)
return c.text(msgs.FailedClearSentItemsMsg, 500)
}
return c.json({
success: success

View File

@@ -14,9 +14,10 @@ import { handleListQuery } from '../common'
export const api = new Hono<HonoCustomType>()
api.post('/api/requset_send_mail_access', async (c) => {
const msgs = i18n.getMessagesbyContext(c);
const { address } = c.get("jwtPayload")
if (!address) {
return c.text("No address", 400)
return c.text(msgs.AddressNotFoundMsg, 400)
}
try {
const default_balance = getIntValue(c.env.DEFAULT_SEND_BALANCE, 0);
@@ -26,14 +27,14 @@ api.post('/api/requset_send_mail_access', async (c) => {
address, default_balance, default_balance > 0 ? 1 : 0
).run();
if (!success) {
return c.text("Failed to request send mail access", 500)
return c.text(msgs.OperationFailedMsg, 500)
}
} catch (e) {
const message = (e as Error).message;
if (message && message.includes("UNIQUE")) {
return c.text("Already requested", 400)
return c.text(msgs.AlreadyRequestedMsg, 400)
}
return c.text("Failed to request send mail access", 500)
return c.text(msgs.OperationFailedMsg, 500)
}
return c.json({ status: "ok" })
})
@@ -126,14 +127,15 @@ export const sendMail = async (
isAdmin?: boolean
}
): Promise<void> => {
const msgs = i18n.getMessagesbyContext(c);
if (!address) {
throw new Error("No address")
throw new Error(msgs.AddressNotFoundMsg)
}
// check domain
const mailDomain = address.split("@")[1];
const domains = getDomains(c);
if (!domains.includes(mailDomain)) {
throw new Error("Invalid domain")
throw new Error(msgs.InvalidDomainMsg)
}
const user_role = c.get("userRolePayload");
const no_limit_roles = getSplitStringListValue(c.env.NO_LIMIT_SEND_ROLE);
@@ -150,7 +152,7 @@ export const sendMail = async (
where address = ? and enabled = 1`
).bind(address).first<number>("balance");
if (!balance || balance <= 0) {
throw new Error("No balance")
throw new Error(msgs.NoBalanceMsg)
}
}
const {
@@ -158,18 +160,18 @@ export const sendMail = async (
subject, content, is_html
} = reqJson;
if (!to_mail) {
throw new Error("Invalid to mail")
throw new Error(msgs.InvalidToMailMsg)
}
// check SEND_BLOCK_LIST_KEY
const sendBlockList = await getJsonSetting(c, CONSTANTS.SEND_BLOCK_LIST_KEY) as string[];
if (sendBlockList && sendBlockList.some((item) => to_mail.includes(item))) {
throw new Error("to_mail address is blocked")
throw new Error(msgs.AddressBlockedMsg)
}
if (!subject) {
throw new Error("Subject is empty")
throw new Error(msgs.SubjectEmptyMsg)
}
if (!content) {
throw new Error("Content is empty")
throw new Error(msgs.ContentEmptyMsg)
}
// send to verified address list, do not update balance
@@ -202,9 +204,9 @@ export const sendMail = async (
}
else {
if (c.env.SEND_MAIL) {
throw new Error(`Please enable resend or smtp for domain ${mailDomain}. Or add ${to_mail} to verified address list`);
throw new Error(`${msgs.EnableResendOrSmtpWithVerifiedMsg} (${mailDomain})`);
}
throw new Error(`Please enable resend or smtp for domain ${mailDomain}`);
throw new Error(`${msgs.EnableResendOrSmtpMsg} (${mailDomain})`);
}
// update balance
@@ -253,11 +255,12 @@ api.post('/api/send_mail', async (c) => {
})
api.post('/external/api/send_mail', async (c) => {
const msgs = i18n.getMessagesbyContext(c);
const { token } = await c.req.json();
try {
const { address } = await Jwt.verify(token, c.env.JWT_SECRET, "HS256");
if (!address) {
return c.text("No address", 400)
return c.text(msgs.AddressNotFoundMsg, 400)
}
const reqJson = await c.req.json();
await sendMail(c, address as string, reqJson);
@@ -289,8 +292,7 @@ api.get('/api/sendbox', async (c) => {
})
api.delete('/api/sendbox/:id', async (c) => {
const lang = c.get("lang") || c.env.DEFAULT_LANG;
const msgs = i18n.getMessages(lang);
const msgs = i18n.getMessagesbyContext(c);
if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) {
return c.text(msgs.UserDeleteEmailDisabledMsg, 403)
}

View File

@@ -2,13 +2,15 @@ import { Context } from "hono";
import { CONSTANTS } from "../constants";
import { AdminWebhookSettings, WebhookSettings } from "../models";
import { commonParseMail, sendWebhook } from "../common";
import i18n from "../i18n";
async function getWebhookSettings(c: Context<HonoCustomType>): Promise<Response> {
const msgs = i18n.getMessagesbyContext(c);
const { address } = c.get("jwtPayload")
const adminSettings = await c.env.KV.get<AdminWebhookSettings>(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json");
if (adminSettings?.enableAllowList && !adminSettings?.allowList.includes(address)) {
return c.text("Webhook settings is not allowed for this user", 403);
return c.text(msgs.WebhookNotAllowedForUserMsg, 403);
}
const settings = await c.env.KV.get<WebhookSettings>(
`${CONSTANTS.WEBHOOK_KV_USER_SETTINGS_KEY}:${address}`, "json"
@@ -18,10 +20,11 @@ async function getWebhookSettings(c: Context<HonoCustomType>): Promise<Response>
async function saveWebhookSettings(c: Context<HonoCustomType>): Promise<Response> {
const msgs = i18n.getMessagesbyContext(c);
const { address } = c.get("jwtPayload")
const adminSettings = await c.env.KV.get<AdminWebhookSettings>(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json");
if (adminSettings?.enableAllowList && !adminSettings?.allowList.includes(address)) {
return c.text("Webhook settings is not allowed for this user", 403);
return c.text(msgs.WebhookNotAllowedForUserMsg, 403);
}
const settings = await c.req.json<WebhookSettings>();
await c.env.KV.put(

View File

@@ -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;

View File

@@ -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",
});

View File

@@ -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);

View File

@@ -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,
])
});
}

View File

@@ -84,6 +84,7 @@ type Bindings = {
TELEGRAM_BOT_TOKEN: string
TG_MAX_ADDRESS: number | undefined
TG_BOT_INFO: string | object | undefined
TG_ALLOW_USER_LANG: string | boolean | undefined
// webhook config
FRONTEND_URL: string | undefined

View File

@@ -32,22 +32,23 @@ const UserBindAddressModule = {
c: Context<HonoCustomType>,
user_id: number | string, address_id: number | string
) => {
const msgs = i18n.getMessagesbyContext(c);
if (!address_id || !user_id) {
return c.text("No address or user token", 400)
return c.text(msgs.NoAddressOrUserTokenMsg, 400)
}
// check if address exists
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("Address not found", 400)
return c.text(msgs.AddressNotFoundMsg, 400)
}
// check if user exists
const db_user_id = await c.env.DB.prepare(
`SELECT id FROM users where id = ?`
).bind(user_id).first("id");
if (!db_user_id) {
return c.text("User not found", 400)
return c.text(msgs.UserNotFoundMsg, 400)
}
// check if binded
const db_user_address_id = await c.env.DB.prepare(
@@ -66,7 +67,7 @@ const UserBindAddressModule = {
`SELECT COUNT(*) as count FROM users_address where user_id = ?`
).bind(user_id).first<{ count: number }>() || { count: 0 };
if (count >= maxAddressCount) {
return c.text("Max address count reached", 400)
return c.text(msgs.MaxAddressCountReachedMsg, 400)
}
}
// bind
@@ -75,36 +76,37 @@ const UserBindAddressModule = {
`INSERT INTO users_address (user_id, address_id) VALUES (?, ?)`
).bind(user_id, address_id).run();
if (!success) {
return c.text("Failed to bind", 500)
return c.text(msgs.OperationFailedMsg, 500)
}
} catch (e) {
const error = e as Error;
if (error.message && error.message.includes("UNIQUE")) {
return c.text("Address already binded, please unbind first", 400)
return c.text(msgs.AddressAlreadyBindedMsg, 400)
}
return c.text("Failed to bind", 500)
return c.text(msgs.OperationFailedMsg, 500)
}
return c.json({ success: true })
},
unbind: async (c: Context<HonoCustomType>) => {
const msgs = i18n.getMessagesbyContext(c);
const { user_id } = c.get("userPayload");
const { address_id } = await c.req.json();
if (!address_id || !user_id) {
return c.text("Invalid address or user token", 400)
return c.text(msgs.InvalidAddressOrUserTokenMsg, 400)
}
// check if address exists
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("Address not found", 400)
return c.text(msgs.AddressNotFoundMsg, 400)
}
// check if user exists
const db_user_id = await c.env.DB.prepare(
`SELECT id FROM users where id = ?`
).bind(user_id).first("id");
if (!db_user_id) {
return c.text("User not found", 400)
return c.text(msgs.UserNotFoundMsg, 400)
}
// unbind
try {
@@ -112,10 +114,10 @@ const UserBindAddressModule = {
`DELETE FROM users_address where user_id = ? and address_id = ?`
).bind(user_id, address_id).run();
if (!success) {
return c.text("Failed to unbind", 500)
return c.text(msgs.OperationFailedMsg, 500)
}
} catch (e) {
return c.text("Failed to unbind", 500)
return c.text(msgs.OperationFailedMsg, 500)
}
return c.json({ success: true })
},
@@ -167,18 +169,19 @@ const UserBindAddressModule = {
return results || [];
},
getBindedAddressJwt: async (c: Context<HonoCustomType>) => {
const msgs = i18n.getMessagesbyContext(c);
const { address_id } = c.req.param();
// check binded
const { user_id } = c.get("userPayload");
if (!address_id || !user_id) {
return c.text("Invalid address or user token", 400)
return c.text(msgs.InvalidAddressOrUserTokenMsg, 400)
}
// check users_address if address binded
const db_user_id = await c.env.DB.prepare(
`SELECT user_id FROM users_address WHERE address_id = ? and user_id = ?`
).bind(address_id, user_id).first("user_id");
if (!db_user_id) {
return c.text("Address not binded", 400)
return c.text(msgs.AddressNotBindedMsg, 400)
}
// generate jwt
const name = await c.env.DB.prepare(
@@ -193,6 +196,7 @@ const UserBindAddressModule = {
})
},
transferAddress: async (c: Context<HonoCustomType>) => {
const msgs = i18n.getMessagesbyContext(c);
const { user_id } = c.get("userPayload");
const { address_id, target_user_email } = await c.req.json();
// check if address exists
@@ -200,21 +204,21 @@ const UserBindAddressModule = {
`SELECT name FROM address where id = ?`
).bind(address_id).first<string>("name");
if (!address) {
return c.text("Address not found", 400)
return c.text(msgs.AddressNotFoundMsg, 400)
}
// check if user exists
const db_user_id = await c.env.DB.prepare(
`SELECT id FROM users where id = ?`
).bind(user_id).first("id");
if (!db_user_id) {
return c.text("User not found", 400)
return c.text(msgs.UserNotFoundMsg, 400)
}
// check if target user exists
const target_user_id = await c.env.DB.prepare(
`SELECT id FROM users where user_email = ?`
).bind(target_user_email).first<number>("id");
if (!target_user_id) {
return c.text("Target user not found", 400)
return c.text(msgs.TargetUserNotFoundMsg, 400)
}
// check target user binded address count
const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY);
@@ -228,14 +232,14 @@ const UserBindAddressModule = {
`SELECT COUNT(*) as count FROM users_address where user_id = ?`
).bind(target_user_id).first<{ count: number }>() || { count: 0 };
if (count >= maxAddressCount) {
return c.text("Target User Max address count reached", 400)
return c.text(msgs.MaxAddressCountReachedMsg, 400)
}
}
// check if binded
const db_user_address_id = await c.env.DB.prepare(
`SELECT user_id FROM users_address where user_id = ? and address_id = ?`
).bind(user_id, address_id).first("user_id");
if (!db_user_address_id) return c.text("Address not binded", 400)
if (!db_user_address_id) return c.text(msgs.AddressNotBindedMsg, 400)
// unbind telegram address
await unbindTelegramByAddress(c, address);
// unbind user address
@@ -244,10 +248,10 @@ const UserBindAddressModule = {
`DELETE FROM users_address where user_id = ? and address_id = ?`
).bind(user_id, address_id).run();
if (!success) {
return c.text("Failed to unbind", 500)
return c.text(msgs.OperationFailedMsg, 500)
}
} catch (e) {
return c.text("Failed to unbind user", 500)
return c.text(msgs.OperationFailedMsg, 500)
}
// delete address
await c.env.DB.prepare(
@@ -258,7 +262,7 @@ const UserBindAddressModule = {
`INSERT INTO address(name) VALUES(?)`
).bind(address).run();
if (!newAddressSuccess) {
throw new Error("Failed to create address")
throw new Error(msgs.FailedCreateAddressMsg)
}
await updateAddressUpdatedAt(c, address);
// find new address id
@@ -266,7 +270,7 @@ const UserBindAddressModule = {
`SELECT id FROM address WHERE name = ?`
).bind(address).first<number | null | undefined>("id");
if (!new_address_id) {
throw new Error("Failed to find new address id")
throw new Error(msgs.OperationFailedMsg)
}
// bind
try {
@@ -274,14 +278,14 @@ const UserBindAddressModule = {
`INSERT INTO users_address (user_id, address_id) VALUES (?, ?)`
).bind(target_user_id, new_address_id).run();
if (!success) {
return c.text("Failed to bind", 500)
return c.text(msgs.OperationFailedMsg, 500)
}
} catch (e) {
const error = e as Error;
if (error.message && error.message.includes("UNIQUE")) {
return c.text("Address already binded, please unbind first", 400)
return c.text(msgs.AddressAlreadyBindedMsg, 400)
}
return c.text("Failed to bind", 500)
return c.text(msgs.OperationFailedMsg, 500)
}
return c.json({ success: true })
}

View File

@@ -11,8 +11,7 @@ export default {
getOauth2LoginUrl: async (c: Context<HonoCustomType>) => {
const settings = await getJsonSetting<UserOauth2Settings[]>(c, CONSTANTS.OAUTH2_SETTINGS_KEY);
const { clientID, state } = c.req.query();
const lang = c.get("lang") || c.env.DEFAULT_LANG;
const msgs = i18n.getMessages(lang);
const msgs = i18n.getMessagesbyContext(c);
const setting = settings?.find(s => s.clientID === clientID);
if (!setting) {
return c.text(msgs.Oauth2ClientIDNotFoundMsg, 400);
@@ -22,8 +21,7 @@ export default {
},
oauth2Login: async (c: Context<HonoCustomType>) => {
const { clientID, code } = await c.req.json<{ clientID?: string, code?: string }>();
const lang = c.get("lang") || c.env.DEFAULT_LANG;
const msgs = i18n.getMessages(lang);
const msgs = i18n.getMessagesbyContext(c);
if (!clientID || !code) {
return c.text(msgs.Oauth2CliendIDOrCodeMissingMsg, 400);
}

View File

@@ -10,6 +10,7 @@ import {
import { Passkey } from '../models';
import { PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types';
import { isoBase64URL } from '@simplewebauthn/server/helpers';
import i18n from '../i18n';
export default {
getPassKeys: async (c: Context<HonoCustomType>) => {
@@ -20,10 +21,11 @@ export default {
return c.json(results);
},
renamePassKey: async (c: Context<HonoCustomType>) => {
const msgs = i18n.getMessagesbyContext(c);
const user = c.get("userPayload");
const { passkey_id, passkey_name } = await c.req.json();
if (!passkey_name || passkey_name.length > 255) {
return c.text("Invalid passkey name", 400);
return c.text(msgs.InvalidPasskeyNameMsg, 400);
}
const { success } = await c.env.DB.prepare(
`UPDATE user_passkeys SET passkey_name = ? WHERE user_id = ? AND passkey_id = ?`
@@ -71,6 +73,7 @@ export default {
return c.json(options);
},
registerResponse: async (c: Context<HonoCustomType>) => {
const msgs = i18n.getMessagesbyContext(c);
const user = c.get("userPayload");
const { credential, origin, passkey_name } = await c.req.json();
// Verify the registration response
@@ -90,7 +93,7 @@ export default {
const { verified, registrationInfo } = verification;
if (!verified || !registrationInfo) {
return c.text("Registration failed", 400);
return c.text(msgs.RegistrationFailedMsg, 400);
}
const {
@@ -131,10 +134,11 @@ export default {
return c.json(options);
},
authenticateResponse: async (c: Context<HonoCustomType>) => {
const msgs = i18n.getMessagesbyContext(c);
const { domain, credential, origin } = await c.req.json();
const passkey_id = credential?.id;
if (!passkey_id) {
return c.text("Invalid request", 400);
return c.text(msgs.InvalidInputMsg, 400);
}
const { user_id, counter, passkey } = await c.env.DB.prepare(
`SELECT user_id, counter, passkey FROM user_passkeys WHERE passkey_id = ?`
@@ -142,7 +146,7 @@ export default {
counter: number; passkey: string; user_id: number;
}>() || {};
if (!passkey) {
return c.text("Passkey not found", 404);
return c.text(msgs.PasskeyNotFoundMsg, 404);
}
const passkeyData = JSON.parse(passkey) as Passkey;
// Verify the registration response
@@ -166,7 +170,7 @@ export default {
});
const { verified, authenticationInfo } = verification;
if (!verified) {
return c.text("Authentication failed", 400);
return c.text(msgs.AuthenticationFailedMsg, 400);
}
if (authenticationInfo) {
@@ -186,7 +190,7 @@ export default {
`SELECT user_email FROM users WHERE id = ?`
).bind(user_id).first<{ user_email: string }>() || {};
if (!user_email) {
return c.text("User not found", 404);
return c.text(msgs.UserNotFoundMsg, 404);
}
// create jwt
const jwt = await Jwt.sign({

View File

@@ -31,8 +31,7 @@ export default {
},
settings: async (c: Context<HonoCustomType>) => {
const user = c.get("userPayload");
const lang = c.get("lang") || c.env.DEFAULT_LANG;
const msgs = i18n.getMessages(lang);
const msgs = i18n.getMessagesbyContext(c);
// check if user exists
const db_user_id = await c.env.DB.prepare(
`SELECT id FROM users where id = ?`

View File

@@ -10,8 +10,7 @@ import { sendMail } from "../mails_api/send_mail_api";
export default {
verifyCode: async (c: Context<HonoCustomType>) => {
const { email, 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);
@@ -61,8 +60,7 @@ export default {
register: async (c: Context<HonoCustomType>) => {
const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY);
const settings = new UserSettings(value)
const lang = c.get("lang") || c.env.DEFAULT_LANG;
const msgs = i18n.getMessages(lang);
const msgs = i18n.getMessagesbyContext(c);
// check enable
if (!settings.enable) {
return c.text(msgs.UserRegistrationDisabledMsg, 403);
@@ -154,8 +152,7 @@ export default {
},
login: async (c: Context<HonoCustomType>) => {
const { email, password } = await c.req.json();
const lang = c.get("lang") || c.env.DEFAULT_LANG;
const msgs = i18n.getMessages(lang);
const msgs = i18n.getMessagesbyContext(c);
if (!email || !password) return c.text(msgs.InvalidEmailOrPasswordMsg, 400);
const { id: user_id, password: dbPassword } = await c.env.DB.prepare(
`SELECT id, password FROM users where user_email = ?`