From 4ad370450d9ac378c8b452e1f74e73e5577598d3 Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Tue, 15 Aug 2023 12:29:45 +0200 Subject: [PATCH] fix: Change slackapp to using scheduleMessage (#4490) ### What This PR changes the slack-app addon to use slack-api's scheduleMessage instead of postMessage. ### Why When using postMessage we had to find the channel id in order to be able to post the message to the channel. scheduleMessage allows using the channel name instead of the id, which saves the entire struggle of finding the channel name. It did mean that we had to move to defining blocks of content instead of the easier formatting we did with postMessage. ### Message look ![image](https://github.com/Unleash/unleash/assets/177402/a9079c4d-07c0-4846-ad0c-67130e77fb3b) --- package.json | 2 +- src/lib/addons/slack-app.ts | 124 ++++++++---------------------------- yarn.lock | 22 +++---- 3 files changed, 37 insertions(+), 111 deletions(-) diff --git a/package.json b/package.json index c0f6416cb7..0e87410fc9 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,7 @@ ] }, "dependencies": { - "@slack/web-api": "^6.8.1", + "@slack/web-api": "^6.9.0", "@unleash/express-openapi": "^0.3.0", "ajv": "^8.11.0", "ajv-formats": "^2.1.1", diff --git a/src/lib/addons/slack-app.ts b/src/lib/addons/slack-app.ts index 1e7198241d..82a33bb039 100644 --- a/src/lib/addons/slack-app.ts +++ b/src/lib/addons/slack-app.ts @@ -1,6 +1,5 @@ import { WebClient, - ConversationsListResponse, ErrorCode, WebClientEvent, CodedError, @@ -13,7 +12,7 @@ import Addon from './addon'; import slackAppDefinition from './slack-app-definition'; import { IAddonConfig } from '../types/model'; - +const SCHEDULE_MESSAGE_DELAY_IN_SECONDS = 10; import { FeatureEventFormatter, FeatureEventFormatterMd, @@ -21,8 +20,6 @@ import { } from './feature-event-formatter-md'; import { IEvent } from '../types/events'; -const CACHE_SECONDS = 30; - interface ISlackAppAddonParameters { accessToken: string; defaultChannels: string; @@ -35,17 +32,12 @@ export default class SlackAppAddon extends Addon { private slackClient?: WebClient; - private slackChannels?: ConversationsListResponse['channels']; - - private slackChannelsCacheTimeout?: NodeJS.Timeout; - constructor(args: IAddonConfig) { super(slackAppDefinition, args); this.msgFormatter = new FeatureEventFormatterMd( args.unleashUrl, LinkStyle.SLACK, ); - this.startCacheInvalidation(); } async handleEvent( @@ -83,95 +75,42 @@ export default class SlackAppAddon extends Addon { this.accessToken = accessToken; } - if (!this.slackChannels) { - const slackConversationsList = - await this.slackClient.conversations.list({ - types: 'public_channel,private_channel', - exclude_archived: true, - limit: 200, - }); - this.slackChannels = slackConversationsList.channels || []; - let nextCursor = - slackConversationsList.response_metadata?.next_cursor; - while (nextCursor !== undefined && nextCursor !== '') { - this.logger.debug('Fetching next page of channels'); - const moreChannels = - await this.slackClient.conversations.list({ - cursor: nextCursor, - types: 'public_channel,private_channel', - exclude_archived: true, - limit: 200, - }); - const channels = moreChannels.channels; - if (channels === undefined) { - this.logger.debug( - 'Channels list was empty, breaking pagination', - ); - nextCursor = undefined; - break; - } - nextCursor = moreChannels.response_metadata?.next_cursor; - this.logger.debug( - `This page had ${channels.length} channels`, - ); - - channels.forEach((channel) => - this.slackChannels?.push(channel), - ); - } - - this.logger.debug( - `Fetched ${ - this.slackChannels.length - } available Slack channels: ${this.slackChannels.map( - ({ name }) => name, - )}`, - ); - } - - const currentSlackChannels = [...this.slackChannels]; - if (!currentSlackChannels.length) { - this.logger.warn('No available Slack channels found.'); - return; - } - const text = this.msgFormatter.format(event); const url = this.msgFormatter.featureLink(event); - - const slackChannelsToPostTo = currentSlackChannels.filter( - ({ id, name }) => id && name && eventChannels.includes(name), - ); - - if (!slackChannelsToPostTo.length) { - this.logger.info('No eligible Slack channel found.'); - return; - } - this.logger.debug( - `Posting event to ${slackChannelsToPostTo.map( - ({ name }) => name, - )}.`, - ); - - const requests = slackChannelsToPostTo.map(({ id }) => - this.slackClient!.chat.postMessage({ - channel: id!, + const requests = eventChannels.map((name) => { + const now = Math.floor(new Date().getTime() / 1000); + const postAt = now + SCHEDULE_MESSAGE_DELAY_IN_SECONDS; + return this.slackClient!.chat.scheduleMessage({ + channel: name, text, - attachments: [ + blocks: [ { - actions: [ + type: 'section', + text: { + type: 'mrkdwn', + text, + }, + }, + { + type: 'actions', + block_id: url, + elements: [ { - name: 'featureToggle', - text: 'Open in Unleash', type: 'button', + url, + text: { + type: 'plain_text', + text: 'Open in Unleash', + }, value: 'featureToggle', style: 'primary', - url, }, ], }, ], - }), - ); + post_at: postAt, + }); + }); const results = await Promise.allSettled(requests); @@ -208,12 +147,6 @@ export default class SlackAppAddon extends Addon { return []; } - startCacheInvalidation(): void { - this.slackChannelsCacheTimeout = setInterval(() => { - this.slackChannels = undefined; - }, CACHE_SECONDS * 1000); - } - logError(event: IEvent, error: Error | CodedError): void { if (!('code' in error)) { this.logger.warn(`Error handling event ${event.type}.`, error); @@ -248,13 +181,6 @@ export default class SlackAppAddon extends Addon { this.logger.warn(`Error handling event ${event.type}.`, error); } } - - destroy(): void { - if (this.slackChannelsCacheTimeout) { - clearInterval(this.slackChannelsCacheTimeout); - this.slackChannelsCacheTimeout = undefined; - } - } } module.exports = SlackAppAddon; diff --git a/yarn.lock b/yarn.lock index 630b3a5755..a69ed06cd3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1068,24 +1068,24 @@ dependencies: "@types/node" ">=12.0.0" -"@slack/types@^2.0.0": +"@slack/types@^2.8.0": version "2.8.0" resolved "https://registry.yarnpkg.com/@slack/types/-/types-2.8.0.tgz#11ea10872262a7e6f86f54e5bcd4f91e3a41fe91" integrity sha512-ghdfZSF0b4NC9ckBA8QnQgC9DJw2ZceDq0BIjjRSv6XAZBXJdWgxIsYz0TYnWSiqsKZGH2ZXbj9jYABZdH3OSQ== -"@slack/web-api@^6.8.1": - version "6.8.1" - resolved "https://registry.yarnpkg.com/@slack/web-api/-/web-api-6.8.1.tgz#c6c1e7405c884c4d9048f8b1d3901bd138d00610" - integrity sha512-eMPk2S99S613gcu7odSw/LV+Qxr8A+RXvBD0GYW510wJuTERiTjP5TgCsH8X09+lxSumbDE88wvWbuFuvGa74g== +"@slack/web-api@^6.9.0": + version "6.9.0" + resolved "https://registry.yarnpkg.com/@slack/web-api/-/web-api-6.9.0.tgz#d829dcfef490dbce8e338912706b6f39dcde3ad2" + integrity sha512-RME5/F+jvQmZHkoP+ogrDbixq1Ms1mBmylzuWq4sf3f7GCpMPWoiZ+WqWk+sism3vrlveKWIgO9R4Qg9fiRyoQ== dependencies: "@slack/logger" "^3.0.0" - "@slack/types" "^2.0.0" + "@slack/types" "^2.8.0" "@types/is-stream" "^1.1.0" "@types/node" ">=12.0.0" axios "^0.27.2" eventemitter3 "^3.1.0" form-data "^2.5.0" - is-electron "2.2.0" + is-electron "2.2.2" is-stream "^1.1.0" p-queue "^6.6.1" p-retry "^4.0.0" @@ -4359,10 +4359,10 @@ is-date-object@^1.0.1: dependencies: has-tostringtag "^1.0.0" -is-electron@2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-electron/-/is-electron-2.2.0.tgz#8943084f09e8b731b3a7a0298a7b5d56f6b7eef0" - integrity sha512-SpMppC2XR3YdxSzczXReBjqs2zGscWQpBIKqwXYBFic0ERaxNVgwLCHwOLZeESfdJQjX0RDvrJ1lBXX2ij+G1Q== +is-electron@2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/is-electron/-/is-electron-2.2.2.tgz#3778902a2044d76de98036f5dc58089ac4d80bb9" + integrity sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg== is-extendable@^0.1.1: version "0.1.1"