Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Slack integration #9

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .env.dist
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@ TSC_NONPOLLING_WATCHER=1
TEMPO_TOKEN=
TEMPO_URL=
CALAMARI_TOKEN=
CALAMARI_URL=
CALAMARI_URL=
SLACK_API_TOKEN=
SLACK_NOTIFICATIONS_CHANNEL=worklog-monitor
NODE_ICU_DATA=/home/node/app/node_modules/node-icu
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@nestjs/core": "^6.7.2",
"@nestjs/platform-express": "^6.7.2",
"@nestjs/swagger": "^3.1.0",
"@slack/web-api": "^5.2.0",
"@typescript-eslint/eslint-plugin": "^2.3.1",
"@typescript-eslint/eslint-plugin-tslint": "^2.3.1",
"@typescript-eslint/parser": "^2.3.1",
Expand All @@ -33,6 +34,7 @@
"jsonfile": "^5.0.0",
"moment": "^2.24.0",
"nestjs-config": "^1.4.4",
"node-icu": "^1.1.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.0",
"rxjs": "^6.5.3",
Expand Down
1 change: 1 addition & 0 deletions src/aggregator/aggregator.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ import { AggregatorController } from './aggregator.controller';
imports: [TempoModule, CalamariModule, MappedUsersModule],
providers: [AggregatorService],
controllers: [AggregatorController],
exports: [AggregatorService],
})
export class AggregatorModule {}
2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as path from 'path';
import { Module } from '@nestjs/common';
import { ConfigModule } from 'nestjs-config';

import { NotificationsModule } from './notifications/notifications.module';
import { AppController } from './app.controller';
import { MappedUsersModule } from './mapped-users/mapped-users.module';
import { TempoModule } from './tempo/tempo.module';
Expand All @@ -17,6 +18,7 @@ import { AggregatorModule } from './aggregator/aggregator.module';
TempoModule,
CalamariModule,
AggregatorModule,
NotificationsModule,
],
controllers: [AppController],
providers: [AggregatorService],
Expand Down
4 changes: 4 additions & 0 deletions src/config/slack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default {
apiToken: process.env.SLACK_API_TOKEN,
bartekczyz marked this conversation as resolved.
Show resolved Hide resolved
channel: process.env.SLACK_NOTIFICATIONS_CHANNEL,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
channel: process.env.SLACK_NOTIFICATIONS_CHANNEL,
channel: process.env.SLACK_NOTIFICATIONS_CHANNEL || '',

};
19 changes: 19 additions & 0 deletions src/notifications/notifications.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Test, TestingModule } from '@nestjs/testing';

import { NotificationsController } from './notifications.controller';

describe('Notifications Controller', () => {
let controller: NotificationsController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [NotificationsController],
}).compile();

controller = module.get<NotificationsController>(NotificationsController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
34 changes: 34 additions & 0 deletions src/notifications/notifications.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Controller, Post, Query } from '@nestjs/common';

import { UserWorklogResult } from '../aggregator/interfaces/user-worklog-result.interface';
import { AggregatorService } from '../aggregator/aggregator.service';

import { NotificationsService } from './notifications.service';

@Controller('notifications')
export class NotificationsController {
constructor(
private readonly aggregatorService: AggregatorService,
private readonly notificationsService: NotificationsService,
) {
}

@Post('/slack/to-users')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Post by default returns 201 Created. I am not sure that this should do this.
Maybe 204 is better?

public async sendNotificationsToUsers(@Query('date') date: string) {
const lastWorkingDate = new Date(date); // todo get last working date
const users: UserWorklogResult[] = await this.aggregatorService.aggregate(lastWorkingDate);
const lazyUsers = users.filter(user => !user.worklogs.length);

lazyUsers.forEach(workLogResult => this.notificationsService.sendToUser(workLogResult, lastWorkingDate));
}

@Post('/slack/to-channel')
public async sendNotificationToChannel(@Query('date') date: string) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Post by default returns 201 Created. I am not sure that this should do this.
Maybe 204 is better?

const lastWorkingDate = new Date(date); // todo get last working date
const users: UserWorklogResult[] = await this.aggregatorService.aggregate(lastWorkingDate);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think, should we move worklogs aggregation to NotificationService? I think it's service work.


const lazyUsers = users.filter(user => !user.worklogs.length);

this.notificationsService.sendToChannel(lazyUsers, lastWorkingDate);
}
}
15 changes: 15 additions & 0 deletions src/notifications/notifications.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Module } from '@nestjs/common';

import { AggregatorModule } from '../aggregator/aggregator.module';

import { NotificationsController } from './notifications.controller';
import { NotificationsService } from './notifications.service';
import { SlackService } from './slack/slack.service';

@Module({
imports: [AggregatorModule],
controllers: [NotificationsController],
providers: [NotificationsService, SlackService],
})
export class NotificationsModule {
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be moved to the same line, but it's nothing special :)

19 changes: 19 additions & 0 deletions src/notifications/notifications.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Test, TestingModule } from '@nestjs/testing';

import { NotificationsService } from './notifications.service';

describe('NotificationsService', () => {
let service: NotificationsService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [NotificationsService],
}).compile();

service = module.get<NotificationsService>(NotificationsService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
36 changes: 36 additions & 0 deletions src/notifications/notifications.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Injectable } from '@nestjs/common';

import { UserWorklogResult } from '../aggregator/interfaces/user-worklog-result.interface';

import { SlackService } from './slack/slack.service';

@Injectable()
export class NotificationsService {
constructor(private readonly slackService: SlackService) {
}

public sendToUser(workLogResult: UserWorklogResult, date: Date) {
const dateString = date.toLocaleDateString('pl', {
year: 'numeric',
month: 'long',
day: 'numeric',
});

const message = `Cześć ${workLogResult.firstName} :wave:. Uzupełnij work log za ${dateString}. Dzięki!`;

return this.slackService.sendToUser(message, workLogResult.email);
}

public sendToChannel(workLogResults: UserWorklogResult[], date: Date) {
const dateString = date.toLocaleDateString('pl', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
const message = `:alert: Brakujące work logi za *${dateString}* :alert: \n\n${workLogResults
.map(result => [`:pisiorek: ${result.firstName} ${result.lastName}`])
.join('\n')}`;

return this.slackService.sendToChannel(message, 'worklog-monitor-int');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded channel.

}
}
19 changes: 19 additions & 0 deletions src/notifications/slack/slack.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Test, TestingModule } from '@nestjs/testing';

import { SlackService } from './slack.service';

describe('SlackService', () => {
let service: SlackService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [SlackService],
}).compile();

service = module.get<SlackService>(SlackService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
49 changes: 49 additions & 0 deletions src/notifications/slack/slack.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { WebAPICallResult, WebClient } from '@slack/web-api';
import { ConfigService } from 'nestjs-config';

@Injectable()
export class SlackService {
private readonly webClient: WebClient;

private readonly channel: string;

constructor(
private readonly configService: ConfigService,
) {
this.webClient = new WebClient(configService.get('slack.apiToken'));
this.channel = configService.get('slack.channel');
}

public async sendToChannel(message: string, channel = null): Promise<WebAPICallResult> {
try {
return this.webClient.chat.postMessage({
channel: channel || this.channel,
text: message,
});
} catch (e) {
throw new BadRequestException(`Unable to post a message to channel ${channel}`);
}
}

public async sendToUser(message: string, email: string): Promise<WebAPICallResult> {
let user;
try {
const userResponse = await this.webClient.users.lookupByEmail({
email,
});
user = userResponse.user;
} catch (e) {
throw new BadRequestException(`Unable to lookup user by email '${email}'`);
}

try {
return await this.webClient.chat.postMessage({
channel: user.id,
text: message,
});
} catch (e) {
throw new BadRequestException(`Unable to post a message to ${email}`);
}
}
}
Loading