mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
feat: implement Slack emoji reaction handling for alerts, incidents, and scheduled maintenance
This commit is contained in:
@@ -731,6 +731,81 @@ export default class SlackAPI {
|
||||
},
|
||||
);
|
||||
|
||||
// Slack Events API endpoint for handling events like emoji reactions
|
||||
router.post(
|
||||
"/slack/events",
|
||||
SlackAuthorization.isAuthorizedSlackRequest,
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
logger.debug("Slack Events API Request received");
|
||||
logger.debug(req.body);
|
||||
|
||||
const payload: JSONObject = req.body;
|
||||
|
||||
// Handle URL verification challenge from Slack
|
||||
if (payload["type"] === "url_verification") {
|
||||
logger.debug("Slack URL verification challenge received");
|
||||
return Response.sendJsonObjectResponse(req, res, {
|
||||
challenge: payload["challenge"],
|
||||
});
|
||||
}
|
||||
|
||||
// Handle event callbacks
|
||||
if (payload["type"] === "event_callback") {
|
||||
const event: JSONObject = payload["event"] as JSONObject;
|
||||
|
||||
if (!event) {
|
||||
logger.debug("No event found in payload");
|
||||
return Response.sendTextResponse(req, res, "ok");
|
||||
}
|
||||
|
||||
// Handle reaction_added events
|
||||
if (event["type"] === "reaction_added") {
|
||||
logger.debug("Reaction added event received");
|
||||
|
||||
// Respond immediately to Slack to prevent retry
|
||||
// Process the event asynchronously
|
||||
Response.sendTextResponse(req, res, "ok");
|
||||
|
||||
const reactionData = {
|
||||
teamId: payload["team_id"] as string,
|
||||
reaction: event["reaction"] as string,
|
||||
userId: event["user"] as string,
|
||||
channelId: (event["item"] as JSONObject)?.["channel"] as string,
|
||||
messageTs: (event["item"] as JSONObject)?.["ts"] as string,
|
||||
};
|
||||
|
||||
// Process emoji reactions for Incidents, Alerts, and Scheduled Maintenance
|
||||
// Each handler will silently ignore if the channel is not linked to their resource type
|
||||
try {
|
||||
await SlackIncidentActions.handleEmojiReaction(reactionData);
|
||||
} catch (err) {
|
||||
logger.error("Error handling incident emoji reaction:");
|
||||
logger.error(err);
|
||||
}
|
||||
|
||||
try {
|
||||
await SlackAlertActions.handleEmojiReaction(reactionData);
|
||||
} catch (err) {
|
||||
logger.error("Error handling alert emoji reaction:");
|
||||
logger.error(err);
|
||||
}
|
||||
|
||||
try {
|
||||
await SlackScheduledMaintenanceActions.handleEmojiReaction(reactionData);
|
||||
} catch (err) {
|
||||
logger.error("Error handling scheduled maintenance emoji reaction:");
|
||||
logger.error(err);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// For any other event types, just acknowledge
|
||||
return Response.sendTextResponse(req, res, "ok");
|
||||
},
|
||||
);
|
||||
|
||||
// options load endpoint.
|
||||
|
||||
router.post(
|
||||
|
||||
@@ -12,6 +12,9 @@ enum SlackActionType {
|
||||
NewIncident = "/incident", // new incident slash command
|
||||
SubmitNewIncident = "SubmitNewIncident",
|
||||
|
||||
// Emoji Reaction Actions
|
||||
EmojiReactionAdded = "EmojiReactionAdded",
|
||||
|
||||
// Alert Actions just like Incident Actions
|
||||
AcknowledgeAlert = "AcknowledgeAlert",
|
||||
ResolveAlert = "ResolveAlert",
|
||||
@@ -41,4 +44,21 @@ enum SlackActionType {
|
||||
ViewOnCallPolicy = "ViewOnCallPolicy",
|
||||
}
|
||||
|
||||
// Emoji names that trigger saving a message as a Private Note (Internal Note)
|
||||
export const PrivateNoteEmojis: string[] = [
|
||||
"pushpin",
|
||||
"round_pushpin",
|
||||
"pin",
|
||||
];
|
||||
|
||||
// Emoji names that trigger saving a message as a Public Note
|
||||
export const PublicNoteEmojis: string[] = [
|
||||
"mega",
|
||||
"loudspeaker",
|
||||
"megaphone",
|
||||
"announcement",
|
||||
"speaking_head_in_silhouette",
|
||||
"speaking_head",
|
||||
];
|
||||
|
||||
export default SlackActionType;
|
||||
|
||||
@@ -3,7 +3,7 @@ import ObjectID from "../../../../../Types/ObjectID";
|
||||
import AlertService from "../../../../Services/AlertService";
|
||||
import { ExpressRequest, ExpressResponse } from "../../../Express";
|
||||
import SlackUtil from "../Slack";
|
||||
import SlackActionType from "./ActionTypes";
|
||||
import SlackActionType, { PrivateNoteEmojis } from "./ActionTypes";
|
||||
import { SlackAction, SlackRequest } from "./Auth";
|
||||
import Response from "../../../Response";
|
||||
import {
|
||||
@@ -25,6 +25,8 @@ import AccessTokenService from "../../../../Services/AccessTokenService";
|
||||
import CaptureSpan from "../../../Telemetry/CaptureSpan";
|
||||
import WorkspaceNotificationLogService from "../../../../Services/WorkspaceNotificationLogService";
|
||||
import WorkspaceType from "../../../../../Types/Workspace/WorkspaceType";
|
||||
import WorkspaceProjectAuthTokenService from "../../../../Services/WorkspaceProjectAuthTokenService";
|
||||
import WorkspaceNotificationLog from "../../../../../Models/DatabaseModels/WorkspaceNotificationLog";
|
||||
|
||||
export default class SlackAlertActions {
|
||||
@CaptureSpan()
|
||||
@@ -773,4 +775,154 @@ export default class SlackAlertActions {
|
||||
new BadDataException("Invalid Action Type"),
|
||||
);
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async handleEmojiReaction(data: {
|
||||
teamId: string;
|
||||
reaction: string;
|
||||
userId: string;
|
||||
channelId: string;
|
||||
messageTs: string;
|
||||
}): Promise<void> {
|
||||
logger.debug("Handling emoji reaction for Alert with data:");
|
||||
logger.debug(data);
|
||||
|
||||
const { teamId, reaction, userId, channelId, messageTs } = data;
|
||||
|
||||
// Alerts only support private notes, so only pushpin emojis work
|
||||
const isPrivateNoteEmoji: boolean = PrivateNoteEmojis.includes(reaction);
|
||||
|
||||
if (!isPrivateNoteEmoji) {
|
||||
logger.debug(
|
||||
`Emoji "${reaction}" is not a supported private note emoji for alerts. Ignoring.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the project auth token using the team ID
|
||||
const projectAuth =
|
||||
await WorkspaceProjectAuthTokenService.findOneBy({
|
||||
query: {
|
||||
workspaceProjectId: teamId,
|
||||
},
|
||||
select: {
|
||||
projectId: true,
|
||||
authToken: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!projectAuth || !projectAuth.projectId || !projectAuth.authToken) {
|
||||
logger.debug("No project auth found for team ID. Ignoring emoji reaction.");
|
||||
return;
|
||||
}
|
||||
|
||||
const projectId: ObjectID = projectAuth.projectId;
|
||||
const authToken: string = projectAuth.authToken;
|
||||
|
||||
// Find the alert linked to this channel
|
||||
const workspaceLog: WorkspaceNotificationLog | null =
|
||||
await WorkspaceNotificationLogService.findOneBy({
|
||||
query: {
|
||||
channelId: channelId,
|
||||
workspaceType: WorkspaceType.Slack,
|
||||
projectId: projectId,
|
||||
},
|
||||
select: {
|
||||
alertId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!workspaceLog || !workspaceLog.alertId) {
|
||||
logger.debug(
|
||||
"No alert found linked to this channel. Ignoring emoji reaction.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const alertId: ObjectID = workspaceLog.alertId;
|
||||
|
||||
// Get the alert number for the confirmation message
|
||||
const alertNumber: number | null =
|
||||
await AlertService.getAlertNumber({
|
||||
alertId: alertId,
|
||||
});
|
||||
|
||||
// Get the user ID in OneUptime based on Slack user ID
|
||||
const oneUptimeUserId: ObjectID | null =
|
||||
await AccessTokenService.getUserIdByWorkspaceUserId({
|
||||
workspaceUserId: userId,
|
||||
workspaceType: WorkspaceType.Slack,
|
||||
projectId: projectId,
|
||||
});
|
||||
|
||||
if (!oneUptimeUserId) {
|
||||
logger.debug(
|
||||
"No OneUptime user found for Slack user. Ignoring emoji reaction.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch the message text using the timestamp
|
||||
let messageText: string | null = null;
|
||||
try {
|
||||
messageText = await SlackUtil.getMessageByTimestamp({
|
||||
authToken: authToken,
|
||||
channelId: channelId,
|
||||
messageTs: messageTs,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error("Error fetching message text:");
|
||||
logger.error(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!messageText) {
|
||||
logger.debug("No message text found. Ignoring emoji reaction.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Save as private note (Alerts only support private notes)
|
||||
try {
|
||||
await AlertInternalNoteService.addNote({
|
||||
alertId: alertId,
|
||||
note: messageText,
|
||||
projectId: projectId,
|
||||
userId: oneUptimeUserId,
|
||||
});
|
||||
logger.debug("Private note added to alert successfully.");
|
||||
} catch (err) {
|
||||
logger.error("Error saving note:");
|
||||
logger.error(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Send confirmation message as a reply to the original message thread
|
||||
try {
|
||||
const alertLink: string = (
|
||||
await AlertService.getAlertLinkInDashboard(projectId, alertId)
|
||||
).toString();
|
||||
|
||||
const confirmationMessage: string =
|
||||
`✅ Message saved as **private note** to [Alert #${alertNumber}](${alertLink}).`;
|
||||
|
||||
await SlackUtil.sendMessageToThread({
|
||||
authToken: authToken,
|
||||
channelId: channelId,
|
||||
threadTs: messageTs,
|
||||
text: confirmationMessage,
|
||||
});
|
||||
|
||||
logger.debug("Confirmation message sent successfully.");
|
||||
} catch (err) {
|
||||
logger.error("Error sending confirmation message:");
|
||||
logger.error(err);
|
||||
// Don't throw - note was saved successfully, confirmation is best effort
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@ import ObjectID from "../../../../../Types/ObjectID";
|
||||
import IncidentService from "../../../../Services/IncidentService";
|
||||
import { ExpressRequest, ExpressResponse } from "../../../Express";
|
||||
import SlackUtil from "../Slack";
|
||||
import SlackActionType from "./ActionTypes";
|
||||
import SlackActionType, {
|
||||
PrivateNoteEmojis,
|
||||
PublicNoteEmojis,
|
||||
} from "./ActionTypes";
|
||||
import { SlackAction, SlackRequest } from "./Auth";
|
||||
import Response from "../../../Response";
|
||||
import {
|
||||
@@ -38,6 +41,8 @@ import LabelService from "../../../../Services/LabelService";
|
||||
import Incident from "../../../../../Models/DatabaseModels/Incident";
|
||||
import AccessTokenService from "../../../../Services/AccessTokenService";
|
||||
import CaptureSpan from "../../../Telemetry/CaptureSpan";
|
||||
import WorkspaceProjectAuthTokenService from "../../../../Services/WorkspaceProjectAuthTokenService";
|
||||
import WorkspaceNotificationLog from "../../../../../Models/DatabaseModels/WorkspaceNotificationLog";
|
||||
|
||||
export default class SlackIncidentActions {
|
||||
@CaptureSpan()
|
||||
@@ -1285,4 +1290,172 @@ export default class SlackIncidentActions {
|
||||
new BadDataException("Invalid Action Type"),
|
||||
);
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async handleEmojiReaction(data: {
|
||||
teamId: string;
|
||||
reaction: string;
|
||||
userId: string;
|
||||
channelId: string;
|
||||
messageTs: string;
|
||||
}): Promise<void> {
|
||||
logger.debug("Handling emoji reaction with data:");
|
||||
logger.debug(data);
|
||||
|
||||
const { teamId, reaction, userId, channelId, messageTs } = data;
|
||||
|
||||
// Check if the emoji is a supported private or public note emoji
|
||||
const isPrivateNoteEmoji: boolean = PrivateNoteEmojis.includes(reaction);
|
||||
const isPublicNoteEmoji: boolean = PublicNoteEmojis.includes(reaction);
|
||||
|
||||
if (!isPrivateNoteEmoji && !isPublicNoteEmoji) {
|
||||
logger.debug(
|
||||
`Emoji "${reaction}" is not a supported note emoji. Ignoring.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the project auth token using the team ID
|
||||
const projectAuth =
|
||||
await WorkspaceProjectAuthTokenService.findOneBy({
|
||||
query: {
|
||||
workspaceProjectId: teamId,
|
||||
},
|
||||
select: {
|
||||
projectId: true,
|
||||
authToken: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!projectAuth || !projectAuth.projectId || !projectAuth.authToken) {
|
||||
logger.debug("No project auth found for team ID. Ignoring emoji reaction.");
|
||||
return;
|
||||
}
|
||||
|
||||
const projectId: ObjectID = projectAuth.projectId;
|
||||
const authToken: string = projectAuth.authToken;
|
||||
|
||||
// Find the incident linked to this channel
|
||||
const workspaceLog: WorkspaceNotificationLog | null =
|
||||
await WorkspaceNotificationLogService.findOneBy({
|
||||
query: {
|
||||
channelId: channelId,
|
||||
workspaceType: WorkspaceType.Slack,
|
||||
projectId: projectId,
|
||||
},
|
||||
select: {
|
||||
incidentId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!workspaceLog || !workspaceLog.incidentId) {
|
||||
logger.debug(
|
||||
"No incident found linked to this channel. Ignoring emoji reaction.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const incidentId: ObjectID = workspaceLog.incidentId;
|
||||
|
||||
// Get the incident number for the confirmation message
|
||||
const incidentNumber: number | null =
|
||||
await IncidentService.getIncidentNumber({
|
||||
incidentId: incidentId,
|
||||
});
|
||||
|
||||
// Get the user ID in OneUptime based on Slack user ID
|
||||
const oneUptimeUserId: ObjectID | null =
|
||||
await AccessTokenService.getUserIdByWorkspaceUserId({
|
||||
workspaceUserId: userId,
|
||||
workspaceType: WorkspaceType.Slack,
|
||||
projectId: projectId,
|
||||
});
|
||||
|
||||
if (!oneUptimeUserId) {
|
||||
logger.debug(
|
||||
"No OneUptime user found for Slack user. Ignoring emoji reaction.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch the message text using the timestamp
|
||||
let messageText: string | null = null;
|
||||
try {
|
||||
messageText = await SlackUtil.getMessageByTimestamp({
|
||||
authToken: authToken,
|
||||
channelId: channelId,
|
||||
messageTs: messageTs,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error("Error fetching message text:");
|
||||
logger.error(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!messageText) {
|
||||
logger.debug("No message text found. Ignoring emoji reaction.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Save the note based on the emoji type
|
||||
let noteType: string;
|
||||
try {
|
||||
if (isPrivateNoteEmoji) {
|
||||
noteType = "private";
|
||||
await IncidentInternalNoteService.addNote({
|
||||
incidentId: incidentId,
|
||||
note: messageText,
|
||||
projectId: projectId,
|
||||
userId: oneUptimeUserId,
|
||||
});
|
||||
logger.debug("Private note added successfully.");
|
||||
} else if (isPublicNoteEmoji) {
|
||||
noteType = "public";
|
||||
await IncidentPublicNoteService.addNote({
|
||||
incidentId: incidentId,
|
||||
note: messageText,
|
||||
projectId: projectId,
|
||||
userId: oneUptimeUserId,
|
||||
});
|
||||
logger.debug("Public note added successfully.");
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("Error saving note:");
|
||||
logger.error(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Send confirmation message as a reply to the original message thread
|
||||
try {
|
||||
const incidentLink: string = (
|
||||
await IncidentService.getIncidentLinkInDashboard(projectId, incidentId)
|
||||
).toString();
|
||||
|
||||
const confirmationMessage: string =
|
||||
noteType === "private"
|
||||
? `✅ Message saved as **private note** to [Incident #${incidentNumber}](${incidentLink}).`
|
||||
: `✅ Message saved as **public note** to [Incident #${incidentNumber}](${incidentLink}). This note will be visible on the status page.`;
|
||||
|
||||
await SlackUtil.sendMessageToThread({
|
||||
authToken: authToken,
|
||||
channelId: channelId,
|
||||
threadTs: messageTs,
|
||||
text: confirmationMessage,
|
||||
});
|
||||
|
||||
logger.debug("Confirmation message sent successfully.");
|
||||
} catch (err) {
|
||||
logger.error("Error sending confirmation message:");
|
||||
logger.error(err);
|
||||
// Don't throw - note was saved successfully, confirmation is best effort
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@ import ObjectID from "../../../../../Types/ObjectID";
|
||||
import ScheduledMaintenanceService from "../../../../Services/ScheduledMaintenanceService";
|
||||
import { ExpressRequest, ExpressResponse } from "../../../Express";
|
||||
import SlackUtil from "../Slack";
|
||||
import SlackActionType from "./ActionTypes";
|
||||
import SlackActionType, {
|
||||
PrivateNoteEmojis,
|
||||
PublicNoteEmojis,
|
||||
} from "./ActionTypes";
|
||||
import { SlackAction, SlackRequest } from "./Auth";
|
||||
import Response from "../../../Response";
|
||||
import {
|
||||
@@ -35,6 +38,8 @@ import AccessTokenService from "../../../../Services/AccessTokenService";
|
||||
import CaptureSpan from "../../../Telemetry/CaptureSpan";
|
||||
import WorkspaceType from "../../../../../Types/Workspace/WorkspaceType";
|
||||
import WorkspaceNotificationLogService from "../../../../Services/WorkspaceNotificationLogService";
|
||||
import WorkspaceProjectAuthTokenService from "../../../../Services/WorkspaceProjectAuthTokenService";
|
||||
import WorkspaceNotificationLog from "../../../../../Models/DatabaseModels/WorkspaceNotificationLog";
|
||||
|
||||
export default class SlackScheduledMaintenanceActions {
|
||||
@CaptureSpan()
|
||||
@@ -1110,4 +1115,172 @@ export default class SlackScheduledMaintenanceActions {
|
||||
new BadDataException("Invalid Action Type"),
|
||||
);
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async handleEmojiReaction(data: {
|
||||
teamId: string;
|
||||
reaction: string;
|
||||
userId: string;
|
||||
channelId: string;
|
||||
messageTs: string;
|
||||
}): Promise<void> {
|
||||
logger.debug("Handling emoji reaction for Scheduled Maintenance with data:");
|
||||
logger.debug(data);
|
||||
|
||||
const { teamId, reaction, userId, channelId, messageTs } = data;
|
||||
|
||||
// Check if the emoji is a supported private or public note emoji
|
||||
const isPrivateNoteEmoji: boolean = PrivateNoteEmojis.includes(reaction);
|
||||
const isPublicNoteEmoji: boolean = PublicNoteEmojis.includes(reaction);
|
||||
|
||||
if (!isPrivateNoteEmoji && !isPublicNoteEmoji) {
|
||||
logger.debug(
|
||||
`Emoji "${reaction}" is not a supported note emoji. Ignoring.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the project auth token using the team ID
|
||||
const projectAuth =
|
||||
await WorkspaceProjectAuthTokenService.findOneBy({
|
||||
query: {
|
||||
workspaceProjectId: teamId,
|
||||
},
|
||||
select: {
|
||||
projectId: true,
|
||||
authToken: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!projectAuth || !projectAuth.projectId || !projectAuth.authToken) {
|
||||
logger.debug("No project auth found for team ID. Ignoring emoji reaction.");
|
||||
return;
|
||||
}
|
||||
|
||||
const projectId: ObjectID = projectAuth.projectId;
|
||||
const authToken: string = projectAuth.authToken;
|
||||
|
||||
// Find the scheduled maintenance linked to this channel
|
||||
const workspaceLog: WorkspaceNotificationLog | null =
|
||||
await WorkspaceNotificationLogService.findOneBy({
|
||||
query: {
|
||||
channelId: channelId,
|
||||
workspaceType: WorkspaceType.Slack,
|
||||
projectId: projectId,
|
||||
},
|
||||
select: {
|
||||
scheduledMaintenanceId: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!workspaceLog || !workspaceLog.scheduledMaintenanceId) {
|
||||
logger.debug(
|
||||
"No scheduled maintenance found linked to this channel. Ignoring emoji reaction.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const scheduledMaintenanceId: ObjectID = workspaceLog.scheduledMaintenanceId;
|
||||
|
||||
// Get the scheduled maintenance number for the confirmation message
|
||||
const scheduledMaintenanceNumber: number | null =
|
||||
await ScheduledMaintenanceService.getScheduledMaintenanceNumber({
|
||||
scheduledMaintenanceId: scheduledMaintenanceId,
|
||||
});
|
||||
|
||||
// Get the user ID in OneUptime based on Slack user ID
|
||||
const oneUptimeUserId: ObjectID | null =
|
||||
await AccessTokenService.getUserIdByWorkspaceUserId({
|
||||
workspaceUserId: userId,
|
||||
workspaceType: WorkspaceType.Slack,
|
||||
projectId: projectId,
|
||||
});
|
||||
|
||||
if (!oneUptimeUserId) {
|
||||
logger.debug(
|
||||
"No OneUptime user found for Slack user. Ignoring emoji reaction.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch the message text using the timestamp
|
||||
let messageText: string | null = null;
|
||||
try {
|
||||
messageText = await SlackUtil.getMessageByTimestamp({
|
||||
authToken: authToken,
|
||||
channelId: channelId,
|
||||
messageTs: messageTs,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error("Error fetching message text:");
|
||||
logger.error(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!messageText) {
|
||||
logger.debug("No message text found. Ignoring emoji reaction.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Save the note based on the emoji type
|
||||
let noteType: string;
|
||||
try {
|
||||
if (isPrivateNoteEmoji) {
|
||||
noteType = "private";
|
||||
await ScheduledMaintenanceInternalNoteService.addNote({
|
||||
scheduledMaintenanceId: scheduledMaintenanceId,
|
||||
note: messageText,
|
||||
projectId: projectId,
|
||||
userId: oneUptimeUserId,
|
||||
});
|
||||
logger.debug("Private note added successfully.");
|
||||
} else if (isPublicNoteEmoji) {
|
||||
noteType = "public";
|
||||
await ScheduledMaintenancePublicNoteService.addNote({
|
||||
scheduledMaintenanceId: scheduledMaintenanceId,
|
||||
note: messageText,
|
||||
projectId: projectId,
|
||||
userId: oneUptimeUserId,
|
||||
});
|
||||
logger.debug("Public note added successfully.");
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("Error saving note:");
|
||||
logger.error(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Send confirmation message as a reply to the original message thread
|
||||
try {
|
||||
const scheduledMaintenanceLink: string = (
|
||||
await ScheduledMaintenanceService.getScheduledMaintenanceLinkInDashboard(projectId, scheduledMaintenanceId)
|
||||
).toString();
|
||||
|
||||
const confirmationMessage: string =
|
||||
noteType === "private"
|
||||
? `✅ Message saved as **private note** to [Scheduled Maintenance #${scheduledMaintenanceNumber}](${scheduledMaintenanceLink}).`
|
||||
: `✅ Message saved as **public note** to [Scheduled Maintenance #${scheduledMaintenanceNumber}](${scheduledMaintenanceLink}). This note will be visible on the status page.`;
|
||||
|
||||
await SlackUtil.sendMessageToThread({
|
||||
authToken: authToken,
|
||||
channelId: channelId,
|
||||
threadTs: messageTs,
|
||||
text: confirmationMessage,
|
||||
});
|
||||
|
||||
logger.debug("Confirmation message sent successfully.");
|
||||
} catch (err) {
|
||||
logger.error("Error sending confirmation message:");
|
||||
logger.error(err);
|
||||
// Don't throw - note was saved successfully, confirmation is best effort
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1156,6 +1156,119 @@ export default class SlackUtil extends WorkspaceBase {
|
||||
};
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async sendMessageToThread(data: {
|
||||
authToken: string;
|
||||
channelId: string;
|
||||
threadTs: string;
|
||||
text: string;
|
||||
}): Promise<void> {
|
||||
logger.debug("Sending message to thread with data:");
|
||||
logger.debug(data);
|
||||
|
||||
const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
|
||||
await API.post({
|
||||
url: URL.fromString("https://slack.com/api/chat.postMessage"),
|
||||
data: {
|
||||
channel: data.channelId,
|
||||
thread_ts: data.threadTs,
|
||||
text: data.text,
|
||||
},
|
||||
headers: {
|
||||
Authorization: `Bearer ${data.authToken}`,
|
||||
["Content-Type"]: "application/json",
|
||||
},
|
||||
options: {
|
||||
retries: 3,
|
||||
exponentialBackoff: true,
|
||||
},
|
||||
});
|
||||
|
||||
logger.debug("Response from Slack API for sending thread message:");
|
||||
logger.debug(response);
|
||||
|
||||
if (response instanceof HTTPErrorResponse) {
|
||||
logger.error("Error response from Slack API:");
|
||||
logger.error(response);
|
||||
throw response;
|
||||
}
|
||||
|
||||
if ((response.jsonData as JSONObject)?.["ok"] !== true) {
|
||||
logger.error("Invalid response from Slack API:");
|
||||
logger.error(response.jsonData);
|
||||
const messageFromSlack: string = (response.jsonData as JSONObject)?.[
|
||||
"error"
|
||||
] as string;
|
||||
throw new BadRequestException("Error from Slack " + messageFromSlack);
|
||||
}
|
||||
|
||||
logger.debug("Thread message sent successfully.");
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async getMessageByTimestamp(data: {
|
||||
authToken: string;
|
||||
channelId: string;
|
||||
messageTs: string;
|
||||
}): Promise<string | null> {
|
||||
logger.debug("Getting message by timestamp with data:");
|
||||
logger.debug(data);
|
||||
|
||||
const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
|
||||
await API.post({
|
||||
url: URL.fromString("https://slack.com/api/conversations.history"),
|
||||
data: {
|
||||
channel: data.channelId,
|
||||
latest: data.messageTs,
|
||||
oldest: data.messageTs,
|
||||
inclusive: true,
|
||||
limit: 1,
|
||||
},
|
||||
headers: {
|
||||
Authorization: `Bearer ${data.authToken}`,
|
||||
["Content-Type"]: "application/x-www-form-urlencoded",
|
||||
},
|
||||
options: {
|
||||
retries: 3,
|
||||
exponentialBackoff: true,
|
||||
},
|
||||
});
|
||||
|
||||
logger.debug("Response from Slack API for getting message:");
|
||||
logger.debug(response);
|
||||
|
||||
if (response instanceof HTTPErrorResponse) {
|
||||
logger.error("Error response from Slack API:");
|
||||
logger.error(response);
|
||||
throw response;
|
||||
}
|
||||
|
||||
if ((response.jsonData as JSONObject)?.["ok"] !== true) {
|
||||
logger.error("Invalid response from Slack API:");
|
||||
logger.error(response.jsonData);
|
||||
const messageFromSlack: string = (response.jsonData as JSONObject)?.[
|
||||
"error"
|
||||
] as string;
|
||||
throw new BadRequestException("Error from Slack " + messageFromSlack);
|
||||
}
|
||||
|
||||
const messages: Array<JSONObject> = (response.jsonData as JSONObject)?.[
|
||||
"messages"
|
||||
] as Array<JSONObject>;
|
||||
|
||||
if (!messages || messages.length === 0) {
|
||||
logger.debug("No messages found for timestamp.");
|
||||
return null;
|
||||
}
|
||||
|
||||
const messageText: string | undefined = messages[0]?.["text"] as string;
|
||||
|
||||
logger.debug("Message text retrieved:");
|
||||
logger.debug(messageText);
|
||||
|
||||
return messageText || null;
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static override getButtonsBlock(data: {
|
||||
payloadButtonsBlock: WorkspacePayloadButtons;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"background_color": "#000000",
|
||||
"long_description": "OneUptime is a comprehensive solution for monitoring and managing your online services. Whether you need to check the availability of your website, dashboard, API, or any other online resource, OneUptime can alert your team when downtime happens and keep your customers informed with a status page. OneUptime also helps you handle incidents, set up on-call rotations, run tests, secure your services, analyze logs, track performance, and debug errors."
|
||||
},
|
||||
|
||||
"features": {
|
||||
"app_home": {
|
||||
"home_tab_enabled": false,
|
||||
@@ -66,10 +67,14 @@
|
||||
"users:read",
|
||||
"groups:read",
|
||||
"groups:write",
|
||||
"groups:history",
|
||||
"im:read",
|
||||
"im:write",
|
||||
"im:history",
|
||||
"mpim:read",
|
||||
"mpim:write"
|
||||
"mpim:write",
|
||||
"mpim:history",
|
||||
"reactions:read"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -81,6 +86,12 @@
|
||||
},
|
||||
"org_deploy_enabled": true,
|
||||
"socket_mode_enabled": false,
|
||||
"token_rotation_enabled": false
|
||||
"token_rotation_enabled": false,
|
||||
"event_subscriptions": {
|
||||
"request_url": "{{SERVER_URL}}/api/slack/events",
|
||||
"bot_events": [
|
||||
"reaction_added"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user