1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-23 13:46:45 +02:00

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)
This commit is contained in:
Christopher Kolstad 2023-08-15 12:29:45 +02:00 committed by Christopher Kolstad
parent 828b8804b1
commit dc434be0a3
No known key found for this signature in database
GPG Key ID: EA1D04EEB7E391BA
3 changed files with 38 additions and 97 deletions

View File

@ -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",

View File

@ -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;

View File

@ -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"