Skip to content

Commit

Permalink
fix: Change slackapp to using scheduleMessage (#4490)
Browse files Browse the repository at this point in the history
### 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)
  • Loading branch information
Christopher Kolstad committed Aug 15, 2023
1 parent 3227e30 commit 4ad3704
Show file tree
Hide file tree
Showing 3 changed files with 37 additions and 111 deletions.
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -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",
Expand Down
124 changes: 25 additions & 99 deletions src/lib/addons/slack-app.ts
@@ -1,6 +1,5 @@
import {
WebClient,
ConversationsListResponse,
ErrorCode,
WebClientEvent,
CodedError,
Expand All @@ -13,16 +12,14 @@ 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,
LinkStyle,
} from './feature-event-formatter-md';
import { IEvent } from '../types/events';

const CACHE_SECONDS = 30;

interface ISlackAppAddonParameters {
accessToken: string;
defaultChannels: string;
Expand All @@ -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(
Expand Down Expand Up @@ -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: [
{
type: 'section',
text: {
type: 'mrkdwn',
text,
},
},
{
actions: [
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);

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
22 changes: 11 additions & 11 deletions yarn.lock
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit 4ad3704

Please sign in to comment.