feat:slack block extension
Signed-off-by: Kai Dubauskas <kai.dubauskas@doordash.com>
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { createExtensionPoint } from '@backstage/backend-plugin-api';
|
||||
import { NotificationPayload } from '@backstage/plugin-notifications-common';
|
||||
import { KnownBlock } from '@slack/web-api';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type SlackBlockKitRenderer = (
|
||||
payload: NotificationPayload,
|
||||
) => KnownBlock[];
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface NotificationsSlackBlockKitExtensionPoint {
|
||||
setBlockKitRenderer(renderer: SlackBlockKitRenderer): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const notificationsSlackBlockKitExtensionPoint =
|
||||
createExtensionPoint<NotificationsSlackBlockKitExtensionPoint>({
|
||||
id: 'notifications.slack.blockkit',
|
||||
});
|
||||
@@ -22,3 +22,4 @@
|
||||
|
||||
export { ANNOTATION_SLACK_BOT_NOTIFY } from './lib';
|
||||
export { notificationsModuleSlack as default } from './module';
|
||||
export * from './extensions';
|
||||
|
||||
@@ -209,6 +209,43 @@ describe('SlackNotificationProcessor', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should use a custom block kit renderer when provided', async () => {
|
||||
const slack = new WebClient();
|
||||
const customBlocks = [
|
||||
{
|
||||
type: 'section',
|
||||
text: { type: 'mrkdwn', text: 'Custom block' },
|
||||
},
|
||||
];
|
||||
|
||||
const processor = SlackNotificationProcessor.fromConfig(config, {
|
||||
auth,
|
||||
logger,
|
||||
catalog: catalogServiceMock({
|
||||
entities: DEFAULT_ENTITIES_RESPONSE.items,
|
||||
}),
|
||||
slack,
|
||||
blockKitRenderer: () => customBlocks,
|
||||
})[0];
|
||||
|
||||
await processor.processOptions({
|
||||
recipients: { type: 'entity', entityRef: 'group:default/mock' },
|
||||
payload: { title: 'notification' },
|
||||
});
|
||||
|
||||
expect(slack.chat.postMessage).toHaveBeenCalledWith({
|
||||
channel: 'C12345678',
|
||||
text: 'notification',
|
||||
attachments: [
|
||||
{
|
||||
color: '#00A699',
|
||||
blocks: customBlocks,
|
||||
fallback: 'notification',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a user notification is sent directly', () => {
|
||||
it('should send a notification to a user', async () => {
|
||||
const slack = new WebClient();
|
||||
|
||||
@@ -37,6 +37,7 @@ import { ANNOTATION_SLACK_BOT_NOTIFY } from './constants';
|
||||
import { BroadcastRoute } from './types';
|
||||
import { ExpiryMap, toChatPostMessageArgs } from './util';
|
||||
import { CatalogService } from '@backstage/plugin-catalog-node';
|
||||
import { SlackBlockKitRenderer } from '../extensions';
|
||||
|
||||
export class SlackNotificationProcessor implements NotificationProcessor {
|
||||
private readonly logger: LoggerService;
|
||||
@@ -54,6 +55,7 @@ export class SlackNotificationProcessor implements NotificationProcessor {
|
||||
private readonly username?: string;
|
||||
private readonly concurrencyLimit: number;
|
||||
private readonly throttleInterval: number;
|
||||
private readonly blockKitRenderer?: SlackBlockKitRenderer;
|
||||
|
||||
static fromConfig(
|
||||
config: Config,
|
||||
@@ -63,6 +65,7 @@ export class SlackNotificationProcessor implements NotificationProcessor {
|
||||
catalog: CatalogService;
|
||||
slack?: WebClient;
|
||||
broadcastChannels?: string[];
|
||||
blockKitRenderer?: SlackBlockKitRenderer;
|
||||
},
|
||||
): SlackNotificationProcessor[] {
|
||||
const slackConfig =
|
||||
@@ -104,6 +107,7 @@ export class SlackNotificationProcessor implements NotificationProcessor {
|
||||
username?: string;
|
||||
concurrencyLimit?: number;
|
||||
throttleInterval?: number;
|
||||
blockKitRenderer?: SlackBlockKitRenderer;
|
||||
}) {
|
||||
const {
|
||||
auth,
|
||||
@@ -115,6 +119,7 @@ export class SlackNotificationProcessor implements NotificationProcessor {
|
||||
username,
|
||||
concurrencyLimit,
|
||||
throttleInterval,
|
||||
blockKitRenderer,
|
||||
} = options;
|
||||
this.logger = logger;
|
||||
this.catalog = catalog;
|
||||
@@ -126,6 +131,7 @@ export class SlackNotificationProcessor implements NotificationProcessor {
|
||||
this.concurrencyLimit = concurrencyLimit ?? 10;
|
||||
this.throttleInterval =
|
||||
throttleInterval ?? durationToMilliseconds({ minutes: 1 });
|
||||
this.blockKitRenderer = blockKitRenderer;
|
||||
|
||||
this.entityLoader = new DataLoader<string, Entity | undefined>(
|
||||
async entityRefs => {
|
||||
@@ -246,6 +252,7 @@ export class SlackNotificationProcessor implements NotificationProcessor {
|
||||
channel,
|
||||
payload: options.payload,
|
||||
username: this.username,
|
||||
blockKitRenderer: this.blockKitRenderer,
|
||||
});
|
||||
|
||||
this.logger.debug(
|
||||
@@ -306,6 +313,7 @@ export class SlackNotificationProcessor implements NotificationProcessor {
|
||||
channel,
|
||||
payload: formattedPayload,
|
||||
username: this.username,
|
||||
blockKitRenderer: this.blockKitRenderer,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -19,13 +19,16 @@ import {
|
||||
NotificationSeverity,
|
||||
} from '@backstage/plugin-notifications-common';
|
||||
import { ChatPostMessageArguments, KnownBlock } from '@slack/web-api';
|
||||
import { SlackBlockKitRenderer } from '../extensions';
|
||||
|
||||
export function toChatPostMessageArgs(options: {
|
||||
channel: string;
|
||||
payload: NotificationPayload;
|
||||
username?: string;
|
||||
blockKitRenderer?: SlackBlockKitRenderer;
|
||||
}): ChatPostMessageArguments {
|
||||
const { channel, payload, username } = options;
|
||||
const { channel, payload, username, blockKitRenderer } = options;
|
||||
const blocks = (blockKitRenderer ?? toSlackBlockKit)(payload);
|
||||
|
||||
const args: ChatPostMessageArguments = {
|
||||
channel,
|
||||
@@ -34,7 +37,7 @@ export function toChatPostMessageArgs(options: {
|
||||
attachments: [
|
||||
{
|
||||
color: getColor(payload.severity),
|
||||
blocks: toSlackBlockKit(payload),
|
||||
blocks,
|
||||
fallback: payload.title,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -20,6 +20,10 @@ import {
|
||||
import { notificationsProcessingExtensionPoint } from '@backstage/plugin-notifications-node';
|
||||
import { SlackNotificationProcessor } from './lib/SlackNotificationProcessor';
|
||||
import { catalogServiceRef } from '@backstage/plugin-catalog-node';
|
||||
import {
|
||||
notificationsSlackBlockKitExtensionPoint,
|
||||
SlackBlockKitRenderer,
|
||||
} from './extensions';
|
||||
|
||||
/**
|
||||
* The Slack notification processor for use with the notifications plugin.
|
||||
@@ -31,6 +35,16 @@ export const notificationsModuleSlack = createBackendModule({
|
||||
pluginId: 'notifications',
|
||||
moduleId: 'slack',
|
||||
register(reg) {
|
||||
let blockKitRenderer: SlackBlockKitRenderer | undefined;
|
||||
reg.registerExtensionPoint(notificationsSlackBlockKitExtensionPoint, {
|
||||
setBlockKitRenderer(renderer) {
|
||||
if (blockKitRenderer) {
|
||||
throw new Error(`Slack block kit renderer was already registered`);
|
||||
}
|
||||
blockKitRenderer = renderer;
|
||||
},
|
||||
});
|
||||
|
||||
reg.registerInit({
|
||||
deps: {
|
||||
auth: coreServices.auth,
|
||||
@@ -45,6 +59,7 @@ export const notificationsModuleSlack = createBackendModule({
|
||||
auth,
|
||||
logger,
|
||||
catalog,
|
||||
blockKitRenderer,
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user