feat: add telegram business connect onboarding and status sync
This commit is contained in:
130
frontend/server/api/omni/telegram/business/webhook.post.ts
Normal file
130
frontend/server/api/omni/telegram/business/webhook.post.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
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" };
|
||||
});
|
||||
Reference in New Issue
Block a user