Files
clientsflow/frontend/server/api/omni/telegram/business/webhook.post.ts

131 lines
4.1 KiB
TypeScript

import { getHeader, readBody } from "h3";
import { prisma } from "../../../../utils/prisma";
import {
extractLinkTokenFromStartText,
getBusinessConnectionFromUpdate,
getTelegramChatIdFromUpdate,
verifyLinkToken,
} from "../../../../utils/telegramBusinessConnect";
function hasValidSecret(event: any) {
const expected = String(process.env.TELEGRAM_WEBHOOK_SECRET || "").trim();
if (!expected) return true;
const incoming = String(getHeader(event, "x-telegram-bot-api-secret-token") || "").trim();
return incoming !== "" && incoming === expected;
}
function pickStartText(update: any): string | null {
const text =
update?.message?.text ??
update?.business_message?.text ??
update?.edited_business_message?.text ??
null;
if (typeof text !== "string") return null;
return text;
}
export default defineEventHandler(async (event) => {
if (!hasValidSecret(event)) {
throw createError({ statusCode: 401, statusMessage: "invalid webhook secret" });
}
const update = await readBody<any>(event);
const nowIso = new Date().toISOString();
const startText = pickStartText(update);
const linkToken = startText ? extractLinkTokenFromStartText(startText) : null;
if (linkToken) {
const payload = verifyLinkToken(linkToken);
if (!payload) return { ok: true, accepted: false, reason: "invalid_or_expired_link_token" };
const pendingId = `pending:${payload.nonce}`;
const chatId = getTelegramChatIdFromUpdate(update);
await prisma.telegramBusinessConnection.updateMany({
where: {
teamId: payload.teamId,
businessConnectionId: pendingId,
},
data: {
rawJson: {
state: "pending_business_connection",
link: {
nonce: payload.nonce,
exp: payload.exp,
linkedAt: nowIso,
telegramUserId: chatId,
chatId,
},
lastStartUpdate: update,
},
},
});
return { ok: true, accepted: true, type: "start_link" };
}
const businessConnection = getBusinessConnectionFromUpdate(update);
if (businessConnection) {
const pendingRows = await prisma.telegramBusinessConnection.findMany({
where: {
businessConnectionId: {
startsWith: "pending:",
},
},
orderBy: { updatedAt: "desc" },
take: 200,
});
const matchedPending = pendingRows.find((row) => {
const raw = (row.rawJson ?? {}) as any;
const linkedTelegramUserId = raw?.link?.telegramUserId != null ? String(raw.link.telegramUserId) : null;
if (!businessConnection.userChatId) return false;
return linkedTelegramUserId === businessConnection.userChatId;
});
if (!matchedPending) {
return { ok: true, accepted: false, reason: "team_not_linked_for_business_connection" };
}
await prisma.$transaction([
prisma.telegramBusinessConnection.upsert({
where: {
teamId_businessConnectionId: {
teamId: matchedPending.teamId,
businessConnectionId: businessConnection.id,
},
},
create: {
teamId: matchedPending.teamId,
businessConnectionId: businessConnection.id,
isEnabled: businessConnection.isEnabled,
canReply: businessConnection.canReply,
rawJson: {
state: "connected",
connectedAt: nowIso,
userChatId: businessConnection.userChatId,
businessConnection: businessConnection.raw,
update,
},
},
update: {
isEnabled: businessConnection.isEnabled,
canReply: businessConnection.canReply,
rawJson: {
state: "connected",
connectedAt: nowIso,
userChatId: businessConnection.userChatId,
businessConnection: businessConnection.raw,
update,
},
},
}),
prisma.telegramBusinessConnection.delete({ where: { id: matchedPending.id } }),
]);
return { ok: true, accepted: true, type: "business_connection" };
}
return { ok: true, accepted: true, type: "ignored" };
});