Skip to content

Commit

Permalink
feat: 添加回复纯文本邮件功能 #23
Browse files Browse the repository at this point in the history
  • Loading branch information
TBXark committed Dec 25, 2024
1 parent 36d1988 commit 015164d
Show file tree
Hide file tree
Showing 10 changed files with 193 additions and 94 deletions.
116 changes: 58 additions & 58 deletions build/index.js

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions src/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ export class Dao {
return defaultStatus;
}

async saveMailStatus(id: string, status: EmailHandleStatus, ttl?: number): Promise<void> {
await this.db.put(id, JSON.stringify(status), { expirationTtl: ttl });
}

async loadMailCache(id: string): Promise<EmailCache | null> {
try {
const raw = await this.db.get(id);
Expand All @@ -69,6 +73,18 @@ export class Dao {
}
return null;
}

async saveMailCache(id: string, cache: EmailCache, ttl?: number): Promise<void> {
await this.db.put(id, JSON.stringify(cache), { expirationTtl: ttl });
}

async telegramIDToMailID(id: string): Promise<string | null> {
return await this.db.get(`TelegramID2MailID:${id}`);
}

async saveTelegramIDToMailID(id: string, mailID: string, ttl?: number): Promise<void> {
await this.db.put(`TelegramID2MailID:${id}`, mailID, { expirationTtl: ttl });
}
}

export function loadArrayFromRaw(raw: string | null): string[] {
Expand Down
4 changes: 2 additions & 2 deletions src/handler/fetch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { Environment } from '../../types';
import { validate } from '@telegram-apps/init-data-node/web';
import { json, Router } from 'itty-router';
import { Dao } from '../../db';
import { createTelegramBotAPI, TelegramCommands, telegramWebhookHandler, tmaHTML } from '../../telegram';
import { createTelegramBotAPI, telegramCommands, telegramWebhookHandler, tmaHTML } from '../../telegram';

class HTTPError extends Error {
readonly status: number;
Expand Down Expand Up @@ -95,7 +95,7 @@ function createRouter(env: Environment): RouterType {
url: `https://${DOMAIN}/telegram/${TELEGRAM_TOKEN}/webhook`,
});
const commands = await api.setMyCommands({
commands: TelegramCommands,
commands: telegramCommands,
});
return {
webhook: await webhook.json(),
Expand Down
20 changes: 13 additions & 7 deletions src/handler/mail/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,22 @@ import { Dao } from '../../db';
import { isMessageBlock, parseEmail, renderEmailListMode } from '../../mail';
import { createTelegramBotAPI } from '../../telegram';

export async function sendMailToTelegram(mail: EmailCache, env: Environment): Promise<void> {
export async function sendMailToTelegram(mail: EmailCache, env: Environment): Promise<number[]> {
const {
TELEGRAM_TOKEN,
TELEGRAM_ID,
} = env;
const req = await renderEmailListMode(mail, env);
const api = createTelegramBotAPI(TELEGRAM_TOKEN);
const messageID: number[] = [];
for (const id of TELEGRAM_ID.split(',')) {
await api.sendMessage({
const msg = await api.sendMessageWithReturns({
chat_id: id,
...req,
});
messageID.push(msg.result.message_id);
}
return messageID;
}

export async function emailHandler(message: ForwardableEmailMessage, env: Environment): Promise<void> {
Expand All @@ -35,7 +38,7 @@ export async function emailHandler(message: ForwardableEmailMessage, env: Enviro
const isBlock = await isMessageBlock(message, env);
const isGuardian = GUARDIAN_MODE === 'true';
const blockPolicy = (BLOCK_POLICY || 'telegram').split(',');
const statusTTL = { expirationTtl: 60 * 60 };
const statusTTL = 60 * 60;
const status = await dao.loadMailStatus(id, isGuardian);

// Reject the email
Expand All @@ -57,7 +60,7 @@ export async function emailHandler(message: ForwardableEmailMessage, env: Enviro
await message.forward(add);
if (isGuardian) {
status.forward.push(add);
await DB.put(id, JSON.stringify(status), statusTTL);
await dao.saveMailStatus(id, status, statusTTL);
}
} catch (e) {
console.error(e);
Expand All @@ -75,12 +78,15 @@ export async function emailHandler(message: ForwardableEmailMessage, env: Enviro
const maxSize = Number.parseInt(MAX_EMAIL_SIZE || '', 10) || 512 * 1024;
const maxSizePolicy = MAX_EMAIL_SIZE_POLICY || 'truncate';
const mail = await parseEmail(message, maxSize, maxSizePolicy);
await DB.put(mail.id, JSON.stringify(mail), { expirationTtl: ttl });
await sendMailToTelegram(mail, env);
await dao.saveMailCache(mail.id, mail, ttl);
const msgIDs = await sendMailToTelegram(mail, env);
for (const msgID of msgIDs) {
await dao.saveTelegramIDToMailID(`${msgID}`, mail.id, ttl);
}
}
if (isGuardian) {
status.telegram = true;
await DB.put(id, JSON.stringify(status), statusTTL);
await dao.saveMailStatus(id, status, statusTTL);
}
} catch (e) {
console.error(e);
Expand Down
1 change: 1 addition & 0 deletions src/mail/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './check';
export * from './parse';
export * from './render';
export * from './resend';
21 changes: 21 additions & 0 deletions src/mail/resend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { EmailCache } from '../types';

export async function replyToEmail(token: string, email: EmailCache, message: string): Promise<void> {
await sendEmail(token, email.to, [email.from], `Re: ${email.subject}`, message);
}

export async function sendEmail(token: string, from: string, to: string[], subject: string, text: string): Promise<void> {
await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from,
to,
subject,
text,
}),
});
}
10 changes: 5 additions & 5 deletions src/telegram/const.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
export const TmaModeDescription: { [key: string]: string } = {
export const tmaModeDescription: { [key: string]: string } = {
test: 'Test an email address',
white: 'Manage the white list',
block: 'Manage the block list',
};

export const TelegramCommands = [
export const telegramCommands = [
{
command: 'id',
description: '/id - Get your chat ID',
},
{
command: 'test',
description: `/test - ${TmaModeDescription.test}`,
description: `/test - ${tmaModeDescription.test}`,
},
{
command: 'white',
description: `/white - ${TmaModeDescription.white}`,
description: `/white - ${tmaModeDescription.white}`,
},
{
command: 'block',
description: `/block - ${TmaModeDescription.block}`,
description: `/block - ${tmaModeDescription.block}`,
},
];
2 changes: 1 addition & 1 deletion src/telegram/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import tmaHTML from '/tma.html';
import tmaHTML from './tma.html';

export * from './api';
export * from './const';
Expand Down
96 changes: 75 additions & 21 deletions src/telegram/telegram.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,29 @@ import type * as Telegram from 'telegram-bot-api-types';
import type { EmailRender } from '../mail';
import type { Environment } from '../types';
import { Dao } from '../db';
import { renderEmailDebugMode, renderEmailListMode, renderEmailPreviewMode, renderEmailSummaryMode } from '../mail';
import { renderEmailDebugMode, renderEmailListMode, renderEmailPreviewMode, renderEmailSummaryMode, replyToEmail } from '../mail';
import { createTelegramBotAPI } from './api';
import { TmaModeDescription } from './const';
import { tmaModeDescription } from './const';

type TelegramMessageHandler = (message: Telegram.Message) => Promise<Response>;
type CommandHandlerGroup = Record<string, TelegramMessageHandler>;

function handleIDCommand(env: Environment): TelegramMessageHandler {
return async (msg: Telegram.Message): Promise<Response> => {
const text = `Your chat ID is ${msg.chat.id}`;
return await handleOpenTMACommand(env, '', text)(msg);
return await handleOpenTMACommand('', text, env)(msg);
};
}

function handleOpenTMACommand(env: Environment, mode: string, text: string | null): TelegramMessageHandler {
function handleOpenTMACommand(mode: string, text: string | null, env: Environment): TelegramMessageHandler {
return async (msg: Telegram.Message): Promise<Response> => {
const {
TELEGRAM_TOKEN,
DOMAIN,
} = env;
const params: Telegram.SendMessageParams = {
chat_id: msg.chat.id,
text: text || TmaModeDescription[mode] || 'Address Manager',
text: text || tmaModeDescription[mode] || 'Address Manager',
reply_markup: {
inline_keyboard: [
[
Expand All @@ -42,7 +42,59 @@ function handleOpenTMACommand(env: Environment, mode: string, text: string | nul
};
}

async function handleReplyEmailCommand(message: Telegram.Message, env: Environment): Promise<void> {
const {
TELEGRAM_TOKEN,
RESEND_API_KEY,
DB,
} = env;
const dao = new Dao(DB);
const api = createTelegramBotAPI(TELEGRAM_TOKEN);
const reply = async (text: string) => {
await api.sendMessage({
chat_id: message.chat.id,
reply_parameters: {
message_id: message.message_id,
},
text,
});
};
if (!RESEND_API_KEY) {
await reply('Resend API is not enabled.');
return;
}
if (!message.text) {
await reply('Please provide a message to resend.');
return;
}
try {
const messageID = message.reply_to_message?.message_id;
if (!messageID) {
await reply('Please reply to a message to resend.');
return;
}
const mailID = await dao.telegramIDToMailID(`${messageID}`);
if (!mailID) {
await reply('Message not found.');
return;
}
const mail = await dao.loadMailCache(mailID);
if (!mail) {
await reply('Message not found or expired.');
return;
}
await replyToEmail(RESEND_API_KEY, mail, message.text);
await reply('Reply sent successfully.');
} catch (e) {
await reply((e as Error).message);
}
}

async function telegramCommandHandler(message: Telegram.Message, env: Environment): Promise<void> {
if (message?.reply_to_message) {
await handleReplyEmailCommand(message, env);
return;
}
let [command] = message.text?.split(/ (.*)/) || [''];
if (!command.startsWith('/')) {
console.log(`Invalid command: ${command}`);
Expand All @@ -52,17 +104,17 @@ async function telegramCommandHandler(message: Telegram.Message, env: Environmen
const handlers: CommandHandlerGroup = {
id: handleIDCommand(env),
start: handleIDCommand(env),
test: handleOpenTMACommand(env, 'test', null),
white: handleOpenTMACommand(env, 'white', null),
block: handleOpenTMACommand(env, 'block', null),
test: handleOpenTMACommand('test', null, env),
white: handleOpenTMACommand('white', null, env),
block: handleOpenTMACommand('block', null, env),
};

if (handlers[command]) {
await handlers[command](message);
return;
}
// 兼容旧版命令返回默认信息
await handleOpenTMACommand(env, '', `Unknown command: ${command}, try to reinitialize the bot.`)(message);
await handleOpenTMACommand('', `Unknown command: ${command}, try to reinitialize the bot.`, env)(message);
}

async function telegramCallbackHandler(callback: Telegram.CallbackQuery, env: Environment): Promise<void> {
Expand All @@ -79,23 +131,24 @@ async function telegramCallbackHandler(callback: Telegram.CallbackQuery, env: En
const dao = new Dao(DB);

if (!data || !chatId || !messageId) {
console.log(`Invalid callback data: ${JSON.stringify({ data, callbackId, chatId, messageId })}`);
return;
}

console.log(`Received callback: ${JSON.stringify({ data, callbackId, chatId, messageId })}`);
const renderHandlerBuilder = (render: EmailRender) => async (arg: string): Promise<void> => {
const value = await dao.loadMailCache(arg);
if (!value) {
throw new Error('Error: Email not found or expired.');
}
const req = await render(value, env);
const params: Telegram.EditMessageTextParams = {
chat_id: chatId,
message_id: messageId,
...req,
const renderHandlerBuilder = (render: EmailRender): (arg: string) => Promise<void> => {
return async (arg: string): Promise<void> => {
const value = await dao.loadMailCache(arg);
if (!value) {
throw new Error('Error: Email not found or expired.');
}
const req = await render(value, env);
const params: Telegram.EditMessageTextParams = {
chat_id: chatId,
message_id: messageId,
...req,
};
await api.editMessageText(params);
};
await api.editMessageText(params);
};

// eslint-disable-next-line unused-imports/no-unused-vars
Expand Down Expand Up @@ -134,6 +187,7 @@ export async function telegramWebhookHandler(req: Request, env: Environment): Pr
const body = await req.json() as Telegram.Update;
if (body?.message) {
await telegramCommandHandler(body?.message, env);
return;
}
if (body?.callback_query) {
await telegramCallbackHandler(body?.callback_query, env);
Expand Down
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export interface Environment {
OPENAI_CHAT_MODEL?: string;
SUMMARY_TARGET_LANG?: string;
GUARDIAN_MODE?: string;
RESEND_API_KEY?: string;
DB: KVNamespace;
DEBUG?: string;
}

0 comments on commit 015164d

Please sign in to comment.