From bf3c372d8cd1840b294c4f8e7f25359b2c9070d4 Mon Sep 17 00:00:00 2001 From: Dream Hunter Date: Sat, 25 May 2024 14:07:00 +0800 Subject: [PATCH] feat: telegram bot global push (#269) --- CHANGELOG.md | 6 ++ frontend/src/views/admin/AccountSettings.vue | 11 ++- frontend/src/views/admin/Telegram.vue | 24 ++++- vitepress-docs/docs/en/cli.md | 5 ++ vitepress-docs/docs/zh/guide/cli/worker.md | 5 ++ .../{admin_user_api.js => admin_user_api.ts} | 31 ++++--- .../{cleanup_api.js => cleanup_api.ts} | 11 ++- worker/src/admin_api/{index.js => index.ts} | 33 ++++--- worker/src/admin_api/webhook_settings.ts | 2 +- worker/src/constants.ts | 1 + worker/src/mails_api/send_mail_api.ts | 39 ++++++++- worker/src/mails_api/webhook_settings.ts | 2 +- worker/src/models/index.js | 87 ------------------- worker/src/models/{models.ts => index.ts} | 34 ++++++++ worker/src/scheduled.ts | 2 +- worker/src/telegram_api/miniapp.ts | 8 +- worker/src/telegram_api/settings.ts | 11 ++- worker/src/telegram_api/telegram.ts | 9 ++ worker/src/types.d.ts | 1 + .../{bind_address.js => bind_address.ts} | 15 ++-- worker/src/user_api/index.ts | 3 - .../src/user_api/{settings.js => settings.ts} | 7 +- worker/src/user_api/{user.js => user.ts} | 28 ++++-- worker/wrangler.toml.template | 4 + 24 files changed, 232 insertions(+), 147 deletions(-) rename worker/src/admin_api/{admin_user_api.js => admin_user_api.ts} (79%) rename worker/src/admin_api/{cleanup_api.js => cleanup_api.ts} (72%) rename worker/src/admin_api/{index.js => index.ts} (89%) delete mode 100644 worker/src/models/index.js rename worker/src/models/{models.ts => index.ts} (68%) rename worker/src/user_api/{bind_address.js => bind_address.ts} (91%) rename worker/src/user_api/{settings.js => settings.ts} (79%) rename worker/src/user_api/{user.js => user.ts} (85%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f8f35d1..e49908ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # CHANGE LOG +## main branch + +- UI lazy load +- telegram bot 添加用户全局推送功能 +- 增加对 cloudflare verified 用户发送邮件 + ## v0.4.4 - 增加 telegram mini app diff --git a/frontend/src/views/admin/AccountSettings.vue b/frontend/src/views/admin/AccountSettings.vue index 85db40a4..892387d7 100644 --- a/frontend/src/views/admin/AccountSettings.vue +++ b/frontend/src/views/admin/AccountSettings.vue @@ -17,6 +17,7 @@ const { t } = useI18n({ address_block_list: 'Address Block Keywords for Users(Admin can skip)', address_block_list_placeholder: 'Please enter the keywords you want to block', send_address_block_list: 'Address Block Keywords for send email', + verified_address_list: 'Verified Address List(Can send email by cf internal api)', }, zh: { save: '保存', @@ -24,18 +25,21 @@ const { t } = useI18n({ address_block_list: '邮件地址屏蔽关键词(管理员可跳过检查)', address_block_list_placeholder: '请输入您想要屏蔽的关键词', send_address_block_list: '发送邮件地址屏蔽关键词', + verified_address_list: '已验证地址列表(可通过 cf 内部 api 发送邮件)', } } }); const addressBlockList = ref([]) const sendAddressBlockList = ref([]) +const verifiedAddressList = ref([]) const fetchData = async () => { try { const res = await api.fetch(`/admin/account_settings`) addressBlockList.value = res.blockList || [] sendAddressBlockList.value = res.sendBlockList || [] + verifiedAddressList.value = res.verifiedAddressList || [] } catch (error) { message.error(error.message || "error"); } @@ -47,7 +51,8 @@ const save = async () => { method: 'POST', body: JSON.stringify({ blockList: addressBlockList.value || [], - sendBlockList: sendAddressBlockList.value || [] + sendBlockList: sendAddressBlockList.value || [], + verifiedAddressList: verifiedAddressList.value || [] }) }) message.success(t('successTip')) @@ -73,6 +78,10 @@ onMounted(async () => { + + + {{ t('save') }} diff --git a/frontend/src/views/admin/Telegram.vue b/frontend/src/views/admin/Telegram.vue index 6a8e0998..2a15516a 100644 --- a/frontend/src/views/admin/Telegram.vue +++ b/frontend/src/views/admin/Telegram.vue @@ -23,6 +23,8 @@ const { t } = useI18n({ telegramAllowList: 'Telegram Allow List', save: 'Save', miniAppUrl: 'Telegram Mini App URL', + enableGlobalMailPush: 'Enable Global Mail Push(Manually input telegram user ID)', + globalMailPushList: 'Global Mail Push List', }, zh: { init: '初始化', @@ -33,6 +35,8 @@ const { t } = useI18n({ telegramAllowList: 'Telegram 白名单', save: '保存', miniAppUrl: '电报小程序 URL(请输入你部署的电报小程序网页地址)', + enableGlobalMailPush: '启用全局邮件推送(手动输入 telegram 用户 ID)', + globalMailPushList: '全局邮件推送用户列表', } } }); @@ -66,15 +70,22 @@ class TelegramSettings { enableAllowList: boolean; allowList: string[]; miniAppUrl: string; + enableGlobalMailPush: boolean; + globalMailPushList: string[]; - constructor(enableAllowList: boolean, allowList: string[], miniAppUrl: string) { + constructor( + enableAllowList: boolean, allowList: string[], miniAppUrl: string, + enableGlobalMailPush: boolean, globalMailPushList: string[] + ) { this.enableAllowList = enableAllowList; this.allowList = allowList; this.miniAppUrl = miniAppUrl; + this.enableGlobalMailPush = enableGlobalMailPush; + this.globalMailPushList = globalMailPushList; } } -const settings = ref(new TelegramSettings(false, [], '')) +const settings = ref(new TelegramSettings(false, [], '', false, [])) const getSettings = async () => { try { @@ -115,6 +126,15 @@ onMounted(async () => { :placeholder="t('telegramAllowList')" /> + + + + {{ t('enable') }} + + + + diff --git a/vitepress-docs/docs/en/cli.md b/vitepress-docs/docs/en/cli.md index 2f929fc1..9f608ef8 100644 --- a/vitepress-docs/docs/en/cli.md +++ b/vitepress-docs/docs/en/cli.md @@ -68,6 +68,11 @@ node_compat = true # [triggers] # crons = [ "0 0 * * *" ] +# send mail by cf mail +# send_email = [ +# { name = "SEND_MAIL" }, +# ] + [vars] PREFIX = "tmp" # The mailbox name prefix to be processed # If you want your site to be private, uncomment below and change your password diff --git a/vitepress-docs/docs/zh/guide/cli/worker.md b/vitepress-docs/docs/zh/guide/cli/worker.md index 7623667b..6740faff 100644 --- a/vitepress-docs/docs/zh/guide/cli/worker.md +++ b/vitepress-docs/docs/zh/guide/cli/worker.md @@ -36,6 +36,11 @@ node_compat = true # [triggers] # crons = [ "0 0 * * *" ] +# 通过 Cloudflare 发送邮件 +# send_email = [ +# { name = "SEND_MAIL" }, +# ] + [vars] PREFIX = "tmp" # 要处理的邮箱名称前缀,不需要后缀可配置为空字符串 # 如果你想要你的网站私有,取消下面的注释,并修改密码 diff --git a/worker/src/admin_api/admin_user_api.js b/worker/src/admin_api/admin_user_api.ts similarity index 79% rename from worker/src/admin_api/admin_user_api.js rename to worker/src/admin_api/admin_user_api.ts index 8a8ba644..bae73dc1 100644 --- a/worker/src/admin_api/admin_user_api.js +++ b/worker/src/admin_api/admin_user_api.ts @@ -1,21 +1,27 @@ +import { Context } from 'hono'; + import { CONSTANTS } from '../constants'; import { getJsonSetting, saveSetting, checkUserPassword, getDomains } from '../utils'; import { UserSettings, GeoData, UserInfo } from "../models"; import { handleListQuery } from '../common' +import { HonoCustomType } from '../types'; export default { - getSetting: async (c) => { + getSetting: async (c: Context) => { const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY); const settings = new UserSettings(value); return c.json(settings) }, - saveSetting: async (c) => { + saveSetting: async (c: Context) => { 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) } - if (settings.enableMailVerify) { + if (settings.enableMailVerify && !settings.verifyMailSender) { + return c.text("Please provide verifyMailSender", 400) + } + if (settings.enableMailVerify && settings.verifyMailSender) { const mailDomain = settings.verifyMailSender.split("@")[1]; const domains = getDomains(c); if (!domains.includes(mailDomain)) { @@ -28,7 +34,7 @@ export default { await saveSetting(c, CONSTANTS.USER_SETTINGS_KEY, JSON.stringify(settings)); return c.json({ success: true }) }, - getUsers: async (c) => { + getUsers: async (c: Context) => { const { limit, offset, query } = c.req.query(); if (query) { return await handleListQuery(c, @@ -48,15 +54,15 @@ export default { [], limit, offset ); }, - createUser: async (c) => { + createUser: async (c: Context) => { const { email, password } = await c.req.json(); if (!email || !password) { return c.text("Invalid email or password", 400) } // geo data const reqIp = c.req.raw.headers.get("cf-connecting-ip") - const geoData = new GeoData(reqIp, c.req.raw.cf); - const userInfo = new UserInfo(geoData); + const geoData = new GeoData(reqIp, c.req.raw.cf as any); + const userInfo = new UserInfo(geoData, email); try { checkUserPassword(password); const { success } = await c.env.DB.prepare( @@ -69,14 +75,15 @@ export default { return c.text("Failed to register", 500) } } catch (e) { - if (e.message && e.message.includes("UNIQUE")) { + const errorMsg = (e as Error).message; + if (errorMsg && errorMsg.includes("UNIQUE")) { return c.text("User already exists", 400) } - return c.text(`Failed to register: ${e.message}`, 500) + return c.text(`Failed to register: ${errorMsg}`, 500) } return c.json({ success: true }) }, - deleteUser: async (c) => { + deleteUser: async (c: Context) => { const { user_id } = c.req.param(); if (!user_id) return c.text("Invalid user_id", 400); const { success } = await c.env.DB.prepare( @@ -90,7 +97,7 @@ export default { } return c.json({ success: true }) }, - resetPassword: async (c) => { + resetPassword: async (c: Context) => { const { user_id } = c.req.param(); const { password } = await c.req.json(); if (!user_id) return c.text("Invalid user_id", 400); @@ -103,7 +110,7 @@ export default { return c.text("Failed to reset password", 500) } } catch (e) { - return c.text(`Failed to reset password: ${e.message}`, 500) + return c.text(`Failed to reset password: ${(e as Error).message}`, 500) } return c.json({ success: true }); }, diff --git a/worker/src/admin_api/cleanup_api.js b/worker/src/admin_api/cleanup_api.ts similarity index 72% rename from worker/src/admin_api/cleanup_api.js rename to worker/src/admin_api/cleanup_api.ts index a2bdc7f3..65a89703 100644 --- a/worker/src/admin_api/cleanup_api.js +++ b/worker/src/admin_api/cleanup_api.ts @@ -1,25 +1,28 @@ +import { Context } from 'hono'; + import { cleanup } from '../common'; import { CONSTANTS } from '../constants'; import { getJsonSetting, saveSetting } from '../utils'; import { CleanupSettings } from '../models'; +import { HonoCustomType } from '../types'; export default { - cleanup: async (c) => { + cleanup: async (c: Context) => { 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.message}`, 500) + return c.text(`Failed to cleanup ${(error as Error).message}`, 500) } return c.json({ success: true }) }, - getCleanup: async (c) => { + getCleanup: async (c: Context) => { const value = await getJsonSetting(c, CONSTANTS.AUTO_CLEANUP_KEY); const cleanupSetting = new CleanupSettings(value); return c.json(cleanupSetting) }, - saveCleanup: async (c) => { + saveCleanup: async (c: Context) => { const value = await c.req.json(); const cleanupSetting = new CleanupSettings(value); await saveSetting(c, CONSTANTS.AUTO_CLEANUP_KEY, JSON.stringify(cleanupSetting)); diff --git a/worker/src/admin_api/index.js b/worker/src/admin_api/index.ts similarity index 89% rename from worker/src/admin_api/index.js rename to worker/src/admin_api/index.ts index 3cca9deb..63e5bc81 100644 --- a/worker/src/admin_api/index.js +++ b/worker/src/admin_api/index.ts @@ -1,5 +1,7 @@ import { Hono } from 'hono' import { Jwt } from 'hono/utils/jwt' + +import { HonoCustomType } from '../types' import { sendAdminInternalMail, getJsonSetting, saveSetting } from '../utils' import { newAddress, handleListQuery } from '../common' import { CONSTANTS } from '../constants' @@ -7,7 +9,7 @@ import cleanup_api from './cleanup_api' import admin_user_api from './admin_user_api' import webhook_settings from './webhook_settings' -const api = new Hono() +export const api = new Hono() api.get('/admin/address', async (c) => { const { limit, offset, query } = c.req.query(); @@ -41,7 +43,7 @@ api.post('/admin/new_address', async (c) => { const res = await newAddress(c, name, domain, enablePrefix); return c.json(res); } catch (e) { - return c.text(`Failed create address: ${e.message}`, 400) + return c.text(`Failed create address: ${(e as Error).message}`, 400) } }) @@ -181,16 +183,16 @@ api.get('/admin/sendbox', async (c) => { api.get('/admin/statistics', async (c) => { const { count: mailCount } = await c.env.DB.prepare( `SELECT count(*) as count FROM raw_mails` - ).first(); + ).first<{ count: number }>() || {}; const { count: addressCount } = await c.env.DB.prepare( `SELECT count(*) as count FROM address` - ).first(); + ).first<{ count: number }>() || {}; const { count: activeUserCount7days } = await c.env.DB.prepare( `SELECT count(*) as count FROM address where updated_at > datetime('now', '-7 day')` - ).first(); + ).first<{ count: number }>() || {}; const { count: sendMailCount } = await c.env.DB.prepare( `SELECT count(*) as count FROM sendbox` - ).first(); + ).first<{ count: number }>() || {}; return c.json({ mailCount: mailCount, userCount: addressCount, @@ -201,13 +203,13 @@ api.get('/admin/statistics', async (c) => { api.get('/admin/account_settings', async (c) => { try { - /** @type {Array|undefined|null} */ const blockList = await getJsonSetting(c, CONSTANTS.ADDRESS_BLOCK_LIST_KEY); - /** @type {Array|undefined|null} */ const sendBlockList = await getJsonSetting(c, CONSTANTS.SEND_BLOCK_LIST_KEY); + const verifiedAddressList = await getJsonSetting(c, CONSTANTS.VERIFIED_ADDRESS_LIST_KEY); return c.json({ blockList: blockList || [], - sendBlockList: sendBlockList || [] + sendBlockList: sendBlockList || [], + verifiedAddressList: verifiedAddressList || [] }) } catch (error) { console.error(error); @@ -217,10 +219,13 @@ api.get('/admin/account_settings', async (c) => { api.post('/admin/account_settings', async (c) => { /** @type {{ blockList: Array, sendBlockList: Array }} */ - const { blockList, sendBlockList } = await c.req.json(); - if (!blockList || !sendBlockList) { + const { blockList, sendBlockList, verifiedAddressList } = await c.req.json(); + if (!blockList || !sendBlockList || !verifiedAddressList) { return c.text("Invalid blockList or sendBlockList", 400) } + if (!c.env.SEND_MAIL && verifiedAddressList.length > 0) { + return c.text("Please enable SEND_MAIL to use verifiedAddressList", 400) + } await saveSetting( c, CONSTANTS.ADDRESS_BLOCK_LIST_KEY, JSON.stringify(blockList) @@ -229,6 +234,10 @@ api.post('/admin/account_settings', async (c) => { c, CONSTANTS.SEND_BLOCK_LIST_KEY, JSON.stringify(sendBlockList) ); + await saveSetting( + c, CONSTANTS.VERIFIED_ADDRESS_LIST_KEY, + JSON.stringify(verifiedAddressList) + ) return c.json({ success: true }) @@ -245,5 +254,3 @@ api.post('/admin/users', admin_user_api.createUser) api.post('/admin/users/:user_id/reset_password', admin_user_api.resetPassword) api.get("/admin/webhook/settings", webhook_settings.getWebhookSettings); api.post("/admin/webhook/settings", webhook_settings.saveWebhookSettings); - -export { api } diff --git a/worker/src/admin_api/webhook_settings.ts b/worker/src/admin_api/webhook_settings.ts index 8cc7b1e0..0834a6ba 100644 --- a/worker/src/admin_api/webhook_settings.ts +++ b/worker/src/admin_api/webhook_settings.ts @@ -1,7 +1,7 @@ import { Context } from "hono"; import { HonoCustomType } from "../types"; import { CONSTANTS } from "../constants"; -import { AdminWebhookSettings } from "../models/models"; +import { AdminWebhookSettings } from "../models"; async function getWebhookSettings(c: Context): Promise { const settings = await c.env.KV.get(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json"); diff --git a/worker/src/constants.ts b/worker/src/constants.ts index b0edfe87..572d70d0 100644 --- a/worker/src/constants.ts +++ b/worker/src/constants.ts @@ -6,6 +6,7 @@ export const CONSTANTS = { SEND_BLOCK_LIST_KEY: 'send_block_list', AUTO_CLEANUP_KEY: 'auto_cleanup', USER_SETTINGS_KEY: 'user_settings', + VERIFIED_ADDRESS_LIST_KEY: 'verified_address_list', // KV TG_KV_PREFIX: "temp-mail-telegram", diff --git a/worker/src/mails_api/send_mail_api.ts b/worker/src/mails_api/send_mail_api.ts index 5179cdcc..262fd15f 100644 --- a/worker/src/mails_api/send_mail_api.ts +++ b/worker/src/mails_api/send_mail_api.ts @@ -1,8 +1,10 @@ import { Context, Hono } from 'hono' import { Jwt } from 'hono/utils/jwt' +import { createMimeMessage } from 'mimetext'; + import { CONSTANTS } from '../constants' import { getJsonSetting, getDomains, getIntValue } from '../utils'; -import { GeoData } from '../models/models' +import { GeoData } from '../models' import { handleListQuery } from '../common' import { HonoCustomType } from '../types'; @@ -34,6 +36,30 @@ api.post('/api/requset_send_mail_access', async (c) => { return c.json({ status: "ok" }) }) +export const sendMailToVerifyAddress = async ( + c: Context, address: string, + reqJson: { + from_name: string, to_mail: string, to_name: string, + subject: string, content: string, is_html: boolean + } +) => { + const { + from_name, to_mail, to_name, + subject, content, is_html + } = reqJson; + const msg = createMimeMessage(); + msg.setSender({ name: from_name, addr: address }); + msg.setRecipient({ name: to_name, addr: to_mail }); + msg.setSubject(subject); + msg.addMessage({ + contentType: is_html ? 'text/html' : 'text/plain', + data: content + }); + const { EmailMessage } = await import('cloudflare:email'); + const message = new EmailMessage(address, to_mail, msg.asRaw()); + await c.env.SEND_MAIL.send(message); +} + export const sendMail = async ( c: Context, address: string, reqJson: { @@ -78,6 +104,15 @@ export const sendMail = async ( if (!content) { throw new Error("Invalid content") } + // send to verified address list, do not update balance + if (c.env.SEND_MAIL) { + const verifiedAddressList = await getJsonSetting(c, CONSTANTS.VERIFIED_ADDRESS_LIST_KEY) || []; + if (verifiedAddressList.includes(to_mail)) { + return sendMailToVerifyAddress(c, address, { + from_name, to_mail, to_name, subject, content, is_html + }); + } + } let dmikBody = {} if (c.env.DKIM_SELECTOR && c.env.DKIM_PRIVATE_KEY && address.includes("@")) { dmikBody = { @@ -169,7 +204,7 @@ api.post('/external/api/send_mail', async (c) => { return c.text("No address", 400) } const reqJson = await c.req.json(); - await sendMail(c, address, reqJson); + await sendMail(c, address as string, reqJson); return c.json({ status: "ok" }) } catch (e) { console.error("Failed to send mail", e); diff --git a/worker/src/mails_api/webhook_settings.ts b/worker/src/mails_api/webhook_settings.ts index 50626904..98e1900a 100644 --- a/worker/src/mails_api/webhook_settings.ts +++ b/worker/src/mails_api/webhook_settings.ts @@ -1,7 +1,7 @@ import { Context } from "hono"; import { HonoCustomType } from "../types"; import { CONSTANTS } from "../constants"; -import { AdminWebhookSettings, WebhookMail } from "../models/models"; +import { AdminWebhookSettings, WebhookMail } from "../models"; import { getBooleanValue } from "../utils"; import PostalMime from 'postal-mime'; diff --git a/worker/src/models/index.js b/worker/src/models/index.js deleted file mode 100644 index 7ec5efde..00000000 --- a/worker/src/models/index.js +++ /dev/null @@ -1,87 +0,0 @@ -export class UserSettings { - /** @param {UserSettings|undefined|null} data */ - constructor(data) { - if (data === null) { - return; - } - const { - enable, enableMailVerify, verifyMailSender, - enableMailAllowList, mailAllowList, maxAddressCount - } = data || {}; - /** @type {boolean|undefined} */ - this.enable = enable; - /** @type {boolean|undefined} */ - this.enableMailVerify = enableMailVerify; - /** @type {string|undefined} */ - this.verifyMailSender = verifyMailSender; - /** @type {boolean|undefined} */ - this.enableMailAllowList = enableMailAllowList; - /** @type {Array|undefined} */ - this.mailAllowList = mailAllowList; - /** @type {number|undefined} */ - this.maxAddressCount = maxAddressCount || 5; - } -} - -export class CleanupSettings { - /** @param {CleanupSettings|undefined|null} data */ - constructor(data) { - const { - enableMailsAutoCleanup, cleanMailsDays, - enableUnknowMailsAutoCleanup, cleanUnknowMailsDays, - enableSendBoxAutoCleanup, cleanSendBoxDays - } = data || {}; - /** @type {boolean|undefined} */ - this.enableMailsAutoCleanup = enableMailsAutoCleanup; - /** @type {number|undefined} */ - this.cleanMailsDays = cleanMailsDays; - /** @type {boolean|undefined} */ - this.enableUnknowMailsAutoCleanup = enableUnknowMailsAutoCleanup; - /** @type {number|undefined} */ - this.cleanUnknowMailsDays = cleanUnknowMailsDays; - /** @type {boolean|undefined} */ - this.enableSendBoxAutoCleanup = enableSendBoxAutoCleanup; - /** @type {number|undefined} */ - this.cleanSendBoxDays = cleanSendBoxDays; - } -} - -export class GeoData { - /** @param {string} ip @param {GeoData|undefined|null} data */ - constructor(ip, data) { - const { - country, city, timezone, postalCode, region, - latitude, longitude, regionCode, asOrganization - } = data || {}; - /** @type {string} */ - this.ip = ip; - /** @type {string|undefined} */ - this.country = country; - /** @type {string|undefined} */ - this.city = city; - /** @type {string|undefined} */ - this.timezone = timezone; - /** @type {string|undefined} */ - this.postalCode = postalCode; - /** @type {string|undefined} */ - this.region = region; - /** @type {number|undefined} */ - this.latitude = latitude; - /** @type {number|undefined} */ - this.longitude = longitude; - /** @type {string|undefined} */ - this.regionCode = regionCode; - /** @type {string|undefined} */ - this.asOrganization = asOrganization; - } -} - -export class UserInfo { - /** @param {GeoData} geoData @param {string} userEmail */ - constructor(geoData, userEmail) { - /** @type {geoData} */ - this.geoData = geoData; - /** @type {string} */ - this.userEmail = userEmail; - } -} diff --git a/worker/src/models/models.ts b/worker/src/models/index.ts similarity index 68% rename from worker/src/models/models.ts rename to worker/src/models/index.ts index a0f9ef85..003afa7c 100644 --- a/worker/src/models/models.ts +++ b/worker/src/models/index.ts @@ -70,3 +70,37 @@ export class GeoData { this.asOrganization = asOrganization; } } + +export class UserSettings { + + enable: boolean | undefined; + enableMailVerify: boolean | undefined; + verifyMailSender: string | undefined; + enableMailAllowList: boolean | undefined; + mailAllowList: string[] | undefined; + maxAddressCount: number; + + constructor(data: UserSettings | undefined | null) { + const { + enable, enableMailVerify, verifyMailSender, + enableMailAllowList, mailAllowList, maxAddressCount + } = data || {}; + this.enable = enable; + this.enableMailVerify = enableMailVerify; + this.verifyMailSender = verifyMailSender; + this.enableMailAllowList = enableMailAllowList; + this.mailAllowList = mailAllowList; + this.maxAddressCount = maxAddressCount || 5; + } +} + +export class UserInfo { + + geoData: GeoData; + userEmail: string; + + constructor(geoData: GeoData, userEmail: string) { + this.geoData = geoData; + this.userEmail = userEmail; + } +} diff --git a/worker/src/scheduled.ts b/worker/src/scheduled.ts index 92bb6bf6..0bd9723d 100644 --- a/worker/src/scheduled.ts +++ b/worker/src/scheduled.ts @@ -2,7 +2,7 @@ import { Context } from 'hono'; import { cleanup } from './common' import { CONSTANTS } from './constants' import { getJsonSetting } from './utils'; -import { CleanupSettings } from './models/models'; +import { CleanupSettings } from './models'; import { Bindings, HonoCustomType } from './types'; export async function scheduled(event: ScheduledEvent, env: Bindings, ctx: any) { diff --git a/worker/src/telegram_api/miniapp.ts b/worker/src/telegram_api/miniapp.ts index 776a6f33..dc062df1 100644 --- a/worker/src/telegram_api/miniapp.ts +++ b/worker/src/telegram_api/miniapp.ts @@ -4,6 +4,7 @@ import { HonoCustomType } from "../types"; import { CONSTANTS } from "../constants"; import { bindTelegramAddress, jwtListToAddressData, tgUserNewAddress, unbindTelegramAddress } from "./common"; import { checkCfTurnstile } from "../utils"; +import { TelegramSettings } from "./settings"; const encoder = new TextEncoder(); const TG_AUTH_TIMEOUT = 300; @@ -130,7 +131,12 @@ async function getMail(c: Context): Promise { const result = await c.env.DB.prepare( `SELECT * FROM raw_mails where id = ?` ).bind(mailId).first(); - if (result?.address && !(result.address as string in addressIdMap)) { + const settings = await c.env.KV.get(CONSTANTS.TG_KV_SETTINGS_KEY, "json"); + const superUser = settings?.enableGlobalMailPush && settings?.globalMailPushList.includes(userId); + if ( + !superUser && result?.address && + !(result.address as string in addressIdMap) + ) { return c.text("无权查看此邮件", 403); } const address_id = addressIdMap[result?.address as string]; diff --git a/worker/src/telegram_api/settings.ts b/worker/src/telegram_api/settings.ts index 7cd73154..667961ed 100644 --- a/worker/src/telegram_api/settings.ts +++ b/worker/src/telegram_api/settings.ts @@ -6,17 +6,24 @@ export class TelegramSettings { enableAllowList: boolean; allowList: string[]; miniAppUrl: string; + enableGlobalMailPush: boolean; + globalMailPushList: string[]; - constructor(enableAllowList: boolean, allowList: string[], miniAppUrl: string) { + constructor( + enableAllowList: boolean, allowList: string[], miniAppUrl: string, + enableGlobalMailPush: boolean, globalMailPushList: string[] + ) { this.enableAllowList = enableAllowList; this.allowList = allowList; this.miniAppUrl = miniAppUrl; + this.enableGlobalMailPush = enableGlobalMailPush; + this.globalMailPushList = globalMailPushList; } } async function getTelegramSettings(c: Context): Promise { const settings = await c.env.KV.get(CONSTANTS.TG_KV_SETTINGS_KEY, "json"); - return c.json(settings || new TelegramSettings(false, [], "")); + return c.json(settings || new TelegramSettings(false, [], "", false, [])); } diff --git a/worker/src/telegram_api/telegram.ts b/worker/src/telegram_api/telegram.ts index 95c22347..6081cccb 100644 --- a/worker/src/telegram_api/telegram.ts +++ b/worker/src/telegram_api/telegram.ts @@ -318,6 +318,15 @@ export async function sendMailToTelegram( url.searchParams.set("mail_id", mailId); miniAppButtons.push(Markup.button.webApp("查看邮件", url.toString())); } + if (settings?.enableGlobalMailPush && settings?.globalMailPushList) { + for (const pushId of settings.globalMailPushList) { + await bot.telegram.sendMessage(pushId, mail, { + ...Markup.inlineKeyboard([ + ...miniAppButtons, + ]) + }); + } + } await bot.telegram.sendMessage(userId, mail, { ...Markup.inlineKeyboard([ ...miniAppButtons, diff --git a/worker/src/types.d.ts b/worker/src/types.d.ts index d753ef9d..92f373fa 100644 --- a/worker/src/types.d.ts +++ b/worker/src/types.d.ts @@ -3,6 +3,7 @@ export type Bindings = { DB: D1Database KV: KVNamespace RATE_LIMITER: any + SEND_MAIL: any // config PREFIX: string | undefined diff --git a/worker/src/user_api/bind_address.js b/worker/src/user_api/bind_address.ts similarity index 91% rename from worker/src/user_api/bind_address.js rename to worker/src/user_api/bind_address.ts index 46585ccb..2859a7d8 100644 --- a/worker/src/user_api/bind_address.js +++ b/worker/src/user_api/bind_address.ts @@ -1,11 +1,13 @@ +import { Context } from 'hono'; import { Jwt } from 'hono/utils/jwt' +import { HonoCustomType } from '../types'; import { UserSettings } from "../models"; import { getJsonSetting } from "../utils" import { CONSTANTS } from "../constants"; export default { - bind: async (c) => { + bind: async (c: Context) => { const { user_id } = c.get("userPayload"); const { address_id } = c.get("jwtPayload"); if (!address_id || !user_id) { @@ -36,7 +38,7 @@ export default { if (settings.maxAddressCount > 0) { const { count } = await c.env.DB.prepare( `SELECT COUNT(*) as count FROM users_address where user_id = ?` - ).bind(user_id).first(); + ).bind(user_id).first<{ count: number }>() || { count: 0 }; if (count >= settings.maxAddressCount) { return c.text("Max address count reached", 400) } @@ -50,14 +52,15 @@ export default { return c.text("Failed to bind", 500) } } catch (e) { - if (e.message && e.message.includes("UNIQUE")) { + 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("Failed to bind", 500) } return c.json({ success: true }) }, - unbind: async (c) => { + unbind: async (c: Context) => { const { user_id } = c.get("userPayload"); const { address_id } = await c.req.json(); if (!address_id || !user_id) { @@ -90,7 +93,7 @@ export default { } return c.json({ success: true }) }, - getBindedAddresses: async (c) => { + getBindedAddresses: async (c: Context) => { const { user_id } = c.get("userPayload"); if (!user_id) { return c.text("No user token", 400) @@ -110,7 +113,7 @@ export default { results: results, }) }, - getBindedAddressJwt: async (c) => { + getBindedAddressJwt: async (c: Context) => { const { address_id } = c.req.param(); // check binded const { user_id } = c.get("userPayload"); diff --git a/worker/src/user_api/index.ts b/worker/src/user_api/index.ts index aa4a4191..78674635 100644 --- a/worker/src/user_api/index.ts +++ b/worker/src/user_api/index.ts @@ -1,11 +1,8 @@ import { Hono } from 'hono'; import { HonoCustomType } from '../types'; -// @ts-ignore import settings from './settings'; -// @ts-ignore import user from './user'; -// @ts-ignore import bind_address from './bind_address'; export const api = new Hono(); diff --git a/worker/src/user_api/settings.js b/worker/src/user_api/settings.ts similarity index 79% rename from worker/src/user_api/settings.js rename to worker/src/user_api/settings.ts index 685aabb3..f575719e 100644 --- a/worker/src/user_api/settings.js +++ b/worker/src/user_api/settings.ts @@ -1,9 +1,12 @@ +import { Context } from "hono"; + +import { HonoCustomType } from "../types"; import { UserSettings } from "../models"; import { getJsonSetting } from "../utils" import { CONSTANTS } from "../constants"; export default { - openSettings: async (c) => { + openSettings: async (c: Context) => { const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY); const settings = new UserSettings(value); return c.json({ @@ -11,7 +14,7 @@ export default { enableMailVerify: settings.enableMailVerify, }) }, - settings: async (c) => { + settings: async (c: Context) => { const user = c.get("userPayload"); // check if user exists const db_user_id = await c.env.DB.prepare( diff --git a/worker/src/user_api/user.js b/worker/src/user_api/user.ts similarity index 85% rename from worker/src/user_api/user.js rename to worker/src/user_api/user.ts index 500d8f1c..ffe617a3 100644 --- a/worker/src/user_api/user.js +++ b/worker/src/user_api/user.ts @@ -1,12 +1,14 @@ +import { Context } from 'hono'; import { Jwt } from 'hono/utils/jwt' +import { HonoCustomType } from '../types'; import { checkCfTurnstile, getJsonSetting, checkUserPassword } from "../utils" import { CONSTANTS } from "../constants"; import { GeoData, UserInfo, UserSettings } from "../models"; import { sendMail } from "../mails_api/send_mail_api"; export default { - verifyCode: async (c) => { + verifyCode: async (c: Context) => { const { email, cf_token } = await c.req.json(); // check cf turnstile try { @@ -24,6 +26,9 @@ export default { ) { return c.text(`Mail domain must in ${JSON.stringify(settings.mailAllowList, null, 2)}`, 400) } + if (!settings.verifyMailSender) { + return c.text("Verify mail sender not set", 400) + } // check if code exists in KV const tmpcode = await c.env.KV.get(`temp-mail:${email}`) if (tmpcode) { @@ -34,12 +39,15 @@ export default { // send code to email try { await sendMail(c, settings.verifyMailSender, { - to_mail: email, + from_name: "Temp Mail Verify", + to_name: '', + to_mail: email as string, subject: "Temp Mail Verify code", content: `Your verify code is ${code}`, + is_html: false, }) } catch (e) { - return c.text(`Failed to send verify code: ${e.message}`, 500) + return c.text(`Failed to send verify code: ${(e as Error).message}`, 500) } // save to KV await c.env.KV.put(`temp-mail:${email}`, code, { expirationTtl: 300 }); @@ -48,7 +56,7 @@ export default { expirationTtl: 300 }) }, - register: async (c) => { + register: async (c: Context) => { const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY); const settings = new UserSettings(value) // check enable @@ -67,6 +75,7 @@ export default { // check mail domain allow list const mailDomain = email.split("@")[1]; if (settings.enableMailAllowList + && settings.mailAllowList && !settings.mailAllowList.includes(mailDomain) ) { return c.text(`Mail domain must in ${JSON.stringify(settings.mailAllowList, null, 2)}`, 400) @@ -80,8 +89,8 @@ export default { } // geo data const reqIp = c.req.raw.headers.get("cf-connecting-ip") - const geoData = new GeoData(reqIp, c.req.raw.cf); - const userInfo = new UserInfo(geoData); + const geoData = new GeoData(reqIp, c.req.raw.cf as any); + const userInfo = new UserInfo(geoData, email); // if not enable mail verify, do not on conflict update if (!settings.enableMailVerify) { try { @@ -95,10 +104,11 @@ export default { return c.text("Failed to register", 500) } } catch (e) { - if (e.message && e.message.includes("UNIQUE")) { + const error = e as Error; + if (error.message && error.message.includes("UNIQUE")) { return c.text("User already exists, please login", 400) } - return c.text(`Failed to register: ${e.message}`, 500) + return c.text(`Failed to register: ${error.message}`, 500) } return c.json({ success: true }) } @@ -116,7 +126,7 @@ export default { } return c.json({ success: true }) }, - login: async (c) => { + login: async (c: Context) => { const { email, password } = await c.req.json(); if (!email || !password) return c.text("Invalid email or password", 400); const { id: user_id, password: dbPassword } = await c.env.DB.prepare( diff --git a/worker/wrangler.toml.template b/worker/wrangler.toml.template index a66cfd82..69e51d14 100644 --- a/worker/wrangler.toml.template +++ b/worker/wrangler.toml.template @@ -11,6 +11,10 @@ node_compat = true # [triggers] # crons = [ "0 0 * * *" ] +# send_email = [ +# { name = "SEND_MAIL" }, +# ] + [vars] PREFIX = "tmp" # IF YOU WANT TO MAKE YOUR SITE PRIVATE, UNCOMMENT THE FOLLOWING LINES