From dc434be0a3366c439f3c8f05267cd4cf3ddc6e60 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) This PR changes the slack-app addon to use slack-api's scheduleMessage instead of postMessage. 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. ![image](https://github.com/Unleash/unleash/assets/177402/a9079c4d-07c0-4846-ad0c-67130e77fb3b) --- package.json | 2 +- src/lib/addons/slack-app.ts | 111 +++++++++--------------------------- yarn.lock | 22 +++---- 3 files changed, 38 insertions(+), 97 deletions(-) diff --git a/package.json b/package.json index ba82a74150..22121adffd 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,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 6dfc7a43bb..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( @@ -70,6 +62,7 @@ export default class SlackAppAddon extends Addon { ); return; } + this.logger.debug(`Found candidate channels: ${eventChannels}.`); if (!this.slackClient || this.accessToken !== accessToken) { const client = new WebClient(accessToken); @@ -82,81 +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} Slack channels`, - ); - } - - const currentSlackChannels = [...this.slackChannels]; - if (!currentSlackChannels.length) { - this.logger.warn('No 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), - ); - - 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); @@ -193,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); @@ -233,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 9f57585583..5ca5812ea6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1063,24 +1063,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" @@ -4211,10 +4211,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"