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"