1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-23 00:22:19 +01:00

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)
This commit is contained in:
Christopher Kolstad 2023-08-15 12:29:45 +02:00 committed by GitHub
parent 3227e30f12
commit 4ad370450d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 37 additions and 111 deletions

View File

@ -104,7 +104,7 @@
] ]
}, },
"dependencies": { "dependencies": {
"@slack/web-api": "^6.8.1", "@slack/web-api": "^6.9.0",
"@unleash/express-openapi": "^0.3.0", "@unleash/express-openapi": "^0.3.0",
"ajv": "^8.11.0", "ajv": "^8.11.0",
"ajv-formats": "^2.1.1", "ajv-formats": "^2.1.1",

View File

@ -1,6 +1,5 @@
import { import {
WebClient, WebClient,
ConversationsListResponse,
ErrorCode, ErrorCode,
WebClientEvent, WebClientEvent,
CodedError, CodedError,
@ -13,7 +12,7 @@ import Addon from './addon';
import slackAppDefinition from './slack-app-definition'; import slackAppDefinition from './slack-app-definition';
import { IAddonConfig } from '../types/model'; import { IAddonConfig } from '../types/model';
const SCHEDULE_MESSAGE_DELAY_IN_SECONDS = 10;
import { import {
FeatureEventFormatter, FeatureEventFormatter,
FeatureEventFormatterMd, FeatureEventFormatterMd,
@ -21,8 +20,6 @@ import {
} from './feature-event-formatter-md'; } from './feature-event-formatter-md';
import { IEvent } from '../types/events'; import { IEvent } from '../types/events';
const CACHE_SECONDS = 30;
interface ISlackAppAddonParameters { interface ISlackAppAddonParameters {
accessToken: string; accessToken: string;
defaultChannels: string; defaultChannels: string;
@ -35,17 +32,12 @@ export default class SlackAppAddon extends Addon {
private slackClient?: WebClient; private slackClient?: WebClient;
private slackChannels?: ConversationsListResponse['channels'];
private slackChannelsCacheTimeout?: NodeJS.Timeout;
constructor(args: IAddonConfig) { constructor(args: IAddonConfig) {
super(slackAppDefinition, args); super(slackAppDefinition, args);
this.msgFormatter = new FeatureEventFormatterMd( this.msgFormatter = new FeatureEventFormatterMd(
args.unleashUrl, args.unleashUrl,
LinkStyle.SLACK, LinkStyle.SLACK,
); );
this.startCacheInvalidation();
} }
async handleEvent( async handleEvent(
@ -83,95 +75,42 @@ export default class SlackAppAddon extends Addon {
this.accessToken = accessToken; 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 text = this.msgFormatter.format(event);
const url = this.msgFormatter.featureLink(event); const url = this.msgFormatter.featureLink(event);
const requests = eventChannels.map((name) => {
const slackChannelsToPostTo = currentSlackChannels.filter( const now = Math.floor(new Date().getTime() / 1000);
({ id, name }) => id && name && eventChannels.includes(name), const postAt = now + SCHEDULE_MESSAGE_DELAY_IN_SECONDS;
); return this.slackClient!.chat.scheduleMessage({
channel: 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!,
text, text,
attachments: [ blocks: [
{ {
actions: [ type: 'section',
text: {
type: 'mrkdwn',
text,
},
},
{
type: 'actions',
block_id: url,
elements: [
{ {
name: 'featureToggle',
text: 'Open in Unleash',
type: 'button', type: 'button',
url,
text: {
type: 'plain_text',
text: 'Open in Unleash',
},
value: 'featureToggle', value: 'featureToggle',
style: 'primary', style: 'primary',
url,
}, },
], ],
}, },
], ],
}), post_at: postAt,
); });
});
const results = await Promise.allSettled(requests); const results = await Promise.allSettled(requests);
@ -208,12 +147,6 @@ export default class SlackAppAddon extends Addon {
return []; return [];
} }
startCacheInvalidation(): void {
this.slackChannelsCacheTimeout = setInterval(() => {
this.slackChannels = undefined;
}, CACHE_SECONDS * 1000);
}
logError(event: IEvent, error: Error | CodedError): void { logError(event: IEvent, error: Error | CodedError): void {
if (!('code' in error)) { if (!('code' in error)) {
this.logger.warn(`Error handling event ${event.type}.`, 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); this.logger.warn(`Error handling event ${event.type}.`, error);
} }
} }
destroy(): void {
if (this.slackChannelsCacheTimeout) {
clearInterval(this.slackChannelsCacheTimeout);
this.slackChannelsCacheTimeout = undefined;
}
}
} }
module.exports = SlackAppAddon; module.exports = SlackAppAddon;

View File

@ -1068,24 +1068,24 @@
dependencies: dependencies:
"@types/node" ">=12.0.0" "@types/node" ">=12.0.0"
"@slack/types@^2.0.0": "@slack/types@^2.8.0":
version "2.8.0" version "2.8.0"
resolved "https://registry.yarnpkg.com/@slack/types/-/types-2.8.0.tgz#11ea10872262a7e6f86f54e5bcd4f91e3a41fe91" resolved "https://registry.yarnpkg.com/@slack/types/-/types-2.8.0.tgz#11ea10872262a7e6f86f54e5bcd4f91e3a41fe91"
integrity sha512-ghdfZSF0b4NC9ckBA8QnQgC9DJw2ZceDq0BIjjRSv6XAZBXJdWgxIsYz0TYnWSiqsKZGH2ZXbj9jYABZdH3OSQ== integrity sha512-ghdfZSF0b4NC9ckBA8QnQgC9DJw2ZceDq0BIjjRSv6XAZBXJdWgxIsYz0TYnWSiqsKZGH2ZXbj9jYABZdH3OSQ==
"@slack/web-api@^6.8.1": "@slack/web-api@^6.9.0":
version "6.8.1" version "6.9.0"
resolved "https://registry.yarnpkg.com/@slack/web-api/-/web-api-6.8.1.tgz#c6c1e7405c884c4d9048f8b1d3901bd138d00610" resolved "https://registry.yarnpkg.com/@slack/web-api/-/web-api-6.9.0.tgz#d829dcfef490dbce8e338912706b6f39dcde3ad2"
integrity sha512-eMPk2S99S613gcu7odSw/LV+Qxr8A+RXvBD0GYW510wJuTERiTjP5TgCsH8X09+lxSumbDE88wvWbuFuvGa74g== integrity sha512-RME5/F+jvQmZHkoP+ogrDbixq1Ms1mBmylzuWq4sf3f7GCpMPWoiZ+WqWk+sism3vrlveKWIgO9R4Qg9fiRyoQ==
dependencies: dependencies:
"@slack/logger" "^3.0.0" "@slack/logger" "^3.0.0"
"@slack/types" "^2.0.0" "@slack/types" "^2.8.0"
"@types/is-stream" "^1.1.0" "@types/is-stream" "^1.1.0"
"@types/node" ">=12.0.0" "@types/node" ">=12.0.0"
axios "^0.27.2" axios "^0.27.2"
eventemitter3 "^3.1.0" eventemitter3 "^3.1.0"
form-data "^2.5.0" form-data "^2.5.0"
is-electron "2.2.0" is-electron "2.2.2"
is-stream "^1.1.0" is-stream "^1.1.0"
p-queue "^6.6.1" p-queue "^6.6.1"
p-retry "^4.0.0" p-retry "^4.0.0"
@ -4359,10 +4359,10 @@ is-date-object@^1.0.1:
dependencies: dependencies:
has-tostringtag "^1.0.0" has-tostringtag "^1.0.0"
is-electron@2.2.0: is-electron@2.2.2:
version "2.2.0" version "2.2.2"
resolved "https://registry.yarnpkg.com/is-electron/-/is-electron-2.2.0.tgz#8943084f09e8b731b3a7a0298a7b5d56f6b7eef0" resolved "https://registry.yarnpkg.com/is-electron/-/is-electron-2.2.2.tgz#3778902a2044d76de98036f5dc58089ac4d80bb9"
integrity sha512-SpMppC2XR3YdxSzczXReBjqs2zGscWQpBIKqwXYBFic0ERaxNVgwLCHwOLZeESfdJQjX0RDvrJ1lBXX2ij+G1Q== integrity sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==
is-extendable@^0.1.1: is-extendable@^0.1.1:
version "0.1.1" version "0.1.1"