feat: add postedFromSlackMessageId to internal and public note models and services for duplicate prevention

This commit is contained in:
Nawaz Dhandala
2025-12-11 18:22:04 +00:00
parent efc7a99982
commit 60f292048d
13 changed files with 363 additions and 0 deletions

View File

@@ -424,4 +424,33 @@ export default class AlertInternalNote extends BaseModel {
default: false,
})
public isOwnerNotified?: boolean = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateAlertInternalNote,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertInternalNote,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.LongText,
title: "Posted from Slack Message ID",
description:
"Unique identifier for the Slack message this note was created from (channel_id:message_ts). Used to prevent duplicate notes when multiple users react to the same message.",
required: false,
})
@Column({
type: ColumnType.LongText,
nullable: true,
})
public postedFromSlackMessageId?: string = undefined;
}

View File

@@ -424,4 +424,33 @@ export default class IncidentInternalNote extends BaseModel {
default: false,
})
public isOwnerNotified?: boolean = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateIncidentInternalNote,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadIncidentInternalNote,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.LongText,
title: "Posted from Slack Message ID",
description:
"Unique identifier for the Slack message this note was created from (channel_id:message_ts). Used to prevent duplicate notes when multiple users react to the same message.",
required: false,
})
@Column({
type: ColumnType.LongText,
nullable: true,
})
public postedFromSlackMessageId?: string = undefined;
}

View File

@@ -555,4 +555,33 @@ export default class IncidentPublicNote extends BaseModel {
unique: false,
})
public postedAt?: Date = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateIncidentPublicNote,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadIncidentPublicNote,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.LongText,
title: "Posted from Slack Message ID",
description:
"Unique identifier for the Slack message this note was created from (channel_id:message_ts). Used to prevent duplicate notes when multiple users react to the same message.",
required: false,
})
@Column({
type: ColumnType.LongText,
nullable: true,
})
public postedFromSlackMessageId?: string = undefined;
}

View File

@@ -424,4 +424,33 @@ export default class ScheduledMaintenanceInternalNote extends BaseModel {
default: false,
})
public isOwnerNotified?: boolean = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateScheduledMaintenanceInternalNote,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadScheduledMaintenanceInternalNote,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.LongText,
title: "Posted from Slack Message ID",
description:
"Unique identifier for the Slack message this note was created from (channel_id:message_ts). Used to prevent duplicate notes when multiple users react to the same message.",
required: false,
})
@Column({
type: ColumnType.LongText,
nullable: true,
})
public postedFromSlackMessageId?: string = undefined;
}

View File

@@ -556,4 +556,33 @@ export default class ScheduledMaintenancePublicNote extends BaseModel {
unique: false,
})
public postedAt?: Date = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateScheduledMaintenancePublicNote,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadScheduledMaintenancePublicNote,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.LongText,
title: "Posted from Slack Message ID",
description:
"Unique identifier for the Slack message this note was created from (channel_id:message_ts). Used to prevent duplicate notes when multiple users react to the same message.",
required: false,
})
@Column({
type: ColumnType.LongText,
nullable: true,
})
public postedFromSlackMessageId?: string = undefined;
}

View File

@@ -24,6 +24,7 @@ export class Service extends DatabaseService<Model> {
projectId: ObjectID;
note: string;
attachmentFileIds?: Array<ObjectID>;
postedFromSlackMessageId?: string;
}): Promise<Model> {
const internalNote: Model = new Model();
internalNote.createdByUserId = data.userId;
@@ -31,6 +32,10 @@ export class Service extends DatabaseService<Model> {
internalNote.projectId = data.projectId;
internalNote.note = data.note;
if (data.postedFromSlackMessageId) {
internalNote.postedFromSlackMessageId = data.postedFromSlackMessageId;
}
if (data.attachmentFileIds && data.attachmentFileIds.length > 0) {
internalNote.attachments = data.attachmentFileIds.map(
(fileId: ObjectID) => {
@@ -49,6 +54,27 @@ export class Service extends DatabaseService<Model> {
});
}
@CaptureSpan()
public async hasNoteFromSlackMessage(data: {
alertId: ObjectID;
postedFromSlackMessageId: string;
}): Promise<boolean> {
const existingNote: Model | null = await this.findOneBy({
query: {
alertId: data.alertId,
postedFromSlackMessageId: data.postedFromSlackMessageId,
},
select: {
_id: true,
},
props: {
isRoot: true,
},
});
return existingNote !== null;
}
@CaptureSpan()
public override async onCreateSuccess(
_onCreate: OnCreate<Model>,

View File

@@ -24,6 +24,7 @@ export class Service extends DatabaseService<Model> {
projectId: ObjectID;
note: string;
attachmentFileIds?: Array<ObjectID>;
postedFromSlackMessageId?: string;
}): Promise<Model> {
const internalNote: Model = new Model();
internalNote.createdByUserId = data.userId;
@@ -31,6 +32,10 @@ export class Service extends DatabaseService<Model> {
internalNote.projectId = data.projectId;
internalNote.note = data.note;
if (data.postedFromSlackMessageId) {
internalNote.postedFromSlackMessageId = data.postedFromSlackMessageId;
}
if (data.attachmentFileIds && data.attachmentFileIds.length > 0) {
internalNote.attachments = data.attachmentFileIds.map(
(fileId: ObjectID) => {
@@ -49,6 +54,27 @@ export class Service extends DatabaseService<Model> {
});
}
@CaptureSpan()
public async hasNoteFromSlackMessage(data: {
incidentId: ObjectID;
postedFromSlackMessageId: string;
}): Promise<boolean> {
const existingNote: Model | null = await this.findOneBy({
query: {
incidentId: data.incidentId,
postedFromSlackMessageId: data.postedFromSlackMessageId,
},
select: {
_id: true,
},
props: {
isRoot: true,
},
});
return existingNote !== null;
}
@CaptureSpan()
public override async onCreateSuccess(
_onCreate: OnCreate<Model>,

View File

@@ -27,6 +27,7 @@ export class Service extends DatabaseService<Model> {
projectId: ObjectID;
note: string;
attachmentFileIds?: Array<ObjectID>;
postedFromSlackMessageId?: string;
}): Promise<Model> {
const publicNote: Model = new Model();
publicNote.createdByUserId = data.userId;
@@ -35,6 +36,10 @@ export class Service extends DatabaseService<Model> {
publicNote.note = data.note;
publicNote.postedAt = OneUptimeDate.getCurrentDate();
if (data.postedFromSlackMessageId) {
publicNote.postedFromSlackMessageId = data.postedFromSlackMessageId;
}
if (data.attachmentFileIds && data.attachmentFileIds.length > 0) {
publicNote.attachments = data.attachmentFileIds.map(
(fileId: ObjectID) => {
@@ -53,6 +58,27 @@ export class Service extends DatabaseService<Model> {
});
}
@CaptureSpan()
public async hasNoteFromSlackMessage(data: {
incidentId: ObjectID;
postedFromSlackMessageId: string;
}): Promise<boolean> {
const existingNote: Model | null = await this.findOneBy({
query: {
incidentId: data.incidentId,
postedFromSlackMessageId: data.postedFromSlackMessageId,
},
select: {
_id: true,
},
props: {
isRoot: true,
},
});
return existingNote !== null;
}
@CaptureSpan()
protected override async onBeforeCreate(
createBy: CreateBy<Model>,

View File

@@ -24,6 +24,7 @@ export class Service extends DatabaseService<Model> {
projectId: ObjectID;
note: string;
attachmentFileIds?: Array<ObjectID>;
postedFromSlackMessageId?: string;
}): Promise<Model> {
const internalNote: Model = new Model();
internalNote.createdByUserId = data.userId;
@@ -31,6 +32,10 @@ export class Service extends DatabaseService<Model> {
internalNote.projectId = data.projectId;
internalNote.note = data.note;
if (data.postedFromSlackMessageId) {
internalNote.postedFromSlackMessageId = data.postedFromSlackMessageId;
}
if (data.attachmentFileIds && data.attachmentFileIds.length > 0) {
internalNote.attachments = data.attachmentFileIds.map(
(fileId: ObjectID) => {
@@ -49,6 +54,27 @@ export class Service extends DatabaseService<Model> {
});
}
@CaptureSpan()
public async hasNoteFromSlackMessage(data: {
scheduledMaintenanceId: ObjectID;
postedFromSlackMessageId: string;
}): Promise<boolean> {
const existingNote: Model | null = await this.findOneBy({
query: {
scheduledMaintenanceId: data.scheduledMaintenanceId,
postedFromSlackMessageId: data.postedFromSlackMessageId,
},
select: {
_id: true,
},
props: {
isRoot: true,
},
});
return existingNote !== null;
}
@CaptureSpan()
public override async onCreateSuccess(
_onCreate: OnCreate<Model>,

View File

@@ -162,6 +162,7 @@ ${(updatedItem.note || "") + attachmentsMarkdown}
projectId: ObjectID;
note: string;
attachmentFileIds?: Array<ObjectID>;
postedFromSlackMessageId?: string;
}): Promise<Model> {
const publicNote: Model = new Model();
publicNote.createdByUserId = data.userId;
@@ -170,6 +171,10 @@ ${(updatedItem.note || "") + attachmentsMarkdown}
publicNote.note = data.note;
publicNote.postedAt = OneUptimeDate.getCurrentDate();
if (data.postedFromSlackMessageId) {
publicNote.postedFromSlackMessageId = data.postedFromSlackMessageId;
}
if (data.attachmentFileIds && data.attachmentFileIds.length > 0) {
publicNote.attachments = data.attachmentFileIds.map(
(fileId: ObjectID) => {
@@ -188,6 +193,27 @@ ${(updatedItem.note || "") + attachmentsMarkdown}
});
}
@CaptureSpan()
public async hasNoteFromSlackMessage(data: {
scheduledMaintenanceId: ObjectID;
postedFromSlackMessageId: string;
}): Promise<boolean> {
const existingNote: Model | null = await this.findOneBy({
query: {
scheduledMaintenanceId: data.scheduledMaintenanceId,
postedFromSlackMessageId: data.postedFromSlackMessageId,
},
select: {
_id: true,
},
props: {
isRoot: true,
},
});
return existingNote !== null;
}
private async getAttachmentsMarkdown(
modelId: ObjectID,
attachmentApiPath: string,

View File

@@ -901,6 +901,23 @@ export default class SlackAlertActions {
return;
}
// Create a unique identifier for this Slack message to prevent duplicate notes
const postedFromSlackMessageId: string = `${channelId}:${messageTs}`;
// Check if a note from this Slack message already exists
const hasExistingNote: boolean =
await AlertInternalNoteService.hasNoteFromSlackMessage({
alertId: alertId,
postedFromSlackMessageId: postedFromSlackMessageId,
});
if (hasExistingNote) {
logger.debug(
"Private note from this Slack message already exists. Skipping duplicate.",
);
return;
}
// Save as private note (Alerts only support private notes)
try {
await AlertInternalNoteService.addNote({
@@ -908,6 +925,7 @@ export default class SlackAlertActions {
note: messageText,
projectId: projectId,
userId: oneUptimeUserId,
postedFromSlackMessageId: postedFromSlackMessageId,
});
logger.debug("Private note added to alert successfully.");
} catch (err) {

View File

@@ -1418,25 +1418,60 @@ export default class SlackIncidentActions {
return;
}
// Create a unique identifier for this Slack message to prevent duplicate notes
const postedFromSlackMessageId: string = `${channelId}:${messageTs}`;
// Save the note based on the emoji type
let noteType: string;
try {
if (isPrivateNoteEmoji) {
noteType = "private";
// Check if a note from this Slack message already exists
const hasExistingNote: boolean =
await IncidentInternalNoteService.hasNoteFromSlackMessage({
incidentId: incidentId,
postedFromSlackMessageId: postedFromSlackMessageId,
});
if (hasExistingNote) {
logger.debug(
"Private note from this Slack message already exists. Skipping duplicate.",
);
return;
}
await IncidentInternalNoteService.addNote({
incidentId: incidentId,
note: messageText,
projectId: projectId,
userId: oneUptimeUserId,
postedFromSlackMessageId: postedFromSlackMessageId,
});
logger.debug("Private note added successfully.");
} else if (isPublicNoteEmoji) {
noteType = "public";
// Check if a note from this Slack message already exists
const hasExistingNote: boolean =
await IncidentPublicNoteService.hasNoteFromSlackMessage({
incidentId: incidentId,
postedFromSlackMessageId: postedFromSlackMessageId,
});
if (hasExistingNote) {
logger.debug(
"Public note from this Slack message already exists. Skipping duplicate.",
);
return;
}
await IncidentPublicNoteService.addNote({
incidentId: incidentId,
note: messageText,
projectId: projectId,
userId: oneUptimeUserId,
postedFromSlackMessageId: postedFromSlackMessageId,
});
logger.debug("Public note added successfully.");
} else {

View File

@@ -1246,25 +1246,60 @@ export default class SlackScheduledMaintenanceActions {
return;
}
// Create a unique identifier for this Slack message to prevent duplicate notes
const postedFromSlackMessageId: string = `${channelId}:${messageTs}`;
// Save the note based on the emoji type
let noteType: string;
try {
if (isPrivateNoteEmoji) {
noteType = "private";
// Check if a note from this Slack message already exists
const hasExistingNote: boolean =
await ScheduledMaintenanceInternalNoteService.hasNoteFromSlackMessage({
scheduledMaintenanceId: scheduledMaintenanceId,
postedFromSlackMessageId: postedFromSlackMessageId,
});
if (hasExistingNote) {
logger.debug(
"Private note from this Slack message already exists. Skipping duplicate.",
);
return;
}
await ScheduledMaintenanceInternalNoteService.addNote({
scheduledMaintenanceId: scheduledMaintenanceId,
note: messageText,
projectId: projectId,
userId: oneUptimeUserId,
postedFromSlackMessageId: postedFromSlackMessageId,
});
logger.debug("Private note added successfully.");
} else if (isPublicNoteEmoji) {
noteType = "public";
// Check if a note from this Slack message already exists
const hasExistingNote: boolean =
await ScheduledMaintenancePublicNoteService.hasNoteFromSlackMessage({
scheduledMaintenanceId: scheduledMaintenanceId,
postedFromSlackMessageId: postedFromSlackMessageId,
});
if (hasExistingNote) {
logger.debug(
"Public note from this Slack message already exists. Skipping duplicate.",
);
return;
}
await ScheduledMaintenancePublicNoteService.addNote({
scheduledMaintenanceId: scheduledMaintenanceId,
note: messageText,
projectId: projectId,
userId: oneUptimeUserId,
postedFromSlackMessageId: postedFromSlackMessageId,
});
logger.debug("Public note added successfully.");
} else {