Spaces:
Running
Running
Download apps/api/dist/network/network.processor.js from Benaah/MicProtocol: direct link, hf CLI and curl.
- Browser
- Download file 25 kB
-
https://huggingface.co/spaces/Benaah/MicProtocol/resolve/main/apps/api/dist/network/network.processor.js
- Command line
-
hf download hf://spaces/Benaah/MicProtocol/apps/api/dist/network/network.processor.js
-
curl -L -o network.processor.js https://huggingface.co/spaces/Benaah/MicProtocol/resolve/main/apps/api/dist/network/network.processor.js
25 kB
| ; | |
| 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 __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 NetworkProcessor_1; | |
| Object.defineProperty(exports, "__esModule", { value: true }); | |
| exports.NetworkQueueEvents = exports.NetworkProcessor = void 0; | |
| const bullmq_1 = require("@nestjs/bullmq"); | |
| const common_1 = require("@nestjs/common"); | |
| const bullmq_2 = require("bullmq"); | |
| const bullmq_3 = require("@nestjs/bullmq"); | |
| const bullmq_4 = require("bullmq"); | |
| const shared_types_1 = require("@micprotocol/shared-types"); | |
| const prisma_service_1 = require("../prisma/prisma.service"); | |
| const reputation_service_1 = require("./reputation.service"); | |
| const sms_service_1 = require("./sms.service"); | |
| const graph_service_1 = require("./graph.service"); | |
| const meilisearch_service_1 = require("./meilisearch.service"); | |
| const escape_html_1 = require("../common/escape-html"); | |
| let NetworkProcessor = NetworkProcessor_1 = class NetworkProcessor extends bullmq_1.WorkerHost { | |
| prisma; | |
| reputationEngine; | |
| smsService; | |
| graphService; | |
| meilisearch; | |
| emailQueue; | |
| logger = new common_1.Logger(NetworkProcessor_1.name); | |
| constructor(prisma, reputationEngine, smsService, graphService, meilisearch, emailQueue) { | |
| super(); | |
| this.prisma = prisma; | |
| this.reputationEngine = reputationEngine; | |
| this.smsService = smsService; | |
| this.graphService = graphService; | |
| this.meilisearch = meilisearch; | |
| this.emailQueue = emailQueue; | |
| } | |
| async process(job) { | |
| this.logger.log(`Processing network job ${job.id}: ${job.name}`); | |
| switch (job.name) { | |
| case "booking-request.notify": | |
| return this.handleBookingRequestNotify(job.data); | |
| case "booking-request.response-notify": | |
| return this.handleBookingResponseNotify(job.data); | |
| case "booking-request.accepted": | |
| return this.handleBookingAccepted(job.data); | |
| case "reputation.recalculate": | |
| return this.handleReputationRecalculate(job.data); | |
| case "graph.ingest-profile": | |
| return this.handleGraphIngest(job.data); | |
| case "digest.weekly": | |
| return this.handleWeeklyDigest(job.data); | |
| case "search.reindex": | |
| return this.handleSearchReindex(); | |
| case "search.index-profile": | |
| return this.handleSearchIndexProfile(job.data); | |
| case "search.remove-profile": | |
| return this.handleSearchRemoveProfile(job.data); | |
| default: | |
| this.logger.warn(`Unknown job type: ${job.name}`); | |
| return { success: false, error: "Unknown job type" }; | |
| } | |
| } | |
| extractFirstProposedDate(proposedDates) { | |
| if (!Array.isArray(proposedDates)) { | |
| return null; | |
| } | |
| for (const candidate of proposedDates) { | |
| if (typeof candidate !== "string") { | |
| continue; | |
| } | |
| const parsedDate = new Date(candidate); | |
| if (!Number.isNaN(parsedDate.getTime())) { | |
| return parsedDate; | |
| } | |
| } | |
| return null; | |
| } | |
| async handleBookingRequestNotify(data) { | |
| this.logger.log(`Sending booking request notification to ${data.guestEmail}`); | |
| const channels = []; | |
| try { | |
| const profile = await this.prisma.guestPublicProfile.findFirst({ | |
| where: { user: { email: data.guestEmail } }, | |
| select: { vanityUrl: true }, | |
| }); | |
| const actionUrl = profile | |
| ? `${process.env.APP_URL || "https://micprotocol.io"}/network/booking/${data.requestId}` | |
| : process.env.APP_URL || "https://micprotocol.io"; | |
| if (data.guestWhatsapp || data.guestPhone) { | |
| const phone = data.guestWhatsapp || data.guestPhone; | |
| const lang = this.smsService.detectLanguage(phone); | |
| const result = await this.smsService.sendBookingRequest(phone, { | |
| podcastName: data.podcastTitle, | |
| message: data.message, | |
| actionUrl, | |
| }, lang); | |
| if (result.success) { | |
| channels.push(result.channel); | |
| } | |
| } | |
| try { | |
| const safePodcastTitle = (0, escape_html_1.escapeHtml)(data.podcastTitle); | |
| const safeMessage = (0, escape_html_1.escapeHtml)(data.message); | |
| const safeActionUrl = encodeURI(actionUrl); | |
| await this.emailQueue.add("send-email", { | |
| to: data.guestEmail, | |
| subject: `Booking request from ${data.podcastTitle}`, | |
| bodyHtml: ` | |
| <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;"> | |
| <h2 style="color: #1a1a2e;">You have a booking request!</h2> | |
| <p style="color: #4a4a6a; line-height: 1.6;"> | |
| <strong>${safePodcastTitle}</strong> would like to invite you as a guest. | |
| </p> | |
| <blockquote style="border-left: 3px solid #FF6B35; padding-left: 16px; margin: 20px 0; color: #4a4a6a; font-style: italic;"> | |
| ${safeMessage} | |
| </blockquote> | |
| <div style="text-align: center; margin: 30px 0;"> | |
| <a href="${safeActionUrl}" | |
| style="background: #FF6B35; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; font-weight: 600;"> | |
| View Request | |
| </a> | |
| </div> | |
| <p style="color: #888; font-size: 12px;"> | |
| This request was sent via the MicProtocol Verified Guest Network. | |
| </p> | |
| </div> | |
| `, | |
| bodyText: `Booking request from ${data.podcastTitle}: ${data.message}\n\nView: ${actionUrl}`, | |
| }); | |
| channels.push("email"); | |
| } | |
| catch (emailError) { | |
| this.logger.warn(`Failed to queue booking email: ${emailError instanceof Error ? emailError.message : String(emailError)}`); | |
| } | |
| this.logger.log(`Notification sent via: ${channels.join(", ")}`); | |
| return { success: true, channels }; | |
| } | |
| catch (error) { | |
| this.logger.error(`Failed to send booking notification: ${error}`); | |
| throw error; | |
| } | |
| } | |
| async handleBookingResponseNotify(data) { | |
| this.logger.log(`Sending booking response notification for ${data.requestId}`); | |
| try { | |
| const request = await this.prisma.networkBookingRequest.findUnique({ | |
| where: { id: data.requestId }, | |
| include: { | |
| toProfile: { | |
| select: { | |
| headline: true, | |
| phoneNumber: true, | |
| whatsappNumber: true, | |
| }, | |
| }, | |
| fromPodcast: { | |
| select: { | |
| title: true, | |
| workspace: { | |
| select: { | |
| members: { | |
| where: { role: "owner" }, | |
| select: { | |
| user: { | |
| select: { email: true }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }); | |
| if (!request) { | |
| throw new Error("Booking request not found"); | |
| } | |
| const req = request; | |
| const hostEmails = req.fromPodcast?.workspace?.members | |
| ?.map((m) => m.user.email) | |
| .filter(Boolean) || []; | |
| const guestName = req.toProfile?.headline?.split("|")[0]?.trim() || "Guest"; | |
| const podcastTitle = req.fromPodcast?.title || "the podcast"; | |
| const statusText = data.status === "accepted" ? "accepted" : "declined"; | |
| const actionUrl = `${process.env.APP_URL || "https://micprotocol.io"}/network/requests/${data.requestId}`; | |
| if (hostEmails.length > 0) { | |
| const safeGuestName = (0, escape_html_1.escapeHtml)(guestName); | |
| const safePodcastTitle = (0, escape_html_1.escapeHtml)(podcastTitle); | |
| const safeActionUrl = encodeURI(actionUrl); | |
| const headerColor = data.status === "accepted" ? "#00B894" : "#E17055"; | |
| const headerText = data.status === "accepted" ? "Booking Accepted" : "Booking Declined"; | |
| const bodyPara = data.status === "accepted" | |
| ? `<p style="color: #4a4a6a;">The episode and booking have been automatically created in your pipeline. Check your dashboard for details.</p>` | |
| : `<p style="color: #4a4a6a;">You can look for other guests in the Verified Guest Network.</p>`; | |
| try { | |
| await this.emailQueue.add("send-email", { | |
| to: hostEmails[0], | |
| subject: `Guest ${statusText}: ${guestName} — ${podcastTitle}`, | |
| bodyHtml: ` | |
| <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;"> | |
| <h2 style="color: ${headerColor};"> | |
| ${headerText} | |
| </h2> | |
| <p style="color: #4a4a6a; line-height: 1.6;"> | |
| <strong>${safeGuestName}</strong> has ${statusText} your booking request for | |
| <strong>${safePodcastTitle}</strong>. | |
| </p> | |
| ${bodyPara} | |
| <div style="text-align: center; margin: 30px 0;"> | |
| <a href="${safeActionUrl}" | |
| style="background: #FF6B35; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; font-weight: 600;"> | |
| View Details | |
| </a> | |
| </div> | |
| </div> | |
| `, | |
| bodyText: `Guest ${statusText}: ${guestName} for ${podcastTitle}.\n\nView: ${actionUrl}`, | |
| }); | |
| for (const email of hostEmails.slice(1)) { | |
| await this.emailQueue.add("send-email", { | |
| to: email, | |
| subject: `Guest ${statusText}: ${guestName} — ${podcastTitle}`, | |
| bodyHtml: ` | |
| <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;"> | |
| <p style="color: #4a4a6a;"> | |
| <strong>${safeGuestName}</strong> has ${statusText} the booking request for | |
| <strong>${safePodcastTitle}</strong>. | |
| </p> | |
| <p><a href="${safeActionUrl}">View Details</a></p> | |
| </div> | |
| `, | |
| bodyText: `Guest ${statusText}: ${guestName}. View: ${actionUrl}`, | |
| }); | |
| } | |
| } | |
| catch (emailError) { | |
| this.logger.warn(`Failed to send response email: ${emailError instanceof Error ? emailError.message : String(emailError)}`); | |
| } | |
| } | |
| const hostPhone = req.toProfile?.whatsappNumber || req.toProfile?.phoneNumber; | |
| if (hostPhone) { | |
| try { | |
| const lang = this.smsService.detectLanguage(hostPhone); | |
| await this.smsService.sendBookingRequest(hostPhone, { | |
| podcastName: podcastTitle, | |
| message: `Your booking request was ${statusText} by ${guestName}.`, | |
| actionUrl, | |
| }, lang); | |
| } | |
| catch (smsError) { | |
| this.logger.warn(`Failed to send response SMS: ${smsError instanceof Error ? smsError.message : String(smsError)}`); | |
| } | |
| } | |
| return { success: true }; | |
| } | |
| catch (error) { | |
| this.logger.error(`Failed to send response notification: ${error}`); | |
| throw error; | |
| } | |
| } | |
| async handleBookingAccepted(data) { | |
| this.logger.log(`Creating pipeline entry for accepted booking ${data.requestId}`); | |
| try { | |
| const [request, profile] = await Promise.all([ | |
| this.prisma.networkBookingRequest.findUnique({ | |
| where: { id: data.requestId }, | |
| include: { | |
| fromPodcast: { | |
| select: { title: true }, | |
| }, | |
| }, | |
| }), | |
| this.prisma.guestPublicProfile.findUnique({ | |
| where: { id: data.profileId }, | |
| include: { user: { select: { email: true } } }, | |
| }), | |
| ]); | |
| if (!request || !profile) { | |
| throw new Error("Request or profile not found"); | |
| } | |
| const fullName = profile.headline?.split("|")[0]?.trim() || profile.user.email.split("@")[0] || "Network Guest"; | |
| const company = profile.headline?.split("|")[1]?.trim() || null; | |
| const existingGuest = await this.prisma.guest.findUnique({ | |
| where: { | |
| podcastId_email: { | |
| podcastId: data.podcastId, | |
| email: profile.user.email, | |
| }, | |
| }, | |
| }); | |
| const guest = existingGuest | |
| ? await this.prisma.guest.update({ | |
| where: { id: existingGuest.id }, | |
| data: { | |
| fullName, | |
| company, | |
| bio: profile.fullBio, | |
| socialLinks: profile.socialProof || {}, | |
| status: "scheduled", | |
| lastContactAt: new Date(), | |
| }, | |
| }) | |
| : await this.prisma.guest.create({ | |
| data: { | |
| podcastId: data.podcastId, | |
| email: profile.user.email, | |
| fullName, | |
| company, | |
| title: "", | |
| bio: profile.fullBio, | |
| socialLinks: profile.socialProof || {}, | |
| status: "scheduled", | |
| lastContactAt: new Date(), | |
| }, | |
| }); | |
| if (!existingGuest || existingGuest.status !== "scheduled") { | |
| await this.prisma.guestJourneyEvent.create({ | |
| data: { | |
| guestId: guest.id, | |
| eventType: shared_types_1.GuestJourneyEventType.SCHEDULED, | |
| title: "Guest marked as scheduled", | |
| details: "Guest lifecycle moved to scheduled via accepted network booking.", | |
| metadata: { | |
| source: "network.processor.handleBookingAccepted", | |
| requestId: data.requestId, | |
| }, | |
| }, | |
| }); | |
| } | |
| const requestMarker = `[network-request:${request.id}]`; | |
| const proposedStart = this.extractFirstProposedDate(request.proposedDates); | |
| let episode = await this.prisma.episode.findFirst({ | |
| where: { | |
| podcastId: data.podcastId, | |
| guestId: guest.id, | |
| hostNotes: { | |
| contains: requestMarker, | |
| }, | |
| }, | |
| orderBy: { createdAt: "desc" }, | |
| }); | |
| if (!episode) { | |
| episode = await this.prisma.episode.create({ | |
| data: { | |
| podcastId: data.podcastId, | |
| guestId: guest.id, | |
| title: `Network booking: ${fullName}`, | |
| status: proposedStart ? "scheduled" : "draft", | |
| scheduledAt: proposedStart, | |
| hostNotes: `${requestMarker}\n\nNetwork booking request accepted`, | |
| }, | |
| }); | |
| } | |
| else { | |
| await this.prisma.episode.update({ | |
| where: { id: episode.id }, | |
| data: { | |
| status: "scheduled", | |
| scheduledAt: episode.scheduledAt || proposedStart, | |
| }, | |
| }); | |
| } | |
| if (proposedStart) { | |
| const existingBooking = await this.prisma.booking.findFirst({ | |
| where: { episodeId: episode.id }, | |
| }); | |
| if (!existingBooking) { | |
| const proposedEnd = new Date(proposedStart.getTime() + 60 * 60 * 1000); | |
| await this.prisma.booking.create({ | |
| data: { | |
| episodeId: episode.id, | |
| startTime: proposedStart, | |
| endTime: proposedEnd, | |
| timezone: profile.timezone || "UTC", | |
| meetLink: null, | |
| isConfirmed: false, | |
| }, | |
| }); | |
| } | |
| } | |
| const existingBookingHistory = await this.prisma.bookingHistory.findFirst({ | |
| where: { | |
| profileId: data.profileId, | |
| episodeId: episode.id, | |
| }, | |
| }); | |
| if (!existingBookingHistory) { | |
| await this.prisma.bookingHistory.create({ | |
| data: { | |
| profileId: data.profileId, | |
| episodeId: episode.id, | |
| podcastName: request.fromPodcast.title, | |
| appearedAt: new Date(), | |
| isPublic: true, | |
| topicsDiscussed: request.proposedTopics, | |
| }, | |
| }); | |
| } | |
| this.logger.log(`Pipeline entry created: guest=${guest.id}, episode=${episode.id}`); | |
| return { | |
| success: true, | |
| guestId: guest.id, | |
| episodeId: episode.id, | |
| }; | |
| } | |
| catch (error) { | |
| this.logger.error(`Failed to create pipeline entry: ${error}`); | |
| throw error; | |
| } | |
| } | |
| async handleReputationRecalculate(data) { | |
| this.logger.log(`Recalculating reputation for ${data.profileId} (trigger: ${data.trigger})`); | |
| try { | |
| const score = await this.reputationEngine.calculate(data.profileId); | |
| this.logger.log(`Reputation updated: ${data.profileId} = ${score.composite} (${score.tier})`); | |
| const profile = await this.prisma.guestPublicProfile.findUnique({ | |
| where: { id: data.profileId }, | |
| }); | |
| if (profile) { | |
| await this.meilisearch.updateProfile(profile); | |
| } | |
| return { | |
| success: true, | |
| score: score.composite, | |
| tier: score.tier, | |
| }; | |
| } | |
| catch (error) { | |
| this.logger.error(`Failed to recalculate reputation: ${error}`); | |
| throw error; | |
| } | |
| } | |
| async handleGraphIngest(data) { | |
| this.logger.log(`Ingesting profile ${data.profileId} into graph`); | |
| try { | |
| await this.graphService.ingestProfile(data.profileId); | |
| return { success: true }; | |
| } | |
| catch (error) { | |
| this.logger.error(`Failed to ingest profile: ${error}`); | |
| throw error; | |
| } | |
| } | |
| async handleSearchIndexProfile(data) { | |
| this.logger.log(`Indexing profile ${data.profileId} in search`); | |
| try { | |
| const profile = await this.prisma.guestPublicProfile.findUnique({ | |
| where: { id: data.profileId }, | |
| include: { expertiseTags: true }, | |
| }); | |
| if (profile) { | |
| await this.meilisearch.indexProfile(profile); | |
| } | |
| return { success: true }; | |
| } | |
| catch (error) { | |
| this.logger.error(`Failed to index profile: ${error}`); | |
| throw error; | |
| } | |
| } | |
| async handleSearchRemoveProfile(data) { | |
| this.logger.log(`Removing profile ${data.profileId} from search`); | |
| try { | |
| await this.meilisearch.removeProfile(data.profileId); | |
| return { success: true }; | |
| } | |
| catch (error) { | |
| this.logger.error(`Failed to remove profile from search: ${error}`); | |
| throw error; | |
| } | |
| } | |
| async handleSearchReindex() { | |
| this.logger.log("Starting search reindex via background job"); | |
| try { | |
| return await this.meilisearch.bulkIndex(); | |
| } | |
| catch (error) { | |
| this.logger.error("Search reindex failed", error); | |
| throw error; | |
| } | |
| } | |
| async handleWeeklyDigest(data) { | |
| this.logger.log(`Sending weekly digest for ${data.profileId}`); | |
| try { | |
| const profile = await this.prisma.guestPublicProfile.findUnique({ | |
| where: { id: data.profileId }, | |
| include: { | |
| user: { select: { email: true } }, | |
| }, | |
| }); | |
| if (!profile || !profile.isPublic) { | |
| return { success: false }; | |
| } | |
| const viewsLastWeek = await this.prisma.profileView.count({ | |
| where: { | |
| profileId: data.profileId, | |
| viewedAt: { | |
| gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), | |
| }, | |
| }, | |
| }); | |
| const pendingRequests = await this.prisma.networkBookingRequest.count({ | |
| where: { | |
| toProfileId: data.profileId, | |
| status: "pending", | |
| }, | |
| }); | |
| if (viewsLastWeek > 10 || pendingRequests > 0) { | |
| if (profile.whatsappNumber || profile.phoneNumber) { | |
| await this.smsService.sendProfileViewNotification(profile.whatsappNumber || profile.phoneNumber, viewsLastWeek); | |
| } | |
| } | |
| return { success: true }; | |
| } | |
| catch (error) { | |
| this.logger.error(`Failed to send weekly digest: ${error}`); | |
| throw error; | |
| } | |
| } | |
| }; | |
| exports.NetworkProcessor = NetworkProcessor; | |
| exports.NetworkProcessor = NetworkProcessor = NetworkProcessor_1 = __decorate([ | |
| (0, bullmq_1.Processor)("network"), | |
| __param(5, (0, bullmq_3.InjectQueue)("email")), | |
| __metadata("design:paramtypes", [prisma_service_1.PrismaService, | |
| reputation_service_1.ReputationEngine, | |
| sms_service_1.SmsService, | |
| graph_service_1.GraphService, | |
| meilisearch_service_1.MeilisearchService, | |
| bullmq_4.Queue]) | |
| ], NetworkProcessor); | |
| class NetworkQueueEvents extends bullmq_1.QueueEventsHost { | |
| logger = new common_1.Logger(NetworkQueueEvents.name); | |
| onCompleted(job) { | |
| this.logger.debug(`Job ${job.id} completed successfully`); | |
| } | |
| onFailed(job, error) { | |
| this.logger.error(`Job ${job.id} failed: ${error.message}`); | |
| } | |
| onStalled(jobId) { | |
| this.logger.warn(`Job ${jobId} has stalled`); | |
| } | |
| } | |
| exports.NetworkQueueEvents = NetworkQueueEvents; | |
| __decorate([ | |
| (0, bullmq_1.OnQueueEvent)("completed"), | |
| __metadata("design:type", Function), | |
| __metadata("design:paramtypes", [bullmq_2.Job]), | |
| __metadata("design:returntype", void 0) | |
| ], NetworkQueueEvents.prototype, "onCompleted", null); | |
| __decorate([ | |
| (0, bullmq_1.OnQueueEvent)("failed"), | |
| __metadata("design:type", Function), | |
| __metadata("design:paramtypes", [bullmq_2.Job, Error]), | |
| __metadata("design:returntype", void 0) | |
| ], NetworkQueueEvents.prototype, "onFailed", null); | |
| __decorate([ | |
| (0, bullmq_1.OnQueueEvent)("stalled"), | |
| __metadata("design:type", Function), | |
| __metadata("design:paramtypes", [String]), | |
| __metadata("design:returntype", void 0) | |
| ], NetworkQueueEvents.prototype, "onStalled", null); | |
| //# sourceMappingURL=network.processor.js.map |