mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-09-05 07:27:27 +08:00
feat: telegram mini app open mail from bot (#256)
This commit is contained in:
@@ -2,6 +2,60 @@ import { Context } from "hono";
|
||||
import { Jwt } from "hono/utils/jwt";
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { HonoCustomType } from "../types";
|
||||
import { getIntValue, getJsonSetting } from "../utils";
|
||||
import { newAddress } from "../common";
|
||||
|
||||
export const tgUserNewAddress = async (
|
||||
c: Context<HonoCustomType>, userId: string, address: string
|
||||
): Promise<{ address: string, jwt: string }> => {
|
||||
if (c.env.RATE_LIMITER) {
|
||||
const { success } = await c.env.RATE_LIMITER.limit(
|
||||
{ key: `${CONSTANTS.TG_KV_PREFIX}:${userId}` }
|
||||
)
|
||||
if (!success) {
|
||||
throw Error("Rate limit exceeded")
|
||||
}
|
||||
}
|
||||
// @ts-ignore
|
||||
address = address || Math.random().toString(36).substring(2, 15);
|
||||
console.log(`new address: ${address}`);
|
||||
const [name, domain] = address.includes("@") ? address.split("@") : [address, 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("绑定地址数量已达上限");
|
||||
}
|
||||
// check name block list
|
||||
const value = await getJsonSetting(c, CONSTANTS.ADDRESS_BLOCK_LIST_KEY);
|
||||
const blockList = (value || []) as string[];
|
||||
if (blockList.some((item) => name.includes(item))) {
|
||||
throw Error(`Name[${name}]is blocked`);
|
||||
}
|
||||
const res = await newAddress(c,
|
||||
name || Math.random().toString(36).substring(2, 15),
|
||||
domain, true
|
||||
);
|
||||
// for mail push to telegram
|
||||
await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, JSON.stringify([...jwtList, res.jwt]));
|
||||
await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${res.address}`, userId.toString());
|
||||
return res;
|
||||
}
|
||||
|
||||
export const bindTelegramAddress = async (
|
||||
c: Context<HonoCustomType>, userId: string, jwt: string
|
||||
): Promise<string> => {
|
||||
const { address } = await Jwt.verify(jwt, c.env.JWT_SECRET, "HS256");
|
||||
if (!address) {
|
||||
throw Error("无效凭证");
|
||||
}
|
||||
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("绑定地址数量已达上限");
|
||||
}
|
||||
await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, JSON.stringify([...jwtList, jwt]));
|
||||
// for mail push to telegram
|
||||
await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${address}`, userId.toString());
|
||||
return address;
|
||||
}
|
||||
|
||||
export const unbindTelegramAddress = async (
|
||||
c: Context<HonoCustomType>, userId: string, address: string
|
||||
|
||||
@@ -68,4 +68,8 @@ 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);
|
||||
api.post("/telegram/get_bind_address", miniapp.getTelegramBindAddress);
|
||||
api.post("/telegram/new_address", miniapp.newTelegramAddress);
|
||||
api.post("/telegram/bind_address", miniapp.bindAddress);
|
||||
api.post("/telegram/unbind_address", miniapp.unbindAddress);
|
||||
api.post("/telegram/get_mail", miniapp.getMail);
|
||||
|
||||
@@ -2,26 +2,28 @@ import { Context } from "hono";
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
import { HonoCustomType } from "../types";
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { bindTelegramAddress, tgUserNewAddress, unbindTelegramAddress } from "./common";
|
||||
import { checkCfTurnstile } from "../utils";
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const TG_AUTH_TIMEOUT = 300;
|
||||
|
||||
|
||||
async function getTelegramBindAddress(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const checkTelegramAuth = async (
|
||||
c: Context<HonoCustomType>, initData: string
|
||||
): Promise<string> => {
|
||||
// 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);
|
||||
if (auth_date + TG_AUTH_TIMEOUT < (new Date().getTime() / 1000)) {
|
||||
throw Error("Auth date expired");
|
||||
}
|
||||
const user = initDataObj.get('user');
|
||||
if (!hash || !user) {
|
||||
return c.text("Invalid initData", 400);
|
||||
throw Error("Invalid initData");
|
||||
}
|
||||
const { id: userId } = JSON.parse(user);
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
@@ -48,23 +50,109 @@ async function getTelegramBindAddress(c: Context<HonoCustomType>): Promise<Respo
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
if (calcHash != hash) {
|
||||
return c.text("Invalid initData", 400);
|
||||
throw Error("Invalid initData");
|
||||
}
|
||||
// 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 userId;
|
||||
}
|
||||
|
||||
async function getTelegramBindAddress(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const { initData } = await c.req.json();
|
||||
try {
|
||||
const userId = await checkTelegramAuth(c, initData);
|
||||
// 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);
|
||||
}
|
||||
catch (e) {
|
||||
return c.text((e as Error).message, 400);
|
||||
}
|
||||
}
|
||||
|
||||
async function newTelegramAddress(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const { initData, address, cf_token } = await c.req.json();
|
||||
// check cf turnstile
|
||||
try {
|
||||
await checkCfTurnstile(c, cf_token);
|
||||
} catch (error) {
|
||||
return c.text("Failed to check cf turnstile", 500)
|
||||
}
|
||||
try {
|
||||
const userId = await checkTelegramAuth(c, initData);
|
||||
// get the address list from the KV
|
||||
const res = await tgUserNewAddress(c, userId, address)
|
||||
return c.json(res);
|
||||
}
|
||||
catch (e) {
|
||||
return c.text((e as Error).message, 400);
|
||||
}
|
||||
}
|
||||
|
||||
async function bindAddress(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const { initData, jwt } = await c.req.json();
|
||||
try {
|
||||
const userId = await checkTelegramAuth(c, initData);
|
||||
await bindTelegramAddress(c, userId, jwt);
|
||||
return c.json({ success: true });
|
||||
}
|
||||
catch (e) {
|
||||
return c.text((e as Error).message, 400);
|
||||
}
|
||||
}
|
||||
|
||||
async function unbindAddress(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const { initData, address } = await c.req.json();
|
||||
try {
|
||||
const userId = await checkTelegramAuth(c, initData);
|
||||
await unbindTelegramAddress(c, userId, address);
|
||||
return c.json({ success: true });
|
||||
}
|
||||
catch (e) {
|
||||
return c.text((e as Error).message, 400);
|
||||
}
|
||||
}
|
||||
|
||||
async function getMail(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const { initData, mailId } = await c.req.json();
|
||||
try {
|
||||
const userId = await checkTelegramAuth(c, initData);
|
||||
const jwtList = await c.env.KV.get<string[]>(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || [];
|
||||
const addressList = [];
|
||||
for (const jwt of jwtList) {
|
||||
try {
|
||||
const { address } = await Jwt.verify(jwt, c.env.JWT_SECRET, "HS256");
|
||||
addressList.push(address);
|
||||
} catch (e) {
|
||||
addressList.push("此凭证无效");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const result = await c.env.DB.prepare(
|
||||
`SELECT * FROM raw_mails where id = ?`
|
||||
).bind(mailId).first();
|
||||
if (result?.address && !addressList.includes(result.address)) {
|
||||
return c.text("无权查看此邮件", 403);
|
||||
}
|
||||
return c.json(result);
|
||||
}
|
||||
catch (e) {
|
||||
return c.text((e as Error).message, 400);
|
||||
}
|
||||
return c.json(res);
|
||||
}
|
||||
|
||||
export default {
|
||||
getTelegramBindAddress
|
||||
getTelegramBindAddress,
|
||||
newTelegramAddress,
|
||||
bindAddress,
|
||||
unbindAddress,
|
||||
getMail,
|
||||
}
|
||||
|
||||
@@ -5,16 +5,18 @@ import { CONSTANTS } from "../constants";
|
||||
export class TelegramSettings {
|
||||
enableAllowList: boolean;
|
||||
allowList: string[];
|
||||
miniAppUrl: string;
|
||||
|
||||
constructor(enableAllowList: boolean, allowList: string[]) {
|
||||
constructor(enableAllowList: boolean, allowList: string[], miniAppUrl: string) {
|
||||
this.enableAllowList = enableAllowList;
|
||||
this.allowList = allowList;
|
||||
this.miniAppUrl = miniAppUrl;
|
||||
}
|
||||
}
|
||||
|
||||
async function getTelegramSettings(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const settings = await c.env.KV.get<TelegramSettings>(CONSTANTS.TG_KV_SETTINGS_KEY, "json");
|
||||
return c.json(settings || new TelegramSettings(false, []));
|
||||
return c.json(settings || new TelegramSettings(false, [], ""));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -7,11 +7,10 @@ import PostalMime from 'postal-mime';
|
||||
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { getIntValue, getDomains, getStringValue } from '../utils';
|
||||
// @ts-ignore
|
||||
import { deleteAddressWithData, newAddress } from '../common'
|
||||
import { deleteAddressWithData } from '../common'
|
||||
import { HonoCustomType } from "../types";
|
||||
import { TelegramSettings } from "./settings";
|
||||
import { unbindTelegramAddress } from "./common";
|
||||
import { bindTelegramAddress, tgUserNewAddress, unbindTelegramAddress } from "./common";
|
||||
|
||||
const COMMANDS = [
|
||||
{
|
||||
@@ -48,6 +47,9 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
|
||||
const bot = new Telegraf(token);
|
||||
|
||||
bot.use(async (ctx, next) => {
|
||||
// skip non-message
|
||||
if (ctx.updateType != "message") return await next();
|
||||
// check if in private chat
|
||||
if (ctx.chat?.type !== "private") {
|
||||
return await ctx.reply("请在私聊中使用");
|
||||
}
|
||||
@@ -75,38 +77,23 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
|
||||
const prefix = getStringValue(c.env.PREFIX)
|
||||
const domains = getDomains(c);
|
||||
return await ctx.reply(
|
||||
"欢迎使用本机器人, 您可以点击左下角打开 mini app \n\n"
|
||||
"欢迎使用本机器人, 您可以打开 mini app \n\n"
|
||||
+ (prefix ? `当前已启用前缀: ${prefix}\n` : '')
|
||||
+ `当前可用域名: ${JSON.stringify(domains)}\n`
|
||||
+ "请使用以下命令:\n"
|
||||
+ COMMANDS.map(c => `/${c.command}: ${c.description}`).join("\n")
|
||||
);
|
||||
});
|
||||
|
||||
bot.command("new", async (ctx: TgContext) => {
|
||||
const userId = ctx?.message?.from?.id;
|
||||
if (!userId) {
|
||||
return await ctx.reply("无法获取用户信息");
|
||||
}
|
||||
try {
|
||||
if (c.env.RATE_LIMITER) {
|
||||
const { success } = await c.env.RATE_LIMITER.limit(
|
||||
{ key: `${CONSTANTS.TG_KV_PREFIX}:${userId}` }
|
||||
)
|
||||
if (!success) {
|
||||
return await ctx.reply("操作过于频繁");
|
||||
}
|
||||
}
|
||||
// @ts-ignore
|
||||
const address = ctx?.message?.text.slice("/new".length).trim() || Math.random().toString(36).substring(2, 15);
|
||||
const [name, domain] = address.includes("@") ? address.split("@") : [address, 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)) {
|
||||
return await ctx.reply("绑定地址数量已达上限");
|
||||
}
|
||||
const res = await newAddress(c, name, domain, true);
|
||||
// for mail push to telegram
|
||||
await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, JSON.stringify([...jwtList, res.jwt]));
|
||||
await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${res.address}`, userId.toString());
|
||||
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.jwt}\n`
|
||||
@@ -127,17 +114,7 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
|
||||
if (!jwt) {
|
||||
return await ctx.reply("请输入凭证");
|
||||
}
|
||||
const { address } = await Jwt.verify(jwt, c.env.JWT_SECRET, "HS256");
|
||||
if (!address) {
|
||||
return await ctx.reply("凭证无效");
|
||||
}
|
||||
const jwtList = await c.env.KV.get<string[]>(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || [];
|
||||
if (jwtList.length >= getIntValue(c.env.TG_MAX_ADDRESS, 5)) {
|
||||
return await ctx.reply("绑定地址数量已达上限");
|
||||
}
|
||||
await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, JSON.stringify([...jwtList, jwt]));
|
||||
// for mail push to telegram
|
||||
await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${address}`, userId.toString());
|
||||
const address = await bindTelegramAddress(c, userId.toString(), jwt);
|
||||
return await ctx.reply(`绑定成功:\n`
|
||||
+ `地址: ${address}`
|
||||
);
|
||||
@@ -218,7 +195,6 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
|
||||
const { address } = await Jwt.verify(jwt, c.env.JWT_SECRET, "HS256");
|
||||
addressList.push(address);
|
||||
} catch (e) {
|
||||
addressList.push("此凭证无效");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -228,18 +204,27 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
|
||||
if (!addressList.includes(queryAddress)) {
|
||||
return await ctx.reply(`未绑定此地址 ${queryAddress}`);
|
||||
}
|
||||
const raw = await c.env.DB.prepare(
|
||||
const { raw, id: mailId } = await c.env.DB.prepare(
|
||||
`SELECT * FROM raw_mails where address = ? `
|
||||
+ ` order by id desc limit 1 offset ?`
|
||||
).bind(
|
||||
queryAddress, mailIndex
|
||||
).first<string>("raw");
|
||||
const { mail } = await parseMail(raw);
|
||||
).first<{ raw: string, id: string }>() || {};
|
||||
const { mail } = raw ? await parseMail(raw) : { mail: "已经没有邮件了" };
|
||||
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("OpenApp", url.toString()));
|
||||
}
|
||||
if (edit) {
|
||||
return await ctx.editMessageText(mail || "无邮件",
|
||||
{
|
||||
...Markup.inlineKeyboard([
|
||||
Markup.button.callback("上一条", `mail_${queryAddress}_${mailIndex - 1}`, mailIndex <= 0),
|
||||
...miniAppButtons,
|
||||
Markup.button.callback("下一条", `mail_${queryAddress}_${mailIndex + 1}`, !raw),
|
||||
])
|
||||
},
|
||||
@@ -249,6 +234,7 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
|
||||
{
|
||||
...Markup.inlineKeyboard([
|
||||
Markup.button.callback("上一条", `mail_${queryAddress}_${mailIndex - 1}`, mailIndex <= 0),
|
||||
...miniAppButtons,
|
||||
Markup.button.callback("下一条", `mail_${queryAddress}_${mailIndex + 1}`, !raw),
|
||||
])
|
||||
},
|
||||
@@ -303,7 +289,7 @@ const parseMail = async (raw_mail: string | undefined | null) => {
|
||||
+ `To: ${parsedEmail.to?.map(t => `${t.name}[${t.address}]`).join(" ")}\n`
|
||||
+ `Subject: ${parsedEmail.subject}\n`
|
||||
+ `Date: ${parsedEmail.date}\n`
|
||||
+ `Content:\n${parsedEmail.text || "解析失败,请点击左下角打开 mini app 查看"}`
|
||||
+ `Content:\n${parsedEmail.text || "解析失败,请打开 mini app 查看"}`
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user