2023-07-20 14:37:06 +02:00
|
|
|
import {
|
|
|
|
WebClient,
|
|
|
|
ErrorCode,
|
|
|
|
WebClientEvent,
|
|
|
|
CodedError,
|
|
|
|
WebAPIPlatformError,
|
|
|
|
WebAPIRequestError,
|
|
|
|
WebAPIRateLimitedError,
|
|
|
|
WebAPIHTTPError,
|
2023-09-29 17:11:59 +02:00
|
|
|
KnownBlock,
|
|
|
|
Block,
|
2023-07-20 14:37:06 +02:00
|
|
|
} from '@slack/web-api';
|
2023-07-14 10:49:34 +02:00
|
|
|
import Addon from './addon';
|
|
|
|
|
|
|
|
import slackAppDefinition from './slack-app-definition';
|
|
|
|
import { IAddonConfig } from '../types/model';
|
|
|
|
import {
|
|
|
|
FeatureEventFormatter,
|
|
|
|
FeatureEventFormatterMd,
|
|
|
|
LinkStyle,
|
|
|
|
} from './feature-event-formatter-md';
|
|
|
|
import { IEvent } from '../types/events';
|
|
|
|
|
|
|
|
interface ISlackAppAddonParameters {
|
|
|
|
accessToken: string;
|
2023-07-21 15:15:43 +02:00
|
|
|
defaultChannels: string;
|
2023-07-14 10:49:34 +02:00
|
|
|
}
|
2023-07-20 14:37:06 +02:00
|
|
|
|
2023-07-14 10:49:34 +02:00
|
|
|
export default class SlackAppAddon extends Addon {
|
|
|
|
private msgFormatter: FeatureEventFormatter;
|
|
|
|
|
2023-07-20 14:37:06 +02:00
|
|
|
private accessToken?: string;
|
|
|
|
|
2023-07-14 10:49:34 +02:00
|
|
|
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> {
|
2023-07-20 14:37:06 +02:00
|
|
|
try {
|
2023-09-18 09:32:37 +02:00
|
|
|
const { accessToken, defaultChannels } = parameters;
|
2023-07-20 14:37:06 +02:00
|
|
|
if (!accessToken) {
|
|
|
|
this.logger.warn('No access token provided.');
|
|
|
|
return;
|
|
|
|
}
|
2023-09-14 09:19:38 +02:00
|
|
|
|
2023-07-20 14:37:06 +02:00
|
|
|
const taggedChannels = this.findTaggedChannels(event);
|
2023-09-18 09:32:37 +02:00
|
|
|
const eventChannels = [
|
|
|
|
...new Set(
|
|
|
|
taggedChannels.concat(
|
|
|
|
this.getDefaultChannels(defaultChannels),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
];
|
2023-07-21 15:15:43 +02:00
|
|
|
|
|
|
|
if (!eventChannels.length) {
|
2023-07-20 14:37:06 +02:00
|
|
|
this.logger.debug(
|
2023-07-21 15:15:43 +02:00
|
|
|
`No Slack channels found for event ${event.type}.`,
|
2023-07-20 14:37:06 +02:00
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
2023-08-02 15:03:51 +02:00
|
|
|
this.logger.debug(`Found candidate channels: ${eventChannels}.`);
|
2023-07-14 10:49:34 +02:00
|
|
|
|
2023-07-20 14:37:06 +02:00
|
|
|
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;
|
|
|
|
}
|
2023-07-14 10:49:34 +02:00
|
|
|
|
2023-09-29 17:11:59 +02:00
|
|
|
const { text, url } = this.msgFormatter.format(event);
|
|
|
|
|
|
|
|
const blocks: (Block | KnownBlock)[] = [
|
|
|
|
{
|
|
|
|
type: 'section',
|
|
|
|
text: {
|
|
|
|
type: 'mrkdwn',
|
|
|
|
text,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
];
|
|
|
|
|
|
|
|
if (url) {
|
|
|
|
blocks.push({
|
|
|
|
type: 'actions',
|
|
|
|
elements: [
|
2023-08-15 12:29:45 +02:00
|
|
|
{
|
2023-09-29 17:11:59 +02:00
|
|
|
type: 'button',
|
|
|
|
url,
|
2023-08-15 12:29:45 +02:00
|
|
|
text: {
|
2023-09-29 17:11:59 +02:00
|
|
|
type: 'plain_text',
|
|
|
|
text: 'Open in Unleash',
|
2023-08-15 12:29:45 +02:00
|
|
|
},
|
2023-09-29 17:11:59 +02:00
|
|
|
value: 'featureToggle',
|
|
|
|
style: 'primary',
|
2023-07-14 10:49:34 +02:00
|
|
|
},
|
|
|
|
],
|
2023-08-15 12:29:45 +02:00
|
|
|
});
|
2023-09-29 17:11:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
const requests = eventChannels.map((name) => {
|
|
|
|
return this.slackClient!.chat.postMessage({
|
|
|
|
channel: name,
|
|
|
|
text,
|
|
|
|
blocks,
|
|
|
|
});
|
2023-08-15 12:29:45 +02:00
|
|
|
});
|
2023-07-14 10:49:34 +02:00
|
|
|
|
2023-07-20 14:37:06 +02:00
|
|
|
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);
|
2023-07-14 10:49:34 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
findTaggedChannels({ tags }: Pick<IEvent, 'tags'>): string[] {
|
|
|
|
if (tags) {
|
|
|
|
return tags
|
|
|
|
.filter((tag) => tag.type === 'slack')
|
|
|
|
.map((t) => t.value);
|
|
|
|
}
|
|
|
|
return [];
|
|
|
|
}
|
2023-07-20 14:37:06 +02:00
|
|
|
|
2023-07-21 15:15:43 +02:00
|
|
|
getDefaultChannels(defaultChannels?: string): string[] {
|
|
|
|
if (defaultChannels) {
|
|
|
|
return defaultChannels.split(',').map((c) => c.trim());
|
|
|
|
}
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
2023-07-20 14:37:06 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
2023-07-14 10:49:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = SlackAppAddon;
|