1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-20 00:08:02 +01:00
unleash.unleash/src/lib/addons/slack-app.ts

186 lines
6.3 KiB
TypeScript
Raw Normal View History

import {
WebClient,
ErrorCode,
WebClientEvent,
CodedError,
WebAPIPlatformError,
WebAPIRequestError,
WebAPIRateLimitedError,
WebAPIHTTPError,
} from '@slack/web-api';
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';
interface ISlackAppAddonParameters {
accessToken: string;
defaultChannels: string;
}
export default class SlackAppAddon extends Addon {
private msgFormatter: FeatureEventFormatter;
private accessToken?: string;
private slackClient?: WebClient;
constructor(args: IAddonConfig) {
super(slackAppDefinition, args);
this.msgFormatter = new FeatureEventFormatterMd(
args.unleashUrl,
LinkStyle.SLACK,
);
}
async handleEvent(
event: IEvent,
parameters: ISlackAppAddonParameters,
): Promise<void> {
try {
const { accessToken, defaultChannels } = parameters;
if (!accessToken) {
this.logger.warn('No access token provided.');
return;
}
const taggedChannels = this.findTaggedChannels(event);
const eventChannels = taggedChannels.length
? taggedChannels
: this.getDefaultChannels(defaultChannels);
if (!eventChannels.length) {
this.logger.debug(
`No Slack channels found for event ${event.type}.`,
);
return;
}
this.logger.debug(`Found candidate channels: ${eventChannels}.`);
if (!this.slackClient || this.accessToken !== accessToken) {
const client = new WebClient(accessToken);
client.on(WebClientEvent.RATE_LIMITED, (numSeconds) => {
this.logger.debug(
`Rate limit reached for event ${event.type}. Retry scheduled after ${numSeconds} seconds`,
);
});
this.slackClient = client;
this.accessToken = accessToken;
}
const text = this.msgFormatter.format(event);
const url = this.msgFormatter.featureLink(event);
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,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text,
},
},
{
type: 'actions',
elements: [
{
type: 'button',
url,
text: {
type: 'plain_text',
text: 'Open in Unleash',
},
value: 'featureToggle',
style: 'primary',
},
],
},
],
post_at: postAt,
});
});
const results = await Promise.allSettled(requests);
results
.filter(({ status }) => status === 'rejected')
.map(({ reason }: PromiseRejectedResult) =>
this.logError(event, reason),
);
this.logger.info(
`Handled event ${event.type} dispatching ${
results.filter(({ status }) => status === 'fulfilled')
.length
} out of ${requests.length} messages successfully.`,
);
} catch (error) {
this.logError(event, error);
}
}
findTaggedChannels({ tags }: Pick<IEvent, 'tags'>): string[] {
if (tags) {
return tags
.filter((tag) => tag.type === 'slack')
.map((t) => t.value);
}
return [];
}
getDefaultChannels(defaultChannels?: string): string[] {
if (defaultChannels) {
return defaultChannels.split(',').map((c) => c.trim());
}
return [];
}
logError(event: IEvent, error: Error | CodedError): void {
if (!('code' in error)) {
this.logger.warn(`Error handling event ${event.type}.`, error);
return;
}
if (error.code === ErrorCode.PlatformError) {
const { data } = error as WebAPIPlatformError;
this.logger.warn(
`Error handling event ${event.type}. A platform error occurred: ${data}`,
error,
);
} else if (error.code === ErrorCode.RequestError) {
const { original } = error as WebAPIRequestError;
this.logger.warn(
`Error handling event ${event.type}. A request error occurred: ${original}`,
error,
);
} else if (error.code === ErrorCode.RateLimitedError) {
const { retryAfter } = error as WebAPIRateLimitedError;
this.logger.warn(
`Error handling event ${event.type}. A rate limit error occurred: retry after ${retryAfter} seconds`,
error,
);
} else if (error.code === ErrorCode.HTTPError) {
const { statusCode } = error as WebAPIHTTPError;
this.logger.warn(
`Error handling event ${event.type}. An HTTP error occurred: status code ${statusCode}`,
error,
);
} else {
this.logger.warn(`Error handling event ${event.type}.`, error);
}
}
}
module.exports = SlackAppAddon;