feat: add telegram business connect onboarding and status sync

This commit is contained in:
Ruslan Bakiev
2026-02-21 16:27:04 +07:00
parent f6d4f87f00
commit 052f37d0ec
7 changed files with 501 additions and 2 deletions

View File

@@ -0,0 +1,71 @@
import { readBody } from "h3";
import { getAuthContext } from "../../../../../utils/auth";
import { prisma } from "../../../../../utils/prisma";
import { telegramBotApi } from "../../../../../utils/telegram";
type RefreshBody = {
businessConnectionId?: string;
};
function mapFlags(raw: any) {
const isEnabled = typeof raw?.is_enabled === "boolean" ? raw.is_enabled : null;
const canReply = typeof raw?.can_reply === "boolean"
? raw.can_reply
: typeof raw?.rights?.can_reply === "boolean"
? raw.rights.can_reply
: null;
return { isEnabled, canReply };
}
export default defineEventHandler(async (event) => {
const auth = await getAuthContext(event);
const body = await readBody<RefreshBody>(event);
const businessConnectionId = String(body?.businessConnectionId ?? "").trim();
if (!businessConnectionId) {
throw createError({ statusCode: 400, statusMessage: "businessConnectionId is required" });
}
const existing = await prisma.telegramBusinessConnection.findFirst({
where: {
teamId: auth.teamId,
businessConnectionId,
},
select: { id: true },
});
if (!existing) {
throw createError({ statusCode: 404, statusMessage: "business connection not found" });
}
const response = await telegramBotApi<any>("getBusinessConnection", { business_connection_id: businessConnectionId });
const { isEnabled, canReply } = mapFlags(response);
const updated = await prisma.telegramBusinessConnection.update({
where: { id: existing.id },
data: {
isEnabled,
canReply,
rawJson: {
state: "connected",
refreshedAt: new Date().toISOString(),
businessConnection: response,
},
},
select: {
businessConnectionId: true,
isEnabled: true,
canReply: true,
updatedAt: true,
},
});
return {
ok: true,
connection: {
businessConnectionId: updated.businessConnectionId,
isEnabled: updated.isEnabled,
canReply: updated.canReply,
updatedAt: updated.updatedAt.toISOString(),
},
};
});

View File

@@ -0,0 +1,51 @@
import { getAuthContext } from "../../../../../utils/auth";
import { prisma } from "../../../../../utils/prisma";
import { buildTelegramStartUrl, issueLinkToken } from "../../../../../utils/telegramBusinessConnect";
export default defineEventHandler(async (event) => {
const auth = await getAuthContext(event);
const { token, payload } = issueLinkToken({ teamId: auth.teamId, userId: auth.userId });
const pendingId = `pending:${payload.nonce}`;
await prisma.telegramBusinessConnection.upsert({
where: {
teamId_businessConnectionId: {
teamId: auth.teamId,
businessConnectionId: pendingId,
},
},
create: {
teamId: auth.teamId,
businessConnectionId: pendingId,
rawJson: {
state: "pending_link",
link: {
nonce: payload.nonce,
exp: payload.exp,
createdAt: new Date().toISOString(),
createdByUserId: auth.userId,
},
},
},
update: {
isEnabled: null,
canReply: null,
rawJson: {
state: "pending_link",
link: {
nonce: payload.nonce,
exp: payload.exp,
createdAt: new Date().toISOString(),
createdByUserId: auth.userId,
},
},
},
});
return {
ok: true,
status: "pending_link",
connectUrl: buildTelegramStartUrl(token),
expiresAt: new Date(payload.exp * 1000).toISOString(),
};
});

View File

@@ -0,0 +1,60 @@
import { getAuthContext } from "../../../../../utils/auth";
import { prisma } from "../../../../../utils/prisma";
function normalizeStatus(input: {
pendingCount: number;
linkedPendingCount: number;
connectedCount: number;
enabledCount: number;
replyEnabledCount: number;
}) {
if (input.connectedCount > 0) {
if (input.replyEnabledCount > 0 && input.enabledCount > 0) return "connected";
if (input.enabledCount === 0) return "disabled";
return "no_reply_rights";
}
if (input.linkedPendingCount > 0) return "pending_business_connection";
if (input.pendingCount > 0) return "pending_link";
return "not_connected";
}
export default defineEventHandler(async (event) => {
const auth = await getAuthContext(event);
const rows = await prisma.telegramBusinessConnection.findMany({
where: { teamId: auth.teamId },
orderBy: { updatedAt: "desc" },
take: 50,
});
const pending = rows.filter((r) => r.businessConnectionId.startsWith("pending:"));
const active = rows.filter((r) => !r.businessConnectionId.startsWith("pending:"));
const linkedPendingCount = pending.filter((r) => {
const raw = (r.rawJson ?? {}) as any;
return Boolean(raw?.link?.telegramUserId || raw?.link?.chatId);
}).length;
const enabledCount = active.filter((r) => r.isEnabled !== false).length;
const replyEnabledCount = active.filter((r) => r.canReply === true).length;
const status = normalizeStatus({
pendingCount: pending.length,
linkedPendingCount,
connectedCount: active.length,
enabledCount,
replyEnabledCount,
});
return {
ok: true,
status,
pendingCount: pending.length,
connectedCount: active.length,
connections: active.map((r) => ({
businessConnectionId: r.businessConnectionId,
isEnabled: r.isEnabled,
canReply: r.canReply,
updatedAt: r.updatedAt.toISOString(),
})),
};
});