mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-06-27 18:33:02 +08:00
feat: telegram bot unbind && delete address (#254)
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
## main branch
|
||||
|
||||
- telegram mini app
|
||||
- telegram mini 增加 `ubind`, `delete` 指令
|
||||
|
||||
## v0.4.3
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cloudflare_temp_email",
|
||||
"version": "0.4.3",
|
||||
"version": "0.4.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { Context } from "hono";
|
||||
import { Bindings } from "../types";
|
||||
import { HonoCustomType } from "../types";
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { AdminWebhookSettings } from "../models/models";
|
||||
// @ts-ignore
|
||||
import { getBooleanValue } from "../utils";
|
||||
|
||||
async function getWebhookSettings(c: Context<{ Bindings: Bindings }>): Promise<Response> {
|
||||
async function getWebhookSettings(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const settings = await c.env.KV.get<AdminWebhookSettings>(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json");
|
||||
return c.json(settings || new AdminWebhookSettings([]));
|
||||
}
|
||||
|
||||
async function saveWebhookSettings(c: Context<{ Bindings: Bindings }>): Promise<Response> {
|
||||
async function saveWebhookSettings(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const settings = await c.req.json<AdminWebhookSettings>();
|
||||
await c.env.KV.put(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, JSON.stringify(settings));
|
||||
return c.json({ success: true })
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Hono } from 'hono'
|
||||
|
||||
// @ts-ignore
|
||||
import { getDomains, getPasswords, getBooleanValue } from './utils';
|
||||
import { CONSTANTS } from './constants';
|
||||
import { Bindings } from './types';
|
||||
import { HonoCustomType } from './types';
|
||||
|
||||
const api = new Hono<{ Bindings: Bindings }>
|
||||
const api = new Hono<HonoCustomType>
|
||||
|
||||
api.get('/open_api/settings', async (c) => {
|
||||
// check header x-custom-auth
|
||||
@@ -13,7 +12,7 @@ api.get('/open_api/settings', async (c) => {
|
||||
const passwords = getPasswords(c);
|
||||
if (passwords && passwords.length > 0) {
|
||||
const auth = c.req.raw.headers.get("x-custom-auth");
|
||||
needAuth = !passwords.includes(auth);
|
||||
needAuth = !auth || !passwords.includes(auth);
|
||||
}
|
||||
return c.json({
|
||||
"prefix": c.env.PREFIX,
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import { Context } from 'hono';
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
|
||||
import { getDomains, getStringValue } from './utils';
|
||||
import { HonoCustomType } from './types';
|
||||
import { CONSTANTS } from './constants';
|
||||
import { unbindTelegramByAddress } from './telegram_api/common';
|
||||
|
||||
export const newAddress = async (c, name, domain, enablePrefix) => {
|
||||
export const newAddress = async (
|
||||
c: Context<HonoCustomType>,
|
||||
name: string, domain: string | undefined | null,
|
||||
enablePrefix: boolean
|
||||
): Promise<{ address: string, jwt: string }> => {
|
||||
// remove special characters
|
||||
name = name.replace(/[^a-zA-Z0-9.]/g, '')
|
||||
// check name length
|
||||
@@ -36,10 +44,9 @@ export const newAddress = async (c, name, domain, enablePrefix) => {
|
||||
}
|
||||
throw new Error("Failed to create address")
|
||||
}
|
||||
let address_id = 0;
|
||||
address_id = await c.env.DB.prepare(
|
||||
const address_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM address where name = ?`
|
||||
).bind(name).first("id");
|
||||
).bind(name).first<number>("id");
|
||||
// create jwt
|
||||
const jwt = await Jwt.sign({
|
||||
address: name,
|
||||
@@ -51,7 +58,11 @@ export const newAddress = async (c, name, domain, enablePrefix) => {
|
||||
}
|
||||
}
|
||||
|
||||
export const cleanup = async (c, cleanType, cleanDays) => {
|
||||
export const cleanup = async (
|
||||
c: Context<HonoCustomType>,
|
||||
cleanType: string | undefined | null,
|
||||
cleanDays: number | undefined | null
|
||||
): Promise<boolean> => {
|
||||
if (!cleanType || !cleanDays || cleanDays < 0 || cleanDays > 30) {
|
||||
throw new Error("Invalid cleanType or cleanDays")
|
||||
}
|
||||
@@ -80,17 +91,57 @@ export const cleanup = async (c, cleanType, cleanDays) => {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} c context
|
||||
* @param {*} query @type {string} query
|
||||
* @param {*} countQuery @type {string} countQuery
|
||||
* @param {*} limit @type {number} limit
|
||||
* @param {*} offset @type {number} offset
|
||||
* @returns {Promise} Promise
|
||||
* TODO: need senbox delete?
|
||||
*/
|
||||
export const deleteAddressWithData = async (
|
||||
c: Context<HonoCustomType>,
|
||||
address: string | undefined | null,
|
||||
address_id: number | undefined | null
|
||||
): Promise<boolean> => {
|
||||
if (!address && !address_id) {
|
||||
throw new Error("Address or address_id required")
|
||||
}
|
||||
// get address_id or address
|
||||
if (!address_id) {
|
||||
address_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM address where name = ?`
|
||||
).bind(address).first<number>("id");
|
||||
} else if (!address) {
|
||||
address = await c.env.DB.prepare(
|
||||
`SELECT name FROM address where id = ?`
|
||||
).bind(address_id).first<string>("name");
|
||||
}
|
||||
// check address again
|
||||
if (!address || !address_id) {
|
||||
throw new Error("Can't find address");
|
||||
}
|
||||
// unbind telegram
|
||||
await unbindTelegramByAddress(c, address);
|
||||
// delete address and related data
|
||||
const { success: mailSuccess } = await c.env.DB.prepare(
|
||||
`DELETE FROM raw_mails WHERE address = ? `
|
||||
).bind(address).run();
|
||||
const { success: sendAccess } = await c.env.DB.prepare(
|
||||
`DELETE FROM address_sender WHERE address = ? `
|
||||
).bind(address).run();
|
||||
const { success: addressSuccess } = await c.env.DB.prepare(
|
||||
`DELETE FROM users_address WHERE address_id = ? `
|
||||
).bind(address_id).run();
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`DELETE FROM address WHERE name = ? `
|
||||
).bind(address).run();
|
||||
if (!success || !mailSuccess || !addressSuccess || !sendAccess) {
|
||||
throw new Error("Failed to delete address")
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export const handleListQuery = async (
|
||||
c, query, countQuery, params, limit, offset
|
||||
) => {
|
||||
c: Context<HonoCustomType>,
|
||||
query: string, countQuery: string, params: string[],
|
||||
limit: number | undefined | null,
|
||||
offset: number | undefined | null
|
||||
): Promise<Response> => {
|
||||
if (!limit || limit < 0 || limit > 100) {
|
||||
return c.text("Invalid limit", 400)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
export const CONSTANTS = {
|
||||
VERSION: 'v0.4.3',
|
||||
VERSION: 'v0.4.4',
|
||||
|
||||
// DB settings
|
||||
ADDRESS_BLOCK_LIST_KEY: 'address_block_list',
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createMimeMessage } from "mimetext";
|
||||
// @ts-ignore
|
||||
import { getBooleanValue } from "../utils";
|
||||
import { Bindings } from "../types";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Context } from "hono";
|
||||
|
||||
import { sendMailToTelegram } from "../telegram_api";
|
||||
import { Bindings } from "../types";
|
||||
import { Bindings, HonoCustomType } from "../types";
|
||||
import { auto_reply } from "./auto_reply";
|
||||
import { trigerWebhook } from "../mails_api/webhook_settings";
|
||||
|
||||
@@ -28,7 +28,7 @@ async function email(message: ForwardableEmailMessage, env: Bindings, ctx: Execu
|
||||
// send email to telegram
|
||||
try {
|
||||
await sendMailToTelegram(
|
||||
{ env: env } as Context<{ Bindings: Bindings }>,
|
||||
{ env: env } as Context<HonoCustomType>,
|
||||
message.to, rawEmail);
|
||||
} catch (error) {
|
||||
console.log("send mail to telegram error", error);
|
||||
@@ -37,7 +37,7 @@ async function email(message: ForwardableEmailMessage, env: Bindings, ctx: Execu
|
||||
// send webhook
|
||||
try {
|
||||
await trigerWebhook(
|
||||
{ env: env } as Context<{ Bindings: Bindings }>,
|
||||
{ env: env } as Context<HonoCustomType>,
|
||||
message.to, rawEmail
|
||||
);
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Context } from "hono";
|
||||
import { Bindings, Variables } from "../types";
|
||||
import { HonoCustomType } from "../types";
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { AdminWebhookSettings, WebhookMail } from "../models/models";
|
||||
// @ts-ignore
|
||||
import { getBooleanValue } from "../utils";
|
||||
import PostalMime from 'postal-mime';
|
||||
|
||||
@@ -24,9 +23,7 @@ class WebhookSettings {
|
||||
}
|
||||
|
||||
|
||||
async function getWebhookSettings(
|
||||
c: Context<{ Bindings: Bindings, Variables: Variables }>
|
||||
): Promise<Response> {
|
||||
async function getWebhookSettings(c: Context<HonoCustomType>): Promise<Response> {
|
||||
if (!c.env.KV) {
|
||||
return c.text("KV is not available", 400);
|
||||
}
|
||||
@@ -45,9 +42,7 @@ async function getWebhookSettings(
|
||||
}
|
||||
|
||||
|
||||
async function saveWebhookSettings(
|
||||
c: Context<{ Bindings: Bindings, Variables: Variables }>
|
||||
): Promise<Response> {
|
||||
async function saveWebhookSettings(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const { address } = c.get("jwtPayload")
|
||||
const adminSettings = await c.env.KV.get<AdminWebhookSettings>(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json");
|
||||
if (!adminSettings?.allowList.includes(address)) {
|
||||
@@ -79,7 +74,7 @@ async function sendWebhook(settings: WebhookSettings, formatMap: WebhookMail): P
|
||||
}
|
||||
|
||||
export async function trigerWebhook(
|
||||
c: Context<{ Bindings: Bindings }>,
|
||||
c: Context<HonoCustomType>,
|
||||
address: string,
|
||||
raw_mail: string
|
||||
): Promise<void> {
|
||||
@@ -110,9 +105,7 @@ export async function trigerWebhook(
|
||||
}
|
||||
}
|
||||
|
||||
async function testWebhookSettings(
|
||||
c: Context<{ Bindings: Bindings, Variables: Variables }>
|
||||
): Promise<Response> {
|
||||
async function testWebhookSettings(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const settings = await c.req.json<WebhookSettings>();
|
||||
const res = await sendWebhook(settings, {
|
||||
from: "from@test.com",
|
||||
|
||||
@@ -14,3 +14,27 @@ export type WebhookMail = {
|
||||
raw: string;
|
||||
parsedText: string;
|
||||
}
|
||||
|
||||
export class CleanupSettings {
|
||||
|
||||
enableMailsAutoCleanup: boolean | undefined;
|
||||
cleanMailsDays: number;
|
||||
enableUnknowMailsAutoCleanup: boolean | undefined;
|
||||
cleanUnknowMailsDays: number;
|
||||
enableSendBoxAutoCleanup: boolean | undefined;
|
||||
cleanSendBoxDays: number;
|
||||
|
||||
constructor(data: CleanupSettings | undefined | null) {
|
||||
const {
|
||||
enableMailsAutoCleanup, cleanMailsDays,
|
||||
enableUnknowMailsAutoCleanup, cleanUnknowMailsDays,
|
||||
enableSendBoxAutoCleanup, cleanSendBoxDays
|
||||
} = data || {};
|
||||
this.enableMailsAutoCleanup = enableMailsAutoCleanup;
|
||||
this.cleanMailsDays = cleanMailsDays || 0;
|
||||
this.enableUnknowMailsAutoCleanup = enableUnknowMailsAutoCleanup;
|
||||
this.cleanUnknowMailsDays = cleanUnknowMailsDays || 0;
|
||||
this.enableSendBoxAutoCleanup = enableSendBoxAutoCleanup;
|
||||
this.cleanSendBoxDays = cleanSendBoxDays || 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,35 @@
|
||||
import { Context } from 'hono';
|
||||
import { cleanup } from './common'
|
||||
import { CONSTANTS } from './constants'
|
||||
import { getJsonSetting } from './utils';
|
||||
import { CleanupSettings } from './models';
|
||||
import { CleanupSettings } from './models/models';
|
||||
import { Bindings, HonoCustomType } from './types';
|
||||
|
||||
export async function scheduled(event, env, ctx) {
|
||||
export async function scheduled(event: ScheduledEvent, env: Bindings, ctx: any) {
|
||||
console.log("Scheduled event: ", event);
|
||||
const value = await getJsonSetting(
|
||||
{ env: env, },
|
||||
{ env: env, } as Context<HonoCustomType>,
|
||||
CONSTANTS.AUTO_CLEANUP_KEY
|
||||
);
|
||||
const autoCleanupSetting = new CleanupSettings(value);
|
||||
console.log("autoCleanupSetting:", JSON.stringify(autoCleanupSetting));
|
||||
if (autoCleanupSetting.enableMailsAutoCleanup && autoCleanupSetting.cleanMailsDays > 0) {
|
||||
await cleanup(
|
||||
{ env: env, },
|
||||
{ env: env, } as Context<HonoCustomType>,
|
||||
"mails",
|
||||
autoCleanupSetting.cleanMailsDays
|
||||
);
|
||||
}
|
||||
if (autoCleanupSetting.enableUnknowMailsAutoCleanup && autoCleanupSetting.cleanUnknowMailsDays > 0) {
|
||||
await cleanup(
|
||||
{ env: env, },
|
||||
{ env: env, } as Context<HonoCustomType>,
|
||||
"mails_unknow",
|
||||
autoCleanupSetting.cleanUnknowMailsDays
|
||||
);
|
||||
}
|
||||
if (autoCleanupSetting.enableSendBoxAutoCleanup && autoCleanupSetting.cleanSendBoxDays > 0) {
|
||||
await cleanup(
|
||||
{ env: env, },
|
||||
{ env: env, } as Context<HonoCustomType>,
|
||||
"sendbox",
|
||||
autoCleanupSetting.cleanSendBoxDays
|
||||
);
|
||||
35
worker/src/telegram_api/common.ts
Normal file
35
worker/src/telegram_api/common.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Context } from "hono";
|
||||
import { Jwt } from "hono/utils/jwt";
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { HonoCustomType } from "../types";
|
||||
|
||||
export const unbindTelegramAddress = async (
|
||||
c: Context<HonoCustomType>, userId: string, address: string
|
||||
): Promise<boolean> => {
|
||||
const jwtList = await c.env.KV.get<string[]>(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || [];
|
||||
const newJwtList = [];
|
||||
for (const jwt of jwtList) {
|
||||
try {
|
||||
const { address: kvAddress } = await Jwt.verify(jwt, c.env.JWT_SECRET, "HS256");
|
||||
if (kvAddress == address) {
|
||||
continue;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`解绑失败: ${(e as Error).message}`);
|
||||
}
|
||||
newJwtList.push(jwt);
|
||||
}
|
||||
await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, JSON.stringify(newJwtList));
|
||||
await c.env.KV.delete(`${CONSTANTS.TG_KV_PREFIX}:${address}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
export const unbindTelegramByAddress = async (
|
||||
c: Context<HonoCustomType>, address: string
|
||||
): Promise<boolean> => {
|
||||
const userId = await c.env.KV.get<string>(`${CONSTANTS.TG_KV_PREFIX}:${address}`)
|
||||
if (userId) {
|
||||
return await unbindTelegramAddress(c, userId, address);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -2,12 +2,12 @@ import { Hono } from 'hono'
|
||||
import { ServerResponse } from 'node:http'
|
||||
import { Writable } from 'node:stream'
|
||||
|
||||
import { Bindings } from '../types'
|
||||
import { HonoCustomType } from '../types'
|
||||
import { newTelegramBot, initTelegramBotCommands, sendMailToTelegram } from './telegram'
|
||||
import settings from './settings'
|
||||
import miniapp from './miniapp'
|
||||
|
||||
export const api = new Hono<{ Bindings: Bindings }>();
|
||||
export const api = new Hono<HonoCustomType>();
|
||||
export { sendMailToTelegram }
|
||||
|
||||
api.use("/telegram/*", async (c, next) => {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Context } from "hono";
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
import { Bindings } from "../types";
|
||||
import { HonoCustomType } from "../types";
|
||||
import { CONSTANTS } from "../constants";
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
|
||||
async function getTelegramBindAddress(c: Context<{ Bindings: Bindings }>): Promise<Response> {
|
||||
async function getTelegramBindAddress(c: Context<HonoCustomType>): Promise<Response> {
|
||||
// check if the request is from telegram
|
||||
const { initData } = await c.req.json();
|
||||
const initDataObj = new URLSearchParams(initData);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Context } from "hono";
|
||||
import { Bindings } from "../types";
|
||||
import { HonoCustomType } from "../types";
|
||||
import { CONSTANTS } from "../constants";
|
||||
|
||||
export class TelegramSettings {
|
||||
@@ -12,13 +12,13 @@ export class TelegramSettings {
|
||||
}
|
||||
}
|
||||
|
||||
async function getTelegramSettings(c: Context<{ Bindings: Bindings }>): Promise<Response> {
|
||||
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, []));
|
||||
}
|
||||
|
||||
|
||||
async function saveTelegramSettings(c: Context<{ Bindings: Bindings }>): Promise<Response> {
|
||||
async function saveTelegramSettings(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const settings = await c.req.json<TelegramSettings>();
|
||||
await c.env.KV.put(CONSTANTS.TG_KV_SETTINGS_KEY, JSON.stringify(settings));
|
||||
return c.json({ success: true })
|
||||
|
||||
@@ -6,12 +6,12 @@ import { callbackQuery } from "telegraf/filters";
|
||||
import PostalMime from 'postal-mime';
|
||||
|
||||
import { CONSTANTS } from "../constants";
|
||||
// @ts-ignore
|
||||
import { getIntValue, getDomains, getStringValue } from '../utils';
|
||||
// @ts-ignore
|
||||
import { newAddress } from '../common'
|
||||
import { Bindings } from "../types";
|
||||
import { deleteAddressWithData, newAddress } from '../common'
|
||||
import { HonoCustomType } from "../types";
|
||||
import { TelegramSettings } from "./settings";
|
||||
import { unbindTelegramAddress } from "./common";
|
||||
|
||||
const COMMANDS = [
|
||||
{
|
||||
@@ -30,13 +30,21 @@ const COMMANDS = [
|
||||
command: "bind",
|
||||
description: "绑定邮箱地址, 请输入 /bind <邮箱地址凭证>"
|
||||
},
|
||||
{
|
||||
command: "unbind",
|
||||
description: "解绑邮箱地址, 请输入 /unbind <邮箱地址>"
|
||||
},
|
||||
{
|
||||
command: "delete",
|
||||
description: "删除邮箱地址, 请输入 /delete <邮箱地址>"
|
||||
},
|
||||
{
|
||||
command: "mails",
|
||||
description: "查看邮件, 请输入 /mails <邮箱地址>, 不输入地址默认查看第一个地址"
|
||||
},
|
||||
]
|
||||
|
||||
export function newTelegramBot(c: Context<{ Bindings: Bindings }>, token: string): Telegraf {
|
||||
export function newTelegramBot(c: Context<HonoCustomType>, token: string): Telegraf {
|
||||
const bot = new Telegraf(token);
|
||||
|
||||
bot.use(async (ctx, next) => {
|
||||
@@ -55,8 +63,12 @@ export function newTelegramBot(c: Context<{ Bindings: Bindings }>, token: string
|
||||
) {
|
||||
return await ctx.reply("您没有权限使用此机器人");
|
||||
}
|
||||
|
||||
await next();
|
||||
try {
|
||||
await next();
|
||||
} catch (error) {
|
||||
console.error(`Error: ${error}`);
|
||||
return await ctx.reply(`Error: ${error}`);
|
||||
}
|
||||
})
|
||||
|
||||
bot.command("start", async (ctx: TgContext) => {
|
||||
@@ -65,8 +77,6 @@ export function newTelegramBot(c: Context<{ Bindings: Bindings }>, token: string
|
||||
return await ctx.reply(
|
||||
"欢迎使用本机器人, 您可以点击左下角打开 mini app \n\n"
|
||||
+ (prefix ? `当前已启用前缀: ${prefix}\n` : '')
|
||||
+ "新建邮箱地址, 如果要自定义邮箱地址, "
|
||||
+ "请输入 /new <name>@<domain>, name [a-zA-Z0-9.] 有效\n"
|
||||
+ `当前可用域名: ${JSON.stringify(domains)}\n`
|
||||
+ "请使用以下命令:\n"
|
||||
+ COMMANDS.map(c => `/${c.command}: ${c.description}`).join("\n")
|
||||
@@ -137,6 +147,40 @@ export function newTelegramBot(c: Context<{ Bindings: Bindings }>, token: string
|
||||
}
|
||||
});
|
||||
|
||||
bot.command("unbind", async (ctx: TgContext) => {
|
||||
const userId = ctx?.message?.from?.id;
|
||||
if (!userId) {
|
||||
return await ctx.reply("无法获取用户信息");
|
||||
}
|
||||
try {
|
||||
// @ts-ignore
|
||||
const address = ctx?.message?.text.slice("/unbind".length).trim();
|
||||
if (!address) {
|
||||
return await ctx.reply("请输入地址");
|
||||
}
|
||||
await unbindTelegramAddress(c, userId.toString(), address);
|
||||
return await ctx.reply(`解绑成功:\n地址: ${address}`
|
||||
);
|
||||
}
|
||||
catch (e) {
|
||||
return await ctx.reply(`解绑失败: ${(e as Error).message}`);
|
||||
}
|
||||
})
|
||||
|
||||
bot.command("delete", async (ctx: TgContext) => {
|
||||
try {
|
||||
// @ts-ignore
|
||||
const address = ctx?.message?.text.slice("/unbind".length).trim();
|
||||
if (!address) {
|
||||
return await ctx.reply("请输入地址");
|
||||
}
|
||||
await deleteAddressWithData(c, address, null)
|
||||
return await ctx.reply(`删除成功: ${address}`);
|
||||
} catch (e) {
|
||||
return await ctx.reply(`删除失败: ${(e as Error).message}`);
|
||||
}
|
||||
});
|
||||
|
||||
bot.command("address", async (ctx) => {
|
||||
const userId = ctx?.message?.from?.id;
|
||||
if (!userId) {
|
||||
@@ -251,7 +295,7 @@ 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) + "...";
|
||||
parsedEmail.text = parsedEmail.text.substring(0, 1000) + "...消息过长请到miniapp查看";
|
||||
}
|
||||
return {
|
||||
isHtml: false,
|
||||
@@ -270,7 +314,7 @@ const parseMail = async (raw_mail: string | undefined | null) => {
|
||||
}
|
||||
|
||||
|
||||
export async function sendMailToTelegram(c: Context<{ Bindings: Bindings }>, address: string, raw_mail: string) {
|
||||
export async function sendMailToTelegram(c: Context<HonoCustomType>, address: string, raw_mail: string) {
|
||||
if (!c.env.TELEGRAM_BOT_TOKEN || !c.env.KV) {
|
||||
return;
|
||||
}
|
||||
|
||||
9
worker/src/types.d.ts
vendored
9
worker/src/types.d.ts
vendored
@@ -6,6 +6,9 @@ export type Bindings = {
|
||||
|
||||
// config
|
||||
PREFIX: string | undefined
|
||||
DOMAINS: string | string[] | undefined
|
||||
PASSWORDS: string | string[] | undefined
|
||||
ADMIN_PASSWORDS: string | string[] | undefined
|
||||
JWT_SECRET: string
|
||||
BLACK_LIST: string | undefined
|
||||
ENABLE_AUTO_REPLY: string | boolean | undefined
|
||||
@@ -18,6 +21,7 @@ export type Bindings = {
|
||||
|
||||
// cf turnstile
|
||||
CF_TURNSTILE_SITE_KEY: string | undefined
|
||||
CF_TURNSTILE_SECRET_KEY: string | undefined
|
||||
|
||||
// telegram config
|
||||
TELEGRAM_BOT_TOKEN: string
|
||||
@@ -40,3 +44,8 @@ type Variables = {
|
||||
userPayload: UserPayload,
|
||||
jwtPayload: JwtPayload
|
||||
}
|
||||
|
||||
type HonoCustomType = {
|
||||
"Bindings": Bindings;
|
||||
"Variables": Variables;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Context } from "hono";
|
||||
import { createMimeMessage } from "mimetext";
|
||||
import { HonoCustomType } from "./types";
|
||||
|
||||
export const getJsonSetting = async (c, key) => {
|
||||
export const getJsonSetting = async (
|
||||
c: Context<HonoCustomType>, key: string
|
||||
): Promise<any> => {
|
||||
const value = await getSetting(c, key);
|
||||
if (!value) {
|
||||
return null;
|
||||
@@ -13,11 +17,13 @@ export const getJsonSetting = async (c, key) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
export const getSetting = async (c, key) => {
|
||||
export const getSetting = async (
|
||||
c: Context<HonoCustomType>, key: string
|
||||
): Promise<string | null> => {
|
||||
try {
|
||||
const value = await c.env.DB.prepare(
|
||||
`SELECT value FROM settings where key = ?`
|
||||
).bind(key).first("value");
|
||||
).bind(key).first<string>("value");
|
||||
return value;
|
||||
} catch (error) {
|
||||
console.error(`GetSetting: Failed to get ${key}`, error);
|
||||
@@ -25,7 +31,10 @@ export const getSetting = async (c, key) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
export const saveSetting = async (c, key, value) => {
|
||||
export const saveSetting = async (
|
||||
c: Context<HonoCustomType>,
|
||||
key: string, value: string
|
||||
) => {
|
||||
await c.env.DB.prepare(
|
||||
`INSERT or REPLACE INTO settings (key, value) VALUES (?, ?)`
|
||||
+ ` ON CONFLICT(key) DO UPDATE SET value = ?, updated_at = datetime('now')`
|
||||
@@ -33,14 +42,16 @@ export const saveSetting = async (c, key, value) => {
|
||||
return true;
|
||||
}
|
||||
|
||||
export const getStringValue = (value) => {
|
||||
export const getStringValue = (value: any): string => {
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export const getBooleanValue = (value) => {
|
||||
export const getBooleanValue = (
|
||||
value: boolean | string | any
|
||||
): boolean => {
|
||||
if (typeof value === "boolean") {
|
||||
return value;
|
||||
}
|
||||
@@ -51,7 +62,10 @@ export const getBooleanValue = (value) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
export const getIntValue = (value, defaultValue = 0) => {
|
||||
export const getIntValue = (
|
||||
value: number | string | any,
|
||||
defaultValue: number = 0
|
||||
): number => {
|
||||
if (typeof value === "number") {
|
||||
return value;
|
||||
}
|
||||
@@ -65,7 +79,7 @@ export const getIntValue = (value, defaultValue = 0) => {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
export const getDomains = (c) => {
|
||||
export const getDomains = (c: Context<HonoCustomType>): string[] => {
|
||||
if (!c.env.DOMAINS) {
|
||||
return [];
|
||||
}
|
||||
@@ -81,14 +95,14 @@ export const getDomains = (c) => {
|
||||
return c.env.DOMAINS;
|
||||
}
|
||||
|
||||
export const getPasswords = (c) => {
|
||||
export const getPasswords = (c: Context<HonoCustomType>): string[] => {
|
||||
if (!c.env.PASSWORDS) {
|
||||
return [];
|
||||
}
|
||||
// check if PASSWORDS is an array, if not use json.parse
|
||||
if (!Array.isArray(c.env.PASSWORDS)) {
|
||||
try {
|
||||
let res = JSON.parse(c.env.PASSWORDS);
|
||||
const res = JSON.parse(c.env.PASSWORDS) as string[];
|
||||
return res.filter((item) => item.length > 0);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse PASSWORDS", e);
|
||||
@@ -98,23 +112,26 @@ export const getPasswords = (c) => {
|
||||
return c.env.PASSWORDS.filter((item) => item.length > 0);
|
||||
}
|
||||
|
||||
export const getAdminPasswords = (c) => {
|
||||
export const getAdminPasswords = (c: Context<HonoCustomType>): string[] => {
|
||||
if (!c.env.ADMIN_PASSWORDS) {
|
||||
return [];
|
||||
}
|
||||
// check if ADMIN_PASSWORDS is an array, if not use json.parse
|
||||
if (!Array.isArray(c.env.ADMIN_PASSWORDS)) {
|
||||
try {
|
||||
return JSON.parse(c.env.ADMIN_PASSWORDS);
|
||||
const res = JSON.parse(c.env.ADMIN_PASSWORDS) as string[];
|
||||
return res.filter((item) => item.length > 0);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse ADMIN_PASSWORDS", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return c.env.ADMIN_PASSWORDS;
|
||||
return c.env.ADMIN_PASSWORDS.filter((item) => item.length > 0);
|
||||
}
|
||||
|
||||
export const sendAdminInternalMail = async (c, toMail, subject, text) => {
|
||||
export const sendAdminInternalMail = async (
|
||||
c: Context<HonoCustomType>, toMail: string, subject: string, text: string
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
|
||||
const msg = createMimeMessage();
|
||||
@@ -144,31 +161,33 @@ export const sendAdminInternalMail = async (c, toMail, subject, text) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const checkCfTurnstile = async (c, token) => {
|
||||
if (!c.env.CF_TURNSTILE_SITE_KEY) {
|
||||
export const checkCfTurnstile = async (
|
||||
c: Context<HonoCustomType>, token: string | undefined | null
|
||||
): Promise<void> => {
|
||||
if (!c.env.CF_TURNSTILE_SITE_KEY || !c.env.CF_TURNSTILE_SECRET_KEY) {
|
||||
return;
|
||||
}
|
||||
if (!token) {
|
||||
throw new Error("Captcha token is required");
|
||||
}
|
||||
const reqIp = c.req.raw.headers.get("cf-connecting-ip")
|
||||
const reqIp = c.req.raw.headers.get("cf-connecting-ip");
|
||||
let formData = new FormData();
|
||||
formData.append('secret', c.env.CF_TURNSTILE_SECRET_KEY);
|
||||
formData.append('response', token);
|
||||
formData.append('remoteip', reqIp);
|
||||
if (reqIp) formData.append('remoteip', reqIp);
|
||||
const url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
|
||||
const result = await fetch(url, {
|
||||
body: formData,
|
||||
method: 'POST',
|
||||
});
|
||||
const captchaRes = await result.json();
|
||||
const captchaRes: any = await result.json();
|
||||
if (!captchaRes.success) {
|
||||
console.log("Captcha failed", captchaRes);
|
||||
throw new Error("Captcha failed");
|
||||
}
|
||||
}
|
||||
|
||||
export const checkUserPassword = (password) => {
|
||||
export const checkUserPassword = (password: string) => {
|
||||
if (!password || password.length < 1 || password.length > 100) {
|
||||
throw new Error("Invalid password")
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import { cors } from 'hono/cors';
|
||||
import { jwt } from 'hono/jwt'
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
|
||||
// @ts-ignore
|
||||
import { api as commonApi } from './commom_api';
|
||||
// @ts-ignore
|
||||
import { api as mailsApi } from './mails_api'
|
||||
@@ -18,13 +17,11 @@ import { api as apiSendMail } from './mails_api/send_mail_api'
|
||||
import { api as telegramApi } from './telegram_api'
|
||||
|
||||
import { email } from './email';
|
||||
// @ts-ignore
|
||||
import { scheduled } from './scheduled';
|
||||
// @ts-ignore
|
||||
import { getAdminPasswords, getPasswords, getBooleanValue } from './utils';
|
||||
import { Bindings } from './types';
|
||||
import { HonoCustomType } from './types';
|
||||
|
||||
const app = new Hono<{ Bindings: Bindings }>()
|
||||
const app = new Hono<HonoCustomType>()
|
||||
//cors
|
||||
app.use('/*', cors());
|
||||
// rate limit
|
||||
@@ -88,6 +85,7 @@ app.use('/user_api/*', async (c, next) => {
|
||||
}
|
||||
try {
|
||||
const token = c.req.raw.headers.get("x-user-token");
|
||||
if (!token) return c.text("Need User Token", 401)
|
||||
const payload = await Jwt.verify(token, c.env.JWT_SECRET, "HS256");
|
||||
// check expired
|
||||
if (!payload.exp) return c.text("Invalid Token", 401);
|
||||
|
||||
Reference in New Issue
Block a user