Skip to content

Commit

Permalink
[FEATURE]: Call communication execute with queue (#375)
Browse files Browse the repository at this point in the history
* chore: add variables environment for slack

* feat: add communication module

* chore: add variables environment for slack

* feat: add communication module

* fix: imports

* fix: remove unnecessary if clause

* chore: add redis configuration

* chore: add package.json script queue command

* chore: add queue module

* chore: run queues in another process

* chore: remove unnecessary consumer

* refactor: clean and reorganize imports

* refactor: reorganize imports

* chore: implement queue producer and consumer

* chore: remove innecessary files

* chore: improve log information

* test: fix errors after add queues

* chore: move consumer and producer to communication module

* chore: remove test outside src folder

* chore: move queue module to module folder

* chore: remove unnecessary test

* fix: errors after move queue module

* chore: add unit tests

* chore: fix typo and remove console.log

* chore: add tls configuration for redis on queue module

* chore: remove unnecessary console.log

* chore: move queue registration to the interested module

* fix: slack communication consumer path

* refactor: use assign by destructuring

* fix: merge conflicts
  • Loading branch information
mourabraz authored Aug 11, 2022
1 parent bd95d15 commit 5e201be
Show file tree
Hide file tree
Showing 20 changed files with 1,430 additions and 8,366 deletions.
1,490 changes: 1,119 additions & 371 deletions backend/package-lock.json

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@
"pre-commit": "lint-staged"
},
"dependencies": {
"@faker-js/faker": "^7.3.0",
"@nestjs-modules/mailer": "^1.8.1",
"@faker-js/faker": "^6.1.2",
"@nestjs-modules/mailer": "^1.6.1",
"@nestjs/bull": "^0.6.0",
"@nestjs/common": "^9.0.0",
"@nestjs/config": "^2.0.0",
"@nestjs/core": "^9.0.0",
Expand All @@ -40,6 +41,7 @@
"@slack/web-api": "^6.7.1",
"axios": "^0.27.2",
"bcrypt": "^5.0.1",
"bull": "^4.8.4",
"class-transformer": "^0.5.1",
"class-validator": "^0.13.2",
"cron": "2.1.0",
Expand Down Expand Up @@ -70,6 +72,7 @@
"@nestjs/schematics": "^9.0.1",
"@nestjs/testing": "^9.0.0",
"@types/bcrypt": "^5.0.0",
"@types/bull": "^3.15.8",
"@types/express": "^4.17.13",
"@types/jest": "^28.1.6",
"@types/node": "^17.0.23",
Expand Down
6 changes: 3 additions & 3 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { CardsModule } from 'modules/cards/cards.module';
import { CommentsModule } from 'modules/comments/comments.module';
import { CommunicationModule } from 'modules/communication/communication.module';
import EmailModule from 'modules/mailer/mailer.module';
import { QueueModule } from 'modules/queue/queue.module';
import SocketModule from 'modules/socket/socket.module';
import TeamsModule from 'modules/teams/teams.module';
import UsersModule from 'modules/users/users.module';
Expand Down Expand Up @@ -41,10 +42,9 @@ if (configuration().azure.enabled) {
if (configuration().smtp.enabled) {
imports.push(EmailModule);
}

if (configuration().slack.enable) {
imports.push(CommunicationModule);
}
if (configuration().slack.enable) {
imports.push(QueueModule);
imports.push(CommunicationModule);
}

Expand Down
6 changes: 5 additions & 1 deletion backend/src/infrastructure/config/config.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,11 @@ const NODE_ENV = process.env.NODE_ENV;
is: 'true',
then: Joi.required(),
otherwise: Joi.optional()
})
}),
REDIS_USER: Joi.string(),
REDIS_PASSWORD: Joi.string(),
REDIS_HOST: Joi.string().required(),
REDIS_PORT: Joi.number().required()
})
})
],
Expand Down
6 changes: 6 additions & 0 deletions backend/src/infrastructure/config/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ export const configuration = (): Configuration => {
botToken: process.env.SLACK_API_BOT_TOKEN as string,
masterChannelId: process.env.SLACK_MASTER_CHANNEL_ID as string,
channelPrefix: process.env.SLACK_CHANNEL_PREFIX as string
},
redis: {
user: process.env.REDIS_USER as string,
password: process.env.REDIS_PASSWORD as string,
host: process.env.REDIS_HOST as string,
port: parseInt(process.env.REDIS_PORT as string, 10)
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,10 @@ export interface Configuration extends AzureConfiguration {
masterChannelId: string;
channelPrefix: string;
};
redis: {
user?: string;
password?: string;
host: string;
port: number;
};
}
20 changes: 19 additions & 1 deletion backend/src/libs/test-utils/mocks/configService.mock.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,32 @@
import { SLACK_CHANNEL_PREFIX, SLACK_MASTER_CHANNEL_ID } from '../../constants/slack';
import { FRONTEND_URL } from 'libs/constants/frontend';

import {
SLACK_API_BOT_TOKEN,
SLACK_CHANNEL_PREFIX,
SLACK_MASTER_CHANNEL_ID
} from '../../constants/slack';

const configService = {
get(key: string) {
switch (key) {
case 'JWT_ACCESS_TOKEN_EXPIRATION_TIME':
return '3600';
default:
return 'UNKNOWN';
}
},
getOrThrow(key: string) {
switch (key) {
case 'JWT_ACCESS_TOKEN_EXPIRATION_TIME':
return '3600';
case SLACK_API_BOT_TOKEN:
return 'ANY_SLACK_API_BOT_TOKEN';
case SLACK_MASTER_CHANNEL_ID:
return 'ANY_SLACK_CHANNEL_ID';
case SLACK_CHANNEL_PREFIX:
return 'ANY_PREFIX_';
case FRONTEND_URL:
return 'ANY_FRONTEND_URL';
default:
return 'UNKNOWN';
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { faker } from '@faker-js/faker';
import { Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import {
ChatMeMessageArguments,
ConversationsCreateArguments,
Expand All @@ -10,6 +9,12 @@ import {
} from '@slack/web-api';
import * as WebClientSlackApi from '@slack/web-api';

import { FRONTEND_URL } from 'libs/constants/frontend';
import {
SLACK_API_BOT_TOKEN,
SLACK_CHANNEL_PREFIX,
SLACK_MASTER_CHANNEL_ID
} from 'libs/constants/slack';
import configService from 'libs/test-utils/mocks/configService.mock';
import { CreateChannelError } from 'modules/communication/errors/create-channel.error';
import { GetProfileError } from 'modules/communication/errors/get-profile.error';
Expand Down Expand Up @@ -125,6 +130,13 @@ jest.mock('@slack/web-api', () => ({
}
}));

const getConfiguration = () => ({
slackApiBotToken: configService.get(SLACK_API_BOT_TOKEN),
slackMasterChannelId: configService.get(SLACK_MASTER_CHANNEL_ID),
slackChannelPrefix: configService.get(SLACK_CHANNEL_PREFIX),
frontendUrl: configService.get(FRONTEND_URL)
});

describe('SlackCommunicationGateAdapter', () => {
let adapter: SlackCommunicationGateAdapter;

Expand All @@ -133,9 +145,7 @@ describe('SlackCommunicationGateAdapter', () => {
jest.spyOn(Logger.prototype, 'warn').mockImplementation(jest.fn);
jest.spyOn(Logger.prototype, 'verbose').mockImplementation(jest.fn);

adapter = new SlackCommunicationGateAdapter(
configService as unknown as ConfigService<Record<string, unknown>, false>
);
adapter = new SlackCommunicationGateAdapter(getConfiguration());
});

it('should be defined', () => {
Expand Down Expand Up @@ -248,9 +258,7 @@ describe('SlackCommunicationGateAdapter', () => {
jest.spyOn(Logger.prototype, 'error').mockImplementation(LoggerErrorMock);
jest.spyOn(Logger.prototype, 'verbose').mockImplementation(jest.fn);

adapterWithErrors = new SlackCommunicationGateAdapter(
configService as unknown as ConfigService<Record<string, unknown>, false>
);
adapterWithErrors = new SlackCommunicationGateAdapter(getConfiguration());
});

afterEach(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { WebClient } from '@slack/web-api';

import { SLACK_API_BOT_TOKEN } from 'libs/constants/slack';
import { ConfigurationType } from 'modules/communication/dto/types';
import { CreateChannelError } from 'modules/communication/errors/create-channel.error';
import { GetProfileError } from 'modules/communication/errors/get-profile.error';
import { GetUsersFromChannelError } from 'modules/communication/errors/get-users-from-channel.error';
Expand All @@ -17,8 +16,8 @@ export class SlackCommunicationGateAdapter implements CommunicationGateInterface

private client: WebClient;

constructor(private readonly configService: ConfigService) {
this.client = new WebClient(this.configService.get(SLACK_API_BOT_TOKEN));
constructor(private readonly config: ConfigurationType) {
this.client = new WebClient(this.config.slackApiBotToken);

this.logger.verbose('@slack/web-api client created');
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { faker } from '@faker-js/faker';
import { Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import {
ChatMeMessageArguments,
ConversationsCreateArguments,
Expand All @@ -9,7 +8,12 @@ import {
UsersProfileGetArguments
} from '@slack/web-api';

// import * as WebClientSlackApi from '@slack/web-api';
import { FRONTEND_URL } from 'libs/constants/frontend';
import {
SLACK_API_BOT_TOKEN,
SLACK_CHANNEL_PREFIX,
SLACK_MASTER_CHANNEL_ID
} from 'libs/constants/slack';
import configService from 'libs/test-utils/mocks/configService.mock';
import {
fillDividedBoardsUsersWithTeamUsers,
Expand All @@ -20,7 +24,6 @@ import { SlackExecuteCommunication } from 'modules/communication/applications/sl
import { ChatSlackHandler } from 'modules/communication/handlers/chat-slack.handler';
import { ConversationsSlackHandler } from 'modules/communication/handlers/conversations-slack.handler';
import { UsersSlackHandler } from 'modules/communication/handlers/users-slack.handler';
import { SlackExecuteCommunicationService } from 'modules/communication/services/slack-execute-communication.service';

const slackUsersIds = [
'U023BECGF',
Expand Down Expand Up @@ -140,31 +143,31 @@ jest.spyOn(Logger.prototype, 'error').mockImplementation(jest.fn);
jest.spyOn(Logger.prototype, 'warn').mockImplementation(jest.fn);
jest.spyOn(Logger.prototype, 'verbose').mockImplementation(jest.fn);

function MakeSlackCommunicationGateAdapterStub() {
return new SlackCommunicationGateAdapter(configService as unknown as ConfigService);
}
const getConfiguration = () => ({
slackApiBotToken: configService.getOrThrow(SLACK_API_BOT_TOKEN),
slackMasterChannelId: configService.getOrThrow(SLACK_MASTER_CHANNEL_ID),
slackChannelPrefix: configService.getOrThrow(SLACK_CHANNEL_PREFIX),
frontendUrl: configService.getOrThrow(FRONTEND_URL)
});

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

const communicationGateAdapterMocked = MakeSlackCommunicationGateAdapterStub();
let application: SlackExecuteCommunication;
const communicationGateAdapterMocked = new SlackCommunicationGateAdapter(getConfiguration());

beforeAll(async () => {
const application = new SlackExecuteCommunication(
configService as unknown as ConfigService,
application = new SlackExecuteCommunication(
getConfiguration(),
new ConversationsSlackHandler(communicationGateAdapterMocked),
new UsersSlackHandler(communicationGateAdapterMocked),
new ChatSlackHandler(communicationGateAdapterMocked)
);

service = new SlackExecuteCommunicationService(application);
});

it('should be defined', () => {
expect(service).toBeDefined();
expect(application).toBeDefined();
});

it('shoult create channels, invite users, post messages into slack platfomr and returns all teams created', async () => {
it('shoult create channels, invite users, post messages into slack platform and returns all teams created', async () => {
let givenBoard: any = {
_id: 'main-board',
title: 'Main Board',
Expand Down Expand Up @@ -251,7 +254,7 @@ describe('SlackExecuteCommunication', () => {
}
})),
{
role: 'stackholder',
role: 'stakeholder',
user: {
_id: 'any_id',
firstName: 'any_first_name',
Expand All @@ -266,7 +269,7 @@ describe('SlackExecuteCommunication', () => {
givenBoard = translateBoard(givenBoard);
givenBoard = fillDividedBoardsUsersWithTeamUsers(givenBoard);

const result = await service.execute(givenBoard);
const result = await application.execute(givenBoard);

const expected = [
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

import { FRONTEND_URL } from 'libs/constants/frontend';
import { SLACK_CHANNEL_PREFIX, SLACK_MASTER_CHANNEL_ID } from 'libs/constants/slack';
import { TeamDto } from 'modules/communication/dto/team.dto';
import { BoardRoles, BoardType } from 'modules/communication/dto/types';
import { BoardRoles, BoardType, ConfigurationType } from 'modules/communication/dto/types';
import { UserDto } from 'modules/communication/dto/user.dto';
import { BoardNotValidError } from 'modules/communication/errors/board-not-valid.error';
import { ChatHandlerInterface } from 'modules/communication/interfaces/chat.handler.interface';
Expand All @@ -16,25 +13,18 @@ export class SlackExecuteCommunication implements ExecuteCommunicationInterface
private logger = new Logger(SlackExecuteCommunication.name);

constructor(
private readonly configService: ConfigService,
private readonly config: ConfigurationType,
private readonly conversationsHandler: ConversationsHandlerInterface,
private readonly usersHandler: UsersHandlerInterface,
private readonly chatHandler: ChatHandlerInterface
) {}

public async execute(board: BoardType): Promise<TeamDto[]> {
let teams = this.makeTeams(board);

teams = await this.addSlackIdOnTeams(teams);

// create all channels
teams = await this.createAllChannels(teams);

// invite memebers for each channel
teams = await this.inviteAllMembers(teams);

await this.postMessageOnEachChannel(teams);

await this.postMessageOnMasterChannel(teams);

return teams;
Expand Down Expand Up @@ -68,23 +58,20 @@ export class SlackExecuteCommunication implements ExecuteCommunicationInterface
(Note: currently, retrobot does not check if the chosen responsibles joined xgeeks less than 3 months ago, so, if that happens, you have to decide who will take that role in the team. In the future, retrobot will automatically validate this rule.)\n\n
Talent wins games, but teamwork and intelligence wins championships. :fire: :muscle:`;

await this.chatHandler.postMessage(
this.configService.get(SLACK_MASTER_CHANNEL_ID) as string,
generalText
);
await this.chatHandler.postMessage(this.config.slackMasterChannelId, generalText);
}

private async postMessageOnEachChannel(teams: TeamDto[]): Promise<void> {
const generalText = {
member: (
boardId: string
) => `<!channel> In order to proceed with the retro of this month, here is the board link: \n\n
${this.configService.get(FRONTEND_URL)}/boards/${boardId}
${this.config.frontendUrl}/boards/${boardId}
`,
responsible: (
boardId: string
) => `<!channel> In order to proceed with the retro of this month, here is the main board link: \n\n
${this.configService.get(FRONTEND_URL)}/boards/${boardId}
${this.config.frontendUrl}/boards/${boardId}
`
};

Expand Down Expand Up @@ -149,7 +136,7 @@ export class SlackExecuteCommunication implements ExecuteCommunicationInterface

private async addSlackIdOnTeams(teams: TeamDto[]): Promise<TeamDto[]> {
const usersIdsOnSlack = await this.conversationsHandler.getUsersFromChannelSlowly(
this.configService.get(SLACK_MASTER_CHANNEL_ID) as string
this.config.slackMasterChannelId as string
);
const usersProfiles = await this.usersHandler.getProfilesByIds(usersIdsOnSlack);

Expand Down Expand Up @@ -180,9 +167,9 @@ export class SlackExecuteCommunication implements ExecuteCommunicationInterface

const normalizeName = (name: string) => {
// only contain lowercase letters, numbers, hyphens, and underscores, and must be 80 characters or less
const fullName = `${
process.env.NODE_ENV === 'dev' ? new Date().getTime() : ''
}${this.configService.get(SLACK_CHANNEL_PREFIX)}${name}`;
const fullName = `${process.env.NODE_ENV === 'dev' ? new Date().getTime() : ''}${
this.config.slackChannelPrefix
}${name}`;
return fullName
.replace(/\s/, '_')
.replace(/[^a-zA-Z0-9-_]/g, '')
Expand Down
Loading

0 comments on commit 5e201be

Please sign in to comment.