Spaces:
Running
Running
Download apps/api/dist/integrations/integrations-webhook.controller.js from Benaah/MicProtocol: direct link, hf CLI and curl.
- Browser
- Download file 33.5 kB
-
https://huggingface.co/spaces/Benaah/MicProtocol/resolve/main/apps/api/dist/integrations/integrations-webhook.controller.js
- Command line
-
hf download hf://spaces/Benaah/MicProtocol/apps/api/dist/integrations/integrations-webhook.controller.js
-
curl -L -o integrations-webhook.controller.js https://huggingface.co/spaces/Benaah/MicProtocol/resolve/main/apps/api/dist/integrations/integrations-webhook.controller.js
33.5 kB
| ; | |
| var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | |
| if (k2 === undefined) k2 = k; | |
| var desc = Object.getOwnPropertyDescriptor(m, k); | |
| if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { | |
| desc = { enumerable: true, get: function() { return m[k]; } }; | |
| } | |
| Object.defineProperty(o, k2, desc); | |
| }) : (function(o, m, k, k2) { | |
| if (k2 === undefined) k2 = k; | |
| o[k2] = m[k]; | |
| })); | |
| var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { | |
| Object.defineProperty(o, "default", { enumerable: true, value: v }); | |
| }) : function(o, v) { | |
| o["default"] = v; | |
| }); | |
| var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { | |
| var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; | |
| if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); | |
| else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; | |
| return c > 3 && r && Object.defineProperty(target, key, r), r; | |
| }; | |
| var __importStar = (this && this.__importStar) || (function () { | |
| var ownKeys = function(o) { | |
| ownKeys = Object.getOwnPropertyNames || function (o) { | |
| var ar = []; | |
| for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; | |
| return ar; | |
| }; | |
| return ownKeys(o); | |
| }; | |
| return function (mod) { | |
| if (mod && mod.__esModule) return mod; | |
| var result = {}; | |
| if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); | |
| __setModuleDefault(result, mod); | |
| return result; | |
| }; | |
| })(); | |
| var __metadata = (this && this.__metadata) || function (k, v) { | |
| if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); | |
| }; | |
| var __param = (this && this.__param) || function (paramIndex, decorator) { | |
| return function (target, key) { decorator(target, key, paramIndex); } | |
| }; | |
| var IntegrationsWebhookController_1; | |
| Object.defineProperty(exports, "__esModule", { value: true }); | |
| exports.IntegrationsWebhookController = void 0; | |
| const common_1 = require("@nestjs/common"); | |
| const config_1 = require("@nestjs/config"); | |
| const prisma_service_1 = require("../prisma/prisma.service"); | |
| const crypto = __importStar(require("crypto")); | |
| const webhook_idempotency_service_1 = require("./webhook-idempotency.service"); | |
| const self_hosted_clients_service_1 = require("./clients/self-hosted-clients.service"); | |
| const chatwoot_sync_service_1 = require("../support/chatwoot-sync.service"); | |
| const events_gateway_1 = require("../websocket/events.gateway"); | |
| function timingSafeCompare(provided, expected) { | |
| if (!provided || !expected) | |
| return false; | |
| const bufProvided = Buffer.from(provided); | |
| const bufExpected = Buffer.from(expected); | |
| if (bufProvided.length !== bufExpected.length) | |
| return false; | |
| return crypto.timingSafeEqual(bufProvided, bufExpected); | |
| } | |
| function verifyRawBodyHmac(rawBody, signature, secret, timestamp, allowSha1 = false) { | |
| if (!rawBody || !signature || !secret) | |
| return false; | |
| const provided = signature.replace(/^sha256=/i, "").replace(/^sha1=/i, ""); | |
| const messages = [rawBody]; | |
| if (timestamp) { | |
| messages.push(Buffer.from(`${timestamp}.${rawBody.toString("utf8")}`, "utf8")); | |
| } | |
| const candidates = []; | |
| for (const msg of messages) { | |
| candidates.push(crypto.createHmac("sha256", secret).update(msg).digest("hex")); | |
| if (allowSha1) { | |
| candidates.push(crypto.createHmac("sha1", secret).update(msg).digest("hex")); | |
| } | |
| } | |
| return candidates.some((expected) => timingSafeCompare(provided, expected)); | |
| } | |
| function verifyStandardWebhooksSignature(rawBody, signatureHeader, idHeader, timestampHeader, secret) { | |
| if (!rawBody || !signatureHeader || !idHeader || !timestampHeader || !secret) { | |
| return false; | |
| } | |
| const nowSec = Math.floor(Date.now() / 1000); | |
| const ts = Number(timestampHeader); | |
| if (!Number.isFinite(ts) || Math.abs(nowSec - ts) > 300) | |
| return false; | |
| let secretBytes; | |
| const stripped = secret.replace(/^whsec_/i, ""); | |
| try { | |
| const decoded = Buffer.from(stripped, "base64"); | |
| if (decoded.toString("base64").replace(/=+$/, "") === stripped.replace(/=+$/, "")) { | |
| secretBytes = decoded; | |
| } | |
| else { | |
| secretBytes = Buffer.from(secret, "utf8"); | |
| } | |
| } | |
| catch { | |
| secretBytes = Buffer.from(secret, "utf8"); | |
| } | |
| const signedContent = `${idHeader}.${timestampHeader}.${rawBody.toString("utf8")}`; | |
| const expected = crypto | |
| .createHmac("sha256", secretBytes) | |
| .update(signedContent) | |
| .digest("base64"); | |
| return signatureHeader.split(/\s+/).some((entry) => { | |
| const [version, sig] = entry.split(",", 2); | |
| if (version !== "v1" || !sig) | |
| return false; | |
| return timingSafeCompare(sig, expected); | |
| }); | |
| } | |
| let IntegrationsWebhookController = IntegrationsWebhookController_1 = class IntegrationsWebhookController { | |
| configService; | |
| prisma; | |
| idempotency; | |
| selfHostedClients; | |
| chatwootSync; | |
| eventsGateway; | |
| logger = new common_1.Logger(IntegrationsWebhookController_1.name); | |
| constructor(configService, prisma, idempotency, selfHostedClients, chatwootSync, eventsGateway) { | |
| this.configService = configService; | |
| this.prisma = prisma; | |
| this.idempotency = idempotency; | |
| this.selfHostedClients = selfHostedClients; | |
| this.chatwootSync = chatwootSync; | |
| this.eventsGateway = eventsGateway; | |
| } | |
| requireSecret(envName, configured) { | |
| if (!configured && process.env.NODE_ENV === "production") { | |
| this.logger.error(`${envName} is not configured — rejecting webhook`); | |
| throw new common_1.UnauthorizedException(`${envName} is not configured`); | |
| } | |
| } | |
| async checkIdempotency(provider, body) { | |
| const eventKey = this.idempotency.generateEventKey(provider, body); | |
| const isDuplicate = await this.idempotency.checkAndMark(eventKey); | |
| if (isDuplicate) { | |
| this.logger.warn(`Duplicate webhook ignored: ${provider}:${eventKey}`); | |
| } | |
| return isDuplicate; | |
| } | |
| verifyLiveKitJwt(authorization, apiKey, apiSecret, rawBody) { | |
| if (!authorization?.startsWith("Bearer ")) | |
| return false; | |
| const token = authorization.slice("Bearer ".length).trim(); | |
| const parts = token.split("."); | |
| if (parts.length !== 3) | |
| return false; | |
| const [headerB64, payloadB64, signatureB64] = parts; | |
| try { | |
| const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8")); | |
| if (payload.iss !== apiKey) | |
| return false; | |
| const nowSec = Math.floor(Date.now() / 1000); | |
| if (typeof payload.exp === "number" && payload.exp < nowSec) | |
| return false; | |
| if (typeof payload.nbf === "number" && payload.nbf > nowSec + 60) | |
| return false; | |
| const signingInput = `${headerB64}.${payloadB64}`; | |
| const expected = crypto | |
| .createHmac("sha256", apiSecret) | |
| .update(signingInput) | |
| .digest("base64url"); | |
| if (!timingSafeCompare(expected, signatureB64)) | |
| return false; | |
| if (typeof payload.sha256 !== "string") | |
| return false; | |
| if (rawBody === undefined) { | |
| return process.env.NODE_ENV !== "production"; | |
| } | |
| const bodyBuf = typeof rawBody === "string" ? Buffer.from(rawBody, "utf8") : rawBody; | |
| const bodyHash = crypto | |
| .createHash("sha256") | |
| .update(bodyBuf) | |
| .digest("base64"); | |
| return timingSafeCompare(payload.sha256, bodyHash); | |
| } | |
| catch { | |
| return false; | |
| } | |
| } | |
| async handleCalComWebhook(body, secret) { | |
| const expected = this.configService.get("CALCOM_WEBHOOK_SECRET", ""); | |
| this.requireSecret("CALCOM_WEBHOOK_SECRET", expected); | |
| if (expected && !timingSafeCompare(secret, expected)) { | |
| throw new common_1.UnauthorizedException("Invalid Cal.com webhook secret"); | |
| } | |
| if (await this.checkIdempotency("calcom", body)) { | |
| return { received: true, duplicate: true }; | |
| } | |
| const eventType = String(body?.triggerEvent || body?.event || "unknown"); | |
| const payload = (body?.payload || body); | |
| this.logger.log(`Cal.com webhook received: ${eventType}`); | |
| await this.handleCalComEvent(eventType, payload); | |
| return { received: true, event: eventType }; | |
| } | |
| async handleCalComEvent(eventType, payload) { | |
| const uid = String(payload?.uid || payload?.bookingId || ""); | |
| if (!uid) | |
| return; | |
| const booking = await this.prisma.booking.findFirst({ | |
| where: { OR: [{ calendarEventId: uid }, { meetLink: { contains: uid } }] }, | |
| include: { | |
| episode: { | |
| include: { | |
| podcast: { | |
| select: { workspaceId: true }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }); | |
| if (!booking) { | |
| this.logger.warn(`No booking found for Cal.com UID: ${uid}`); | |
| return; | |
| } | |
| switch (eventType) { | |
| case "BOOKING_CREATED": | |
| case "BOOKING_CONFIRMED": | |
| await this.prisma.booking.update({ | |
| where: { id: booking.id }, | |
| data: { isConfirmed: true, updatedAt: new Date() }, | |
| }); | |
| this.logger.log(`Booking ${booking.id} confirmed via Cal.com`); | |
| if (booking.episode?.podcast?.workspaceId) { | |
| this.eventsGateway.notifyCalComBookingCreated(booking.episode.podcast.workspaceId, { id: booking.id, isConfirmed: true }); | |
| } | |
| break; | |
| case "BOOKING_CANCELLED": | |
| case "BOOKING_REJECTED": | |
| await this.prisma.booking.update({ | |
| where: { id: booking.id }, | |
| data: { isConfirmed: false, updatedAt: new Date() }, | |
| }); | |
| this.logger.log(`Booking ${booking.id} cancelled via Cal.com`); | |
| if (booking.episode?.podcast?.workspaceId) { | |
| this.eventsGateway.notifyCalComBookingCancelled(booking.episode.podcast.workspaceId, booking.id); | |
| } | |
| break; | |
| case "BOOKING_RESCHEDULED": | |
| const newStart = String(payload?.startTime || payload?.rescheduledStartTime || ""); | |
| const newEnd = String(payload?.endTime || payload?.rescheduledEndTime || ""); | |
| if (newStart && newEnd) { | |
| await this.prisma.booking.update({ | |
| where: { id: booking.id }, | |
| data: { | |
| startTime: new Date(newStart), | |
| endTime: new Date(newEnd), | |
| updatedAt: new Date(), | |
| }, | |
| }); | |
| this.logger.log(`Booking ${booking.id} rescheduled to ${newStart}`); | |
| if (booking.episode?.podcast?.workspaceId) { | |
| this.eventsGateway.notifyCalComBookingRescheduled(booking.episode.podcast.workspaceId, booking.id); | |
| } | |
| } | |
| break; | |
| case "BOOKING_NO_SHOW": | |
| this.logger.log(`Booking ${booking.id} no-show (no status field available)`); | |
| break; | |
| case "BOOKING_ATTENDEE_ADDED": | |
| case "BOOKING_ATTENDEE_REMOVED": | |
| this.logger.log(`Attendee change for booking ${booking.id}: ${eventType}`); | |
| break; | |
| default: | |
| this.logger.debug(`Unhandled Cal.com event type: ${eventType}`); | |
| } | |
| } | |
| async handleChatwootWebhook(accountId, req, body, signature, timestamp) { | |
| const expected = this.configService.get("CHATWOOT_WEBHOOK_SECRET", ""); | |
| this.requireSecret("CHATWOOT_WEBHOOK_SECRET", expected); | |
| if (expected && | |
| !verifyRawBodyHmac(req.rawBody, signature, expected, timestamp, true)) { | |
| throw new common_1.UnauthorizedException("Invalid Chatwoot webhook signature"); | |
| } | |
| if (await this.checkIdempotency("chatwoot", body)) { | |
| return { received: true, duplicate: true }; | |
| } | |
| const event = body?.event || "unknown"; | |
| this.logger.log(`Chatwoot webhook: ${event} for account ${accountId}`); | |
| await this.handleChatwootEvent(event, body, accountId); | |
| return { received: true, event, accountId }; | |
| } | |
| async handleChatwootEvent(event, body, accountId) { | |
| switch (event) { | |
| case "message_created": | |
| if (body?.message_type === "incoming" && body?.content) { | |
| const conversationId = body?.conversation?.id; | |
| const contactId = body?.conversation?.contact_inbox?.contact_id; | |
| const messageId = body?.message?.id; | |
| this.logger.log(`Incoming Chatwoot message in conversation ${conversationId} from contact ${contactId}`); | |
| if (contactId) { | |
| await this.syncChatwootContact(contactId, accountId); | |
| } | |
| if (conversationId && messageId && body.content) { | |
| await this.chatwootSync.syncIncomingMessage(accountId, conversationId, messageId, body.content, "incoming"); | |
| } | |
| } | |
| break; | |
| case "conversation_created": | |
| this.logger.log(`New Chatwoot conversation: ${body?.conversation?.id}`); | |
| break; | |
| case "conversation_status_changed": { | |
| const newStatus = body?.conversation?.status; | |
| const conversationId = body?.conversation?.id; | |
| this.logger.log(`Conversation ${conversationId} status changed to ${newStatus}`); | |
| if (conversationId && newStatus) { | |
| await this.chatwootSync.syncConversationStatus(accountId, conversationId, newStatus); | |
| } | |
| break; | |
| } | |
| case "conversation_assigned": { | |
| const conversationId = body?.conversation?.id; | |
| const assigneeId = body?.conversation?.assignee?.id; | |
| this.logger.log(`Conversation ${conversationId} assigned to ${assigneeId}`); | |
| if (conversationId) { | |
| await this.chatwootSync.syncConversationAssignment(accountId, conversationId, assigneeId); | |
| } | |
| break; | |
| } | |
| case "contact_created": | |
| case "contact_updated": | |
| const contactId = body?.contact?.id; | |
| if (contactId) { | |
| await this.syncChatwootContact(contactId, accountId); | |
| } | |
| break; | |
| default: | |
| this.logger.debug(`Unhandled Chatwoot event: ${event}`); | |
| } | |
| } | |
| async syncChatwootContact(contactId, accountId) { | |
| try { | |
| const integration = await this.prisma.workspaceIntegration.findFirst({ | |
| where: { | |
| provider: "chatwoot", | |
| status: "connected", | |
| metadata: { path: ["accountId"], equals: accountId }, | |
| }, | |
| }); | |
| if (!integration) { | |
| this.logger.warn(`No workspace integration found for Chatwoot account ${accountId}`); | |
| return; | |
| } | |
| const apiKey = integration.accessToken; | |
| if (!apiKey) { | |
| this.logger.warn(`No API key found for Chatwoot integration ${integration.id}`); | |
| return; | |
| } | |
| const contact = await this.selfHostedClients.getChatwootContact(apiKey, contactId); | |
| if (!contact?.email) { | |
| this.logger.warn(`Chatwoot contact ${contactId} has no email, skipping sync`); | |
| return; | |
| } | |
| const workspace = await this.prisma.workspace.findUnique({ | |
| where: { id: integration.workspaceId }, | |
| include: { podcasts: { take: 1 } }, | |
| }); | |
| const podcastId = workspace?.podcasts?.[0]?.id; | |
| if (!podcastId) { | |
| this.logger.warn(`No podcast found for workspace ${integration.workspaceId}, cannot create guest`); | |
| return; | |
| } | |
| const existingGuest = await this.prisma.guest.findFirst({ | |
| where: { podcastId, email: contact.email }, | |
| }); | |
| if (existingGuest) { | |
| const currentCustomFields = existingGuest.customFields || {}; | |
| await this.prisma.guest.update({ | |
| where: { id: existingGuest.id }, | |
| data: { | |
| fullName: contact.name || existingGuest.fullName, | |
| lastContactAt: new Date(), | |
| customFields: { | |
| ...currentCustomFields, | |
| chatwootContactId: contactId, | |
| chatwootAccountId: accountId, | |
| chatwootPhone: contact.phone_number, | |
| chatwootCustomAttributes: contact.custom_attributes, | |
| }, | |
| }, | |
| }); | |
| this.logger.log(`Updated guest ${existingGuest.id} from Chatwoot contact ${contactId}`); | |
| } | |
| else { | |
| await this.prisma.guest.create({ | |
| data: { | |
| podcastId, | |
| email: contact.email, | |
| fullName: contact.name || contact.email.split("@")[0], | |
| status: "prospect", | |
| customFields: { | |
| chatwootContactId: contactId, | |
| chatwootAccountId: accountId, | |
| chatwootPhone: contact.phone_number, | |
| chatwootCustomAttributes: contact.custom_attributes, | |
| source: "chatwoot", | |
| }, | |
| }, | |
| }); | |
| this.logger.log(`Created guest from Chatwoot contact ${contactId} (${contact.email})`); | |
| } | |
| } | |
| catch (error) { | |
| const message = error instanceof Error ? error.message : String(error); | |
| this.logger.error(`Failed to sync Chatwoot contact ${contactId}: ${message}`); | |
| } | |
| } | |
| async handleDocumensoWebhook(body, secret) { | |
| const expected = this.configService.get("DOCUMENSO_WEBHOOK_SECRET", ""); | |
| this.requireSecret("DOCUMENSO_WEBHOOK_SECRET", expected); | |
| if (expected && !timingSafeCompare(secret, expected)) { | |
| throw new common_1.UnauthorizedException("Invalid Documenso webhook secret"); | |
| } | |
| if (await this.checkIdempotency("documenso", body)) { | |
| return { received: true, duplicate: true }; | |
| } | |
| const event = body?.event || "unknown"; | |
| const documentId = body?.documentId || body?.data?.id; | |
| this.logger.log(`Documenso webhook: ${event} docId=${documentId ?? ""}`); | |
| if (documentId && (event === "document_completed" || event === "document_declined")) { | |
| this.logger.log(`Document ${documentId} status: ${event}`); | |
| } | |
| return { received: true, event, documentId }; | |
| } | |
| async handleFormbricksWebhook(req, body, signature, webhookId, webhookTimestamp) { | |
| const expected = this.configService.get("FORMBRICKS_WEBHOOK_SECRET", ""); | |
| this.requireSecret("FORMBRICKS_WEBHOOK_SECRET", expected); | |
| if (expected && | |
| !verifyStandardWebhooksSignature(req.rawBody, signature, webhookId, webhookTimestamp, expected)) { | |
| throw new common_1.UnauthorizedException("Invalid Formbricks webhook signature"); | |
| } | |
| if (await this.checkIdempotency("formbricks", body)) { | |
| return { received: true, duplicate: true }; | |
| } | |
| const event = body?.event || body?.type || "unknown"; | |
| const surveyId = body?.surveyId || body?.data?.surveyId; | |
| const responseId = body?.responseId || body?.data?.id; | |
| this.logger.log(`Formbricks webhook: ${event} surveyId=${surveyId ?? ""} responseId=${responseId ?? ""}`); | |
| if (body?.data?.userId) { | |
| const guest = await this.prisma.guest.findFirst({ | |
| where: { id: body.data.userId }, | |
| }); | |
| if (guest) { | |
| await this.prisma.guestJourneyEvent.create({ | |
| data: { | |
| guestId: guest.id, | |
| eventType: "follow_up", | |
| title: "Completed Survey", | |
| details: `Completed survey ${surveyId || ""}`, | |
| metadata: JSON.parse(JSON.stringify(body?.data || {})), | |
| }, | |
| }); | |
| this.logger.log(`Recorded survey completion journey event for guest ${guest.id}`); | |
| } | |
| } | |
| return { received: true, event, surveyId, responseId }; | |
| } | |
| async handleListmonkWebhook(body, secret) { | |
| const expected = this.configService.get("LISTMONK_WEBHOOK_SECRET", ""); | |
| this.requireSecret("LISTMONK_WEBHOOK_SECRET", expected); | |
| if (expected && !timingSafeCompare(secret, expected)) { | |
| throw new common_1.UnauthorizedException("Invalid Listmonk webhook secret"); | |
| } | |
| if (await this.checkIdempotency("listmonk", body)) { | |
| return { received: true, duplicate: true }; | |
| } | |
| const event = body?.event || "unknown"; | |
| const subscriberId = body?.data?.subscriber?.id || body?.subscriber_id; | |
| this.logger.log(`Listmonk webhook: ${event} subscriberId=${subscriberId ?? ""}`); | |
| await this.handleListmonkEvent(event, body); | |
| return { received: true, event, subscriberId }; | |
| } | |
| async handleListmonkEvent(event, body) { | |
| const subscriber = body?.data?.subscriber || body?.subscriber; | |
| if (!subscriber) | |
| return; | |
| const email = subscriber.email; | |
| const name = subscriber.name || ""; | |
| const status = subscriber.status || "enabled"; | |
| const lists = subscriber.lists || []; | |
| switch (event) { | |
| case "subscriber.created": | |
| case "subscriber.updated": | |
| await this.syncListmonkSubscriber(subscriber); | |
| break; | |
| case "subscriber.deleted": | |
| await this.unsubscribeListmonkSubscriber(email); | |
| break; | |
| case "subscriber.subscribed": | |
| case "subscriber.unsubscribed": | |
| await this.handleListmonkSubscriptionChange(email, lists, event); | |
| break; | |
| default: | |
| this.logger.debug(`Unhandled Listmonk event: ${event}`); | |
| } | |
| } | |
| async syncListmonkSubscriber(subscriber) { | |
| const email = subscriber.email; | |
| if (!email) | |
| return; | |
| try { | |
| const guest = await this.prisma.guest.findFirst({ | |
| where: { email }, | |
| }); | |
| if (guest) { | |
| await this.prisma.guest.update({ | |
| where: { id: guest.id }, | |
| data: { | |
| customFields: { | |
| listmonkSubscriberId: subscriber.id, | |
| listmonkStatus: subscriber.status, | |
| listmonkLists: subscriber.lists || [], | |
| }, | |
| }, | |
| }); | |
| this.logger.log(`Synced Listmonk subscriber ${subscriber.id} to guest ${guest.id}`); | |
| } | |
| else { | |
| this.logger.log(`Listmonk subscriber ${email} not found in guests, would create contact`); | |
| } | |
| } | |
| catch (error) { | |
| const message = error instanceof Error ? error.message : String(error); | |
| this.logger.error(`Failed to sync Listmonk subscriber ${subscriber.id}: ${message}`); | |
| } | |
| } | |
| async unsubscribeListmonkSubscriber(email) { | |
| try { | |
| const guest = await this.prisma.guest.findFirst({ where: { email } }); | |
| if (guest) { | |
| await this.prisma.guest.update({ | |
| where: { id: guest.id }, | |
| data: { | |
| customFields: { | |
| listmonkUnsubscribedAt: new Date().toISOString(), | |
| }, | |
| }, | |
| }); | |
| this.logger.log(`Marked guest ${guest.id} as unsubscribed via Listmonk webhook`); | |
| } | |
| } | |
| catch (error) { | |
| const message = error instanceof Error ? error.message : String(error); | |
| this.logger.error(`Failed to unsubscribe guest for email ${email}: ${message}`); | |
| } | |
| } | |
| async handleListmonkSubscriptionChange(email, lists, event) { | |
| this.logger.log(`Listmonk subscription change for ${email}: ${event} on lists ${lists.map(l => l.id).join(", ")}`); | |
| } | |
| async handleLiveKitWebhook(req, body, authorization) { | |
| const apiKey = this.configService.get("LIVEKIT_API_KEY", ""); | |
| const apiSecret = this.configService.get("LIVEKIT_API_SECRET", ""); | |
| const rawBody = req.rawBody; | |
| if (process.env.NODE_ENV === "production") { | |
| if (!apiKey || | |
| !apiSecret || | |
| !this.verifyLiveKitJwt(authorization, apiKey, apiSecret, rawBody)) { | |
| throw new common_1.UnauthorizedException("Invalid LiveKit webhook authorization"); | |
| } | |
| } | |
| else if (apiKey && apiSecret && !this.verifyLiveKitJwt(authorization, apiKey, apiSecret, rawBody)) { | |
| throw new common_1.UnauthorizedException("Invalid LiveKit webhook authorization"); | |
| } | |
| else if (!apiKey || !apiSecret) { | |
| this.logger.warn("LIVEKIT_API_KEY / LIVEKIT_API_SECRET not configured — LiveKit webhook verification skipped (dev)"); | |
| } | |
| if (await this.checkIdempotency("livekit", body)) { | |
| return { received: true, duplicate: true }; | |
| } | |
| const event = String(body?.event || "unknown"); | |
| const roomName = body?.room?.name || body?.egressInfo?.roomName || String(body?.roomName || ""); | |
| this.logger.log(`LiveKit webhook: ${event} room=${roomName ?? ""}`); | |
| if (event === "room_finished" && roomName) { | |
| const episodeId = this.extractEpisodeIdFromRoom(roomName); | |
| if (episodeId) { | |
| const session = await this.prisma.recordingSession.findUnique({ | |
| where: { episodeId }, | |
| }); | |
| if (session && session.status === "active") { | |
| await this.prisma.recordingSession.update({ | |
| where: { id: session.id }, | |
| data: { status: "completed", endedAt: new Date() }, | |
| }); | |
| this.logger.log(`Marked recording session ${session.id} as completed via LiveKit webhook`); | |
| } | |
| } | |
| else { | |
| this.logger.warn(`Could not extract episode ID from room name: ${roomName}`); | |
| } | |
| } | |
| if (event === "egress_ended" || | |
| event === "egress_completed" || | |
| event === "recording.completed" || | |
| event === "recording_completed") { | |
| await this.handleLiveKitRecordingFinished(body); | |
| } | |
| if (event === "participant_joined" || event === "participant_left") { | |
| const participant = body?.participant; | |
| this.logger.log(`Participant ${participant?.identity} ${event} in room ${roomName}`); | |
| } | |
| return { received: true, event, roomName }; | |
| } | |
| extractEpisodeIdFromRoom(roomName) { | |
| const match = roomName.match(/^episode-(.+)$/); | |
| return match ? match[1] : null; | |
| } | |
| async handleLiveKitRecordingFinished(body) { | |
| const egressInfo = body?.recording; | |
| const roomName = egressInfo?.name || body?.room?.name; | |
| if (!roomName) { | |
| this.logger.warn("LiveKit recording/egress event missing room name"); | |
| return; | |
| } | |
| const episodeId = this.extractEpisodeIdFromRoom(roomName); | |
| if (!episodeId) { | |
| this.logger.warn(`Could not extract episode ID from room name: ${roomName}`); | |
| return; | |
| } | |
| const session = await this.prisma.recordingSession.findUnique({ | |
| where: { episodeId }, | |
| }); | |
| if (!session) { | |
| this.logger.warn(`No recording session found for episode ${episodeId}`); | |
| return; | |
| } | |
| if (session.status === "completed" || session.status === "processing") { | |
| this.logger.log(`Session ${session.id} already ${session.status}, skipping`); | |
| return; | |
| } | |
| await this.prisma.recordingSession.update({ | |
| where: { id: session.id }, | |
| data: { status: "processing", endedAt: new Date() }, | |
| }); | |
| this.logger.log(`Session ${session.id} marked as processing after LiveKit recording finished (${egressInfo?.status ?? "unknown"})`); | |
| } | |
| }; | |
| exports.IntegrationsWebhookController = IntegrationsWebhookController; | |
| __decorate([ | |
| (0, common_1.Post)("calcom"), | |
| (0, common_1.HttpCode)(common_1.HttpStatus.OK), | |
| __param(0, (0, common_1.Body)()), | |
| __param(1, (0, common_1.Headers)("x-cal-secret-key")), | |
| __metadata("design:type", Function), | |
| __metadata("design:paramtypes", [Object, String]), | |
| __metadata("design:returntype", Promise) | |
| ], IntegrationsWebhookController.prototype, "handleCalComWebhook", null); | |
| __decorate([ | |
| (0, common_1.Post)("chatwoot/:accountId"), | |
| (0, common_1.HttpCode)(common_1.HttpStatus.OK), | |
| __param(0, (0, common_1.Param)("accountId")), | |
| __param(1, (0, common_1.Req)()), | |
| __param(2, (0, common_1.Body)()), | |
| __param(3, (0, common_1.Headers)("x-chatwoot-signature")), | |
| __param(4, (0, common_1.Headers)("x-chatwoot-timestamp")), | |
| __metadata("design:type", Function), | |
| __metadata("design:paramtypes", [String, Object, Object, String, String]), | |
| __metadata("design:returntype", Promise) | |
| ], IntegrationsWebhookController.prototype, "handleChatwootWebhook", null); | |
| __decorate([ | |
| (0, common_1.Post)("documenso"), | |
| (0, common_1.HttpCode)(common_1.HttpStatus.OK), | |
| __param(0, (0, common_1.Body)()), | |
| __param(1, (0, common_1.Headers)("x-documenso-secret")), | |
| __metadata("design:type", Function), | |
| __metadata("design:paramtypes", [Object, String]), | |
| __metadata("design:returntype", Promise) | |
| ], IntegrationsWebhookController.prototype, "handleDocumensoWebhook", null); | |
| __decorate([ | |
| (0, common_1.Post)("formbricks"), | |
| (0, common_1.HttpCode)(common_1.HttpStatus.OK), | |
| __param(0, (0, common_1.Req)()), | |
| __param(1, (0, common_1.Body)()), | |
| __param(2, (0, common_1.Headers)("webhook-signature")), | |
| __param(3, (0, common_1.Headers)("webhook-id")), | |
| __param(4, (0, common_1.Headers)("webhook-timestamp")), | |
| __metadata("design:type", Function), | |
| __metadata("design:paramtypes", [Object, Object, String, String, String]), | |
| __metadata("design:returntype", Promise) | |
| ], IntegrationsWebhookController.prototype, "handleFormbricksWebhook", null); | |
| __decorate([ | |
| (0, common_1.Post)("listmonk"), | |
| (0, common_1.HttpCode)(common_1.HttpStatus.OK), | |
| __param(0, (0, common_1.Body)()), | |
| __param(1, (0, common_1.Headers)("x-listmonk-secret")), | |
| __metadata("design:type", Function), | |
| __metadata("design:paramtypes", [Object, String]), | |
| __metadata("design:returntype", Promise) | |
| ], IntegrationsWebhookController.prototype, "handleListmonkWebhook", null); | |
| __decorate([ | |
| (0, common_1.Post)("livekit"), | |
| (0, common_1.HttpCode)(common_1.HttpStatus.OK), | |
| __param(0, (0, common_1.Req)()), | |
| __param(1, (0, common_1.Body)()), | |
| __param(2, (0, common_1.Headers)("authorization")), | |
| __metadata("design:type", Function), | |
| __metadata("design:paramtypes", [Object, Object, String]), | |
| __metadata("design:returntype", Promise) | |
| ], IntegrationsWebhookController.prototype, "handleLiveKitWebhook", null); | |
| exports.IntegrationsWebhookController = IntegrationsWebhookController = IntegrationsWebhookController_1 = __decorate([ | |
| (0, common_1.Controller)("integrations/webhooks"), | |
| __metadata("design:paramtypes", [config_1.ConfigService, | |
| prisma_service_1.PrismaService, | |
| webhook_idempotency_service_1.WebhookIdempotencyService, | |
| self_hosted_clients_service_1.SelfHostedClientsService, | |
| chatwoot_sync_service_1.ChatwootSyncService, | |
| events_gateway_1.EventsGateway]) | |
| ], IntegrationsWebhookController); | |
| //# sourceMappingURL=integrations-webhook.controller.js.map |