mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
feat: add postedFromSlackMessageId to internal and public note models and services for duplicate prevention
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user