198 lines
6.3 KiB
TypeScript
198 lines
6.3 KiB
TypeScript
import { getHeader, readBody } from "h3";
|
|
import { prisma } from "../../../../utils/prisma";
|
|
import { telegramBotApi } from "../../../../utils/telegram";
|
|
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;
|
|
}
|
|
|
|
function crmConnectUrl() {
|
|
return String(process.env.CRM_APP_URL || "https://clientsflow.dsrptlab.com").trim();
|
|
}
|
|
|
|
function crmConnectButton(linkToken?: string) {
|
|
const base = crmConnectUrl();
|
|
let target = base;
|
|
if (linkToken) {
|
|
try {
|
|
const u = new URL(base);
|
|
u.searchParams.set("tg_link_token", linkToken);
|
|
target = u.toString();
|
|
} catch {
|
|
target = `${base}${base.includes("?") ? "&" : "?"}tg_link_token=${encodeURIComponent(linkToken)}`;
|
|
}
|
|
}
|
|
|
|
return {
|
|
inline_keyboard: [
|
|
[
|
|
{
|
|
text: "Открыть CRM и подтвердить",
|
|
url: target,
|
|
},
|
|
],
|
|
],
|
|
};
|
|
}
|
|
|
|
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;
|
|
const startChatId = getTelegramChatIdFromUpdate(update);
|
|
|
|
if (startText && !linkToken) {
|
|
if (startChatId) {
|
|
void telegramBotApi("sendMessage", {
|
|
chat_id: startChatId,
|
|
text: "Чтобы привязать Telegram Business к CRM, открой CRM → Settings → Telegram Business → Connect. Кнопка сгенерирует персональную ссылку привязки.",
|
|
reply_markup: crmConnectButton(),
|
|
}).catch(() => {});
|
|
}
|
|
return { ok: true, accepted: true, type: "start_without_link_token" };
|
|
}
|
|
|
|
if (linkToken) {
|
|
const payload = verifyLinkToken(linkToken);
|
|
if (!payload) {
|
|
if (startChatId) {
|
|
void telegramBotApi("sendMessage", {
|
|
chat_id: startChatId,
|
|
text: "Ссылка привязки недействительна или истекла. Вернись в CRM и нажми Connect заново.",
|
|
reply_markup: crmConnectButton(),
|
|
}).catch(() => {});
|
|
}
|
|
return { ok: true, accepted: false, reason: "invalid_or_expired_link_token" };
|
|
}
|
|
|
|
const pendingId = `pending:${payload.nonce}`;
|
|
const chatId = startChatId;
|
|
|
|
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,
|
|
},
|
|
},
|
|
});
|
|
|
|
if (chatId) {
|
|
void telegramBotApi("sendMessage", {
|
|
chat_id: chatId,
|
|
text: "CRM: связка аккаунта получена. Нажми кнопку ниже и вернись в CRM для подтверждения.",
|
|
reply_markup: crmConnectButton(linkToken),
|
|
}).catch(() => {});
|
|
}
|
|
|
|
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 } }),
|
|
]);
|
|
|
|
if (businessConnection.userChatId) {
|
|
void telegramBotApi("sendMessage", {
|
|
chat_id: businessConnection.userChatId,
|
|
text: "CRM: Telegram Business подключен. Теперь входящие сообщения будут появляться в CRM.",
|
|
}).catch(() => {});
|
|
}
|
|
|
|
return { ok: true, accepted: true, type: "business_connection" };
|
|
}
|
|
|
|
return { ok: true, accepted: true, type: "ignored" };
|
|
});
|