import RunCron from "../../Utils/Cron"; import { FileRoute } from "Common/ServiceRoute"; import Hostname from "Common/Types/API/Hostname"; import Protocol from "Common/Types/API/Protocol"; import URL from "Common/Types/API/URL"; import LIMIT_MAX, { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax"; import OneUptimeDate from "Common/Types/Date"; import Dictionary from "Common/Types/Dictionary"; import EmailTemplateType from "Common/Types/Email/EmailTemplateType"; import ObjectID from "Common/Types/ObjectID"; import SMS from "Common/Types/SMS/SMS"; import Text from "Common/Types/Text"; import { EVERY_MINUTE } from "Common/Utils/CronTime"; import DatabaseConfig from "Common/Server/DatabaseConfig"; import MailService from "Common/Server/Services/MailService"; import ProjectCallSMSConfigService from "Common/Server/Services/ProjectCallSMSConfigService"; import ProjectSmtpConfigService from "Common/Server/Services/ProjectSmtpConfigService"; import ScheduledMaintenanceService from "Common/Server/Services/ScheduledMaintenanceService"; import ScheduledMaintenanceStateTimelineService from "Common/Server/Services/ScheduledMaintenanceStateTimelineService"; import SmsService from "Common/Server/Services/SmsService"; import StatusPageResourceService from "Common/Server/Services/StatusPageResourceService"; import StatusPageService from "Common/Server/Services/StatusPageService"; import StatusPageSubscriberService from "Common/Server/Services/StatusPageSubscriberService"; import QueryHelper from "Common/Server/Types/Database/QueryHelper"; import logger from "Common/Server/Utils/Logger"; import Monitor from "Common/Models/DatabaseModels/Monitor"; import ScheduledMaintenance from "Common/Models/DatabaseModels/ScheduledMaintenance"; import ScheduledMaintenanceStateTimeline from "Common/Models/DatabaseModels/ScheduledMaintenanceStateTimeline"; import StatusPage from "Common/Models/DatabaseModels/StatusPage"; import StatusPageResource from "Common/Models/DatabaseModels/StatusPageResource"; import StatusPageSubscriber from "Common/Models/DatabaseModels/StatusPageSubscriber"; RunCron( "ScheduledMaintenanceStateTimeline:SendNotificationToSubscribers", { schedule: EVERY_MINUTE, runOnStartup: false }, async () => { const host: Hostname = await DatabaseConfig.getHost(); const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol(); const scheduledEventStateTimelines: Array = await ScheduledMaintenanceStateTimelineService.findBy({ query: { isStatusPageSubscribersNotified: false, shouldStatusPageSubscribersBeNotified: true, createdAt: QueryHelper.lessThan(OneUptimeDate.getCurrentDate()), }, props: { isRoot: true, }, limit: LIMIT_MAX, skip: 0, select: { _id: true, scheduledMaintenanceId: true, scheduledMaintenanceStateId: true, scheduledMaintenanceState: { name: true, }, }, }); for (const scheduledEventStateTimeline of scheduledEventStateTimelines) { await ScheduledMaintenanceStateTimelineService.updateOneById({ id: scheduledEventStateTimeline.id!, data: { isStatusPageSubscribersNotified: true, }, props: { isRoot: true, ignoreHooks: true, }, }); if ( !scheduledEventStateTimeline.scheduledMaintenanceId || !scheduledEventStateTimeline.scheduledMaintenanceStateId ) { continue; } if (!scheduledEventStateTimeline.scheduledMaintenanceState?.name) { continue; } // get all scheduled events of all the projects. const event: ScheduledMaintenance | null = await ScheduledMaintenanceService.findOneById({ id: scheduledEventStateTimeline.scheduledMaintenanceId!, props: { isRoot: true, }, select: { _id: true, title: true, description: true, startsAt: true, monitors: { _id: true, }, statusPages: { _id: true, }, }, }); if (!event) { continue; } // get status page resources from monitors. let statusPageResources: Array = []; if (event.monitors && event.monitors.length > 0) { statusPageResources = await StatusPageResourceService.findBy({ query: { monitorId: QueryHelper.any( event.monitors .filter((m: Monitor) => { return m._id; }) .map((m: Monitor) => { return new ObjectID(m._id!); }), ), }, props: { isRoot: true, ignoreHooks: true, }, skip: 0, limit: LIMIT_PER_PROJECT, select: { _id: true, displayName: true, statusPageId: true, }, }); } const statusPageToResources: Dictionary> = {}; for (const resource of statusPageResources) { if (!resource.statusPageId) { continue; } if (!statusPageToResources[resource.statusPageId?.toString()]) { statusPageToResources[resource.statusPageId?.toString()] = []; } statusPageToResources[resource.statusPageId?.toString()]?.push( resource, ); } const statusPages: Array = await StatusPageSubscriberService.getStatusPagesToSendNotification( event.statusPages?.map((i: StatusPage) => { return i.id!; }) || [], ); for (const statuspage of statusPages) { if (!statuspage.id) { continue; } const subscribers: Array = await StatusPageSubscriberService.getSubscribersByStatusPage( statuspage.id!, { isRoot: true, ignoreHooks: true, }, ); const statusPageURL: string = await StatusPageService.getStatusPageURL( statuspage.id, ); const statusPageName: string = statuspage.pageTitle || statuspage.name || "Status Page"; // Send email to Email subscribers. for (const subscriber of subscribers) { if (!subscriber._id) { continue; } const shouldNotifySubscriber: boolean = StatusPageSubscriberService.shouldSendNotification({ subscriber: subscriber, statusPageResources: statusPageToResources[statuspage._id!] || [], statusPage: statuspage, }); if (!shouldNotifySubscriber) { continue; } const unsubscribeUrl: string = StatusPageSubscriberService.getUnsubscribeLink( URL.fromString(statusPageURL), subscriber.id!, ).toString(); if (subscriber.subscriberPhone) { const sms: SMS = { message: ` ${statusPageName} - Scheduled maintenance event - ${ event.title || "" } - state changed to ${ scheduledEventStateTimeline .scheduledMaintenanceState?.name } To view this note, visit ${statusPageURL} To update notification preferences or unsubscribe, visit ${unsubscribeUrl} `, to: subscriber.subscriberPhone, }; // send sms here. SmsService.sendSms(sms, { projectId: statuspage.projectId, customTwilioConfig: ProjectCallSMSConfigService.toTwilioConfig( statuspage.callSmsConfig, ), }).catch((err: Error) => { logger.error(err); }); } if (subscriber.subscriberEmail) { // send email here. MailService.sendMail( { toEmail: subscriber.subscriberEmail, templateType: EmailTemplateType.SubscriberScheduledMaintenanceEventStateChanged, vars: { statusPageName: statusPageName, statusPageUrl: statusPageURL, logoUrl: statuspage.logoFileId ? new URL(httpProtocol, host) .addRoute(FileRoute) .addRoute("/image/" + statuspage.logoFileId) .toString() : "", isPublicStatusPage: statuspage.isPublicStatusPage ? "true" : "false", resourcesAffected: statusPageToResources[statuspage._id!] ?.map((r: StatusPageResource) => { return r.displayName; }) .join(", ") || "", eventState: scheduledEventStateTimeline.scheduledMaintenanceState ?.name || "", scheduledAt: OneUptimeDate.getDateAsFormattedString( event.startsAt!, ), eventTitle: event.title || "", eventDescription: event.description || "", unsubscribeUrl: unsubscribeUrl, }, subject: `[Scheduled Maintenance ${Text.uppercaseFirstLetter( scheduledEventStateTimeline.scheduledMaintenanceState?.name, )}] ${statusPageName}`, }, { mailServer: ProjectSmtpConfigService.toEmailServer( statuspage.smtpConfig, ), projectId: statuspage.projectId, }, ).catch((err: Error) => { logger.error(err); }); } } } } }, );