format user entity ref mentions to be Slack compatible
Signed-off-by: Jackson Chen <jacksonc@spotify.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-notifications-backend-module-slack': patch
|
||||
---
|
||||
|
||||
Notifications which mention user entity refs are now replaced with Slack compatible mentions.
|
||||
|
||||
Example: `Welcome <@user:default/billy>!` -> `Welcome <@U123456890>!`
|
||||
+123
@@ -543,4 +543,127 @@ describe('SlackNotificationProcessor', () => {
|
||||
expect(slack.chat.postMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when replacing user entity refs with Slack IDs', () => {
|
||||
const createBaseMessage = (text: string) => ({
|
||||
channel: 'U12345678',
|
||||
text: 'notification',
|
||||
attachments: [
|
||||
{
|
||||
color: '#00A699',
|
||||
blocks: [
|
||||
{
|
||||
type: 'section',
|
||||
text: {
|
||||
type: 'mrkdwn',
|
||||
text,
|
||||
},
|
||||
accessory: {
|
||||
type: 'button',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: 'View More',
|
||||
},
|
||||
action_id: 'button-action',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'context',
|
||||
elements: [
|
||||
{
|
||||
type: 'plain_text',
|
||||
text: 'Severity: normal',
|
||||
emoji: true,
|
||||
},
|
||||
{
|
||||
type: 'plain_text',
|
||||
text: 'Topic: N/A',
|
||||
emoji: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
fallback: 'notification',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
it('should replace user entity refs with Slack compatible mentions', async () => {
|
||||
const slack = new WebClient();
|
||||
const processor = SlackNotificationProcessor.fromConfig(config, {
|
||||
auth,
|
||||
discovery,
|
||||
logger,
|
||||
catalog: catalogServiceMock({
|
||||
entities: DEFAULT_ENTITIES_RESPONSE.items,
|
||||
}),
|
||||
slack,
|
||||
})[0];
|
||||
|
||||
await processor.postProcess(
|
||||
{
|
||||
origin: 'plugin',
|
||||
id: '1234',
|
||||
user: 'user:default/mock',
|
||||
created: new Date(),
|
||||
payload: {
|
||||
title: 'notification',
|
||||
description:
|
||||
'Hello <@user:default/mock> and <@user:default/mock-without-slack-annotation>',
|
||||
},
|
||||
},
|
||||
{
|
||||
recipients: { type: 'entity', entityRef: 'user:default/mock' },
|
||||
payload: {
|
||||
title: 'notification',
|
||||
description:
|
||||
'Hello <@user:default/mock> and <@user:default/mock-without-slack-annotation>',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(slack.chat.postMessage).toHaveBeenCalledWith(
|
||||
createBaseMessage(
|
||||
'Hello <@U12345678> and <@user:default/mock-without-slack-annotation>',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle text without user entity refs', async () => {
|
||||
const slack = new WebClient();
|
||||
const processor = SlackNotificationProcessor.fromConfig(config, {
|
||||
auth,
|
||||
discovery,
|
||||
logger,
|
||||
catalog: catalogServiceMock({
|
||||
entities: DEFAULT_ENTITIES_RESPONSE.items,
|
||||
}),
|
||||
slack,
|
||||
})[0];
|
||||
|
||||
await processor.postProcess(
|
||||
{
|
||||
origin: 'plugin',
|
||||
id: '1234',
|
||||
user: 'user:default/mock',
|
||||
created: new Date(),
|
||||
payload: {
|
||||
title: 'notification',
|
||||
description: 'Hello world',
|
||||
},
|
||||
},
|
||||
{
|
||||
recipients: { type: 'entity', entityRef: 'user:default/mock' },
|
||||
payload: {
|
||||
title: 'notification',
|
||||
description: 'Hello world',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(slack.chat.postMessage).toHaveBeenCalledWith(
|
||||
createBaseMessage('Hello world'),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -236,8 +236,11 @@ export class SlackNotificationProcessor implements NotificationProcessor {
|
||||
}
|
||||
|
||||
// Prepare outbound messages
|
||||
const formattedPayload = await this.formatPayloadDescriptionForSlack(
|
||||
options.payload,
|
||||
);
|
||||
const outbound = destinations.map(channel =>
|
||||
toChatPostMessageArgs({ channel, payload: options.payload }),
|
||||
toChatPostMessageArgs({ channel, payload: formattedPayload }),
|
||||
);
|
||||
|
||||
// Log debug info
|
||||
@@ -249,6 +252,15 @@ export class SlackNotificationProcessor implements NotificationProcessor {
|
||||
await this.sendNotifications(outbound);
|
||||
}
|
||||
|
||||
private async formatPayloadDescriptionForSlack(
|
||||
payload: Notification['payload'],
|
||||
) {
|
||||
return {
|
||||
...payload,
|
||||
description: await this.replaceUserRefsWithSlackIds(payload.description),
|
||||
};
|
||||
}
|
||||
|
||||
async getEntities(
|
||||
entityRefs: readonly string[],
|
||||
): Promise<(Entity | undefined)[]> {
|
||||
@@ -274,6 +286,44 @@ export class SlackNotificationProcessor implements NotificationProcessor {
|
||||
return response.items;
|
||||
}
|
||||
|
||||
async replaceUserRefsWithSlackIds(
|
||||
text?: string,
|
||||
): Promise<string | undefined> {
|
||||
if (!text) return undefined;
|
||||
|
||||
// Match user entity refs like "<@user:default/billy>"
|
||||
const userRefRegex = /<@(user:[^>]+)>/gi;
|
||||
const matches = [...text.matchAll(userRefRegex)];
|
||||
|
||||
if (matches.length === 0) return text;
|
||||
|
||||
const uniqueUserRefs = new Set(
|
||||
matches.map(match => match[1].toLowerCase()),
|
||||
);
|
||||
|
||||
const slackIdMap = new Map<string, string>();
|
||||
|
||||
await Promise.all(
|
||||
[...uniqueUserRefs].map(async userRef => {
|
||||
try {
|
||||
const slackId = await this.getSlackNotificationTarget(userRef);
|
||||
if (slackId) {
|
||||
slackIdMap.set(userRef, `<@${slackId}>`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`Failed to resolve Slack ID for user ref "${userRef}": ${error}`,
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
return text.replace(userRefRegex, (match, userRef) => {
|
||||
const slackId = slackIdMap.get(userRef.toLowerCase());
|
||||
return slackId ?? match;
|
||||
});
|
||||
}
|
||||
|
||||
async getSlackNotificationTarget(
|
||||
entityRef: string,
|
||||
): Promise<string | undefined> {
|
||||
|
||||
Reference in New Issue
Block a user