diff --git a/plugins/notifications-backend-module-slack/src/extensions.ts b/plugins/notifications-backend-module-slack/src/extensions.ts new file mode 100644 index 0000000000..0229e6fd58 --- /dev/null +++ b/plugins/notifications-backend-module-slack/src/extensions.ts @@ -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({ + id: 'notifications.slack.blockkit', + }); diff --git a/plugins/notifications-backend-module-slack/src/index.ts b/plugins/notifications-backend-module-slack/src/index.ts index f8a9e248f7..178c5dbc54 100644 --- a/plugins/notifications-backend-module-slack/src/index.ts +++ b/plugins/notifications-backend-module-slack/src/index.ts @@ -22,3 +22,4 @@ export { ANNOTATION_SLACK_BOT_NOTIFY } from './lib'; export { notificationsModuleSlack as default } from './module'; +export * from './extensions'; diff --git a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts index 9490ff7dde..f9bd3f5ab3 100644 --- a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts +++ b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.test.ts @@ -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(); diff --git a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts index e8f8d9eda4..3bc833bcab 100644 --- a/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts +++ b/plugins/notifications-backend-module-slack/src/lib/SlackNotificationProcessor.ts @@ -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( 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, }), ); diff --git a/plugins/notifications-backend-module-slack/src/lib/util.ts b/plugins/notifications-backend-module-slack/src/lib/util.ts index 33293ea24f..0c13b21be6 100644 --- a/plugins/notifications-backend-module-slack/src/lib/util.ts +++ b/plugins/notifications-backend-module-slack/src/lib/util.ts @@ -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, }, ], diff --git a/plugins/notifications-backend-module-slack/src/module.ts b/plugins/notifications-backend-module-slack/src/module.ts index ff783ef104..e3d6aebee5 100644 --- a/plugins/notifications-backend-module-slack/src/module.ts +++ b/plugins/notifications-backend-module-slack/src/module.ts @@ -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, }), ); },