mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
- Added ManualAPI for manually triggering workflows via GET and POST requests. - Introduced WorkflowAPI for updating workflows with authorization checks. - Created documentation for JavaScript and Webhook components. - Established WorkflowFeatureSet to initialize routing and job processing. - Developed QueueWorkflow service for managing workflow queue operations. - Implemented RunWorkflow service to execute workflows with error handling and logging. - Added utility for loading component metadata dynamically.
735 lines
29 KiB
TypeScript
735 lines
29 KiB
TypeScript
import RunCron from "../../Utils/Cron";
|
|
import { StatusPageApiRoute } 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 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 { EVERY_MINUTE } from "Common/Utils/CronTime";
|
|
import DatabaseConfig from "Common/Server/DatabaseConfig";
|
|
import IncidentEpisodePublicNoteService from "Common/Server/Services/IncidentEpisodePublicNoteService";
|
|
import IncidentEpisodeService from "Common/Server/Services/IncidentEpisodeService";
|
|
import IncidentEpisodeMemberService from "Common/Server/Services/IncidentEpisodeMemberService";
|
|
import MailService from "Common/Server/Services/MailService";
|
|
import ProjectCallSMSConfigService from "Common/Server/Services/ProjectCallSMSConfigService";
|
|
import ProjectSmtpConfigService from "Common/Server/Services/ProjectSmtpConfigService";
|
|
import SmsService from "Common/Server/Services/SmsService";
|
|
import StatusPageResourceService from "Common/Server/Services/StatusPageResourceService";
|
|
import StatusPageService, {
|
|
Service as StatusPageServiceType,
|
|
} from "Common/Server/Services/StatusPageService";
|
|
import StatusPageSubscriberService from "Common/Server/Services/StatusPageSubscriberService";
|
|
import Markdown, { MarkdownContentType } from "Common/Server/Types/Markdown";
|
|
import logger from "Common/Server/Utils/Logger";
|
|
import IncidentEpisode from "Common/Models/DatabaseModels/IncidentEpisode";
|
|
import IncidentEpisodeMember from "Common/Models/DatabaseModels/IncidentEpisodeMember";
|
|
import IncidentEpisodePublicNote from "Common/Models/DatabaseModels/IncidentEpisodePublicNote";
|
|
import StatusPage from "Common/Models/DatabaseModels/StatusPage";
|
|
import StatusPageResource from "Common/Models/DatabaseModels/StatusPageResource";
|
|
import StatusPageSubscriber from "Common/Models/DatabaseModels/StatusPageSubscriber";
|
|
import StatusPageEventType from "Common/Types/StatusPage/StatusPageEventType";
|
|
import StatusPageSubscriberNotificationStatus from "Common/Types/StatusPage/StatusPageSubscriberNotificationStatus";
|
|
import StatusPageSubscriberNotificationTemplateService, {
|
|
Service as StatusPageSubscriberNotificationTemplateServiceClass,
|
|
} from "Common/Server/Services/StatusPageSubscriberNotificationTemplateService";
|
|
import StatusPageSubscriberNotificationTemplate from "Common/Models/DatabaseModels/StatusPageSubscriberNotificationTemplate";
|
|
import StatusPageSubscriberNotificationEventType from "Common/Types/StatusPage/StatusPageSubscriberNotificationEventType";
|
|
import StatusPageSubscriberNotificationMethod from "Common/Types/StatusPage/StatusPageSubscriberNotificationMethod";
|
|
import IncidentEpisodeFeedService from "Common/Server/Services/IncidentEpisodeFeedService";
|
|
import { IncidentEpisodeFeedEventType } from "Common/Models/DatabaseModels/IncidentEpisodeFeed";
|
|
import { Blue500 } from "Common/Types/BrandColors";
|
|
import SlackUtil from "Common/Server/Utils/Workspace/Slack/Slack";
|
|
import MicrosoftTeamsUtil from "Common/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams";
|
|
import StatusPageResourceUtil from "Common/Server/Utils/StatusPageResource";
|
|
|
|
RunCron(
|
|
"IncidentEpisodePublicNote:SendNotificationToSubscribers",
|
|
{ schedule: EVERY_MINUTE, runOnStartup: false },
|
|
async () => {
|
|
// First, mark public notes as Skipped if they should not be notified
|
|
const notesToSkip: Array<IncidentEpisodePublicNote> =
|
|
await IncidentEpisodePublicNoteService.findBy({
|
|
query: {
|
|
subscriberNotificationStatusOnNoteCreated:
|
|
StatusPageSubscriberNotificationStatus.Pending,
|
|
shouldStatusPageSubscribersBeNotifiedOnNoteCreated: false,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
limit: LIMIT_MAX,
|
|
skip: 0,
|
|
select: {
|
|
_id: true,
|
|
},
|
|
});
|
|
|
|
logger.debug(
|
|
`Found ${notesToSkip.length} episode public note(s) to mark as Skipped (subscribers should not be notified).`,
|
|
);
|
|
|
|
for (const note of notesToSkip) {
|
|
logger.debug(
|
|
`Marking episode public note ${note.id} as Skipped for subscriber notifications.`,
|
|
);
|
|
await IncidentEpisodePublicNoteService.updateOneById({
|
|
id: note.id!,
|
|
data: {
|
|
subscriberNotificationStatusOnNoteCreated:
|
|
StatusPageSubscriberNotificationStatus.Skipped,
|
|
subscriberNotificationStatusMessage:
|
|
"Notifications skipped as subscribers are not to be notified for this note.",
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
ignoreHooks: true,
|
|
},
|
|
});
|
|
logger.debug(
|
|
`Episode public note ${note.id} marked as Skipped for subscriber notifications.`,
|
|
);
|
|
}
|
|
|
|
// get all episode public notes that need notification
|
|
|
|
const host: Hostname = await DatabaseConfig.getHost();
|
|
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
|
|
|
const episodePublicNotes: Array<IncidentEpisodePublicNote> =
|
|
await IncidentEpisodePublicNoteService.findBy({
|
|
query: {
|
|
subscriberNotificationStatusOnNoteCreated:
|
|
StatusPageSubscriberNotificationStatus.Pending,
|
|
shouldStatusPageSubscribersBeNotifiedOnNoteCreated: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
limit: LIMIT_MAX,
|
|
skip: 0,
|
|
select: {
|
|
_id: true,
|
|
note: true,
|
|
incidentEpisodeId: true,
|
|
projectId: true,
|
|
},
|
|
});
|
|
|
|
logger.debug(
|
|
`Found ${episodePublicNotes.length} episode public note(s) to notify subscribers for.`,
|
|
);
|
|
|
|
for (const episodePublicNote of episodePublicNotes) {
|
|
try {
|
|
logger.debug(`Processing episode public note ${episodePublicNote.id}.`);
|
|
if (!episodePublicNote.incidentEpisodeId) {
|
|
logger.debug(
|
|
`Episode public note ${episodePublicNote.id} has no incidentEpisodeId; skipping.`,
|
|
);
|
|
continue; // skip if incidentEpisodeId is not set
|
|
}
|
|
|
|
// get the episode
|
|
const episode: IncidentEpisode | null =
|
|
await IncidentEpisodeService.findOneById({
|
|
id: episodePublicNote.incidentEpisodeId!,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
title: true,
|
|
description: true,
|
|
projectId: true,
|
|
incidentSeverity: {
|
|
name: true,
|
|
},
|
|
isVisibleOnStatusPage: true,
|
|
episodeNumber: true,
|
|
episodeNumberWithPrefix: true,
|
|
},
|
|
});
|
|
|
|
if (!episode) {
|
|
logger.debug(
|
|
`Episode ${episodePublicNote.incidentEpisodeId} not found; marking public note ${episodePublicNote.id} as Skipped.`,
|
|
);
|
|
await IncidentEpisodePublicNoteService.updateOneById({
|
|
id: episodePublicNote.id!,
|
|
data: {
|
|
subscriberNotificationStatusOnNoteCreated:
|
|
StatusPageSubscriberNotificationStatus.Skipped,
|
|
subscriberNotificationStatusMessage:
|
|
"Related episode not found. Skipping notifications to subscribers.",
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
ignoreHooks: true,
|
|
},
|
|
});
|
|
continue;
|
|
}
|
|
|
|
// Get monitors from member incidents
|
|
const episodeMembers: Array<IncidentEpisodeMember> =
|
|
await IncidentEpisodeMemberService.findBy({
|
|
query: {
|
|
incidentEpisodeId: episode.id!,
|
|
},
|
|
select: {
|
|
incident: {
|
|
monitors: {
|
|
_id: true,
|
|
},
|
|
},
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
limit: LIMIT_PER_PROJECT,
|
|
skip: 0,
|
|
});
|
|
|
|
// Collect all unique monitors from member incidents
|
|
const monitorIds: Set<string> = new Set();
|
|
for (const member of episodeMembers) {
|
|
if (member.incident?.monitors) {
|
|
for (const monitor of member.incident.monitors) {
|
|
if (monitor._id) {
|
|
monitorIds.add(monitor._id.toString());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (monitorIds.size === 0) {
|
|
logger.debug(
|
|
`Episode ${episode.id} has no monitors; marking public note ${episodePublicNote.id} as Skipped.`,
|
|
);
|
|
await IncidentEpisodePublicNoteService.updateOneById({
|
|
id: episodePublicNote.id!,
|
|
data: {
|
|
subscriberNotificationStatusOnNoteCreated:
|
|
StatusPageSubscriberNotificationStatus.Skipped,
|
|
subscriberNotificationStatusMessage:
|
|
"No monitors are attached to the incidents in this episode. Skipping notifications.",
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
ignoreHooks: true,
|
|
},
|
|
});
|
|
continue;
|
|
}
|
|
|
|
// Set status to InProgress
|
|
await IncidentEpisodePublicNoteService.updateOneById({
|
|
id: episodePublicNote.id!,
|
|
data: {
|
|
subscriberNotificationStatusOnNoteCreated:
|
|
StatusPageSubscriberNotificationStatus.InProgress,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
ignoreHooks: true,
|
|
},
|
|
});
|
|
logger.debug(
|
|
`Episode public note ${episodePublicNote.id} status set to InProgress for subscriber notifications.`,
|
|
);
|
|
|
|
if (!episode.isVisibleOnStatusPage) {
|
|
// Set status to Skipped for non-visible episodes
|
|
logger.debug(
|
|
`Episode ${episode.id} is not visible on status page; marking public note ${episodePublicNote.id} as Skipped.`,
|
|
);
|
|
await IncidentEpisodePublicNoteService.updateOneById({
|
|
id: episodePublicNote.id!,
|
|
data: {
|
|
subscriberNotificationStatusOnNoteCreated:
|
|
StatusPageSubscriberNotificationStatus.Skipped,
|
|
subscriberNotificationStatusMessage:
|
|
"Notifications skipped as episode is not visible on status page.",
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
ignoreHooks: true,
|
|
},
|
|
});
|
|
continue;
|
|
}
|
|
|
|
// get status page resources from monitors.
|
|
const statusPageResources: Array<StatusPageResource> =
|
|
await StatusPageResourceService.findByMonitors({
|
|
monitorIds: Array.from(monitorIds).map((id: string) => {
|
|
return new ObjectID(id);
|
|
}),
|
|
select: {
|
|
_id: true,
|
|
displayName: true,
|
|
statusPageId: true,
|
|
statusPageGroupId: true,
|
|
statusPageGroup: {
|
|
name: true,
|
|
},
|
|
},
|
|
});
|
|
|
|
logger.debug(
|
|
`Found ${statusPageResources.length} status page resource(s) for episode ${episode.id}.`,
|
|
);
|
|
|
|
const statusPageToResources: Dictionary<Array<StatusPageResource>> = {};
|
|
|
|
for (const resource of statusPageResources) {
|
|
if (!resource.statusPageId) {
|
|
continue;
|
|
}
|
|
|
|
if (!statusPageToResources[resource.statusPageId?.toString()]) {
|
|
statusPageToResources[resource.statusPageId?.toString()] = [];
|
|
}
|
|
|
|
statusPageToResources[resource.statusPageId?.toString()]?.push(
|
|
resource,
|
|
);
|
|
}
|
|
|
|
logger.debug(
|
|
`Episode ${episode.id} maps to ${Object.keys(statusPageToResources).length} status page(s) for public note notifications.`,
|
|
);
|
|
|
|
const statusPages: Array<StatusPage> =
|
|
await StatusPageSubscriberService.getStatusPagesToSendNotification(
|
|
Object.keys(statusPageToResources).map((i: string) => {
|
|
return new ObjectID(i);
|
|
}),
|
|
);
|
|
|
|
for (const statuspage of statusPages) {
|
|
logger.debug("Encountered a status page without an id; skipping.");
|
|
if (!statuspage.id) {
|
|
continue;
|
|
}
|
|
|
|
logger.debug(
|
|
`Status page ${statuspage.id} hides episodes; skipping.`,
|
|
);
|
|
if (!statuspage.showEpisodesOnStatusPage) {
|
|
continue; // Do not send notification to subscribers if episodes are not visible on status page.
|
|
}
|
|
|
|
const subscribers: Array<StatusPageSubscriber> =
|
|
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";
|
|
const statusPageIdString: string | null =
|
|
statuspage.id?.toString() || statuspage._id?.toString() || null;
|
|
|
|
const episodeDetailsUrl: string =
|
|
episode.id && statusPageURL
|
|
? URL.fromString(statusPageURL)
|
|
.addRoute(`/episodes/${episode.id.toString()}`)
|
|
.toString()
|
|
: statusPageURL;
|
|
|
|
logger.debug(
|
|
`Status page ${statuspage.id} (${statusPageName}) has ${subscribers.length} subscriber(s) for public note ${episodePublicNote.id}.`,
|
|
);
|
|
|
|
// Fetch custom templates for this status page (if any)
|
|
const [emailTemplate, smsTemplate, slackTemplate, teamsTemplate]: [
|
|
StatusPageSubscriberNotificationTemplate | null,
|
|
StatusPageSubscriberNotificationTemplate | null,
|
|
StatusPageSubscriberNotificationTemplate | null,
|
|
StatusPageSubscriberNotificationTemplate | null,
|
|
] = await Promise.all([
|
|
StatusPageSubscriberNotificationTemplateService.getTemplateForStatusPage(
|
|
{
|
|
statusPageId: statuspage.id!,
|
|
eventType:
|
|
StatusPageSubscriberNotificationEventType.SubscriberEpisodeNoteCreated,
|
|
notificationMethod:
|
|
StatusPageSubscriberNotificationMethod.Email,
|
|
},
|
|
),
|
|
StatusPageSubscriberNotificationTemplateService.getTemplateForStatusPage(
|
|
{
|
|
statusPageId: statuspage.id!,
|
|
eventType:
|
|
StatusPageSubscriberNotificationEventType.SubscriberEpisodeNoteCreated,
|
|
notificationMethod: StatusPageSubscriberNotificationMethod.SMS,
|
|
},
|
|
),
|
|
StatusPageSubscriberNotificationTemplateService.getTemplateForStatusPage(
|
|
{
|
|
statusPageId: statuspage.id!,
|
|
eventType:
|
|
StatusPageSubscriberNotificationEventType.SubscriberEpisodeNoteCreated,
|
|
notificationMethod:
|
|
StatusPageSubscriberNotificationMethod.Slack,
|
|
},
|
|
),
|
|
StatusPageSubscriberNotificationTemplateService.getTemplateForStatusPage(
|
|
{
|
|
statusPageId: statuspage.id!,
|
|
eventType:
|
|
StatusPageSubscriberNotificationEventType.SubscriberEpisodeNoteCreated,
|
|
notificationMethod:
|
|
StatusPageSubscriberNotificationMethod.MicrosoftTeams,
|
|
},
|
|
),
|
|
]);
|
|
|
|
// Prepare template variables for custom templates
|
|
const resourcesAffectedString: string =
|
|
StatusPageResourceUtil.getResourcesGroupedByGroupName(
|
|
statusPageToResources[statuspage._id!] || [],
|
|
);
|
|
|
|
const templateVariables: Record<string, string> = {
|
|
statusPageName: statusPageName,
|
|
statusPageUrl: statusPageURL,
|
|
detailsUrl: episodeDetailsUrl,
|
|
resourcesAffected: resourcesAffectedString,
|
|
episodeSeverity: episode.incidentSeverity?.name || " - ",
|
|
episodeTitle: episode.title || "",
|
|
note: episodePublicNote.note || "",
|
|
};
|
|
|
|
// Prepare SMS-specific template variables with plain text (no HTML/Markdown)
|
|
const smsTemplateVariables: Record<string, string> = {
|
|
...templateVariables,
|
|
note: Markdown.convertToPlainText(episodePublicNote.note || ""),
|
|
};
|
|
|
|
// Send email to Email subscribers.
|
|
|
|
for (const subscriber of subscribers) {
|
|
if (!subscriber._id) {
|
|
logger.debug(
|
|
"Encountered a subscriber without an _id; skipping.",
|
|
);
|
|
continue;
|
|
}
|
|
|
|
const shouldNotifySubscriber: boolean =
|
|
StatusPageSubscriberService.shouldSendNotification({
|
|
subscriber: subscriber,
|
|
statusPageResources:
|
|
statusPageToResources[statuspage._id!] || [],
|
|
statusPage: statuspage,
|
|
eventType: StatusPageEventType.Incident, // Episodes use incident event type
|
|
});
|
|
|
|
if (!shouldNotifySubscriber) {
|
|
logger.debug(
|
|
`Skipping subscriber ${subscriber._id} based on preferences for public note ${episodePublicNote.id}.`,
|
|
);
|
|
continue;
|
|
}
|
|
|
|
const unsubscribeUrl: string =
|
|
StatusPageSubscriberService.getUnsubscribeLink(
|
|
URL.fromString(statusPageURL),
|
|
subscriber.id!,
|
|
).toString();
|
|
|
|
logger.debug(
|
|
`Prepared unsubscribe link for subscriber ${subscriber._id} for public note ${episodePublicNote.id}.`,
|
|
);
|
|
|
|
// Add unsubscribeUrl to template variables
|
|
const subscriberTemplateVariables: Record<string, string> = {
|
|
...templateVariables,
|
|
unsubscribeUrl: unsubscribeUrl,
|
|
};
|
|
|
|
if (subscriber.subscriberPhone) {
|
|
const phoneStr: string = subscriber.subscriberPhone.toString();
|
|
const phoneMasked: string = `${phoneStr.slice(0, 2)}******${phoneStr.slice(-2)}`;
|
|
logger.debug(
|
|
`Queueing SMS notification to subscriber ${subscriber._id} at ${phoneMasked} for public note ${episodePublicNote.id}.`,
|
|
);
|
|
|
|
// SMS-specific template variables with unsubscribe URL
|
|
const subscriberSmsTemplateVariables: Record<string, string> = {
|
|
...smsTemplateVariables,
|
|
unsubscribeUrl: unsubscribeUrl,
|
|
};
|
|
|
|
let smsMessage: string;
|
|
if (smsTemplate?.templateBody && statuspage.callSmsConfig) {
|
|
// Use custom template only when custom Twilio is configured
|
|
smsMessage =
|
|
StatusPageSubscriberNotificationTemplateServiceClass.compileTemplate(
|
|
smsTemplate.templateBody,
|
|
subscriberSmsTemplateVariables,
|
|
);
|
|
} else {
|
|
// Use default hard-coded template
|
|
smsMessage = `Incident update: ${episode.title || "-"} on ${statusPageName}. A new note is posted. Details: ${episodeDetailsUrl}. Unsub: ${unsubscribeUrl}`;
|
|
}
|
|
|
|
const sms: SMS = {
|
|
message: smsMessage,
|
|
to: subscriber.subscriberPhone,
|
|
};
|
|
|
|
// send sms here.
|
|
SmsService.sendSms(sms, {
|
|
projectId: statuspage.projectId,
|
|
customTwilioConfig: ProjectCallSMSConfigService.toTwilioConfig(
|
|
statuspage.callSmsConfig,
|
|
),
|
|
statusPageId: statuspage.id!,
|
|
}).catch((err: Error) => {
|
|
logger.error(err);
|
|
});
|
|
}
|
|
|
|
if (subscriber.subscriberEmail) {
|
|
// send email here.
|
|
logger.debug(
|
|
`Queueing email notification to subscriber ${subscriber._id} at ${subscriber.subscriberEmail} for public note ${episodePublicNote.id}.`,
|
|
);
|
|
|
|
if (emailTemplate?.templateBody && statuspage.smtpConfig) {
|
|
// Use custom template with BlankTemplate only when custom SMTP is configured
|
|
const compiledBody: string =
|
|
StatusPageSubscriberNotificationTemplateServiceClass.compileTemplate(
|
|
emailTemplate.templateBody,
|
|
subscriberTemplateVariables,
|
|
);
|
|
const compiledSubject: string = emailTemplate.emailSubject
|
|
? StatusPageSubscriberNotificationTemplateServiceClass.compileTemplate(
|
|
emailTemplate.emailSubject,
|
|
subscriberTemplateVariables,
|
|
)
|
|
: "[Incident Update] " + episode.title || "";
|
|
|
|
MailService.sendMail(
|
|
{
|
|
toEmail: subscriber.subscriberEmail,
|
|
templateType: EmailTemplateType.BlankTemplate,
|
|
vars: {
|
|
body: compiledBody,
|
|
},
|
|
subject: compiledSubject,
|
|
},
|
|
{
|
|
mailServer: ProjectSmtpConfigService.toEmailServer(
|
|
statuspage.smtpConfig,
|
|
),
|
|
projectId: statuspage.projectId,
|
|
statusPageId: statuspage.id!,
|
|
},
|
|
).catch((err: Error) => {
|
|
logger.error(err);
|
|
});
|
|
} else {
|
|
// Use default hard-coded template
|
|
MailService.sendMail(
|
|
{
|
|
toEmail: subscriber.subscriberEmail,
|
|
templateType:
|
|
EmailTemplateType.SubscriberEpisodeNoteCreated,
|
|
vars: {
|
|
note: await Markdown.convertToHTML(
|
|
episodePublicNote.note!,
|
|
MarkdownContentType.Email,
|
|
),
|
|
statusPageName: statusPageName,
|
|
statusPageUrl: statusPageURL,
|
|
detailsUrl: episodeDetailsUrl,
|
|
logoUrl:
|
|
statuspage.logoFileId && statusPageIdString
|
|
? new URL(httpProtocol, host)
|
|
.addRoute(StatusPageApiRoute)
|
|
.addRoute(`/logo/${statusPageIdString}`)
|
|
.toString()
|
|
: "",
|
|
isPublicStatusPage: statuspage.isPublicStatusPage
|
|
? "true"
|
|
: "false",
|
|
resourcesAffected: resourcesAffectedString,
|
|
episodeSeverity: episode.incidentSeverity?.name || " - ",
|
|
episodeTitle: episode.title || "",
|
|
episodeDescription: episode.description || "",
|
|
unsubscribeUrl: unsubscribeUrl,
|
|
subscriberEmailNotificationFooterText:
|
|
StatusPageServiceType.getSubscriberEmailFooterText(
|
|
statuspage,
|
|
),
|
|
},
|
|
subject: "[Incident Update] " + episode.title,
|
|
},
|
|
{
|
|
mailServer: ProjectSmtpConfigService.toEmailServer(
|
|
statuspage.smtpConfig,
|
|
),
|
|
projectId: statuspage.projectId,
|
|
statusPageId: statuspage.id!,
|
|
},
|
|
).catch((err: Error) => {
|
|
logger.error(err);
|
|
});
|
|
}
|
|
logger.debug(
|
|
`Email notification queued for subscriber ${subscriber._id} for public note ${episodePublicNote.id}.`,
|
|
);
|
|
}
|
|
|
|
if (subscriber.slackIncomingWebhookUrl) {
|
|
// send slack message here.
|
|
logger.debug(
|
|
`Queueing Slack notification to subscriber ${subscriber._id} via incoming webhook for public note ${episodePublicNote.id}.`,
|
|
);
|
|
|
|
let markdownMessage: string;
|
|
if (slackTemplate?.templateBody) {
|
|
// Use custom template
|
|
markdownMessage =
|
|
StatusPageSubscriberNotificationTemplateServiceClass.compileTemplate(
|
|
slackTemplate.templateBody,
|
|
subscriberTemplateVariables,
|
|
);
|
|
} else {
|
|
// Use default hard-coded template
|
|
markdownMessage = `## Incident - ${episode.title || ""}
|
|
|
|
**New note has been added to an incident**
|
|
|
|
**Resources Affected:** ${resourcesAffectedString}
|
|
**Severity:** ${episode.incidentSeverity?.name || " - "}
|
|
|
|
**Note:**
|
|
${episodePublicNote.note || ""}
|
|
|
|
[View Status Page](${statusPageURL}) | [Unsubscribe](${unsubscribeUrl})`;
|
|
}
|
|
|
|
SlackUtil.sendMessageToChannelViaIncomingWebhook({
|
|
url: subscriber.slackIncomingWebhookUrl,
|
|
text: SlackUtil.convertMarkdownToSlackRichText(markdownMessage),
|
|
}).catch((err: Error) => {
|
|
logger.error(err);
|
|
});
|
|
logger.debug(
|
|
`Slack notification queued for subscriber ${subscriber._id} for public note ${episodePublicNote.id}.`,
|
|
);
|
|
}
|
|
|
|
if (subscriber.microsoftTeamsIncomingWebhookUrl) {
|
|
// send Teams message here.
|
|
logger.debug(
|
|
`Queueing Microsoft Teams notification to subscriber ${subscriber._id} via incoming webhook for public note ${episodePublicNote.id}.`,
|
|
);
|
|
|
|
let markdownMessage: string;
|
|
if (teamsTemplate?.templateBody) {
|
|
// Use custom template
|
|
markdownMessage =
|
|
StatusPageSubscriberNotificationTemplateServiceClass.compileTemplate(
|
|
teamsTemplate.templateBody,
|
|
subscriberTemplateVariables,
|
|
);
|
|
} else {
|
|
// Use default hard-coded template
|
|
markdownMessage = `## Incident - ${episode.title || ""}
|
|
|
|
**New note has been added to an incident**
|
|
|
|
**Resources Affected:** ${resourcesAffectedString}
|
|
**Severity:** ${episode.incidentSeverity?.name || " - "}
|
|
|
|
**Note:**
|
|
${episodePublicNote.note || ""}
|
|
|
|
[View Status Page](${statusPageURL}) | [Unsubscribe](${unsubscribeUrl})`;
|
|
}
|
|
|
|
MicrosoftTeamsUtil.sendMessageToChannelViaIncomingWebhook({
|
|
url: subscriber.microsoftTeamsIncomingWebhookUrl,
|
|
text: markdownMessage,
|
|
}).catch((err: Error) => {
|
|
logger.error(err);
|
|
});
|
|
logger.debug(
|
|
`Microsoft Teams notification queued for subscriber ${subscriber._id} for public note ${episodePublicNote.id}.`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
logger.debug(
|
|
`Notification sent to subscribers for public note added to episode: ${episode.id}`,
|
|
);
|
|
|
|
await IncidentEpisodeFeedService.createIncidentEpisodeFeedItem({
|
|
incidentEpisodeId: episode.id!,
|
|
projectId: episode.projectId!,
|
|
incidentEpisodeFeedEventType:
|
|
IncidentEpisodeFeedEventType.SubscriberNotificationSent,
|
|
displayColor: Blue500,
|
|
feedInfoInMarkdown: `📧 **Notification sent to subscribers** because a public note is added to this [Episode ${episode.episodeNumberWithPrefix || "#" + episode.episodeNumber}](${(await IncidentEpisodeService.getEpisodeLinkInDashboard(episode.projectId!, episode.id!)).toString()}).`,
|
|
moreInformationInMarkdown: `**Public Note:**
|
|
|
|
${episodePublicNote.note}`,
|
|
});
|
|
|
|
logger.debug("Episode Feed created");
|
|
|
|
// Set status to Success after successful notification
|
|
await IncidentEpisodePublicNoteService.updateOneById({
|
|
id: episodePublicNote.id!,
|
|
data: {
|
|
subscriberNotificationStatusOnNoteCreated:
|
|
StatusPageSubscriberNotificationStatus.Success,
|
|
subscriberNotificationStatusMessage:
|
|
"Notifications sent successfully to all subscribers",
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
ignoreHooks: true,
|
|
},
|
|
});
|
|
logger.debug(
|
|
`Episode public note ${episodePublicNote.id} marked as Success for subscriber notifications.`,
|
|
);
|
|
} catch (err) {
|
|
logger.error(
|
|
`Error sending notification for episode public note ${episodePublicNote.id}: ${err}`,
|
|
);
|
|
|
|
// Set status to Failed with error reason
|
|
await IncidentEpisodePublicNoteService.updateOneById({
|
|
id: episodePublicNote.id!,
|
|
data: {
|
|
subscriberNotificationStatusOnNoteCreated:
|
|
StatusPageSubscriberNotificationStatus.Failed,
|
|
subscriberNotificationStatusMessage: (err as Error).message,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
ignoreHooks: true,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
},
|
|
);
|