From 870b7b91980fde993adba36d1cc79dc5711c0e1c Mon Sep 17 00:00:00 2001 From: Dream Hunter Date: Sun, 19 May 2024 00:35:10 +0800 Subject: [PATCH] feat: add telegram mini app (#250) --- CHANGELOG.md | 4 + frontend/.env.example | 1 + frontend/package.json | 2 + frontend/src/App.vue | 22 ++++- frontend/src/store/index.js | 4 + frontend/src/views/Header.vue | 3 +- frontend/src/views/index/AddressBar.vue | 19 +++- frontend/src/views/index/TelegramAddress.vue | 94 +++++++++++++++++++ .../docs/zh/guide/feature/telegram.md | 13 +++ worker/src/telegram_api/index.ts | 2 + worker/src/telegram_api/miniapp.ts | 70 ++++++++++++++ worker/src/telegram_api/telegram.ts | 8 +- 12 files changed, 234 insertions(+), 8 deletions(-) create mode 100644 frontend/src/views/index/TelegramAddress.vue create mode 100644 worker/src/telegram_api/miniapp.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f90796f7..95343f7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGE LOG +## main branch + +- telegram mini app + ## v0.4.3 ### Breaking Changes diff --git a/frontend/.env.example b/frontend/.env.example index 3003b919..e2bdffe4 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,2 +1,3 @@ VITE_API_BASE=https://temp-email-api.xxx.xxx VITE_CF_WEB_ANALY_TOKEN= +VITE_IS_TELEGRAM=false diff --git a/frontend/package.json b/frontend/package.json index 8372b14a..55a8c54e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,7 +8,9 @@ "build": "vite build -m prod --emptyOutDir", "build:release": "vite build -m example --emptyOutDir", "build:pages": "vite build -m pages --emptyOutDir", + "build:telegram": "VITE_IS_TELEGRAM=true vite build -m prod --emptyOutDir", "preview": "vite preview", + "deploy:telegram": "npm run build:telegram && wrangler pages deploy ./dist --branch production", "deploy:preview": "npm run build && wrangler pages deploy ./dist --branch preview", "deploy": "npm run build && wrangler pages deploy ./dist --branch production" }, diff --git a/frontend/src/App.vue b/frontend/src/App.vue index b86ef4d4..1f40e169 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -8,7 +8,10 @@ import Header from './views/Header.vue'; import Footer from './views/Footer.vue'; -const { localeCache, isDark, loading, useSideMargin } = useGlobalState() +const { + localeCache, isDark, loading, useSideMargin, + telegramApp, isTelegram +} = useGlobalState() const theme = computed(() => isDark.value ? darkTheme : null) const localeConfig = computed(() => localeCache.value == 'zh' ? zhCN : null) const isMobile = useIsMobile() @@ -31,6 +34,23 @@ onMounted(async () => { document.body.appendChild(script); } + // check if telegram is enabled + const enableTelegram = import.meta.env.VITE_IS_TELEGRAM; + if ( + (typeof enableTelegram === 'boolean' && enableTelegram === true) + || + (typeof enableTelegram === 'string' && enableTelegram === 'true') + ) { + await new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = 'https://telegram.org/js/telegram-web-app.js'; + script.onload = resolve; + script.onerror = reject; + document.body.appendChild(script); + }); + telegramApp.value = window.Telegram?.WebApp || {}; + isTelegram.value = !!window.Telegram?.WebApp?.initData; + } }); diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js index 1d95e649..3d3f3a2c 100644 --- a/frontend/src/store/index.js +++ b/frontend/src/store/index.js @@ -68,6 +68,8 @@ export const useGlobalState = createGlobalState( /** @type {number} */ user_id: 0, }); + const telegramApp = ref(window.Telegram?.WebApp || {}); + const isTelegram = ref(!!window.Telegram?.WebApp?.initData); return { isDark, toggleDark, @@ -95,6 +97,8 @@ export const useGlobalState = createGlobalState( userSettings, globalTabplacement, useSideMargin, + telegramApp, + isTelegram, } }, ) diff --git a/frontend/src/views/Header.vue b/frontend/src/views/Header.vue index adedfa1f..f68e3bcf 100644 --- a/frontend/src/views/Header.vue +++ b/frontend/src/views/Header.vue @@ -14,7 +14,7 @@ import { api } from '../api' const message = useMessage() const { - localeCache, toggleDark, isDark, openSettings, + localeCache, toggleDark, isDark, isTelegram, showAuth, adminAuth, auth, loading } = useGlobalState() const route = useRoute() @@ -103,6 +103,7 @@ const menuOptions = computed(() => [ } ), key: "user", + show: !isTelegram.value }, { label: () => h( diff --git a/frontend/src/views/index/AddressBar.vue b/frontend/src/views/index/AddressBar.vue index 344fb4cd..33ad32da 100644 --- a/frontend/src/views/index/AddressBar.vue +++ b/frontend/src/views/index/AddressBar.vue @@ -9,13 +9,15 @@ import { useGlobalState } from '../../store' import { api } from '../../api' import Login from '../common/Login.vue' import AddressManagement from '../user/AddressManagement.vue' +import TelegramAddress from './TelegramAddress.vue' const { toClipboard } = useClipboard() const message = useMessage() const router = useRouter() const { - jwt, localeCache, settings, showAddressCredential, userJwt + jwt, localeCache, settings, showAddressCredential, userJwt, + isTelegram } = useGlobalState() const { t } = useI18n({ @@ -45,6 +47,7 @@ const { t } = useI18n({ }); const showChangeAddress = ref(false) +const showTelegramChangeAddress = ref(false) const copy = async () => { try { @@ -69,8 +72,12 @@ onMounted(async () => { {{ settings.address }} - + + {{ t('changeAddress') }} + + {{ t('changeAddress') }} @@ -79,6 +86,9 @@ onMounted(async () => { +
+ +
@@ -94,6 +104,9 @@ onMounted(async () => {
+ + + diff --git a/frontend/src/views/index/TelegramAddress.vue b/frontend/src/views/index/TelegramAddress.vue new file mode 100644 index 00000000..7ead2196 --- /dev/null +++ b/frontend/src/views/index/TelegramAddress.vue @@ -0,0 +1,94 @@ + + + diff --git a/vitepress-docs/docs/zh/guide/feature/telegram.md b/vitepress-docs/docs/zh/guide/feature/telegram.md index 3decb4ec..8bae47cb 100644 --- a/vitepress-docs/docs/zh/guide/feature/telegram.md +++ b/vitepress-docs/docs/zh/guide/feature/telegram.md @@ -1,6 +1,19 @@ # 配置 Telegram Bot +## Bot + +- 可设置白名单用户 - 点击`初始化`即可完成配置。 - 点击`查看状态`,可以查看当前配置的状态。 ![telegram](/feature/telegram.png) + +## Mini App + +```bash +cd frontend +pnpm install +cp .env.example .env.prod +# --project-name 可以单独为 mini app 创建一个 pages, 你也可以公用一个 pages,但是可能遇到 js 加载不了的问题 +pnpm run deploy:telegram --project-name=<你的项目名称> +``` diff --git a/worker/src/telegram_api/index.ts b/worker/src/telegram_api/index.ts index 41012520..0cf95bc9 100644 --- a/worker/src/telegram_api/index.ts +++ b/worker/src/telegram_api/index.ts @@ -5,6 +5,7 @@ import { Writable } from 'node:stream' import { Bindings } from '../types' import { newTelegramBot, initTelegramBotCommands, sendMailToTelegram } from './telegram' import settings from './settings' +import miniapp from './miniapp' export const api = new Hono<{ Bindings: Bindings }>(); export { sendMailToTelegram } @@ -67,3 +68,4 @@ api.get("/admin/telegram/status", async (c) => { api.get("/admin/telegram/settings", settings.getTelegramSettings); api.post("/admin/telegram/settings", settings.saveTelegramSettings); +api.post("/telegram/bind_address", miniapp.getTelegramBindAddress); diff --git a/worker/src/telegram_api/miniapp.ts b/worker/src/telegram_api/miniapp.ts new file mode 100644 index 00000000..4e4dc934 --- /dev/null +++ b/worker/src/telegram_api/miniapp.ts @@ -0,0 +1,70 @@ +import { Context } from "hono"; +import { Jwt } from 'hono/utils/jwt' +import { Bindings } from "../types"; +import { CONSTANTS } from "../constants"; + +const encoder = new TextEncoder(); + + +async function getTelegramBindAddress(c: Context<{ Bindings: Bindings }>): Promise { + // check if the request is from telegram + const { initData } = await c.req.json(); + const initDataObj = new URLSearchParams(initData); + initDataObj.sort() + const hash = initDataObj.get('hash'); + initDataObj.delete("hash"); + const dataToCheck = [...initDataObj.entries()].map(([key, value]) => key + "=" + value).join("\n"); + const auth_date = Number(initDataObj.get('auth_date')); + // valid for 300 seconds + if (auth_date + 300 < (new Date().getTime() / 1000)) { + return c.text("OutDate initData", 400); + } + const user = initDataObj.get('user'); + if (!hash || !user) { + return c.text("Invalid initData", 400); + } + const { id: userId } = JSON.parse(user); + const cryptoKey = await crypto.subtle.importKey( + "raw", + encoder.encode("WebAppData"), + { name: "HMAC", hash: { name: "SHA-256" } }, + false, + ["sign"] + ); + const secretKeyBuffer = await crypto.subtle.sign( + "HMAC", cryptoKey, encoder.encode(c.env.TELEGRAM_BOT_TOKEN) + ); + const secretKey = await crypto.subtle.importKey( + "raw", + secretKeyBuffer, + { name: "HMAC", hash: { name: "SHA-256" } }, + false, + ["sign", "verify"] + ); + const calcHmac = await crypto.subtle.sign( + "HMAC", secretKey, encoder.encode(dataToCheck) + ); + const calcHash = Array.from(new Uint8Array(calcHmac)) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); + if (calcHash != hash) { + return c.text("Invalid initData", 400); + } + // get the address list from the KV + const jwtList = await c.env.KV.get(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || []; + const res = []; + for (const jwt of jwtList) { + try { + const { address } = await Jwt.verify(jwt, c.env.JWT_SECRET, "HS256"); + res.push({ address, jwt }); + } catch (e) { + console.error(`failed to verify jwt with error: ${e}`) + continue; + } + } + return c.json(res); +} + +export default { + getTelegramBindAddress +} diff --git a/worker/src/telegram_api/telegram.ts b/worker/src/telegram_api/telegram.ts index 619803db..8a629b23 100644 --- a/worker/src/telegram_api/telegram.ts +++ b/worker/src/telegram_api/telegram.ts @@ -63,7 +63,7 @@ export function newTelegramBot(c: Context<{ Bindings: Bindings }>, token: string const prefix = getStringValue(c.env.PREFIX) const domains = getDomains(c); return await ctx.reply( - "欢迎使用本机器人\n\n" + "欢迎使用本机器人, 您可以点击左下角打开 mini app \n\n" + (prefix ? `当前已启用前缀: ${prefix}\n` : '') + "新建邮箱地址, 如果要自定义邮箱地址, " + "请输入 /new @, name [a-zA-Z0-9.] 有效\n" @@ -241,7 +241,6 @@ export function newTelegramBot(c: Context<{ Bindings: Bindings }>, token: string export async function initTelegramBotCommands(bot: Telegraf) { - bot.telegram.sendMessage await bot.telegram.setMyCommands(COMMANDS); } @@ -251,13 +250,16 @@ const parseMail = async (raw_mail: string | undefined | null) => { } try { const parsedEmail = await PostalMime.parse(raw_mail); + if (parsedEmail?.text?.length && parsedEmail?.text?.length > 1000) { + parsedEmail.text = parsedEmail.text.substring(0, 1000) + "..."; + } return { isHtml: false, mail: `From: ${parsedEmail.from ? `${parsedEmail.from.name}[${parsedEmail.from.address}]` : "无发件人"}\n` + `To: ${parsedEmail.to?.map(t => `${t.name}[${t.address}]`).join(" ")}\n` + `Subject: ${parsedEmail.subject}\n` + `Date: ${parsedEmail.date}\n` - + `Content:\n${parsedEmail.text?.substring(0, 100) || "解析失败"}` + + `Content:\n${parsedEmail.text || "解析失败,请点击左下角打开 mini app 查看"}` }; } catch (e) { return {