POC timeline activity (#5697)

TODO: 
- remove WorkspaceIsNotAuditLogged decorators on activity/activityTarget
to log task/note creations
- handle attachments
-  fix css and remove unnecessary styled components or duplicates
This commit is contained in:
Weiko
2024-06-11 18:53:28 +02:00
committed by GitHub
parent 64b8e4ec4d
commit be96c68416
60 changed files with 2134 additions and 443 deletions

View File

@ -0,0 +1,64 @@
import { ForbiddenException } from '@nestjs/common';
import groupBy from 'lodash.groupby';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MessageChannelRepository } from 'src/modules/messaging/common/repositories/message-channel.repository';
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
export class CanAccessMessageThreadService {
constructor(
@InjectObjectMetadataRepository(MessageChannelWorkspaceEntity)
private readonly messageChannelService: MessageChannelRepository,
@InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity)
private readonly connectedAccountRepository: ConnectedAccountRepository,
@InjectObjectMetadataRepository(WorkspaceMemberWorkspaceEntity)
private readonly workspaceMemberRepository: WorkspaceMemberRepository,
) {}
public async canAccessMessageThread(
userId: string,
workspaceId: string,
messageChannelMessageAssociations: any[],
) {
const messageChannels = await this.messageChannelService.getByIds(
messageChannelMessageAssociations.map(
(association) => association.messageChannelId,
),
workspaceId,
);
const messageChannelsGroupByVisibility = groupBy(
messageChannels,
(channel) => channel.visibility,
);
if (messageChannelsGroupByVisibility.share_everything) {
return;
}
const currentWorkspaceMember =
await this.workspaceMemberRepository.getByIdOrFail(userId, workspaceId);
const messageChannelsConnectedAccounts =
await this.connectedAccountRepository.getByIds(
messageChannels.map((channel) => channel.connectedAccountId),
workspaceId,
);
const messageChannelsWorkspaceMemberIds =
messageChannelsConnectedAccounts.map(
(connectedAccount) => connectedAccount.accountOwnerId,
);
if (messageChannelsWorkspaceMemberIds.includes(currentWorkspaceMember.id)) {
return;
}
throw new ForbiddenException();
}
}

View File

@ -1,24 +1,16 @@
import {
BadRequestException,
ForbiddenException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import groupBy from 'lodash.groupby';
import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface';
import { FindManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository';
import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { CanAccessMessageThreadService } from 'src/modules/messaging/common/query-hooks/message/can-access-message-thread.service';
import { MessageChannelMessageAssociationRepository } from 'src/modules/messaging/common/repositories/message-channel-message-association.repository';
import { MessageChannelRepository } from 'src/modules/messaging/common/repositories/message-channel.repository';
import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
@Injectable()
export class MessageFindManyPreQueryHook implements WorkspacePreQueryHook {
@ -27,12 +19,7 @@ export class MessageFindManyPreQueryHook implements WorkspacePreQueryHook {
MessageChannelMessageAssociationWorkspaceEntity,
)
private readonly messageChannelMessageAssociationService: MessageChannelMessageAssociationRepository,
@InjectObjectMetadataRepository(MessageChannelWorkspaceEntity)
private readonly messageChannelService: MessageChannelRepository,
@InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity)
private readonly connectedAccountRepository: ConnectedAccountRepository,
@InjectObjectMetadataRepository(WorkspaceMemberWorkspaceEntity)
private readonly workspaceMemberRepository: WorkspaceMemberRepository,
private readonly canAccessMessageThreadService: CanAccessMessageThreadService,
) {}
async execute(
@ -54,52 +41,10 @@ export class MessageFindManyPreQueryHook implements WorkspacePreQueryHook {
throw new NotFoundException();
}
await this.canAccessMessageThread(
await this.canAccessMessageThreadService.canAccessMessageThread(
userId,
workspaceId,
messageChannelMessageAssociations,
);
}
private async canAccessMessageThread(
userId: string,
workspaceId: string,
messageChannelMessageAssociations: any[],
) {
const messageChannels = await this.messageChannelService.getByIds(
messageChannelMessageAssociations.map(
(association) => association.messageChannelId,
),
workspaceId,
);
const messageChannelsGroupByVisibility = groupBy(
messageChannels,
(channel) => channel.visibility,
);
if (messageChannelsGroupByVisibility.SHARE_EVERYTHING) {
return;
}
const currentWorkspaceMember =
await this.workspaceMemberRepository.getByIdOrFail(userId, workspaceId);
const messageChannelsConnectedAccounts =
await this.connectedAccountRepository.getByIds(
messageChannels.map((channel) => channel.connectedAccountId),
workspaceId,
);
const messageChannelsWorkspaceMemberIds =
messageChannelsConnectedAccounts.map(
(connectedAccount) => connectedAccount.accountOwnerId,
);
if (messageChannelsWorkspaceMemberIds.includes(currentWorkspaceMember.id)) {
return;
}
throw new ForbiddenException();
}
}

View File

@ -1,16 +1,43 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Injectable, MethodNotAllowedException } from '@nestjs/common';
import { Injectable, NotFoundException } from '@nestjs/common';
import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface';
import { FindOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { CanAccessMessageThreadService } from 'src/modules/messaging/common/query-hooks/message/can-access-message-thread.service';
import { MessageChannelMessageAssociationRepository } from 'src/modules/messaging/common/repositories/message-channel-message-association.repository';
import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
@Injectable()
export class MessageFindOnePreQueryHook implements WorkspacePreQueryHook {
constructor(
@InjectObjectMetadataRepository(
MessageChannelMessageAssociationWorkspaceEntity,
)
private readonly messageChannelMessageAssociationService: MessageChannelMessageAssociationRepository,
private readonly canAccessMessageThreadService: CanAccessMessageThreadService,
) {}
async execute(
_userId: string,
_workspaceId: string,
_payload: FindOneResolverArgs,
userId: string,
workspaceId: string,
payload: FindOneResolverArgs,
): Promise<void> {
throw new MethodNotAllowedException('Method not allowed.');
const messageChannelMessageAssociations =
await this.messageChannelMessageAssociationService.getByMessageIds(
[payload?.filter?.id?.eq],
workspaceId,
);
if (messageChannelMessageAssociations.length === 0) {
throw new NotFoundException();
}
await this.canAccessMessageThreadService.canAccessMessageThread(
userId,
workspaceId,
messageChannelMessageAssociations,
);
}
}

View File

@ -3,6 +3,7 @@ import { Module } from '@nestjs/common';
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { CanAccessMessageThreadService } from 'src/modules/messaging/common/query-hooks/message/can-access-message-thread.service';
import { MessageFindManyPreQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-find-many.pre-query.hook';
import { MessageFindOnePreQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-find-one.pre-query-hook';
import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
@ -18,6 +19,7 @@ import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/stan
]),
],
providers: [
CanAccessMessageThreadService,
{
provide: MessageFindOnePreQueryHook.name,
useClass: MessageFindOnePreQueryHook,

View File

@ -46,6 +46,23 @@ export class MessageParticipantRepository {
);
}
public async updateParticipantsPersonIdAndReturn(
participantIds: string[],
personId: string,
workspaceId: string,
transactionManager?: EntityManager,
) {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
return await this.workspaceDataSourceService.executeRawQuery(
`UPDATE ${dataSourceSchema}."messageParticipant" SET "personId" = $1 WHERE "id" = ANY($2) RETURNING *`,
[personId, participantIds],
workspaceId,
transactionManager,
);
}
public async updateParticipantsWorkspaceMemberId(
participantIds: string[],
workspaceMemberId: string,

View File

@ -1,4 +1,5 @@
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { EntityManager } from 'typeorm';
@ -24,20 +25,21 @@ export class MessagingMessageParticipantService {
@InjectObjectMetadataRepository(PersonWorkspaceEntity)
private readonly personRepository: PersonRepository,
private readonly addPersonIdAndWorkspaceMemberIdService: AddPersonIdAndWorkspaceMemberIdService,
private readonly eventEmitter: EventEmitter2,
) {}
public async updateMessageParticipantsAfterPeopleCreation(
createdPeople: ObjectRecord<PersonWorkspaceEntity>[],
workspaceId: string,
transactionManager?: EntityManager,
): Promise<void> {
): Promise<ObjectRecord<MessageParticipantWorkspaceEntity>[]> {
const participants = await this.messageParticipantRepository.getByHandles(
createdPeople.map((person) => person.email),
workspaceId,
transactionManager,
);
if (!participants) return;
if (!participants) return [];
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
@ -57,7 +59,7 @@ export class MessagingMessageParticipantService {
)?.id,
}));
if (messageParticipantsToUpdate.length === 0) return;
if (messageParticipantsToUpdate.length === 0) return [];
const { flattenedValues, valuesString } =
getFlattenedValuesAndValuesStringForBatchRawQuery(
@ -68,22 +70,25 @@ export class MessagingMessageParticipantService {
},
);
await this.workspaceDataSourceService.executeRawQuery(
`UPDATE ${dataSourceSchema}."messageParticipant" AS "messageParticipant" SET "personId" = "data"."personId"
return (
await this.workspaceDataSourceService.executeRawQuery(
`UPDATE ${dataSourceSchema}."messageParticipant" AS "messageParticipant" SET "personId" = "data"."personId"
FROM (VALUES ${valuesString}) AS "data"("id", "personId")
WHERE "messageParticipant"."id" = "data"."id"`,
flattenedValues,
workspaceId,
transactionManager,
);
WHERE "messageParticipant"."id" = "data"."id"
RETURNING *`,
flattenedValues,
workspaceId,
transactionManager,
)
).flat();
}
public async saveMessageParticipants(
participants: ParticipantWithMessageId[],
workspaceId: string,
transactionManager?: EntityManager,
): Promise<void> {
if (!participants) return;
): Promise<ObjectRecord<MessageParticipantWorkspaceEntity>[]> {
if (!participants) return [];
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
@ -108,10 +113,10 @@ export class MessagingMessageParticipantService {
},
);
if (messageParticipantsToSave.length === 0) return;
if (messageParticipantsToSave.length === 0) return [];
await this.workspaceDataSourceService.executeRawQuery(
`INSERT INTO ${dataSourceSchema}."messageParticipant" ("messageId", "role", "handle", "displayName", "personId", "workspaceMemberId") VALUES ${valuesString}`,
return await this.workspaceDataSourceService.executeRawQuery(
`INSERT INTO ${dataSourceSchema}."messageParticipant" ("messageId", "role", "handle", "displayName", "personId", "workspaceMemberId") VALUES ${valuesString} RETURNING *`,
flattenedValues,
workspaceId,
transactionManager,
@ -135,11 +140,18 @@ export class MessagingMessageParticipantService {
);
if (personId) {
await this.messageParticipantRepository.updateParticipantsPersonId(
messageParticipantIdsToUpdate,
personId,
const updatedMessageParticipants =
await this.messageParticipantRepository.updateParticipantsPersonIdAndReturn(
messageParticipantIdsToUpdate,
personId,
workspaceId,
);
this.eventEmitter.emit(`messageParticipant.matched`, {
workspaceId,
);
userId: null,
messageParticipants: updatedMessageParticipants,
});
}
if (workspaceMemberId) {
await this.messageParticipantRepository.updateParticipantsWorkspaceMemberId(

View File

@ -1,5 +1,6 @@
import { Injectable, Inject } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { EntityManager, Repository } from 'typeorm';
@ -19,10 +20,12 @@ import {
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import {
GmailMessage,
Participant,
ParticipantWithMessageId,
} from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-message';
import { MessagingMessageService } from 'src/modules/messaging/common/services/messaging-message.service';
import { MessagingMessageParticipantService } from 'src/modules/messaging/common/services/messaging-message-participant.service';
import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity';
@Injectable()
export class MessagingSaveMessagesAndEnqueueContactCreationService {
@ -34,6 +37,7 @@ export class MessagingSaveMessagesAndEnqueueContactCreationService {
private readonly messageParticipantService: MessagingMessageParticipantService,
@InjectRepository(FeatureFlagEntity, 'core')
private readonly featureFlagRepository: Repository<FeatureFlagEntity>,
private readonly eventEmitter: EventEmitter2,
) {}
async saveMessagesAndEnqueueContactCreationJob(
@ -57,6 +61,9 @@ export class MessagingSaveMessagesAndEnqueueContactCreationService {
const isContactCreationForSentAndReceivedEmailsEnabled =
isContactCreationForSentAndReceivedEmailsEnabledFeatureFlag?.value;
let savedMessageParticipants: ObjectRecord<MessageParticipantWorkspaceEntity>[] =
[];
const participantsWithMessageId = await workspaceDataSource?.transaction(
async (transactionManager: EntityManager) => {
const messageExternalIdsAndIdsMap =
@ -74,7 +81,7 @@ export class MessagingSaveMessagesAndEnqueueContactCreationService {
const messageId = messageExternalIdsAndIdsMap.get(message.externalId);
return messageId
? message.participants.map((participant) => ({
? message.participants.map((participant: Participant) => ({
...participant,
messageId,
shouldCreateContact:
@ -86,16 +93,23 @@ export class MessagingSaveMessagesAndEnqueueContactCreationService {
: [];
});
await this.messageParticipantService.saveMessageParticipants(
participantsWithMessageId,
workspaceId,
transactionManager,
);
savedMessageParticipants =
await this.messageParticipantService.saveMessageParticipants(
participantsWithMessageId,
workspaceId,
transactionManager,
);
return participantsWithMessageId;
},
);
this.eventEmitter.emit(`messageParticipant.matched`, {
workspaceId,
userId: connectedAccount.accountOwnerId,
messageParticipants: savedMessageParticipants,
});
if (messageChannel.isContactAutoCreationEnabled) {
const contactsToCreate = participantsWithMessageId.filter(
(participant) => participant.shouldCreateContact,
@ -105,7 +119,7 @@ export class MessagingSaveMessagesAndEnqueueContactCreationService {
CreateCompanyAndContactJob.name,
{
workspaceId,
connectedAccountHandle: connectedAccount.handle,
connectedAccount,
contactsToCreate,
},
);

View File

@ -15,6 +15,8 @@ import { MessageChannelRepository } from 'src/modules/messaging/common/repositor
import { MessageParticipantRepository } from 'src/modules/messaging/common/repositories/message-participant.repository';
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository';
export type MessagingCreateCompanyAndContactAfterSyncJobData = {
workspaceId: string;
@ -36,6 +38,8 @@ export class MessagingCreateCompanyAndContactAfterSyncJob
private readonly messageParticipantRepository: MessageParticipantRepository,
@InjectRepository(FeatureFlagEntity, 'core')
private readonly featureFlagRepository: Repository<FeatureFlagEntity>,
@InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity)
private readonly connectedAccountRepository: ConnectedAccountRepository,
) {}
async handle(
@ -51,12 +55,24 @@ export class MessagingCreateCompanyAndContactAfterSyncJob
workspaceId,
);
const { handle, isContactAutoCreationEnabled } = messageChannel[0];
const { isContactAutoCreationEnabled, connectedAccountId } =
messageChannel[0];
if (!isContactAutoCreationEnabled) {
return;
}
const connectedAccount = await this.connectedAccountRepository.getById(
connectedAccountId,
workspaceId,
);
if (!connectedAccount) {
throw new Error(
`Connected account with id ${connectedAccountId} not found in workspace ${workspaceId}`,
);
}
const isContactCreationForSentAndReceivedEmailsEnabledFeatureFlag =
await this.featureFlagRepository.findOneBy({
workspaceId: workspaceId,
@ -78,7 +94,7 @@ export class MessagingCreateCompanyAndContactAfterSyncJob
);
await this.createCompanyAndContactService.createCompaniesAndContactsAndUpdateParticipants(
handle,
connectedAccount,
contactsToCreate,
workspaceId,
);

View File

@ -0,0 +1,72 @@
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { TimelineActivityRepository } from 'src/modules/timeline/repositiories/timeline-activity.repository';
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity';
@Injectable()
export class MessageParticipantListener {
constructor(
@InjectObjectMetadataRepository(TimelineActivityWorkspaceEntity)
private readonly timelineActivityRepository: TimelineActivityRepository,
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
) {}
@OnEvent('messageParticipant.matched')
public async handleMessageParticipantMatched(payload: {
workspaceId: string;
userId: string;
messageParticipants: ObjectRecord<MessageParticipantWorkspaceEntity>[];
}): Promise<void> {
const messageParticipants = payload.messageParticipants ?? [];
// TODO: move to a job?
const dataSourceSchema = this.workspaceDataSourceService.getSchemaName(
payload.workspaceId,
);
const messageObjectMetadata =
await this.objectMetadataRepository.findOneOrFail({
where: {
nameSingular: 'message',
workspaceId: payload.workspaceId,
},
});
const messageParticipantsWithPersonId = messageParticipants.filter(
(participant) => participant.personId,
);
if (messageParticipantsWithPersonId.length === 0) {
return;
}
await this.timelineActivityRepository.insertTimelineActivitiesForObject(
'person',
messageParticipantsWithPersonId.map((participant) => ({
dataSourceSchema,
name: 'message.linked',
properties: null,
objectName: 'message',
recordId: participant.personId,
workspaceMemberId: payload.userId,
workspaceId: payload.workspaceId,
linkedObjectMetadataId: messageObjectMetadata.id,
linkedRecordId: participant.messageId,
linkedRecordCachedName: '',
})),
payload.workspaceId,
);
}
}

View File

@ -3,9 +3,14 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { AutoCompaniesAndContactsCreationModule } from 'src/modules/connected-account/auto-companies-and-contacts-creation/auto-companies-and-contacts-creation.module';
import { MessagingGmailDriverModule } from 'src/modules/messaging/message-import-manager/drivers/gmail/messaging-gmail-driver.module';
import { MessagingCreateCompanyAndContactAfterSyncJob } from 'src/modules/messaging/message-participants-manager/jobs/messaging-create-company-and-contact-after-sync.job';
import { MessageParticipantListener } from 'src/modules/messaging/message-participants-manager/listeners/message-participant.listener';
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
@Module({
imports: [
@ -13,12 +18,18 @@ import { MessagingCreateCompanyAndContactAfterSyncJob } from 'src/modules/messag
AnalyticsModule,
MessagingGmailDriverModule,
AutoCompaniesAndContactsCreationModule,
WorkspaceDataSourceModule,
ObjectMetadataRepositoryModule.forFeature([
TimelineActivityWorkspaceEntity,
]),
TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'),
],
providers: [
{
provide: MessagingCreateCompanyAndContactAfterSyncJob.name,
useClass: MessagingCreateCompanyAndContactAfterSyncJob,
},
MessageParticipantListener,
],
})
export class MessaginParticipantsManagerModule {}