mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-09-07 00:17:12 +08:00
feat: add telegram mini app (#250)
This commit is contained in:
@@ -1,5 +1,9 @@
|
|||||||
# CHANGE LOG
|
# CHANGE LOG
|
||||||
|
|
||||||
|
## main branch
|
||||||
|
|
||||||
|
- telegram mini app
|
||||||
|
|
||||||
## v0.4.3
|
## v0.4.3
|
||||||
|
|
||||||
### Breaking Changes
|
### Breaking Changes
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
VITE_API_BASE=https://temp-email-api.xxx.xxx
|
VITE_API_BASE=https://temp-email-api.xxx.xxx
|
||||||
VITE_CF_WEB_ANALY_TOKEN=
|
VITE_CF_WEB_ANALY_TOKEN=
|
||||||
|
VITE_IS_TELEGRAM=false
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
"build": "vite build -m prod --emptyOutDir",
|
"build": "vite build -m prod --emptyOutDir",
|
||||||
"build:release": "vite build -m example --emptyOutDir",
|
"build:release": "vite build -m example --emptyOutDir",
|
||||||
"build:pages": "vite build -m pages --emptyOutDir",
|
"build:pages": "vite build -m pages --emptyOutDir",
|
||||||
|
"build:telegram": "VITE_IS_TELEGRAM=true vite build -m prod --emptyOutDir",
|
||||||
"preview": "vite preview",
|
"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:preview": "npm run build && wrangler pages deploy ./dist --branch preview",
|
||||||
"deploy": "npm run build && wrangler pages deploy ./dist --branch production"
|
"deploy": "npm run build && wrangler pages deploy ./dist --branch production"
|
||||||
},
|
},
|
||||||
|
|||||||
+21
-1
@@ -8,7 +8,10 @@ import Header from './views/Header.vue';
|
|||||||
import Footer from './views/Footer.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 theme = computed(() => isDark.value ? darkTheme : null)
|
||||||
const localeConfig = computed(() => localeCache.value == 'zh' ? zhCN : null)
|
const localeConfig = computed(() => localeCache.value == 'zh' ? zhCN : null)
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
@@ -31,6 +34,23 @@ onMounted(async () => {
|
|||||||
document.body.appendChild(script);
|
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;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ export const useGlobalState = createGlobalState(
|
|||||||
/** @type {number} */
|
/** @type {number} */
|
||||||
user_id: 0,
|
user_id: 0,
|
||||||
});
|
});
|
||||||
|
const telegramApp = ref(window.Telegram?.WebApp || {});
|
||||||
|
const isTelegram = ref(!!window.Telegram?.WebApp?.initData);
|
||||||
return {
|
return {
|
||||||
isDark,
|
isDark,
|
||||||
toggleDark,
|
toggleDark,
|
||||||
@@ -95,6 +97,8 @@ export const useGlobalState = createGlobalState(
|
|||||||
userSettings,
|
userSettings,
|
||||||
globalTabplacement,
|
globalTabplacement,
|
||||||
useSideMargin,
|
useSideMargin,
|
||||||
|
telegramApp,
|
||||||
|
isTelegram,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { api } from '../api'
|
|||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
localeCache, toggleDark, isDark, openSettings,
|
localeCache, toggleDark, isDark, isTelegram,
|
||||||
showAuth, adminAuth, auth, loading
|
showAuth, adminAuth, auth, loading
|
||||||
} = useGlobalState()
|
} = useGlobalState()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -103,6 +103,7 @@ const menuOptions = computed(() => [
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
key: "user",
|
key: "user",
|
||||||
|
show: !isTelegram.value
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: () => h(
|
label: () => h(
|
||||||
|
|||||||
@@ -9,13 +9,15 @@ import { useGlobalState } from '../../store'
|
|||||||
import { api } from '../../api'
|
import { api } from '../../api'
|
||||||
import Login from '../common/Login.vue'
|
import Login from '../common/Login.vue'
|
||||||
import AddressManagement from '../user/AddressManagement.vue'
|
import AddressManagement from '../user/AddressManagement.vue'
|
||||||
|
import TelegramAddress from './TelegramAddress.vue'
|
||||||
|
|
||||||
const { toClipboard } = useClipboard()
|
const { toClipboard } = useClipboard()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
jwt, localeCache, settings, showAddressCredential, userJwt
|
jwt, localeCache, settings, showAddressCredential, userJwt,
|
||||||
|
isTelegram
|
||||||
} = useGlobalState()
|
} = useGlobalState()
|
||||||
|
|
||||||
const { t } = useI18n({
|
const { t } = useI18n({
|
||||||
@@ -45,6 +47,7 @@ const { t } = useI18n({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const showChangeAddress = ref(false)
|
const showChangeAddress = ref(false)
|
||||||
|
const showTelegramChangeAddress = ref(false)
|
||||||
|
|
||||||
const copy = async () => {
|
const copy = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -69,8 +72,12 @@ onMounted(async () => {
|
|||||||
<n-alert type="info" :show-icon="false">
|
<n-alert type="info" :show-icon="false">
|
||||||
<span>
|
<span>
|
||||||
<b>{{ settings.address }}</b>
|
<b>{{ settings.address }}</b>
|
||||||
<n-button v-if="userJwt" style="margin-left: 10px" @click="showChangeAddress = true" size="small"
|
<n-button v-if="isTelegram" style="margin-left: 10px" @click="showTelegramChangeAddress = true"
|
||||||
tertiary type="primary">
|
size="small" tertiary type="primary">
|
||||||
|
<n-icon :component="ExchangeAlt" /> {{ t('changeAddress') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button v-else-if="userJwt" style="margin-left: 10px" @click="showChangeAddress = true"
|
||||||
|
size="small" tertiary type="primary">
|
||||||
<n-icon :component="ExchangeAlt" /> {{ t('changeAddress') }}
|
<n-icon :component="ExchangeAlt" /> {{ t('changeAddress') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button style="margin-left: 10px" @click="copy" size="small" tertiary type="primary">
|
<n-button style="margin-left: 10px" @click="copy" size="small" tertiary type="primary">
|
||||||
@@ -79,6 +86,9 @@ onMounted(async () => {
|
|||||||
</span>
|
</span>
|
||||||
</n-alert>
|
</n-alert>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="isTelegram">
|
||||||
|
<TelegramAddress />
|
||||||
|
</div>
|
||||||
<div v-else class="center">
|
<div v-else class="center">
|
||||||
<n-card style="max-width: 600px;">
|
<n-card style="max-width: 600px;">
|
||||||
<n-alert v-if="jwt" type="warning" :show-icon="false" closable>
|
<n-alert v-if="jwt" type="warning" :show-icon="false" closable>
|
||||||
@@ -94,6 +104,9 @@ onMounted(async () => {
|
|||||||
</n-button>
|
</n-button>
|
||||||
</n-card>
|
</n-card>
|
||||||
</div>
|
</div>
|
||||||
|
<n-modal v-model:show="showTelegramChangeAddress" preset="card" :title="t('changeAddress')">
|
||||||
|
<TelegramAddress />
|
||||||
|
</n-modal>
|
||||||
<n-modal v-model:show="showChangeAddress" preset="card" :title="t('changeAddress')">
|
<n-modal v-model:show="showChangeAddress" preset="card" :title="t('changeAddress')">
|
||||||
<AddressManagement />
|
<AddressManagement />
|
||||||
</n-modal>
|
</n-modal>
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, h, onMounted } from 'vue';
|
||||||
|
import { useStorage } from '@vueuse/core';
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { NPopconfirm, NButton } from 'naive-ui'
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import { useGlobalState } from '../../store'
|
||||||
|
// @ts-ignore
|
||||||
|
import { api } from '../../api'
|
||||||
|
|
||||||
|
const { localeCache, jwt, telegramApp } = useGlobalState()
|
||||||
|
// @ts-ignore
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
const { t } = useI18n({
|
||||||
|
locale: localeCache.value || 'zh',
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
success: 'success',
|
||||||
|
address: 'Address',
|
||||||
|
actions: 'Actions',
|
||||||
|
changeMailAddress: 'Change Mail Address',
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
success: '成功',
|
||||||
|
address: '地址',
|
||||||
|
actions: '操作',
|
||||||
|
changeMailAddress: '切换邮箱地址',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = useStorage("telegram-bind-address", [])
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
data.value = await api.fetch(`/telegram/bind_address`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
initData: telegramApp.value.initData
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
message.error((error as Error).message || "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t('address'),
|
||||||
|
key: "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('actions'),
|
||||||
|
key: 'actions',
|
||||||
|
render(row: any) {
|
||||||
|
return h('div', [
|
||||||
|
h(NPopconfirm,
|
||||||
|
{
|
||||||
|
onPositiveClick: () => {
|
||||||
|
jwt.value = row.jwt
|
||||||
|
location.reload()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
trigger: () => h(NButton,
|
||||||
|
{
|
||||||
|
tertiary: true,
|
||||||
|
type: "primary",
|
||||||
|
},
|
||||||
|
{ default: () => t('changeMailAddress') }
|
||||||
|
),
|
||||||
|
default: () => `${t('changeMailAddress')}?`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!telegramApp.value?.initData || data.value.length > 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await fetchData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<n-data-table :columns="columns" :data="data" :bordered="false" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,6 +1,19 @@
|
|||||||
# 配置 Telegram Bot
|
# 配置 Telegram Bot
|
||||||
|
|
||||||
|
## Bot
|
||||||
|
|
||||||
|
- 可设置白名单用户
|
||||||
- 点击`初始化`即可完成配置。
|
- 点击`初始化`即可完成配置。
|
||||||
- 点击`查看状态`,可以查看当前配置的状态。
|
- 点击`查看状态`,可以查看当前配置的状态。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## 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=<你的项目名称>
|
||||||
|
```
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Writable } from 'node:stream'
|
|||||||
import { Bindings } from '../types'
|
import { Bindings } from '../types'
|
||||||
import { newTelegramBot, initTelegramBotCommands, sendMailToTelegram } from './telegram'
|
import { newTelegramBot, initTelegramBotCommands, sendMailToTelegram } from './telegram'
|
||||||
import settings from './settings'
|
import settings from './settings'
|
||||||
|
import miniapp from './miniapp'
|
||||||
|
|
||||||
export const api = new Hono<{ Bindings: Bindings }>();
|
export const api = new Hono<{ Bindings: Bindings }>();
|
||||||
export { sendMailToTelegram }
|
export { sendMailToTelegram }
|
||||||
@@ -67,3 +68,4 @@ api.get("/admin/telegram/status", async (c) => {
|
|||||||
|
|
||||||
api.get("/admin/telegram/settings", settings.getTelegramSettings);
|
api.get("/admin/telegram/settings", settings.getTelegramSettings);
|
||||||
api.post("/admin/telegram/settings", settings.saveTelegramSettings);
|
api.post("/admin/telegram/settings", settings.saveTelegramSettings);
|
||||||
|
api.post("/telegram/bind_address", miniapp.getTelegramBindAddress);
|
||||||
|
|||||||
@@ -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<Response> {
|
||||||
|
// 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<string[]>(`${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
|
||||||
|
}
|
||||||
@@ -63,7 +63,7 @@ export function newTelegramBot(c: Context<{ Bindings: Bindings }>, token: string
|
|||||||
const prefix = getStringValue(c.env.PREFIX)
|
const prefix = getStringValue(c.env.PREFIX)
|
||||||
const domains = getDomains(c);
|
const domains = getDomains(c);
|
||||||
return await ctx.reply(
|
return await ctx.reply(
|
||||||
"欢迎使用本机器人\n\n"
|
"欢迎使用本机器人, 您可以点击左下角打开 mini app \n\n"
|
||||||
+ (prefix ? `当前已启用前缀: ${prefix}\n` : '')
|
+ (prefix ? `当前已启用前缀: ${prefix}\n` : '')
|
||||||
+ "新建邮箱地址, 如果要自定义邮箱地址, "
|
+ "新建邮箱地址, 如果要自定义邮箱地址, "
|
||||||
+ "请输入 /new <name>@<domain>, name [a-zA-Z0-9.] 有效\n"
|
+ "请输入 /new <name>@<domain>, 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) {
|
export async function initTelegramBotCommands(bot: Telegraf) {
|
||||||
bot.telegram.sendMessage
|
|
||||||
await bot.telegram.setMyCommands(COMMANDS);
|
await bot.telegram.setMyCommands(COMMANDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,13 +250,16 @@ const parseMail = async (raw_mail: string | undefined | null) => {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const parsedEmail = await PostalMime.parse(raw_mail);
|
const parsedEmail = await PostalMime.parse(raw_mail);
|
||||||
|
if (parsedEmail?.text?.length && parsedEmail?.text?.length > 1000) {
|
||||||
|
parsedEmail.text = parsedEmail.text.substring(0, 1000) + "...";
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
isHtml: false,
|
isHtml: false,
|
||||||
mail: `From: ${parsedEmail.from ? `${parsedEmail.from.name}[${parsedEmail.from.address}]` : "无发件人"}\n`
|
mail: `From: ${parsedEmail.from ? `${parsedEmail.from.name}[${parsedEmail.from.address}]` : "无发件人"}\n`
|
||||||
+ `To: ${parsedEmail.to?.map(t => `${t.name}[${t.address}]`).join(" ")}\n`
|
+ `To: ${parsedEmail.to?.map(t => `${t.name}[${t.address}]`).join(" ")}\n`
|
||||||
+ `Subject: ${parsedEmail.subject}\n`
|
+ `Subject: ${parsedEmail.subject}\n`
|
||||||
+ `Date: ${parsedEmail.date}\n`
|
+ `Date: ${parsedEmail.date}\n`
|
||||||
+ `Content:\n${parsedEmail.text?.substring(0, 100) || "解析失败"}`
|
+ `Content:\n${parsedEmail.text || "解析失败,请点击左下角打开 mini app 查看"}`
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user