diff --git a/src/app.module.ts b/src/app.module.ts index d9807101d..1203fc5c1 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -33,6 +33,7 @@ import { ElasticSearchModule } from './providers/elasticsearch/elasticsearch.mod import { GeolocationModule } from './providers/geolocation/geolocation.module'; import { MailModule } from './providers/mail/mail.module'; import { PrismaModule } from './providers/prisma/prisma.module'; +import { SlackModule } from './providers/slack/slack.module'; import { TasksModule } from './providers/tasks/tasks.module'; @Module({ @@ -64,6 +65,7 @@ import { TasksModule } from './providers/tasks/tasks.module'; AuditLogsModule, WebhooksModule, ElasticSearchModule, + SlackModule, ], providers: [ { diff --git a/src/config/configuration.interface.ts b/src/config/configuration.interface.ts index 6611653d3..57f6ae607 100644 --- a/src/config/configuration.interface.ts +++ b/src/config/configuration.interface.ts @@ -84,4 +84,11 @@ export interface Configuration { deleteOldLogs: boolean; deleteOldLogsDays: number; }; + + slack: { + token: string; + slackApiUrl?: string; + rejectRateLimitedCalls?: boolean; + retries: number; + }; } diff --git a/src/config/configuration.ts b/src/config/configuration.ts index 39ca1acb4..ca27c0bd8 100644 --- a/src/config/configuration.ts +++ b/src/config/configuration.ts @@ -99,6 +99,12 @@ const configuration: Configuration = { : true, deleteOldLogsDays: int(process.env.TRACKING_DELETE_OLD_LOGS_DAYS, 90), }, + slack: { + token: process.env.SLACK_TOKEN ?? '', + slackApiUrl: process.env.SLACK_API_URL, + rejectRateLimitedCalls: !!process.env.SLACK_REJECT_RATE_LIMITED_CALLS, + retries: int(process.env.SLACK_FAIL_RETRIES, 3), + }, }; const configFunction: ConfigFactory = () => configuration; diff --git a/src/providers/slack/slack.module.ts b/src/providers/slack/slack.module.ts new file mode 100644 index 000000000..a8ab1665b --- /dev/null +++ b/src/providers/slack/slack.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { SlackService } from './slack.service'; + +@Module({ + imports: [ConfigModule], + providers: [SlackService], + exports: [SlackService], +}) +export class SlackModule {} diff --git a/src/providers/slack/slack.service.ts b/src/providers/slack/slack.service.ts new file mode 100644 index 000000000..54cf8299d --- /dev/null +++ b/src/providers/slack/slack.service.ts @@ -0,0 +1,43 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { ChatPostMessageArguments, WebClient } from '@slack/web-api'; +import PQueue from 'p-queue'; +import pRetry from 'p-retry'; +import { Configuration } from '../../config/configuration.interface'; + +@Injectable() +export class SlackService { + slack: WebClient; + private logger = new Logger(SlackService.name); + private queue = new PQueue({ concurrency: 1 }); + + constructor(private configService: ConfigService) { + const config = this.configService.get('slack'); + if (config.token) + this.slack = new WebClient(config.token, { + slackApiUrl: config.slackApiUrl, + rejectRateLimitedCalls: config.rejectRateLimitedCalls, + }); + } + + send(options: ChatPostMessageArguments) { + this.queue + .add(() => + pRetry(() => this.sendMessage(options), { + retries: this.configService.get('slack.retries') ?? 3, + onFailedAttempt: (error) => { + this.logger.error( + `Message to ${options.channel} failed, retrying (${error.retriesLeft} attempts left)`, + error.name, + ); + }, + }), + ) + .then(() => {}) + .catch(() => {}); + } + + private async sendMessage(options: ChatPostMessageArguments) { + return this.slack.chat.postMessage(options); + } +}