diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts index 99f39f892..534ad1a6e 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts @@ -28,14 +28,14 @@ import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inje import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; import { InjectWorkspaceDatasource } from 'src/engine/twenty-orm/decorators/inject-workspace-datasource.decorator'; -import { - CalendarEventsImportJob, - CalendarEventsImportJobData, -} from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-events-import.job'; import { CalendarChannelWorkspaceEntity, CalendarChannelVisibility, } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; +import { + CalendarEventsImportJobData, + CalendarEventListFetchJob, +} from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job'; @Injectable() export class GoogleAPIsService { @@ -167,13 +167,21 @@ export class GoogleAPIsService { } if (isCalendarEnabled) { - await this.calendarQueueService.add( - CalendarEventsImportJob.name, - { - workspaceId, + const calendarChannels = await this.calendarChannelRepository.find({ + where: { connectedAccountId: newOrExistingConnectedAccountId, }, - ); + }); + + for (const calendarChannel of calendarChannels) { + await this.calendarQueueService.add( + CalendarEventListFetchJob.name, + { + calendarChannelId: calendarChannel.id, + workspaceId, + }, + ); + } } } } diff --git a/packages/twenty-server/src/engine/integrations/cache-storage/types/cache-storage-namespace.enum.ts b/packages/twenty-server/src/engine/integrations/cache-storage/types/cache-storage-namespace.enum.ts index 5ef0221bc..e89fb00b0 100644 --- a/packages/twenty-server/src/engine/integrations/cache-storage/types/cache-storage-namespace.enum.ts +++ b/packages/twenty-server/src/engine/integrations/cache-storage/types/cache-storage-namespace.enum.ts @@ -1,4 +1,5 @@ export enum CacheStorageNamespace { Messaging = 'messaging', + Calendar = 'calendar', WorkspaceSchema = 'workspaceSchema', } diff --git a/packages/twenty-server/src/engine/integrations/message-queue/jobs.module.ts b/packages/twenty-server/src/engine/integrations/message-queue/jobs.module.ts index eb107b3fb..fc8db42f1 100644 --- a/packages/twenty-server/src/engine/integrations/message-queue/jobs.module.ts +++ b/packages/twenty-server/src/engine/integrations/message-queue/jobs.module.ts @@ -22,7 +22,7 @@ import { AutoCompaniesAndContactsCreationJobModule } from 'src/modules/connected import { MessagingModule } from 'src/modules/messaging/messaging.module'; import { TimelineJobModule } from 'src/modules/timeline/jobs/timeline-job.module'; import { CalendarModule } from 'src/modules/calendar/calendar.module'; -import { CalendarEventParticipantModule } from 'src/modules/calendar/calendar-event-participant-manager/calendar-event-participant.module'; +import { CalendarEventParticipantManagerModule } from 'src/modules/calendar/calendar-event-participant-manager/calendar-event-participant-manager.module'; @Module({ imports: [ @@ -37,7 +37,7 @@ import { CalendarEventParticipantModule } from 'src/modules/calendar/calendar-ev WorkspaceModule, MessagingModule, CalendarModule, - CalendarEventParticipantModule, + CalendarEventParticipantManagerModule, TimelineActivityModule, StripeModule, WorkspaceQueryRunnerJobModule, diff --git a/packages/twenty-server/src/engine/twenty-orm/twenty-orm.manager.ts b/packages/twenty-server/src/engine/twenty-orm/twenty-orm.manager.ts index b4fc8e292..4d997c527 100644 --- a/packages/twenty-server/src/engine/twenty-orm/twenty-orm.manager.ts +++ b/packages/twenty-server/src/engine/twenty-orm/twenty-orm.manager.ts @@ -2,11 +2,39 @@ import { Injectable, Optional, Type } from '@nestjs/common'; import { ObjectLiteral } from 'typeorm'; -import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory'; import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; -import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; +import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory'; import { WorkspaceDatasourceFactory } from 'src/engine/twenty-orm/factories/workspace-datasource.factory'; -import { ObjectLiteralStorage } from 'src/engine/twenty-orm/storage/object-literal.storage'; +import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; +import { ActivityTargetWorkspaceEntity } from 'src/modules/activity/standard-objects/activity-target.workspace-entity'; +import { ActivityWorkspaceEntity } from 'src/modules/activity/standard-objects/activity.workspace-entity'; +import { CommentWorkspaceEntity } from 'src/modules/activity/standard-objects/comment.workspace-entity'; +import { ApiKeyWorkspaceEntity } from 'src/modules/api-key/standard-objects/api-key.workspace-entity'; +import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity'; +import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity'; +import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; +import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity'; +import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity'; +import { CompanyWorkspaceEntity } from 'src/modules/company/standard-objects/company.workspace-entity'; +import { BlocklistWorkspaceEntity } from 'src/modules/connected-account/standard-objects/blocklist.workspace-entity'; +import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; +import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity'; +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'; +import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity'; +import { MessageThreadWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-thread.workspace-entity'; +import { MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity'; +import { OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-objects/opportunity.workspace-entity'; +import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity'; +import { AuditLogWorkspaceEntity } from 'src/modules/timeline/standard-objects/audit-log.workspace-entity'; +import { BehavioralEventWorkspaceEntity } from 'src/modules/timeline/standard-objects/behavioral-event.workspace-entity'; +import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity'; +import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity'; +import { ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter.workspace-entity'; +import { ViewSortWorkspaceEntity } from 'src/modules/view/standard-objects/view-sort.workspace-entity'; +import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity'; +import { WebhookWorkspaceEntity } from 'src/modules/webhook/standard-objects/webhook.workspace-entity'; +import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @Injectable() export class TwentyORMManager { @@ -33,7 +61,43 @@ export class TwentyORMManager { workspaceId: string, entityClass: Type, ): Promise> { - const entities = ObjectLiteralStorage.getAllEntitySchemas(); + // TODO: This is a temporary solution to get all workspace entities + const workspaceEntities = [ + ActivityTargetWorkspaceEntity, + ActivityWorkspaceEntity, + ApiKeyWorkspaceEntity, + AttachmentWorkspaceEntity, + BlocklistWorkspaceEntity, + BehavioralEventWorkspaceEntity, + CalendarChannelEventAssociationWorkspaceEntity, + CalendarChannelWorkspaceEntity, + CalendarEventParticipantWorkspaceEntity, + CalendarEventWorkspaceEntity, + CommentWorkspaceEntity, + CompanyWorkspaceEntity, + ConnectedAccountWorkspaceEntity, + FavoriteWorkspaceEntity, + AuditLogWorkspaceEntity, + MessageChannelMessageAssociationWorkspaceEntity, + MessageChannelWorkspaceEntity, + MessageParticipantWorkspaceEntity, + MessageThreadWorkspaceEntity, + MessageWorkspaceEntity, + OpportunityWorkspaceEntity, + PersonWorkspaceEntity, + TimelineActivityWorkspaceEntity, + ViewFieldWorkspaceEntity, + ViewFilterWorkspaceEntity, + ViewSortWorkspaceEntity, + ViewWorkspaceEntity, + WebhookWorkspaceEntity, + WorkspaceMemberWorkspaceEntity, + ]; + + const entities = workspaceEntities.map((workspaceEntity) => + this.entitySchemaFactory.create(workspaceEntity as any), + ); + const workspaceDataSource = await this.workspaceDataSourceFactory.create( entities, workspaceId, diff --git a/packages/twenty-server/src/modules/calendar-messaging-participant/services/add-person-id-and-workspace-member-id/add-person-id-and-workspace-member-id.service.ts b/packages/twenty-server/src/modules/calendar-messaging-participant-manager/services/add-person-id-and-workspace-member-id/add-person-id-and-workspace-member-id.service.ts similarity index 97% rename from packages/twenty-server/src/modules/calendar-messaging-participant/services/add-person-id-and-workspace-member-id/add-person-id-and-workspace-member-id.service.ts rename to packages/twenty-server/src/modules/calendar-messaging-participant-manager/services/add-person-id-and-workspace-member-id/add-person-id-and-workspace-member-id.service.ts index aa6eb282a..ef22e2a1c 100644 --- a/packages/twenty-server/src/modules/calendar-messaging-participant/services/add-person-id-and-workspace-member-id/add-person-id-and-workspace-member-id.service.ts +++ b/packages/twenty-server/src/modules/calendar-messaging-participant-manager/services/add-person-id-and-workspace-member-id/add-person-id-and-workspace-member-id.service.ts @@ -7,6 +7,8 @@ import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/work import { PersonRepository } from 'src/modules/person/repositories/person.repository'; import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity'; +// TODO: Move inside person module and workspace-member module + @Injectable() export class AddPersonIdAndWorkspaceMemberIdService { constructor( diff --git a/packages/twenty-server/src/modules/calendar-messaging-participant/utils/__tests__/is-email-blocklisted.util.spec.ts b/packages/twenty-server/src/modules/calendar-messaging-participant-manager/utils/__tests__/is-email-blocklisted.util.spec.ts similarity index 97% rename from packages/twenty-server/src/modules/calendar-messaging-participant/utils/__tests__/is-email-blocklisted.util.spec.ts rename to packages/twenty-server/src/modules/calendar-messaging-participant-manager/utils/__tests__/is-email-blocklisted.util.spec.ts index c7c6af34c..4d5cf1255 100644 --- a/packages/twenty-server/src/modules/calendar-messaging-participant/utils/__tests__/is-email-blocklisted.util.spec.ts +++ b/packages/twenty-server/src/modules/calendar-messaging-participant-manager/utils/__tests__/is-email-blocklisted.util.spec.ts @@ -1,4 +1,4 @@ -import { isEmailBlocklisted } from 'src/modules/calendar-messaging-participant/utils/is-email-blocklisted.util'; +import { isEmailBlocklisted } from 'src/modules/calendar-messaging-participant-manager/utils/is-email-blocklisted.util'; describe('isEmailBlocklisted', () => { it('should return true if email is blocklisted', () => { diff --git a/packages/twenty-server/src/modules/calendar-messaging-participant/utils/is-email-blocklisted.util.ts b/packages/twenty-server/src/modules/calendar-messaging-participant-manager/utils/is-email-blocklisted.util.ts similarity index 91% rename from packages/twenty-server/src/modules/calendar-messaging-participant/utils/is-email-blocklisted.util.ts rename to packages/twenty-server/src/modules/calendar-messaging-participant-manager/utils/is-email-blocklisted.util.ts index af21bbe1e..1ba64ee19 100644 --- a/packages/twenty-server/src/modules/calendar-messaging-participant/utils/is-email-blocklisted.util.ts +++ b/packages/twenty-server/src/modules/calendar-messaging-participant-manager/utils/is-email-blocklisted.util.ts @@ -1,3 +1,5 @@ +// TODO: Move inside blocklist module + export const isEmailBlocklisted = ( channelHandle: string, email: string | null | undefined, diff --git a/packages/twenty-server/src/modules/calendar-messaging-participant/jobs/calendar-messaging-participant-job.module.ts b/packages/twenty-server/src/modules/calendar-messaging-participant/jobs/calendar-messaging-participant-job.module.ts deleted file mode 100644 index d54066bbd..000000000 --- a/packages/twenty-server/src/modules/calendar-messaging-participant/jobs/calendar-messaging-participant-job.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { MatchParticipantJob } from 'src/modules/calendar-messaging-participant/jobs/match-participant.job'; -import { UnmatchParticipantJob } from 'src/modules/calendar-messaging-participant/jobs/unmatch-participant.job'; -import { CalendarEventParticipantModule } from 'src/modules/calendar/calendar-event-participant-manager/calendar-event-participant.module'; -import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module'; - -@Module({ - imports: [CalendarEventParticipantModule, MessagingCommonModule], - providers: [MatchParticipantJob, UnmatchParticipantJob], -}) -export class CalendarMessagingParticipantJobModule {} diff --git a/packages/twenty-server/src/modules/calendar-messaging-participant/services/add-person-id-and-workspace-member-id/add-person-id-and-workspace-member-id.module.ts b/packages/twenty-server/src/modules/calendar-messaging-participant/services/add-person-id-and-workspace-member-id/add-person-id-and-workspace-member-id.module.ts deleted file mode 100644 index eac7dd9c7..000000000 --- a/packages/twenty-server/src/modules/calendar-messaging-participant/services/add-person-id-and-workspace-member-id/add-person-id-and-workspace-member-id.module.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; -import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; -import { AddPersonIdAndWorkspaceMemberIdService } from 'src/modules/calendar-messaging-participant/services/add-person-id-and-workspace-member-id/add-person-id-and-workspace-member-id.service'; -import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity'; - -@Module({ - imports: [ - WorkspaceDataSourceModule, - ObjectMetadataRepositoryModule.forFeature([PersonWorkspaceEntity]), - ], - providers: [AddPersonIdAndWorkspaceMemberIdService], - exports: [AddPersonIdAndWorkspaceMemberIdService], -}) -export class AddPersonIdAndWorkspaceMemberIdModule {} diff --git a/packages/twenty-server/src/modules/calendar/blocklist-manager/jobs/blocklist-reimport-calendar-events.job.ts b/packages/twenty-server/src/modules/calendar/blocklist-manager/jobs/blocklist-reimport-calendar-events.job.ts index 3e44a9761..4eef5ecdb 100644 --- a/packages/twenty-server/src/modules/calendar/blocklist-manager/jobs/blocklist-reimport-calendar-events.job.ts +++ b/packages/twenty-server/src/modules/calendar/blocklist-manager/jobs/blocklist-reimport-calendar-events.job.ts @@ -1,4 +1,6 @@ -import { Logger, Scope } from '@nestjs/common'; +import { Scope } from '@nestjs/common'; + +import { Any } from 'typeorm'; import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; @@ -6,12 +8,16 @@ import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repos 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 { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator'; -import { CalendarEventsImportService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-events-import.service'; +import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator'; +import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; +import { + CalendarChannelSyncStage, + CalendarChannelWorkspaceEntity, +} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; export type BlocklistReimportCalendarEventsJobData = { workspaceId: string; workspaceMemberId: string; - handle: string; }; @Processor({ @@ -19,44 +25,37 @@ export type BlocklistReimportCalendarEventsJobData = { scope: Scope.REQUEST, }) export class BlocklistReimportCalendarEventsJob { - private readonly logger = new Logger(BlocklistReimportCalendarEventsJob.name); - constructor( @InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity) private readonly connectedAccountRepository: ConnectedAccountRepository, - private readonly googleCalendarSyncService: CalendarEventsImportService, + @InjectWorkspaceRepository(CalendarChannelWorkspaceEntity) + private readonly calendarChannelRepository: WorkspaceRepository, ) {} @Process(BlocklistReimportCalendarEventsJob.name) async handle(data: BlocklistReimportCalendarEventsJobData): Promise { - const { workspaceId, workspaceMemberId, handle } = data; + const { workspaceId, workspaceMemberId } = data; - this.logger.log( - `Reimporting calendar events from handle ${handle} in workspace ${workspaceId} for workspace member ${workspaceMemberId}`, - ); - - const connectedAccount = + const connectedAccounts = await this.connectedAccountRepository.getAllByWorkspaceMemberId( workspaceMemberId, workspaceId, ); - if (!connectedAccount || connectedAccount.length === 0) { - this.logger.error( - `No connected account found for workspace member ${workspaceMemberId} in workspace ${workspaceId}`, - ); - + if (!connectedAccounts || connectedAccounts.length === 0) { return; } - await this.googleCalendarSyncService.processCalendarEventsImport( - workspaceId, - connectedAccount[0].id, - handle, - ); - - this.logger.log( - `Reimporting calendar events from ${handle} in workspace ${workspaceId} for workspace member ${workspaceMemberId} done`, + await this.calendarChannelRepository.update( + { + connectedAccountId: Any( + connectedAccounts.map((connectedAccount) => connectedAccount.id), + ), + }, + { + syncStage: + CalendarChannelSyncStage.FULL_CALENDAR_EVENT_LIST_FETCH_PENDING, + }, ); } } diff --git a/packages/twenty-server/src/modules/calendar/blocklist-manager/listeners/calendar-blocklist.listener.ts b/packages/twenty-server/src/modules/calendar/blocklist-manager/listeners/calendar-blocklist.listener.ts index 370ca2f6e..e628f1128 100644 --- a/packages/twenty-server/src/modules/calendar/blocklist-manager/listeners/calendar-blocklist.listener.ts +++ b/packages/twenty-server/src/modules/calendar/blocklist-manager/listeners/calendar-blocklist.listener.ts @@ -46,7 +46,6 @@ export class CalendarBlocklistListener { { workspaceId: payload.workspaceId, workspaceMemberId: payload.properties.before.workspaceMember.id, - handle: payload.properties.before.handle, }, ); } @@ -68,7 +67,6 @@ export class CalendarBlocklistListener { { workspaceId: payload.workspaceId, workspaceMemberId: payload.properties.after.workspaceMember.id, - handle: payload.properties.before.handle, }, ); } diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/calendar-event-import-manager.module.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/calendar-event-import-manager.module.ts index 57cef3e13..cbe63c6c4 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/calendar-event-import-manager.module.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/calendar-event-import-manager.module.ts @@ -8,12 +8,16 @@ import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repos import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { CalendarEventCleanerModule } from 'src/modules/calendar/calendar-event-cleaner/calendar-event-cleaner.module'; -import { CalendarEventsImportCronCommand } from 'src/modules/calendar/calendar-event-import-manager/crons/commands/calendar-events-import.cron.command'; -import { CalendarEventsImportCronJob } from 'src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-events-import.cron.job'; +import { CalendarEventListFetchCronCommand } from 'src/modules/calendar/calendar-event-import-manager/crons/commands/calendar-event-list-fetch.cron.command'; +import { CalendarEventListFetchCronJob } from 'src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-event-list-fetch.cron.job'; import { GoogleCalendarDriverModule } from 'src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/google-calendar-driver.module'; -import { CalendarEventsImportJob } from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-events-import.job'; +import { CalendarEventListFetchJob } from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job'; +import { CalendarChannelSyncStatusService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-channel-sync-status.service'; import { CalendarEventsImportService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-events-import.service'; -import { CalendarEventParticipantModule } from 'src/modules/calendar/calendar-event-participant-manager/calendar-event-participant.module'; +import { CalendarGetCalendarEventsService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-get-events.service'; +import { CalendarSaveEventsService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-save-events.service'; +import { CalendarEventParticipantManagerModule } from 'src/modules/calendar/calendar-event-participant-manager/calendar-event-participant-manager.module'; +import { CalendarCommonModule } from 'src/modules/calendar/common/calendar-common.module'; import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity'; import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity'; @@ -38,7 +42,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta PersonWorkspaceEntity, WorkspaceMemberWorkspaceEntity, ]), - CalendarEventParticipantModule, + CalendarEventParticipantManagerModule, TypeOrmModule.forFeature([FeatureFlagEntity], 'core'), TypeOrmModule.forFeature([DataSourceEntity], 'metadata'), WorkspaceDataSourceModule, @@ -46,12 +50,17 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta GoogleCalendarDriverModule, BillingModule, GoogleAPIRefreshAccessTokenModule, + CalendarCommonModule, + CalendarEventParticipantManagerModule, ], providers: [ + CalendarChannelSyncStatusService, CalendarEventsImportService, - CalendarEventsImportCronJob, - CalendarEventsImportCronCommand, - CalendarEventsImportJob, + CalendarGetCalendarEventsService, + CalendarSaveEventsService, + CalendarEventListFetchCronJob, + CalendarEventListFetchCronCommand, + CalendarEventListFetchJob, ], exports: [CalendarEventsImportService], }) diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/crons/commands/calendar-events-import.cron.command.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/crons/commands/calendar-event-list-fetch.cron.command.ts similarity index 67% rename from packages/twenty-server/src/modules/calendar/calendar-event-import-manager/crons/commands/calendar-events-import.cron.command.ts rename to packages/twenty-server/src/modules/calendar/calendar-event-import-manager/crons/commands/calendar-event-list-fetch.cron.command.ts index df8a47f16..87d139aa4 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/crons/commands/calendar-events-import.cron.command.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/crons/commands/calendar-event-list-fetch.cron.command.ts @@ -3,15 +3,15 @@ import { Command, CommandRunner } from 'nest-commander'; import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator'; import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service'; -import { CalendarEventsImportCronJob } from 'src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-events-import.cron.job'; +import { CalendarEventListFetchCronJob } from 'src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-event-list-fetch.cron.job'; const CALENDAR_EVENTS_IMPORT_CRON_PATTERN = '*/5 * * * *'; @Command({ - name: 'cron:calendar:calendar-events-import', - description: 'Starts a cron job to import calendar events', + name: 'cron:calendar:calendar-event-list-fetch', + description: 'Starts a cron job to fetch the calendar event list', }) -export class CalendarEventsImportCronCommand extends CommandRunner { +export class CalendarEventListFetchCronCommand extends CommandRunner { constructor( @InjectMessageQueue(MessageQueue.cronQueue) private readonly messageQueueService: MessageQueueService, @@ -21,7 +21,7 @@ export class CalendarEventsImportCronCommand extends CommandRunner { async run(): Promise { await this.messageQueueService.addCron( - CalendarEventsImportCronJob.name, + CalendarEventListFetchCronJob.name, undefined, { repeat: { pattern: CALENDAR_EVENTS_IMPORT_CRON_PATTERN }, diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-events-import.cron.job.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-event-list-fetch.cron.job.ts similarity index 75% rename from packages/twenty-server/src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-events-import.cron.job.ts rename to packages/twenty-server/src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-event-list-fetch.cron.job.ts index cbb1eba30..30c6fad2e 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-events-import.cron.job.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-event-list-fetch.cron.job.ts @@ -1,25 +1,28 @@ import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, In } from 'typeorm'; +import { Any, In, Repository } from 'typeorm'; -import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; -import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; -import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator'; -import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator'; -import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator'; -import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service'; import { BillingService } from 'src/engine/core-modules/billing/billing.service'; +import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator'; +import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator'; +import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator'; +import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; +import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service'; +import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { + CalendarEventListFetchJob, CalendarEventsImportJobData, - CalendarEventsImportJob, -} from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-events-import.job'; -import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; +} from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job'; +import { + CalendarChannelSyncStage, + CalendarChannelWorkspaceEntity, +} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; @Processor({ queueName: MessageQueue.cronQueue, }) -export class CalendarEventsImportCronJob { +export class CalendarEventListFetchCronJob { constructor( @InjectRepository(DataSourceEntity, 'metadata') private readonly dataSourceRepository: Repository, @@ -29,7 +32,7 @@ export class CalendarEventsImportCronJob { private readonly twentyORMManager: TwentyORMManager, ) {} - @Process(CalendarEventsImportCronJob.name) + @Process(CalendarEventListFetchCronJob.name) async handle(): Promise { const workspaceIds = await this.billingService.getActiveSubscriptionWorkspaceIds(); @@ -51,18 +54,22 @@ export class CalendarEventsImportCronJob { CalendarChannelWorkspaceEntity, ); - const calendarChannels = await calendarChannelRepository.find({}); + const calendarChannels = await calendarChannelRepository.find({ + where: { + isSyncEnabled: true, + syncStage: Any([ + CalendarChannelSyncStage.FULL_CALENDAR_EVENT_LIST_FETCH_PENDING, + CalendarChannelSyncStage.PARTIAL_CALENDAR_EVENT_LIST_FETCH_PENDING, + ]), + }, + }); for (const calendarChannel of calendarChannels) { - if (!calendarChannel?.isSyncEnabled) { - continue; - } - await this.messageQueueService.add( - CalendarEventsImportJob.name, + CalendarEventListFetchJob.name, { + calendarChannelId: calendarChannel.id, workspaceId, - connectedAccountId: calendarChannel.connectedAccountId, }, ); } diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/google-calendar-driver.module.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/google-calendar-driver.module.ts index e922cb11a..46b209ef9 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/google-calendar-driver.module.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/google-calendar-driver.module.ts @@ -2,11 +2,12 @@ import { Module } from '@nestjs/common'; import { EnvironmentModule } from 'src/engine/integrations/environment/environment.module'; import { GoogleCalendarClientProvider } from 'src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/providers/google-calendar.provider'; +import { GoogleCalendarGetEventsService } from 'src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/services/google-calendar-get-events.service'; import { OAuth2ClientManagerModule } from 'src/modules/connected-account/oauth2-client-manager/oauth2-client-manager.module'; @Module({ imports: [EnvironmentModule, OAuth2ClientManagerModule], - providers: [GoogleCalendarClientProvider], - exports: [GoogleCalendarClientProvider], + providers: [GoogleCalendarClientProvider, GoogleCalendarGetEventsService], + exports: [GoogleCalendarGetEventsService], }) export class GoogleCalendarDriverModule {} diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/providers/google-calendar.provider.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/providers/google-calendar.provider.ts index a211ef6ca..e91758494 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/providers/google-calendar.provider.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/providers/google-calendar.provider.ts @@ -12,7 +12,10 @@ export class GoogleCalendarClientProvider { ) {} public async getGoogleCalendarClient( - connectedAccount: ConnectedAccountWorkspaceEntity, + connectedAccount: Pick< + ConnectedAccountWorkspaceEntity, + 'provider' | 'refreshToken' + >, ): Promise { const oAuth2Client = await this.oAuth2ClientManagerService.getOAuth2Client(connectedAccount); diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/services/google-calendar-get-events.service.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/services/google-calendar-get-events.service.ts new file mode 100644 index 000000000..63b11b19f --- /dev/null +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/services/google-calendar-get-events.service.ts @@ -0,0 +1,93 @@ +import { Injectable } from '@nestjs/common'; + +import { calendar_v3 as calendarV3 } from 'googleapis'; +import { GaxiosError } from 'gaxios'; + +import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; +import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator'; +import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; +import { GoogleCalendarClientProvider } from 'src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/providers/google-calendar.provider'; +import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; +import { GetCalendarEventsResponse } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-get-events.service'; +import { formatGoogleCalendarEvents } from 'src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/utils/format-google-calendar-event.util'; + +@Injectable() +export class GoogleCalendarGetEventsService { + constructor( + private readonly googleCalendarClientProvider: GoogleCalendarClientProvider, + @InjectWorkspaceRepository(CalendarChannelWorkspaceEntity) + private readonly calendarChannelRepository: WorkspaceRepository, + ) {} + + public async getCalendarEvents( + connectedAccount: Pick< + ConnectedAccountWorkspaceEntity, + 'provider' | 'refreshToken' | 'id' + >, + syncCursor?: string, + ): Promise { + const googleCalendarClient = + await this.googleCalendarClientProvider.getGoogleCalendarClient( + connectedAccount, + ); + + let nextSyncToken: string | null | undefined; + let nextPageToken: string | undefined; + const events: calendarV3.Schema$Event[] = []; + + let hasMoreEvents = true; + + while (hasMoreEvents) { + const googleCalendarEvents = await googleCalendarClient.events + .list({ + calendarId: 'primary', + maxResults: 500, + syncToken: syncCursor, + pageToken: nextPageToken, + showDeleted: true, + }) + .catch(async (error: GaxiosError) => { + if (error.response?.status !== 410) { + throw error; + } + + await this.calendarChannelRepository.update( + { + connectedAccountId: connectedAccount.id, + }, + { + syncCursor: '', + }, + ); + + return { + data: { + items: [], + nextSyncToken: undefined, + nextPageToken: undefined, + }, + }; + }); + + nextSyncToken = googleCalendarEvents.data.nextSyncToken; + nextPageToken = googleCalendarEvents.data.nextPageToken || undefined; + + const { items } = googleCalendarEvents.data; + + if (!items || items.length === 0) { + break; + } + + events.push(...items); + + if (!nextPageToken) { + hasMoreEvents = false; + } + } + + return { + calendarEvents: formatGoogleCalendarEvents(events), + nextSyncCursor: nextSyncToken || '', + }; + } +} diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/utils/format-google-calendar-event.util.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/utils/format-google-calendar-event.util.ts similarity index 88% rename from packages/twenty-server/src/modules/calendar/calendar-event-import-manager/utils/format-google-calendar-event.util.ts rename to packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/utils/format-google-calendar-event.util.ts index 10f8e2fc1..08ef35243 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/utils/format-google-calendar-event.util.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/utils/format-google-calendar-event.util.ts @@ -1,16 +1,17 @@ import { calendar_v3 as calendarV3 } from 'googleapis'; -import { v4 } from 'uuid'; import { CalendarEventWithParticipants } from 'src/modules/calendar/common/types/calendar-event'; import { CalendarEventParticipantResponseStatus } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity'; -export const formatGoogleCalendarEvent = ( - event: calendarV3.Schema$Event, - iCalUIDCalendarEventIdMap: Map, -): CalendarEventWithParticipants => { - const id = - (event.iCalUID && iCalUIDCalendarEventIdMap.get(event.iCalUID)) ?? v4(); +export const formatGoogleCalendarEvents = ( + events: calendarV3.Schema$Event[], +): CalendarEventWithParticipants[] => { + return events.map(formatGoogleCalendarEvent); +}; +const formatGoogleCalendarEvent = ( + event: calendarV3.Schema$Event, +): CalendarEventWithParticipants => { const formatResponseStatus = (status: string | null | undefined) => { switch (status) { case 'accepted': @@ -25,7 +26,6 @@ export const formatGoogleCalendarEvent = ( }; return { - id, title: event.summary ?? '', isCanceled: event.status === 'cancelled', isFullDay: event.start?.dateTime == null, @@ -44,12 +44,12 @@ export const formatGoogleCalendarEvent = ( recurringEventExternalId: event.recurringEventId ?? '', participants: event.attendees?.map((attendee) => ({ - calendarEventId: id, iCalUID: event.iCalUID ?? '', handle: attendee.email ?? '', displayName: attendee.displayName ?? '', isOrganizer: attendee.organizer === true, responseStatus: formatResponseStatus(attendee.responseStatus), })) ?? [], + status: event.status ?? '', }; }; diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job.ts new file mode 100644 index 000000000..34ba97fea --- /dev/null +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job.ts @@ -0,0 +1,92 @@ +import { Scope } from '@nestjs/common'; + +import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator'; +import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; +import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator'; +import { CalendarEventsImportService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-events-import.service'; +import { isThrottled } from 'src/modules/connected-account/utils/is-throttled'; +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 { + CalendarChannelSyncStage, + CalendarChannelWorkspaceEntity, +} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; +import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; +import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator'; + +export type CalendarEventsImportJobData = { + calendarChannelId: string; + workspaceId: string; +}; + +@Processor({ + queueName: MessageQueue.calendarQueue, + scope: Scope.REQUEST, +}) +export class CalendarEventListFetchJob { + constructor( + private readonly calendarEventsImportService: CalendarEventsImportService, + @InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity) + private readonly connectedAccountRepository: ConnectedAccountRepository, + @InjectWorkspaceRepository(CalendarChannelWorkspaceEntity) + private readonly calendarChannelRepository: WorkspaceRepository, + ) {} + + @Process(CalendarEventListFetchJob.name) + async handle(data: CalendarEventsImportJobData): Promise { + const { workspaceId, calendarChannelId } = data; + + const calendarChannel = await this.calendarChannelRepository.findOne({ + where: { + id: calendarChannelId, + isSyncEnabled: true, + }, + }); + + if (!calendarChannel) { + return; + } + + if ( + isThrottled( + calendarChannel.syncStageStartedAt, + calendarChannel.throttleFailureCount, + ) + ) { + return; + } + + const connectedAccount = + await this.connectedAccountRepository.getConnectedAccountOrThrow( + workspaceId, + calendarChannel.connectedAccountId, + ); + + switch (calendarChannel.syncStage) { + case CalendarChannelSyncStage.FULL_CALENDAR_EVENT_LIST_FETCH_PENDING: + await this.calendarChannelRepository.update(calendarChannelId, { + syncCursor: '', + syncStageStartedAt: null, + }); + + await this.calendarEventsImportService.processCalendarEventsImport( + calendarChannel, + connectedAccount, + workspaceId, + ); + break; + + case CalendarChannelSyncStage.PARTIAL_CALENDAR_EVENT_LIST_FETCH_PENDING: + await this.calendarEventsImportService.processCalendarEventsImport( + calendarChannel, + connectedAccount, + workspaceId, + ); + break; + + default: + break; + } + } +} diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-events-import.job.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-events-import.job.ts deleted file mode 100644 index 8eb1255e4..000000000 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-events-import.job.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Logger, Scope } from '@nestjs/common'; - -import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator'; -import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; -import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator'; -import { CalendarEventsImportService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-events-import.service'; - -export type CalendarEventsImportJobData = { - workspaceId: string; - connectedAccountId: string; -}; - -@Processor({ - queueName: MessageQueue.calendarQueue, - scope: Scope.REQUEST, -}) -export class CalendarEventsImportJob { - private readonly logger = new Logger(CalendarEventsImportJob.name); - - constructor( - private readonly googleCalendarSyncService: CalendarEventsImportService, - ) {} - - @Process(CalendarEventsImportJob.name) - async handle(data: CalendarEventsImportJobData): Promise { - this.logger.log( - `google calendar sync for workspace ${data.workspaceId} and account ${data.connectedAccountId}`, - ); - - await this.googleCalendarSyncService.processCalendarEventsImport( - data.workspaceId, - data.connectedAccountId, - ); - } -} diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-channel-sync-status.service.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-channel-sync-status.service.ts new file mode 100644 index 000000000..d1e5d0b20 --- /dev/null +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-channel-sync-status.service.ts @@ -0,0 +1,111 @@ +import { Injectable } from '@nestjs/common'; + +import { CacheStorageService } from 'src/engine/integrations/cache-storage/cache-storage.service'; +import { InjectCacheStorage } from 'src/engine/integrations/cache-storage/decorators/cache-storage.decorator'; +import { CacheStorageNamespace } from 'src/engine/integrations/cache-storage/types/cache-storage-namespace.enum'; +import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator'; +import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; +import { + CalendarChannelWorkspaceEntity, + CalendarChannelSyncStage, + CalendarChannelSyncStatus, +} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; + +@Injectable() +export class CalendarChannelSyncStatusService { + constructor( + @InjectWorkspaceRepository(CalendarChannelWorkspaceEntity) + private readonly calendarChannelRepository: WorkspaceRepository, + @InjectCacheStorage(CacheStorageNamespace.Calendar) + private readonly cacheStorage: CacheStorageService, + ) {} + + public async scheduleFullCalendarEventListFetch(calendarChannelId: string) { + await this.calendarChannelRepository.update(calendarChannelId, { + syncStage: + CalendarChannelSyncStage.FULL_CALENDAR_EVENT_LIST_FETCH_PENDING, + }); + } + + public async schedulePartialCalendarEventListFetch( + calendarChannelId: string, + ) { + await this.calendarChannelRepository.update(calendarChannelId, { + syncStage: + CalendarChannelSyncStage.PARTIAL_CALENDAR_EVENT_LIST_FETCH_PENDING, + }); + } + + public async markAsCalendarEventListFetchOngoing(calendarChannelId: string) { + await this.calendarChannelRepository.update(calendarChannelId, { + syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_ONGOING, + syncStatus: CalendarChannelSyncStatus.ONGOING, + syncStageStartedAt: new Date().toISOString(), + }); + } + + public async resetAndScheduleFullCalendarEventListFetch( + calendarChannelId: string, + workspaceId: string, + ) { + await this.cacheStorage.del( + `calendar-events-to-import:${workspaceId}:google-calendar:${calendarChannelId}`, + ); + + await this.calendarChannelRepository.update(calendarChannelId, { + syncCursor: '', + syncStageStartedAt: null, + throttleFailureCount: 0, + }); + + await this.scheduleFullCalendarEventListFetch(calendarChannelId); + } + + public async scheduleCalendarEventsImport(calendarChannelId: string) { + await this.calendarChannelRepository.update(calendarChannelId, { + syncStage: CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_PENDING, + }); + } + + public async markAsCalendarEventsImportOngoing(calendarChannelId: string) { + await this.calendarChannelRepository.update(calendarChannelId, { + syncStage: CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_ONGOING, + syncStatus: CalendarChannelSyncStatus.ONGOING, + }); + } + + public async markAsCalendarEventsImportCompleted(calendarChannelId: string) { + await this.calendarChannelRepository.update(calendarChannelId, { + syncStage: CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_PENDING, + syncStatus: CalendarChannelSyncStatus.ACTIVE, + }); + } + + public async markAsFailedUnknownAndFlushCalendarEventsToImport( + calendarChannelId: string, + workspaceId: string, + ) { + await this.cacheStorage.del( + `calendar-events-to-import:${workspaceId}:google-calendar:${calendarChannelId}`, + ); + + await this.calendarChannelRepository.update(calendarChannelId, { + syncStatus: CalendarChannelSyncStatus.FAILED_UNKNOWN, + syncStage: CalendarChannelSyncStage.FAILED, + }); + } + + public async markAsFailedInsufficientPermissionsAndFlushCalendarEventsToImport( + calendarChannelId: string, + workspaceId: string, + ) { + await this.cacheStorage.del( + `calendar-events-to-import:${workspaceId}:google-calendar:${calendarChannelId}`, + ); + + await this.calendarChannelRepository.update(calendarChannelId, { + syncStatus: CalendarChannelSyncStatus.FAILED_INSUFFICIENT_PERMISSIONS, + syncStage: CalendarChannelSyncStage.FAILED, + }); + } +} diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-events-import.service.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-events-import.service.ts index 3dc89de25..78dabc946 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-events-import.service.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-events-import.service.ts @@ -1,600 +1,111 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { EventEmitter2 } from '@nestjs/event-emitter'; +import { Injectable } from '@nestjs/common'; -import { Any, Repository } from 'typeorm'; -import { calendar_v3 as calendarV3 } from 'googleapis'; -import { GaxiosError } from 'gaxios'; +import { Any } from 'typeorm'; -import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository'; -import { BlocklistRepository } from 'src/modules/connected-account/repositories/blocklist.repository'; -import { - FeatureFlagEntity, - FeatureFlagKeys, -} from 'src/engine/core-modules/feature-flag/feature-flag.entity'; -import { formatGoogleCalendarEvent } from 'src/modules/calendar/calendar-event-import-manager/utils/format-google-calendar-event.util'; -import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; -import { BlocklistWorkspaceEntity } from 'src/modules/connected-account/standard-objects/blocklist.workspace-entity'; -import { - CalendarEventParticipant, - CalendarEventWithParticipants, -} from 'src/modules/calendar/common/types/calendar-event'; -import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator'; -import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; -import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service'; -import { - CreateCompanyAndContactJob, - CreateCompanyAndContactJobData, -} from 'src/modules/connected-account/auto-companies-and-contacts-creation/jobs/create-company-and-contact.job'; import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator'; import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; -import { isDefined } from 'src/utils/is-defined'; -import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; -import { InjectWorkspaceDatasource } from 'src/engine/twenty-orm/decorators/inject-workspace-datasource.decorator'; import { CalendarEventCleanerService } from 'src/modules/calendar/calendar-event-cleaner/services/calendar-event-cleaner.service'; -import { CalendarEventParticipantService } from 'src/modules/calendar/calendar-event-participant-manager/services/calendar-event-participant.service'; -import { GoogleCalendarClientProvider } from 'src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/providers/google-calendar.provider'; +import { CalendarChannelSyncStatusService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-channel-sync-status.service'; +import { CalendarGetCalendarEventsService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-get-events.service'; +import { CalendarSaveEventsService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-save-events.service'; +import { filterEventsAndReturnCancelledEvents } from 'src/modules/calendar/calendar-event-import-manager/utils/filter-events.util'; import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity'; import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; -import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity'; -import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity'; -import { filterOutBlocklistedEvents } from 'src/modules/calendar/calendar-event-import-manager/utils/filter-out-blocklisted-events.util'; +import { BlocklistRepository } from 'src/modules/connected-account/repositories/blocklist.repository'; +import { BlocklistWorkspaceEntity } from 'src/modules/connected-account/standard-objects/blocklist.workspace-entity'; +import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; @Injectable() export class CalendarEventsImportService { - private readonly logger = new Logger(CalendarEventsImportService.name); - constructor( - private readonly googleCalendarClientProvider: GoogleCalendarClientProvider, - @InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity) - private readonly connectedAccountRepository: ConnectedAccountRepository, - @InjectWorkspaceRepository(CalendarEventWorkspaceEntity) - private readonly calendarEventRepository: WorkspaceRepository, @InjectWorkspaceRepository(CalendarChannelWorkspaceEntity) private readonly calendarChannelRepository: WorkspaceRepository, @InjectWorkspaceRepository(CalendarChannelEventAssociationWorkspaceEntity) private readonly calendarChannelEventAssociationRepository: WorkspaceRepository, - @InjectWorkspaceRepository(CalendarEventParticipantWorkspaceEntity) - private readonly calendarEventParticipantsRepository: WorkspaceRepository, @InjectObjectMetadataRepository(BlocklistWorkspaceEntity) private readonly blocklistRepository: BlocklistRepository, - @InjectRepository(FeatureFlagEntity, 'core') - private readonly featureFlagRepository: Repository, - @InjectWorkspaceDatasource() - private readonly workspaceDataSource: WorkspaceDataSource, private readonly calendarEventCleanerService: CalendarEventCleanerService, - private readonly calendarEventParticipantsService: CalendarEventParticipantService, - @InjectMessageQueue(MessageQueue.contactCreationQueue) - private readonly messageQueueService: MessageQueueService, - private readonly eventEmitter: EventEmitter2, + private readonly calendarChannelSyncStatusService: CalendarChannelSyncStatusService, + private readonly getCalendarEventsService: CalendarGetCalendarEventsService, + private readonly calendarSaveEventsService: CalendarSaveEventsService, ) {} public async processCalendarEventsImport( + calendarChannel: CalendarChannelWorkspaceEntity, + connectedAccount: ConnectedAccountWorkspaceEntity, workspaceId: string, - connectedAccountId: string, - emailOrDomainToReimport?: string, ): Promise { - const connectedAccount = await this.connectedAccountRepository.getById( - connectedAccountId, - workspaceId, + await this.calendarChannelSyncStatusService.markAsCalendarEventListFetchOngoing( + calendarChannel.id, ); - if (!connectedAccount) { - return; - } + const { calendarEvents, nextSyncCursor } = + await this.getCalendarEventsService.getCalendarEvents( + connectedAccount, + calendarChannel.syncCursor, + ); - const refreshToken = connectedAccount.refreshToken; - const workspaceMemberId = connectedAccount.accountOwnerId; + if (!calendarEvents || calendarEvents?.length === 0) { + await this.calendarChannelRepository.update( + { + id: calendarChannel.id, + }, + { + syncCursor: nextSyncCursor, + }, + ); - if (!refreshToken) { - throw new Error( - `No refresh token found for connected account ${connectedAccountId} in workspace ${workspaceId} during sync`, + await this.calendarChannelSyncStatusService.schedulePartialCalendarEventListFetch( + calendarChannel.id, ); } - const calendarChannel = await this.calendarChannelRepository.findOneBy({ - connectedAccount: { - id: connectedAccountId, - }, - }); + const blocklist = await this.blocklistRepository.getByWorkspaceMemberId( + connectedAccount.accountOwnerId, + workspaceId, + ); - const syncToken = calendarChannel?.syncCursor || undefined; + const { filteredEvents, cancelledEvents } = + filterEventsAndReturnCancelledEvents( + calendarChannel, + calendarEvents, + blocklist.map((blocklist) => blocklist.handle), + ); - if (!calendarChannel) { - return; - } + const cancelledEventExternalIds = cancelledEvents.map( + (event) => event.externalId, + ); - const calendarChannelId = calendarChannel.id; - - const { events, nextSyncToken } = await this.getEventsFromGoogleCalendar( + await this.calendarSaveEventsService.saveCalendarEventsAndEnqueueContactCreationJob( + filteredEvents, + calendarChannel, connectedAccount, workspaceId, - emailOrDomainToReimport, - syncToken, ); - if (!events || events?.length === 0) { - this.logger.log( - `google calendar sync for workspace ${workspaceId} and account ${connectedAccountId} done with nothing to import.`, - ); - - return; - } - - if (!workspaceMemberId) { - throw new Error( - `Workspace member ID is undefined for connected account ${connectedAccountId} in workspace ${workspaceId}`, - ); - } - - const blocklist = await this.getBlocklist(workspaceMemberId, workspaceId); - - let filteredEvents = filterOutBlocklistedEvents( - calendarChannel.handle, - events, - blocklist, - ).filter((event) => event.status !== 'cancelled'); - - if (emailOrDomainToReimport) { - filteredEvents = filteredEvents.filter( - (event) => - event.attendees?.some( - (attendee) => attendee.email?.endsWith(emailOrDomainToReimport), - ), - ); - } - - const cancelledEventExternalIds = filteredEvents - .filter((event) => event.status === 'cancelled') - .map((event) => event.id as string); - - const existingCalendarEvents = await this.calendarEventRepository.find({ - where: { - iCalUID: Any(filteredEvents.map((event) => event.iCalUID as string)), + await this.calendarChannelEventAssociationRepository.delete({ + eventExternalId: Any(cancelledEventExternalIds), + calendarChannel: { + id: calendarChannel.id, }, }); - const iCalUIDCalendarEventIdMap = new Map( - existingCalendarEvents.map((calendarEvent) => [ - calendarEvent.iCalUID, - calendarEvent.id, - ]), + await this.calendarEventCleanerService.cleanWorkspaceCalendarEvents( + workspaceId, ); - const formattedEvents = filteredEvents.map((event) => - formatGoogleCalendarEvent(event, iCalUIDCalendarEventIdMap), - ); - - // TODO: When we will be able to add unicity contraint on iCalUID, we will do a INSERT ON CONFLICT DO UPDATE - - let startTime = Date.now(); - - const existingEventsICalUIDs = existingCalendarEvents.map( - (calendarEvent) => calendarEvent.iCalUID, - ); - - let endTime = Date.now(); - - const eventsToSave = formattedEvents.filter( - (calendarEvent) => - !existingEventsICalUIDs.includes(calendarEvent.iCalUID), - ); - - const eventsToUpdate = formattedEvents.filter((calendarEvent) => - existingEventsICalUIDs.includes(calendarEvent.iCalUID), - ); - - startTime = Date.now(); - - const existingCalendarChannelEventAssociations = - await this.calendarChannelEventAssociationRepository.find({ - where: { - eventExternalId: Any( - formattedEvents.map((calendarEvent) => calendarEvent.id), - ), - calendarChannel: { - id: calendarChannelId, - }, - }, - }); - - endTime = Date.now(); - - this.logger.log( - `google calendar sync for workspace ${workspaceId} and account ${connectedAccountId}: getting existing calendar channel event associations in ${ - endTime - startTime - }ms.`, - ); - - const calendarChannelEventAssociationsToSave = formattedEvents - .filter( - (calendarEvent) => - !existingCalendarChannelEventAssociations.some( - (association) => association.eventExternalId === calendarEvent.id, - ), - ) - .map((calendarEvent) => ({ - calendarEventId: calendarEvent.id, - eventExternalId: calendarEvent.externalId, - calendarChannelId, - })); - - if (events.length > 0) { - await this.saveGoogleCalendarEvents( - eventsToSave, - eventsToUpdate, - calendarChannelEventAssociationsToSave, - connectedAccount, - calendarChannel, - workspaceId, - ); - - startTime = Date.now(); - - await this.calendarChannelEventAssociationRepository.delete({ - eventExternalId: Any(cancelledEventExternalIds), - calendarChannel: { - id: calendarChannelId, - }, - }); - - endTime = Date.now(); - - this.logger.log( - `google calendar sync for workspace ${workspaceId} and account ${connectedAccountId}: deleting calendar channel event associations in ${ - endTime - startTime - }ms.`, - ); - - startTime = Date.now(); - - await this.calendarEventCleanerService.cleanWorkspaceCalendarEvents( - workspaceId, - ); - - endTime = Date.now(); - - this.logger.log( - `google calendar sync for workspace ${workspaceId} and account ${connectedAccountId}: cleaning calendar events in ${ - endTime - startTime - }ms.`, - ); - } else { - this.logger.log( - `google calendar sync for workspace ${workspaceId} and account ${connectedAccountId} done with nothing to import.`, - ); - } - - if (!nextSyncToken) { - throw new Error( - `No next sync token found for connected account ${connectedAccountId} in workspace ${workspaceId} during sync`, - ); - } - - startTime = Date.now(); - await this.calendarChannelRepository.update( { id: calendarChannel.id, }, { - syncCursor: nextSyncToken, + syncCursor: nextSyncCursor, }, ); - endTime = Date.now(); - - this.logger.log( - `google calendar sync for workspace ${workspaceId} and account ${connectedAccountId}: updating sync cursor in ${ - endTime - startTime - }ms.`, + await this.calendarChannelSyncStatusService.schedulePartialCalendarEventListFetch( + calendarChannel.id, ); - - this.logger.log( - `google calendar sync for workspace ${workspaceId} and account ${connectedAccountId} ${ - syncToken ? `and ${syncToken} syncToken ` : '' - }done.`, - ); - } - - public async getBlocklist(workspaceMemberId: string, workspaceId: string) { - const isBlocklistEnabledFeatureFlag = - await this.featureFlagRepository.findOneBy({ - workspaceId, - key: FeatureFlagKeys.IsBlocklistEnabled, - value: true, - }); - - const isBlocklistEnabled = - isBlocklistEnabledFeatureFlag && isBlocklistEnabledFeatureFlag.value; - - const blocklist = isBlocklistEnabled - ? await this.blocklistRepository.getByWorkspaceMemberId( - workspaceMemberId, - workspaceId, - ) - : []; - - return blocklist.map((blocklist) => blocklist.handle); - } - - public async getEventsFromGoogleCalendar( - connectedAccount: ConnectedAccountWorkspaceEntity, - workspaceId: string, - emailOrDomainToReimport?: string, - syncToken?: string, - ): Promise<{ - events: calendarV3.Schema$Event[]; - nextSyncToken: string | null | undefined; - }> { - const googleCalendarClient = - await this.googleCalendarClientProvider.getGoogleCalendarClient( - connectedAccount, - ); - - const startTime = Date.now(); - - let nextSyncToken: string | null | undefined; - let nextPageToken: string | undefined; - const events: calendarV3.Schema$Event[] = []; - - let hasMoreEvents = true; - - while (hasMoreEvents) { - const googleCalendarEvents = await googleCalendarClient.events - .list({ - calendarId: 'primary', - maxResults: 500, - syncToken: emailOrDomainToReimport ? undefined : syncToken, - pageToken: nextPageToken, - q: emailOrDomainToReimport, - showDeleted: true, - }) - .catch(async (error: GaxiosError) => { - if (error.response?.status !== 410) { - throw error; - } - - await this.calendarChannelRepository.update( - { - id: connectedAccount.id, - }, - { - syncCursor: '', - }, - ); - - this.logger.log( - `Sync token is no longer valid for connected account ${connectedAccount.id} in workspace ${workspaceId}, resetting sync cursor.`, - ); - - return { - data: { - items: [], - nextSyncToken: undefined, - nextPageToken: undefined, - }, - }; - }); - - nextSyncToken = googleCalendarEvents.data.nextSyncToken; - nextPageToken = googleCalendarEvents.data.nextPageToken || undefined; - - const { items } = googleCalendarEvents.data; - - if (!items || items.length === 0) { - break; - } - - events.push(...items); - - if (!nextPageToken) { - hasMoreEvents = false; - } - } - - const endTime = Date.now(); - - this.logger.log( - `google calendar sync for workspace ${workspaceId} and account ${ - connectedAccount.id - } getting events list in ${endTime - startTime}ms.`, - ); - - return { events, nextSyncToken }; - } - - public async saveGoogleCalendarEvents( - eventsToSave: CalendarEventWithParticipants[], - eventsToUpdate: CalendarEventWithParticipants[], - calendarChannelEventAssociationsToSave: { - calendarEventId: string; - eventExternalId: string; - calendarChannelId: string; - }[], - connectedAccount: ConnectedAccountWorkspaceEntity, - calendarChannel: CalendarChannelWorkspaceEntity, - workspaceId: string, - ): Promise { - const participantsToSave = eventsToSave.flatMap( - (event) => event.participants, - ); - - const participantsToUpdate = eventsToUpdate.flatMap( - (event) => event.participants, - ); - - let startTime: number; - let endTime: number; - - const savedCalendarEventParticipantsToEmit: CalendarEventParticipantWorkspaceEntity[] = - []; - - try { - await this.workspaceDataSource?.transaction( - async (transactionManager) => { - startTime = Date.now(); - - await this.calendarEventRepository.save( - eventsToSave, - {}, - transactionManager, - ); - - endTime = Date.now(); - - this.logger.log( - `google calendar sync for workspace ${workspaceId} and account ${ - connectedAccount.id - }: saving ${eventsToSave.length} events in ${ - endTime - startTime - }ms.`, - ); - - startTime = Date.now(); - - await this.calendarChannelRepository.save( - eventsToUpdate, - {}, - transactionManager, - ); - - endTime = Date.now(); - - this.logger.log( - `google calendar sync for workspace ${workspaceId} and account ${ - connectedAccount.id - }: updating ${eventsToUpdate.length} events in ${ - endTime - startTime - }ms.`, - ); - - startTime = Date.now(); - - await this.calendarChannelEventAssociationRepository.save( - calendarChannelEventAssociationsToSave, - {}, - transactionManager, - ); - - endTime = Date.now(); - - this.logger.log( - `google calendar sync for workspace ${workspaceId} and account ${ - connectedAccount.id - }: saving calendar channel event associations in ${ - endTime - startTime - }ms.`, - ); - - startTime = Date.now(); - - const existingCalendarEventParticipants = - await this.calendarEventParticipantsRepository.find({ - where: { - calendarEventId: Any( - participantsToUpdate - .map((participant) => participant.calendarEventId) - .filter(isDefined), - ), - }, - }); - - const { - calendarEventParticipantsToDelete, - newCalendarEventParticipants, - } = participantsToUpdate.reduce( - (acc, calendarEventParticipant) => { - const existingCalendarEventParticipant = - existingCalendarEventParticipants.find( - (existingCalendarEventParticipant) => - existingCalendarEventParticipant.handle === - calendarEventParticipant.handle, - ); - - if (existingCalendarEventParticipant) { - acc.calendarEventParticipantsToDelete.push( - existingCalendarEventParticipant, - ); - } else { - acc.newCalendarEventParticipants.push(calendarEventParticipant); - } - - return acc; - }, - { - calendarEventParticipantsToDelete: - [] as CalendarEventParticipantWorkspaceEntity[], - newCalendarEventParticipants: [] as CalendarEventParticipant[], - }, - ); - - await this.calendarEventParticipantsRepository.delete({ - id: Any( - calendarEventParticipantsToDelete.map( - (calendarEventParticipant) => calendarEventParticipant.id, - ), - ), - }); - - await this.calendarEventParticipantsRepository.save( - participantsToUpdate, - ); - - endTime = Date.now(); - - participantsToSave.push(...newCalendarEventParticipants); - - this.logger.log( - `google calendar sync for workspace ${workspaceId} and account ${ - connectedAccount.id - }: updating participants in ${endTime - startTime}ms.`, - ); - - startTime = Date.now(); - - const savedCalendarEventParticipants = - await this.calendarEventParticipantsService.saveCalendarEventParticipants( - participantsToSave, - workspaceId, - transactionManager, - ); - - savedCalendarEventParticipantsToEmit.push( - ...savedCalendarEventParticipants, - ); - - endTime = Date.now(); - - this.logger.log( - `google calendar sync for workspace ${workspaceId} and account ${ - connectedAccount.id - }: saving participants in ${endTime - startTime}ms.`, - ); - }, - ); - - this.eventEmitter.emit(`calendarEventParticipant.matched`, { - workspaceId, - workspaceMemberId: connectedAccount.accountOwnerId, - calendarEventParticipants: savedCalendarEventParticipantsToEmit, - }); - - if (calendarChannel.isContactAutoCreationEnabled) { - await this.messageQueueService.add( - CreateCompanyAndContactJob.name, - { - workspaceId, - connectedAccount, - contactsToCreate: participantsToSave, - }, - ); - } - } catch (error) { - this.logger.error( - `Error during google calendar sync for workspace ${workspaceId} and account ${connectedAccount.id}: ${error.message}`, - ); - } } } diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-get-events.service.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-get-events.service.ts new file mode 100644 index 000000000..d9fa12110 --- /dev/null +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-get-events.service.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@nestjs/common'; + +import { GoogleCalendarGetEventsService as GoogleCalendarGetCalendarEventsService } from 'src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/services/google-calendar-get-events.service'; +import { CalendarEventWithParticipants } from 'src/modules/calendar/common/types/calendar-event'; +import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; + +export type GetCalendarEventsResponse = { + calendarEvents: CalendarEventWithParticipants[]; + nextSyncCursor: string; +}; + +@Injectable() +export class CalendarGetCalendarEventsService { + constructor( + private readonly googleCalendarGetCalendarEventsService: GoogleCalendarGetCalendarEventsService, + ) {} + + public async getCalendarEvents( + connectedAccount: Pick< + ConnectedAccountWorkspaceEntity, + 'provider' | 'refreshToken' | 'id' + >, + syncCursor?: string, + ): Promise { + switch (connectedAccount.provider) { + case 'google': + return this.googleCalendarGetCalendarEventsService.getCalendarEvents( + connectedAccount, + syncCursor, + ); + default: + throw new Error( + `Provider ${connectedAccount.provider} is not supported.`, + ); + } + } +} diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-save-events.service.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-save-events.service.ts new file mode 100644 index 000000000..dea37cd9b --- /dev/null +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-save-events.service.ts @@ -0,0 +1,160 @@ +import { Injectable } from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; + +import { Any } from 'typeorm'; + +import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator'; +import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; +import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service'; +import { + CreateCompanyAndContactJob, + CreateCompanyAndContactJobData, +} from 'src/modules/connected-account/auto-companies-and-contacts-creation/jobs/create-company-and-contact.job'; +import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator'; +import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; +import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; +import { InjectWorkspaceDatasource } from 'src/engine/twenty-orm/decorators/inject-workspace-datasource.decorator'; +import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity'; +import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; +import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity'; +import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity'; +import { injectIdsInCalendarEvents } from 'src/modules/calendar/calendar-event-import-manager/utils/inject-ids-in-calendar-events.util'; +import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; +import { CalendarEventWithParticipants } from 'src/modules/calendar/common/types/calendar-event'; +import { CalendarEventParticipantService } from 'src/modules/calendar/calendar-event-participant-manager/services/calendar-event-participant.service'; + +@Injectable() +export class CalendarSaveEventsService { + constructor( + @InjectWorkspaceRepository(CalendarEventWorkspaceEntity) + private readonly calendarEventRepository: WorkspaceRepository, + @InjectWorkspaceRepository(CalendarChannelEventAssociationWorkspaceEntity) + private readonly calendarChannelEventAssociationRepository: WorkspaceRepository, + @InjectWorkspaceDatasource() + private readonly workspaceDataSource: WorkspaceDataSource, + private readonly calendarEventParticipantService: CalendarEventParticipantService, + @InjectMessageQueue(MessageQueue.contactCreationQueue) + private readonly messageQueueService: MessageQueueService, + private readonly eventEmitter: EventEmitter2, + ) {} + + public async saveCalendarEventsAndEnqueueContactCreationJob( + filteredEvents: CalendarEventWithParticipants[], + calendarChannel: CalendarChannelWorkspaceEntity, + connectedAccount: ConnectedAccountWorkspaceEntity, + workspaceId: string, + ): Promise { + const existingCalendarEvents = await this.calendarEventRepository.find({ + where: { + iCalUID: Any(filteredEvents.map((event) => event.iCalUID as string)), + }, + }); + + const iCalUIDCalendarEventIdMap = new Map( + existingCalendarEvents.map((calendarEvent) => [ + calendarEvent.iCalUID, + calendarEvent.id, + ]), + ); + + const calendarEventsWithIds = injectIdsInCalendarEvents( + filteredEvents, + iCalUIDCalendarEventIdMap, + ); + + // TODO: When we will be able to add unicity contraint on iCalUID, we will do a INSERT ON CONFLICT DO UPDATE + + const existingEventsICalUIDs = existingCalendarEvents.map( + (calendarEvent) => calendarEvent.iCalUID, + ); + + const eventsToSave = calendarEventsWithIds.filter( + (calendarEvent) => + !existingEventsICalUIDs.includes(calendarEvent.iCalUID), + ); + + const eventsToUpdate = calendarEventsWithIds.filter((calendarEvent) => + existingEventsICalUIDs.includes(calendarEvent.iCalUID), + ); + + const existingCalendarChannelEventAssociations = + await this.calendarChannelEventAssociationRepository.find({ + where: { + eventExternalId: Any( + calendarEventsWithIds.map((calendarEvent) => calendarEvent.id), + ), + calendarChannel: { + id: calendarChannel.id, + }, + }, + }); + + const calendarChannelEventAssociationsToSave = calendarEventsWithIds + .filter( + (calendarEvent) => + !existingCalendarChannelEventAssociations.some( + (association) => association.eventExternalId === calendarEvent.id, + ), + ) + .map((calendarEvent) => ({ + calendarEventId: calendarEvent.id, + eventExternalId: calendarEvent.externalId, + calendarChannelId: calendarChannel.id, + })); + + const participantsToSave = eventsToSave.flatMap( + (event) => event.participants, + ); + + const participantsToUpdate = eventsToUpdate.flatMap( + (event) => event.participants, + ); + + const savedCalendarEventParticipantsToEmit: CalendarEventParticipantWorkspaceEntity[] = + []; + + await this.workspaceDataSource?.transaction(async (transactionManager) => { + await this.calendarEventRepository.save( + eventsToSave, + {}, + transactionManager, + ); + + await this.calendarEventRepository.save( + eventsToUpdate, + {}, + transactionManager, + ); + + await this.calendarChannelEventAssociationRepository.save( + calendarChannelEventAssociationsToSave, + {}, + transactionManager, + ); + + await this.calendarEventParticipantService.upsertAndDeleteCalendarEventParticipants( + participantsToSave, + participantsToUpdate, + workspaceId, + transactionManager, + ); + }); + + this.eventEmitter.emit(`calendarEventParticipant.matched`, { + workspaceId, + workspaceMemberId: connectedAccount.accountOwnerId, + calendarEventParticipants: savedCalendarEventParticipantsToEmit, + }); + + if (calendarChannel.isContactAutoCreationEnabled) { + await this.messageQueueService.add( + CreateCompanyAndContactJob.name, + { + workspaceId, + connectedAccount, + contactsToCreate: participantsToSave, + }, + ); + } + } +} diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/utils/filter-events.util.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/utils/filter-events.util.ts new file mode 100644 index 000000000..b3e7e0d1d --- /dev/null +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/utils/filter-events.util.ts @@ -0,0 +1,40 @@ +import { filterOutBlocklistedEvents } from 'src/modules/calendar/calendar-event-import-manager/utils/filter-out-blocklisted-events.util'; +import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; +import { CalendarEventWithParticipants } from 'src/modules/calendar/common/types/calendar-event'; + +export const filterEventsAndReturnCancelledEvents = ( + calendarChannel: Pick, + events: CalendarEventWithParticipants[], + blocklist: string[], +): { + filteredEvents: CalendarEventWithParticipants[]; + cancelledEvents: CalendarEventWithParticipants[]; +} => { + const filteredEvents = filterOutBlocklistedEvents( + calendarChannel.handle, + events, + blocklist, + ); + + return filteredEvents.reduce( + ( + acc: { + filteredEvents: CalendarEventWithParticipants[]; + cancelledEvents: CalendarEventWithParticipants[]; + }, + event, + ) => { + if (event.status === 'cancelled') { + acc.cancelledEvents.push(event); + } else { + acc.filteredEvents.push(event); + } + + return acc; + }, + { + filteredEvents: [], + cancelledEvents: [], + }, + ); +}; diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/utils/filter-out-blocklisted-events.util.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/utils/filter-out-blocklisted-events.util.ts index 067e49ea5..b28f5801c 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/utils/filter-out-blocklisted-events.util.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/utils/filter-out-blocklisted-events.util.ts @@ -1,20 +1,19 @@ -import { calendar_v3 as calendarV3 } from 'googleapis'; - -import { isEmailBlocklisted } from 'src/modules/calendar-messaging-participant/utils/is-email-blocklisted.util'; +import { isEmailBlocklisted } from 'src/modules/calendar-messaging-participant-manager/utils/is-email-blocklisted.util'; +import { CalendarEventWithParticipants } from 'src/modules/calendar/common/types/calendar-event'; export const filterOutBlocklistedEvents = ( calendarChannelHandle: string, - events: calendarV3.Schema$Event[], + events: CalendarEventWithParticipants[], blocklist: string[], ) => { return events.filter((event) => { - if (!event.attendees) { + if (!event.participants) { return true; } - return event.attendees.every( + return event.participants.every( (attendee) => - !isEmailBlocklisted(calendarChannelHandle, attendee.email, blocklist), + !isEmailBlocklisted(calendarChannelHandle, attendee.handle, blocklist), ); }); }; diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/utils/inject-ids-in-calendar-events.util.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/utils/inject-ids-in-calendar-events.util.ts new file mode 100644 index 000000000..b41e329dc --- /dev/null +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/utils/inject-ids-in-calendar-events.util.ts @@ -0,0 +1,31 @@ +import { v4 } from 'uuid'; + +import { + CalendarEventWithParticipants, + CalendarEventWithParticipantsAndCalendarEventId, +} from 'src/modules/calendar/common/types/calendar-event'; + +export const injectIdsInCalendarEvents = ( + calendarEvents: CalendarEventWithParticipants[], + iCalUIDCalendarEventIdMap: Map, +): CalendarEventWithParticipantsAndCalendarEventId[] => { + return calendarEvents.map((calendarEvent) => { + const id = iCalUIDCalendarEventIdMap.get(calendarEvent.iCalUID) ?? v4(); + + return injectIdInCalendarEvent(calendarEvent, id); + }); +}; + +const injectIdInCalendarEvent = ( + calendarEvent: CalendarEventWithParticipants, + id: string, +): CalendarEventWithParticipantsAndCalendarEventId => { + return { + ...calendarEvent, + id, + participants: calendarEvent.participants.map((participant) => ({ + ...participant, + calendarEventId: id, + })), + }; +}; diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/calendar-event-participant-manager.module.ts b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/calendar-event-participant-manager.module.ts new file mode 100644 index 000000000..8375316b0 --- /dev/null +++ b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/calendar-event-participant-manager.module.ts @@ -0,0 +1,46 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.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 { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; +import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; +import { AddPersonIdAndWorkspaceMemberIdService } from 'src/modules/calendar-messaging-participant-manager/services/add-person-id-and-workspace-member-id/add-person-id-and-workspace-member-id.service'; +import { CalendarCreateCompanyAndContactAfterSyncJob } from 'src/modules/calendar/calendar-event-participant-manager/jobs/calendar-create-company-and-contact-after-sync.job'; +import { CalendarEventParticipantMatchParticipantJob } from 'src/modules/calendar/calendar-event-participant-manager/jobs/calendar-event-participant-match-participant.job'; +import { CalendarEventParticipantUnmatchParticipantJob } from 'src/modules/calendar/calendar-event-participant-manager/jobs/calendar-event-participant-unmatch-participant.job'; +import { CalendarEventParticipantPersonListener } from 'src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant-person.listener'; +import { CalendarEventParticipantWorkspaceMemberListener } from 'src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant-workspace-member.listener'; +import { CalendarEventParticipantListener } from 'src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant.listener'; +import { CalendarEventParticipantService } from 'src/modules/calendar/calendar-event-participant-manager/services/calendar-event-participant.service'; +import { CalendarCommonModule } from 'src/modules/calendar/common/calendar-common.module'; +import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity'; +import { AutoCompaniesAndContactsCreationModule } from 'src/modules/connected-account/auto-companies-and-contacts-creation/auto-companies-and-contacts-creation.module'; +import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity'; + +@Module({ + imports: [ + WorkspaceDataSourceModule, + TwentyORMModule.forFeature([CalendarEventParticipantWorkspaceEntity]), + ObjectMetadataRepositoryModule.forFeature([PersonWorkspaceEntity]), + TypeOrmModule.forFeature( + [ObjectMetadataEntity, FieldMetadataEntity], + 'metadata', + ), + AutoCompaniesAndContactsCreationModule, + CalendarCommonModule, + ], + providers: [ + CalendarEventParticipantService, + CalendarCreateCompanyAndContactAfterSyncJob, + CalendarEventParticipantMatchParticipantJob, + CalendarEventParticipantUnmatchParticipantJob, + CalendarEventParticipantListener, + CalendarEventParticipantPersonListener, + CalendarEventParticipantWorkspaceMemberListener, + AddPersonIdAndWorkspaceMemberIdService, + ], + exports: [CalendarEventParticipantService], +}) +export class CalendarEventParticipantManagerModule {} diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/calendar-event-participant.module.ts b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/calendar-event-participant.module.ts deleted file mode 100644 index dc3cd2a7e..000000000 --- a/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/calendar-event-participant.module.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; - -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.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 { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; -import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; -import { AddPersonIdAndWorkspaceMemberIdModule } from 'src/modules/calendar-messaging-participant/services/add-person-id-and-workspace-member-id/add-person-id-and-workspace-member-id.module'; -import { CalendarEventParticipantListener } from 'src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant.listener'; -import { CalendarEventParticipantService } from 'src/modules/calendar/calendar-event-participant-manager/services/calendar-event-participant.service'; -import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity'; -import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity'; - -@Module({ - imports: [ - WorkspaceDataSourceModule, - TwentyORMModule.forFeature([CalendarEventParticipantWorkspaceEntity]), - ObjectMetadataRepositoryModule.forFeature([PersonWorkspaceEntity]), - TypeOrmModule.forFeature( - [ObjectMetadataEntity, FieldMetadataEntity], - 'metadata', - ), - AddPersonIdAndWorkspaceMemberIdModule, - ], - providers: [ - CalendarEventParticipantService, - CalendarEventParticipantListener, - ], - exports: [CalendarEventParticipantService], -}) -export class CalendarEventParticipantModule {} diff --git a/packages/twenty-server/src/modules/messaging/message-participants-manager/jobs/calendar-create-company-and-contact-after-sync.job.ts b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/jobs/calendar-create-company-and-contact-after-sync.job.ts similarity index 100% rename from packages/twenty-server/src/modules/messaging/message-participants-manager/jobs/calendar-create-company-and-contact-after-sync.job.ts rename to packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/jobs/calendar-create-company-and-contact-after-sync.job.ts diff --git a/packages/twenty-server/src/modules/calendar-messaging-participant/jobs/match-participant.job.ts b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/jobs/calendar-event-participant-match-participant.job.ts similarity index 62% rename from packages/twenty-server/src/modules/calendar-messaging-participant/jobs/match-participant.job.ts rename to packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/jobs/calendar-event-participant-match-participant.job.ts index f18b561a5..e8dc28c0c 100644 --- a/packages/twenty-server/src/modules/calendar-messaging-participant/jobs/match-participant.job.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/jobs/calendar-event-participant-match-participant.job.ts @@ -4,9 +4,8 @@ import { Process } from 'src/engine/integrations/message-queue/decorators/proces import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; import { CalendarEventParticipantService } from 'src/modules/calendar/calendar-event-participant-manager/services/calendar-event-participant.service'; -import { MessagingMessageParticipantService } from 'src/modules/messaging/common/services/messaging-message-participant.service'; -export type MatchParticipantJobData = { +export type CalendarEventParticipantMatchParticipantJobData = { workspaceId: string; email: string; personId?: string; @@ -14,26 +13,20 @@ export type MatchParticipantJobData = { }; @Processor({ - queueName: MessageQueue.messagingQueue, + queueName: MessageQueue.calendarQueue, scope: Scope.REQUEST, }) -export class MatchParticipantJob { +export class CalendarEventParticipantMatchParticipantJob { constructor( - private readonly messageParticipantService: MessagingMessageParticipantService, private readonly calendarEventParticipantService: CalendarEventParticipantService, ) {} - @Process(MatchParticipantJob.name) - async handle(data: MatchParticipantJobData): Promise { + @Process(CalendarEventParticipantMatchParticipantJob.name) + async handle( + data: CalendarEventParticipantMatchParticipantJobData, + ): Promise { const { workspaceId, email, personId, workspaceMemberId } = data; - await this.messageParticipantService.matchMessageParticipants( - workspaceId, - email, - personId, - workspaceMemberId, - ); - await this.calendarEventParticipantService.matchCalendarEventParticipants( workspaceId, email, diff --git a/packages/twenty-server/src/modules/calendar-messaging-participant/jobs/unmatch-participant.job.ts b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/jobs/calendar-event-participant-unmatch-participant.job.ts similarity index 62% rename from packages/twenty-server/src/modules/calendar-messaging-participant/jobs/unmatch-participant.job.ts rename to packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/jobs/calendar-event-participant-unmatch-participant.job.ts index 3579bc8b4..9aa9d3e38 100644 --- a/packages/twenty-server/src/modules/calendar-messaging-participant/jobs/unmatch-participant.job.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/jobs/calendar-event-participant-unmatch-participant.job.ts @@ -2,11 +2,10 @@ import { Scope } from '@nestjs/common'; import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; -import { MessagingMessageParticipantService } from 'src/modules/messaging/common/services/messaging-message-participant.service'; import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator'; import { CalendarEventParticipantService } from 'src/modules/calendar/calendar-event-participant-manager/services/calendar-event-participant.service'; -export type UnmatchParticipantJobData = { +export type CalendarEventParticipantUnmatchParticipantJobData = { workspaceId: string; email: string; personId?: string; @@ -14,26 +13,20 @@ export type UnmatchParticipantJobData = { }; @Processor({ - queueName: MessageQueue.messagingQueue, + queueName: MessageQueue.calendarQueue, scope: Scope.REQUEST, }) -export class UnmatchParticipantJob { +export class CalendarEventParticipantUnmatchParticipantJob { constructor( - private readonly messageParticipantService: MessagingMessageParticipantService, private readonly calendarEventParticipantService: CalendarEventParticipantService, ) {} - @Process(UnmatchParticipantJob.name) - async handle(data: UnmatchParticipantJobData): Promise { + @Process(CalendarEventParticipantUnmatchParticipantJob.name) + async handle( + data: CalendarEventParticipantUnmatchParticipantJobData, + ): Promise { const { workspaceId, email, personId, workspaceMemberId } = data; - await this.messageParticipantService.unmatchMessageParticipants( - workspaceId, - email, - personId, - workspaceMemberId, - ); - await this.calendarEventParticipantService.unmatchCalendarEventParticipants( workspaceId, email, diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant-person.listener.ts b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant-person.listener.ts new file mode 100644 index 000000000..7b369af57 --- /dev/null +++ b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant-person.listener.ts @@ -0,0 +1,74 @@ +import { Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; + +import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event'; +import { ObjectRecordUpdateEvent } from 'src/engine/integrations/event-emitter/types/object-record-update.event'; +import { objectRecordChangedProperties as objectRecordUpdateEventChangedProperties } from 'src/engine/integrations/event-emitter/utils/object-record-changed-properties.util'; +import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator'; +import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; +import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service'; +import { + CalendarEventParticipantMatchParticipantJobData, + CalendarEventParticipantMatchParticipantJob, +} from 'src/modules/calendar/calendar-event-participant-manager/jobs/calendar-event-participant-match-participant.job'; +import { + CalendarEventParticipantUnmatchParticipantJobData, + CalendarEventParticipantUnmatchParticipantJob, +} from 'src/modules/calendar/calendar-event-participant-manager/jobs/calendar-event-participant-unmatch-participant.job'; +import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity'; + +@Injectable() +export class CalendarEventParticipantPersonListener { + constructor( + @InjectMessageQueue(MessageQueue.calendarQueue) + private readonly messageQueueService: MessageQueueService, + ) {} + + @OnEvent('person.created') + async handleCreatedEvent( + payload: ObjectRecordCreateEvent, + ) { + if (payload.properties.after.email === null) { + return; + } + + await this.messageQueueService.add( + CalendarEventParticipantMatchParticipantJob.name, + { + workspaceId: payload.workspaceId, + email: payload.properties.after.email, + personId: payload.recordId, + }, + ); + } + + @OnEvent('person.updated') + async handleUpdatedEvent( + payload: ObjectRecordUpdateEvent, + ) { + if ( + objectRecordUpdateEventChangedProperties( + payload.properties.before, + payload.properties.after, + ).includes('email') + ) { + await this.messageQueueService.add( + CalendarEventParticipantUnmatchParticipantJob.name, + { + workspaceId: payload.workspaceId, + email: payload.properties.before.email, + personId: payload.recordId, + }, + ); + + await this.messageQueueService.add( + CalendarEventParticipantMatchParticipantJob.name, + { + workspaceId: payload.workspaceId, + email: payload.properties.after.email, + personId: payload.recordId, + }, + ); + } + } +} diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant-workspace-member.listener.ts b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant-workspace-member.listener.ts new file mode 100644 index 000000000..1f84fce0d --- /dev/null +++ b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant-workspace-member.listener.ts @@ -0,0 +1,74 @@ +import { Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; + +import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event'; +import { ObjectRecordUpdateEvent } from 'src/engine/integrations/event-emitter/types/object-record-update.event'; +import { objectRecordChangedProperties as objectRecordUpdateEventChangedProperties } from 'src/engine/integrations/event-emitter/utils/object-record-changed-properties.util'; +import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator'; +import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; +import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service'; +import { + CalendarEventParticipantMatchParticipantJob, + CalendarEventParticipantMatchParticipantJobData, +} from 'src/modules/calendar/calendar-event-participant-manager/jobs/calendar-event-participant-match-participant.job'; +import { + CalendarEventParticipantUnmatchParticipantJobData, + CalendarEventParticipantUnmatchParticipantJob, +} from 'src/modules/calendar/calendar-event-participant-manager/jobs/calendar-event-participant-unmatch-participant.job'; +import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; + +@Injectable() +export class CalendarEventParticipantWorkspaceMemberListener { + constructor( + @InjectMessageQueue(MessageQueue.calendarQueue) + private readonly messageQueueService: MessageQueueService, + ) {} + + @OnEvent('workspaceMember.created') + async handleCreatedEvent( + payload: ObjectRecordCreateEvent, + ) { + if (payload.properties.after.userEmail === null) { + return; + } + + await this.messageQueueService.add( + CalendarEventParticipantMatchParticipantJob.name, + { + workspaceId: payload.workspaceId, + email: payload.properties.after.userEmail, + workspaceMemberId: payload.properties.after.id, + }, + ); + } + + @OnEvent('workspaceMember.updated') + async handleUpdatedEvent( + payload: ObjectRecordUpdateEvent, + ) { + if ( + objectRecordUpdateEventChangedProperties( + payload.properties.before, + payload.properties.after, + ).includes('userEmail') + ) { + await this.messageQueueService.add( + CalendarEventParticipantUnmatchParticipantJob.name, + { + workspaceId: payload.workspaceId, + email: payload.properties.before.userEmail, + personId: payload.recordId, + }, + ); + + await this.messageQueueService.add( + CalendarEventParticipantMatchParticipantJob.name, + { + workspaceId: payload.workspaceId, + email: payload.properties.after.userEmail, + workspaceMemberId: payload.recordId, + }, + ); + } + } +} diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/services/calendar-event-participant.service.ts b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/services/calendar-event-participant.service.ts index 9fe93bc6f..da365a29e 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/services/calendar-event-participant.service.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/services/calendar-event-participant.service.ts @@ -2,14 +2,18 @@ import { Injectable } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { Any, EntityManager } from 'typeorm'; +import { isDefined } from 'class-validator'; import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; import { PersonRepository } from 'src/modules/person/repositories/person.repository'; import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity'; import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; import { getFlattenedValuesAndValuesStringForBatchRawQuery } from 'src/modules/calendar/calendar-event-import-manager/utils/get-flattened-values-and-values-string-for-batch-raw-query.util'; -import { CalendarEventParticipant } from 'src/modules/calendar/common/types/calendar-event'; -import { AddPersonIdAndWorkspaceMemberIdService } from 'src/modules/calendar-messaging-participant/services/add-person-id-and-workspace-member-id/add-person-id-and-workspace-member-id.service'; +import { + CalendarEventParticipant, + CalendarEventParticipantWithCalendarEventId, +} from 'src/modules/calendar/common/types/calendar-event'; +import { AddPersonIdAndWorkspaceMemberIdService } from 'src/modules/calendar-messaging-participant-manager/services/add-person-id-and-workspace-member-id/add-person-id-and-workspace-member-id.service'; import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator'; import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity'; @@ -125,6 +129,70 @@ export class CalendarEventParticipantService { ); } + public async upsertAndDeleteCalendarEventParticipants( + participantsToSave: CalendarEventParticipantWithCalendarEventId[], + participantsToUpdate: CalendarEventParticipantWithCalendarEventId[], + workspaceId: string, + transactionManager?: any, + ): Promise { + const existingCalendarEventParticipants = + await this.calendarEventParticipantRepository.find({ + where: { + calendarEventId: Any( + participantsToUpdate + .map((participant) => participant.calendarEventId) + .filter(isDefined), + ), + }, + }); + + const { calendarEventParticipantsToDelete, newCalendarEventParticipants } = + participantsToUpdate.reduce( + (acc, calendarEventParticipant) => { + const existingCalendarEventParticipant = + existingCalendarEventParticipants.find( + (existingCalendarEventParticipant) => + existingCalendarEventParticipant.handle === + calendarEventParticipant.handle, + ); + + if (existingCalendarEventParticipant) { + acc.calendarEventParticipantsToDelete.push( + existingCalendarEventParticipant, + ); + } else { + acc.newCalendarEventParticipants.push(calendarEventParticipant); + } + + return acc; + }, + { + calendarEventParticipantsToDelete: + [] as CalendarEventParticipantWorkspaceEntity[], + newCalendarEventParticipants: + [] as CalendarEventParticipantWithCalendarEventId[], + }, + ); + + await this.calendarEventParticipantRepository.delete({ + id: Any( + calendarEventParticipantsToDelete.map( + (calendarEventParticipant) => calendarEventParticipant.id, + ), + ), + }); + + await this.calendarEventParticipantRepository.save(participantsToUpdate); + + participantsToSave.push(...newCalendarEventParticipants); + + return await this.saveCalendarEventParticipants( + participantsToSave, + workspaceId, + transactionManager, + ); + } + public async matchCalendarEventParticipants( workspaceId: string, email: string, diff --git a/packages/twenty-server/src/modules/calendar/calendar.module.ts b/packages/twenty-server/src/modules/calendar/calendar.module.ts index f7fafef3e..de5759c18 100644 --- a/packages/twenty-server/src/modules/calendar/calendar.module.ts +++ b/packages/twenty-server/src/modules/calendar/calendar.module.ts @@ -3,14 +3,16 @@ import { Module } from '@nestjs/common'; import { CalendarBlocklistManagerModule } from 'src/modules/calendar/blocklist-manager/calendar-blocklist-manager.module'; import { CalendarEventCleanerModule } from 'src/modules/calendar/calendar-event-cleaner/calendar-event-cleaner.module'; import { CalendarEventImportManagerModule } from 'src/modules/calendar/calendar-event-import-manager/calendar-event-import-manager.module'; -import { CalendarEventParticipantModule } from 'src/modules/calendar/calendar-event-participant-manager/calendar-event-participant.module'; +import { CalendarEventParticipantManagerModule } from 'src/modules/calendar/calendar-event-participant-manager/calendar-event-participant-manager.module'; +import { CalendarCommonModule } from 'src/modules/calendar/common/calendar-common.module'; @Module({ imports: [ CalendarBlocklistManagerModule, CalendarEventCleanerModule, CalendarEventImportManagerModule, - CalendarEventParticipantModule, + CalendarEventParticipantManagerModule, + CalendarCommonModule, ], providers: [], exports: [], diff --git a/packages/twenty-server/src/modules/calendar/common/calendar-common.module.ts b/packages/twenty-server/src/modules/calendar/common/calendar-common.module.ts new file mode 100644 index 000000000..3d3ccecd2 --- /dev/null +++ b/packages/twenty-server/src/modules/calendar/common/calendar-common.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; + +import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; +import { AddPersonIdAndWorkspaceMemberIdService } from 'src/modules/calendar-messaging-participant-manager/services/add-person-id-and-workspace-member-id/add-person-id-and-workspace-member-id.service'; + +@Module({ + imports: [WorkspaceDataSourceModule], + providers: [AddPersonIdAndWorkspaceMemberIdService], + exports: [], +}) +export class CalendarCommonModule {} diff --git a/packages/twenty-server/src/modules/calendar/common/types/calendar-event.ts b/packages/twenty-server/src/modules/calendar/common/types/calendar-event.ts index 0e0d3f5be..dcb7d597b 100644 --- a/packages/twenty-server/src/modules/calendar/common/types/calendar-event.ts +++ b/packages/twenty-server/src/modules/calendar/common/types/calendar-event.ts @@ -8,6 +8,7 @@ export type CalendarEvent = Omit< | 'calendarChannelEventAssociations' | 'calendarEventParticipants' | 'conferenceLink' + | 'id' > & { conferenceLinkLabel: string; conferenceLinkUrl: string; @@ -23,15 +24,25 @@ export type CalendarEventParticipant = Omit< | 'person' | 'workspaceMember' | 'calendarEvent' + | 'calendarEventId' > & { iCalUID: string; }; +export type CalendarEventParticipantWithCalendarEventId = + CalendarEventParticipant & { + calendarEventId: string; + }; + export type CalendarEventWithParticipants = CalendarEvent & { externalId: string; participants: CalendarEventParticipant[]; + status: string; }; -export type CalendarEventParticipantWithId = CalendarEventParticipant & { +export type CalendarEventWithParticipantsAndCalendarEventId = CalendarEvent & { id: string; + externalId: string; + participants: CalendarEventParticipantWithCalendarEventId[]; + status: string; }; diff --git a/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/auto-companies-and-contacts-creation.module.ts b/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/auto-companies-and-contacts-creation.module.ts index 3ab85291b..a7737499b 100644 --- a/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/auto-companies-and-contacts-creation.module.ts +++ b/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/auto-companies-and-contacts-creation.module.ts @@ -9,10 +9,9 @@ import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/perso import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; -import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module'; -import { CalendarEventParticipantModule } from 'src/modules/calendar/calendar-event-participant-manager/calendar-event-participant.module'; import { AutoCompaniesAndContactsCreationMessageChannelListener } from 'src/modules/connected-account/auto-companies-and-contacts-creation/listeners/auto-companies-and-contacts-creation-message-channel.listener'; import { AutoCompaniesAndContactsCreationCalendarChannelListener } from 'src/modules/connected-account/auto-companies-and-contacts-creation/listeners/auto-companies-and-contacts-creation-calendar-channel.listener'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; @Module({ imports: [ @@ -22,10 +21,9 @@ import { AutoCompaniesAndContactsCreationCalendarChannelListener } from 'src/mod PersonWorkspaceEntity, WorkspaceMemberWorkspaceEntity, ]), - MessagingCommonModule, WorkspaceDataSourceModule, - CalendarEventParticipantModule, TypeOrmModule.forFeature([FeatureFlagEntity], 'core'), + TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'), ], providers: [ CreateCompanyAndContactService, diff --git a/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/create-company/create-company.service.ts b/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/create-company/create-company.service.ts index 2721623e9..348d62ee1 100644 --- a/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/create-company/create-company.service.ts +++ b/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/create-company/create-company.service.ts @@ -5,9 +5,9 @@ import { v4 } from 'uuid'; import axios, { AxiosInstance } from 'axios'; import { CompanyRepository } from 'src/modules/company/repositories/company.repository'; -import { getCompanyNameFromDomainName } from 'src/modules/calendar-messaging-participant/utils/get-company-name-from-domain-name.util'; import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; import { CompanyWorkspaceEntity } from 'src/modules/company/standard-objects/company.workspace-entity'; +import { getCompanyNameFromDomainName } from 'src/modules/connected-account/auto-companies-and-contacts-creation/utils/get-company-name-from-domain-name.util'; @Injectable() export class CreateCompanyService { private readonly httpService: AxiosInstance; diff --git a/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/create-contact/create-contact.service.ts b/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/create-contact/create-contact.service.ts index 1a63e8cd9..88426bba1 100644 --- a/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/create-contact/create-contact.service.ts +++ b/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/create-contact/create-contact.service.ts @@ -4,7 +4,7 @@ import { EntityManager } from 'typeorm'; import { v4 } from 'uuid'; import { PersonRepository } from 'src/modules/person/repositories/person.repository'; -import { getFirstNameAndLastNameFromHandleAndDisplayName } from 'src/modules/calendar-messaging-participant/utils/get-first-name-and-last-name-from-handle-and-display-name.util'; +import { getFirstNameAndLastNameFromHandleAndDisplayName } from 'src/modules/connected-account/auto-companies-and-contacts-creation/utils/get-first-name-and-last-name-from-handle-and-display-name.util'; import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity'; diff --git a/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/listeners/auto-companies-and-contacts-creation-calendar-channel.listener.ts b/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/listeners/auto-companies-and-contacts-creation-calendar-channel.listener.ts index 85c5467b9..51104de27 100644 --- a/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/listeners/auto-companies-and-contacts-creation-calendar-channel.listener.ts +++ b/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/listeners/auto-companies-and-contacts-creation-calendar-channel.listener.ts @@ -6,11 +6,11 @@ import { objectRecordChangedProperties } from 'src/engine/integrations/event-emi import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator'; import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service'; -import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; import { CalendarCreateCompanyAndContactAfterSyncJobData, CalendarCreateCompanyAndContactAfterSyncJob, -} from 'src/modules/messaging/message-participants-manager/jobs/calendar-create-company-and-contact-after-sync.job'; +} from 'src/modules/calendar/calendar-event-participant-manager/jobs/calendar-create-company-and-contact-after-sync.job'; +import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; @Injectable() export class AutoCompaniesAndContactsCreationCalendarChannelListener { diff --git a/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/listeners/auto-companies-and-contacts-creation-message-channel.listener.ts b/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/listeners/auto-companies-and-contacts-creation-message-channel.listener.ts index 66fb9376c..f1d6362d4 100644 --- a/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/listeners/auto-companies-and-contacts-creation-message-channel.listener.ts +++ b/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/listeners/auto-companies-and-contacts-creation-message-channel.listener.ts @@ -10,7 +10,7 @@ import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/stan import { MessagingCreateCompanyAndContactAfterSyncJobData, MessagingCreateCompanyAndContactAfterSyncJob, -} from 'src/modules/messaging/message-participants-manager/jobs/messaging-create-company-and-contact-after-sync.job'; +} from 'src/modules/messaging/message-participant-manager/jobs/messaging-create-company-and-contact-after-sync.job'; @Injectable() export class AutoCompaniesAndContactsCreationMessageChannelListener { diff --git a/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/services/create-company-and-contact.service.ts b/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/services/create-company-and-contact.service.ts index 1b1bd3a51..940c9e5d4 100644 --- a/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/services/create-company-and-contact.service.ts +++ b/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/services/create-company-and-contact.service.ts @@ -1,25 +1,23 @@ import { Injectable } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; +import { InjectRepository } from '@nestjs/typeorm'; import chunk from 'lodash.chunk'; import compact from 'lodash.compact'; -import { EntityManager } from 'typeorm'; +import { EntityManager, Repository } from 'typeorm'; +import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event'; +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 { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; -import { InjectWorkspaceDatasource } from 'src/engine/twenty-orm/decorators/inject-workspace-datasource.decorator'; -import { getDomainNameFromHandle } from 'src/modules/calendar-messaging-participant/utils/get-domain-name-from-handle.util'; -import { CalendarEventParticipantService } from 'src/modules/calendar/calendar-event-participant-manager/services/calendar-event-participant.service'; -import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity'; +import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; import { CONTACTS_CREATION_BATCH_SIZE } from 'src/modules/connected-account/auto-companies-and-contacts-creation/constants/contacts-creation-batch-size.constant'; import { CreateCompanyService } from 'src/modules/connected-account/auto-companies-and-contacts-creation/create-company/create-company.service'; import { CreateContactService } from 'src/modules/connected-account/auto-companies-and-contacts-creation/create-contact/create-contact.service'; import { Contact } from 'src/modules/connected-account/auto-companies-and-contacts-creation/types/contact.type'; import { filterOutSelfAndContactsFromCompanyOrWorkspace } from 'src/modules/connected-account/auto-companies-and-contacts-creation/utils/filter-out-contacts-from-company-or-workspace.util'; +import { getDomainNameFromHandle } from 'src/modules/connected-account/auto-companies-and-contacts-creation/utils/get-domain-name-from-handle.util'; import { getUniqueContactsAndHandles } from 'src/modules/connected-account/auto-companies-and-contacts-creation/utils/get-unique-contacts-and-handles.util'; import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; -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'; import { PersonRepository } from 'src/modules/person/repositories/person.repository'; import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity'; import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository'; @@ -35,14 +33,12 @@ export class CreateCompanyAndContactService { private readonly personRepository: PersonRepository, @InjectObjectMetadataRepository(WorkspaceMemberWorkspaceEntity) private readonly workspaceMemberRepository: WorkspaceMemberRepository, - @InjectWorkspaceDatasource() - private readonly workspaceDataSource: WorkspaceDataSource, - private readonly messageParticipantService: MessagingMessageParticipantService, - private readonly calendarEventParticipantService: CalendarEventParticipantService, private readonly eventEmitter: EventEmitter2, + @InjectRepository(ObjectMetadataEntity, 'metadata') + private readonly objectMetadataRepository: Repository, ) {} - async createCompaniesAndPeople( + private async createCompaniesAndPeople( connectedAccount: ConnectedAccountWorkspaceEntity, contactsToCreate: Contact[], workspaceId: string, @@ -137,47 +133,37 @@ export class CreateCompanyAndContactService { CONTACTS_CREATION_BATCH_SIZE, ); + // TODO: Remove this when events are emitted directly inside TwentyORM + + const objectMetadata = await this.objectMetadataRepository.findOne({ + where: { + standardId: STANDARD_OBJECT_IDS.person, + workspaceId, + }, + }); + + if (!objectMetadata) { + throw new Error('Object metadata not found'); + } + for (const contactsBatch of contactsBatches) { - let updatedMessageParticipants: MessageParticipantWorkspaceEntity[] = []; - let updatedCalendarEventParticipants: CalendarEventParticipantWorkspaceEntity[] = - []; - - await this.workspaceDataSource?.transaction( - async (transactionManager: EntityManager) => { - const createdPeople = await this.createCompaniesAndPeople( - connectedAccount, - contactsBatch, - workspaceId, - transactionManager, - ); - - updatedMessageParticipants = - await this.messageParticipantService.updateMessageParticipantsAfterPeopleCreation( - createdPeople, - workspaceId, - transactionManager, - ); - - updatedCalendarEventParticipants = - await this.calendarEventParticipantService.updateCalendarEventParticipantsAfterPeopleCreation( - createdPeople, - workspaceId, - transactionManager, - ); - }, + const createdPeople = await this.createCompaniesAndPeople( + connectedAccount, + contactsBatch, + workspaceId, ); - this.eventEmitter.emit(`messageParticipant.matched`, { - workspaceId, - workspaceMemberId: connectedAccount.accountOwnerId, - messageParticipants: updatedMessageParticipants, - }); - - this.eventEmitter.emit(`calendarEventParticipant.matched`, { - workspaceId, - workspaceMemberId: connectedAccount.accountOwnerId, - calendarEventParticipants: updatedCalendarEventParticipants, - }); + for (const createdPerson of createdPeople) { + this.eventEmitter.emit('person.created', { + name: 'person.created', + workspaceId, + recordId: createdPerson.id, + objectMetadata, + properties: { + after: createdPerson, + }, + } satisfies ObjectRecordCreateEvent); + } } } } diff --git a/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/utils/filter-out-contacts-from-company-or-workspace.util.ts b/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/utils/filter-out-contacts-from-company-or-workspace.util.ts index 765cd6f7d..630809173 100644 --- a/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/utils/filter-out-contacts-from-company-or-workspace.util.ts +++ b/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/utils/filter-out-contacts-from-company-or-workspace.util.ts @@ -1,4 +1,4 @@ -import { getDomainNameFromHandle } from 'src/modules/calendar-messaging-participant/utils/get-domain-name-from-handle.util'; +import { getDomainNameFromHandle } from 'src/modules/connected-account/auto-companies-and-contacts-creation/utils/get-domain-name-from-handle.util'; 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 { Contact } from 'src/modules/connected-account/auto-companies-and-contacts-creation/types/contact.type'; diff --git a/packages/twenty-server/src/modules/calendar-messaging-participant/utils/get-company-name-from-domain-name.util.ts b/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/utils/get-company-name-from-domain-name.util.ts similarity index 100% rename from packages/twenty-server/src/modules/calendar-messaging-participant/utils/get-company-name-from-domain-name.util.ts rename to packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/utils/get-company-name-from-domain-name.util.ts diff --git a/packages/twenty-server/src/modules/calendar-messaging-participant/utils/get-domain-name-from-handle.util.ts b/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/utils/get-domain-name-from-handle.util.ts similarity index 100% rename from packages/twenty-server/src/modules/calendar-messaging-participant/utils/get-domain-name-from-handle.util.ts rename to packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/utils/get-domain-name-from-handle.util.ts diff --git a/packages/twenty-server/src/modules/calendar-messaging-participant/utils/get-first-name-and-last-name-from-handle-and-display-name.util.ts b/packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/utils/get-first-name-and-last-name-from-handle-and-display-name.util.ts similarity index 100% rename from packages/twenty-server/src/modules/calendar-messaging-participant/utils/get-first-name-and-last-name-from-handle-and-display-name.util.ts rename to packages/twenty-server/src/modules/connected-account/auto-companies-and-contacts-creation/utils/get-first-name-and-last-name-from-handle-and-display-name.util.ts diff --git a/packages/twenty-server/src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service.ts b/packages/twenty-server/src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service.ts index 4f9198ba5..9585ce5a8 100644 --- a/packages/twenty-server/src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service.ts +++ b/packages/twenty-server/src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service.ts @@ -12,7 +12,10 @@ export class OAuth2ClientManagerService { ) {} public async getOAuth2Client( - connectedAccount: ConnectedAccountWorkspaceEntity, + connectedAccount: Pick< + ConnectedAccountWorkspaceEntity, + 'provider' | 'refreshToken' + >, ): Promise { const { refreshToken } = connectedAccount; diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/is-throttled.ts b/packages/twenty-server/src/modules/connected-account/utils/is-throttled.ts similarity index 100% rename from packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/is-throttled.ts rename to packages/twenty-server/src/modules/connected-account/utils/is-throttled.ts diff --git a/packages/twenty-server/src/modules/messaging/common/messaging-common.module.ts b/packages/twenty-server/src/modules/messaging/common/messaging-common.module.ts index f13575d94..a700e79a7 100644 --- a/packages/twenty-server/src/modules/messaging/common/messaging-common.module.ts +++ b/packages/twenty-server/src/modules/messaging/common/messaging-common.module.ts @@ -6,14 +6,12 @@ import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.mod import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; -import { AddPersonIdAndWorkspaceMemberIdModule } from 'src/modules/calendar-messaging-participant/services/add-person-id-and-workspace-member-id/add-person-id-and-workspace-member-id.module'; +import { AddPersonIdAndWorkspaceMemberIdService } from 'src/modules/calendar-messaging-participant-manager/services/add-person-id-and-workspace-member-id/add-person-id-and-workspace-member-id.service'; import { MessagingChannelSyncStatusService } from 'src/modules/messaging/common/services/messaging-channel-sync-status.service'; import { MessagingErrorHandlingService } from 'src/modules/messaging/common/services/messaging-error-handling.service'; import { MessagingFetchByBatchesService } from 'src/modules/messaging/common/services/messaging-fetch-by-batch.service'; -import { MessagingMessageParticipantService } from 'src/modules/messaging/common/services/messaging-message-participant.service'; import { MessagingMessageThreadService } from 'src/modules/messaging/common/services/messaging-message-thread.service'; import { MessagingMessageService } from 'src/modules/messaging/common/services/messaging-message.service'; -import { MessagingSaveMessagesAndEnqueueContactCreationService } from 'src/modules/messaging/common/services/messaging-save-messages-and-enqueue-contact-creation.service'; import { MessagingTelemetryService } from 'src/modules/messaging/common/services/messaging-telemetry.service'; import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity'; import { MessageThreadWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-thread.workspace-entity'; @@ -34,26 +32,22 @@ import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/perso MessageThreadWorkspaceEntity, ]), TypeOrmModule.forFeature([FeatureFlagEntity], 'core'), - AddPersonIdAndWorkspaceMemberIdModule, ], providers: [ MessagingMessageService, MessagingMessageThreadService, - MessagingSaveMessagesAndEnqueueContactCreationService, MessagingErrorHandlingService, MessagingTelemetryService, MessagingChannelSyncStatusService, - MessagingMessageParticipantService, MessagingFetchByBatchesService, + AddPersonIdAndWorkspaceMemberIdService, ], exports: [ MessagingMessageService, MessagingMessageThreadService, - MessagingSaveMessagesAndEnqueueContactCreationService, MessagingErrorHandlingService, MessagingTelemetryService, MessagingChannelSyncStatusService, - MessagingMessageParticipantService, MessagingFetchByBatchesService, ], }) diff --git a/packages/twenty-server/src/modules/messaging/common/services/messaging-channel-sync-status.service.ts b/packages/twenty-server/src/modules/messaging/common/services/messaging-channel-sync-status.service.ts index fce009b83..0e3d3fe17 100644 --- a/packages/twenty-server/src/modules/messaging/common/services/messaging-channel-sync-status.service.ts +++ b/packages/twenty-server/src/modules/messaging/common/services/messaging-channel-sync-status.service.ts @@ -61,8 +61,6 @@ export class MessagingChannelSyncStatusService { `messages-to-import:${workspaceId}:gmail:${messageChannelId}`, ); - // TODO: remove nextPageToken from cache - await this.messageChannelRepository.resetSyncCursor( messageChannelId, workspaceId, diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/messaging-gmail-driver.module.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/messaging-gmail-driver.module.ts index 0554aad16..f55926995 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/messaging-gmail-driver.module.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/messaging-gmail-driver.module.ts @@ -5,6 +5,7 @@ import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature- import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { EnvironmentModule } from 'src/engine/integrations/environment/environment.module'; import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; +import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { EmailAliasManagerModule } from 'src/modules/connected-account/email-alias-manager/email-alias-manager.module'; import { OAuth2ClientManagerModule } from 'src/modules/connected-account/oauth2-client-manager/oauth2-client-manager.module'; import { GoogleAPIRefreshAccessTokenModule } from 'src/modules/connected-account/services/google-api-refresh-access-token/google-api-refresh-access-token.module'; @@ -20,6 +21,8 @@ import { MessagingGmailFullMessageListFetchService } from 'src/modules/messaging import { MessagingGmailHistoryService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/messaging-gmail-history.service'; import { MessagingGmailMessagesImportService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/messaging-gmail-messages-import.service'; import { MessagingGmailPartialMessageListFetchService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/messaging-gmail-partial-message-list-fetch.service'; +import { MessagingSaveMessagesAndEnqueueContactCreationService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/messaging-save-messages-and-enqueue-contact-creation.service'; +import { MessageParticipantManagerModule } from 'src/modules/messaging/message-participant-manager/message-participant-manager.module'; @Module({ imports: [ @@ -36,6 +39,8 @@ import { MessagingGmailPartialMessageListFetchService } from 'src/modules/messag OAuth2ClientManagerModule, EmailAliasManagerModule, FeatureFlagModule, + WorkspaceDataSourceModule, + MessageParticipantManagerModule, ], providers: [ MessagingGmailClientProvider, @@ -45,6 +50,7 @@ import { MessagingGmailPartialMessageListFetchService } from 'src/modules/messag MessagingGmailFullMessageListFetchService, MessagingGmailMessagesImportService, MessagingGmailFetchMessageIdsToExcludeService, + MessagingSaveMessagesAndEnqueueContactCreationService, ], exports: [ MessagingGmailClientProvider, diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/providers/messaging-gmail-client.provider.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/providers/messaging-gmail-client.provider.ts index 6ef460f87..834fedca4 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/providers/messaging-gmail-client.provider.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/providers/messaging-gmail-client.provider.ts @@ -12,7 +12,10 @@ export class MessagingGmailClientProvider { ) {} public async getGmailClient( - connectedAccount: ConnectedAccountWorkspaceEntity, + connectedAccount: Pick< + ConnectedAccountWorkspaceEntity, + 'provider' | 'refreshToken' + >, ): Promise { const oAuth2Client = await this.oAuth2ClientManagerService.getOAuth2Client(connectedAccount); diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/messaging-gmail-messages-import.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/messaging-gmail-messages-import.service.ts index 9d7798d62..75f48c682 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/messaging-gmail-messages-import.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/messaging-gmail-messages-import.service.ts @@ -15,7 +15,6 @@ import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/s import { MessageChannelRepository } from 'src/modules/messaging/common/repositories/message-channel.repository'; import { MessagingChannelSyncStatusService } from 'src/modules/messaging/common/services/messaging-channel-sync-status.service'; import { MessagingErrorHandlingService } from 'src/modules/messaging/common/services/messaging-error-handling.service'; -import { MessagingSaveMessagesAndEnqueueContactCreationService } from 'src/modules/messaging/common/services/messaging-save-messages-and-enqueue-contact-creation.service'; import { MessagingTelemetryService } from 'src/modules/messaging/common/services/messaging-telemetry.service'; import { MessageChannelSyncStage, @@ -23,6 +22,7 @@ import { } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; import { MESSAGING_GMAIL_USERS_MESSAGES_GET_BATCH_SIZE } from 'src/modules/messaging/message-import-manager/drivers/gmail/constants/messaging-gmail-users-messages-get-batch-size.constant'; import { MessagingGmailFetchMessagesByBatchesService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/messaging-gmail-fetch-messages-by-batches.service'; +import { MessagingSaveMessagesAndEnqueueContactCreationService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/messaging-save-messages-and-enqueue-contact-creation.service'; import { filterEmails } from 'src/modules/messaging/message-import-manager/utils/filter-emails.util'; @Injectable() diff --git a/packages/twenty-server/src/modules/messaging/common/services/messaging-save-messages-and-enqueue-contact-creation.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/messaging-save-messages-and-enqueue-contact-creation.service.ts similarity index 98% rename from packages/twenty-server/src/modules/messaging/common/services/messaging-save-messages-and-enqueue-contact-creation.service.ts rename to packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/messaging-save-messages-and-enqueue-contact-creation.service.ts index 1a7b1176b..bc177114f 100644 --- a/packages/twenty-server/src/modules/messaging/common/services/messaging-save-messages-and-enqueue-contact-creation.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/messaging-save-messages-and-enqueue-contact-creation.service.ts @@ -14,7 +14,6 @@ import { CreateCompanyAndContactJobData, } from 'src/modules/connected-account/auto-companies-and-contacts-creation/jobs/create-company-and-contact.job'; import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; -import { MessagingMessageParticipantService } from 'src/modules/messaging/common/services/messaging-message-participant.service'; import { MessagingMessageService } from 'src/modules/messaging/common/services/messaging-message.service'; import { MessageChannelContactAutoCreationPolicy, @@ -26,6 +25,7 @@ import { Participant, ParticipantWithMessageId, } from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-message'; +import { MessagingMessageParticipantService } from 'src/modules/messaging/message-participant-manager/services/messaging-message-participant.service'; import { isGroupEmail } from 'src/utils/is-group-email'; import { isWorkEmail } from 'src/utils/is-work-email'; diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job.ts index 90ee6dbf5..2a468fa49 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job.ts @@ -14,7 +14,7 @@ import { } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; import { MessagingGmailFullMessageListFetchService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/messaging-gmail-full-message-list-fetch.service'; import { MessagingGmailPartialMessageListFetchService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/messaging-gmail-partial-message-list-fetch.service'; -import { isThrottled } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/is-throttled'; +import { isThrottled } from 'src/modules/connected-account/utils/is-throttled'; export type MessagingMessageListFetchJobData = { messageChannelId: string; diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-messages-import.job.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-messages-import.job.ts index 46f3b89dc..b4cc6ae0b 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-messages-import.job.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-messages-import.job.ts @@ -13,7 +13,7 @@ import { MessageChannelWorkspaceEntity, } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; import { MessagingGmailMessagesImportService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/messaging-gmail-messages-import.service'; -import { isThrottled } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/is-throttled'; +import { isThrottled } from 'src/modules/connected-account/utils/is-throttled'; export type MessagingMessagesImportJobData = { messageChannelId: string; diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts index 4eea3c0d4..b2f951bc9 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts @@ -21,9 +21,11 @@ import { MessagingMessagesImportJob } from 'src/modules/messaging/message-import import { MessagingOngoingStaleJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-ongoing-stale.job'; import { MessagingMessageImportManagerMessageChannelListener } from 'src/modules/messaging/message-import-manager/listeners/messaging-import-manager-message-channel.listener'; import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; +import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; @Module({ imports: [ + WorkspaceDataSourceModule, MessagingGmailDriverModule, MessagingCommonModule, TypeOrmModule.forFeature([Workspace], 'core'), diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/utils/filter-emails.util.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/utils/filter-emails.util.ts index b1ecab082..b87a36146 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/utils/filter-emails.util.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/utils/filter-emails.util.ts @@ -1,4 +1,4 @@ -import { isEmailBlocklisted } from 'src/modules/calendar-messaging-participant/utils/is-email-blocklisted.util'; +import { isEmailBlocklisted } from 'src/modules/calendar-messaging-participant-manager/utils/is-email-blocklisted.util'; import { GmailMessage } from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-message'; // Todo: refactor this into several utils diff --git a/packages/twenty-server/src/modules/messaging/message-participant-manager/jobs/message-participant-match-participant.job.ts b/packages/twenty-server/src/modules/messaging/message-participant-manager/jobs/message-participant-match-participant.job.ts new file mode 100644 index 000000000..a9f486ebf --- /dev/null +++ b/packages/twenty-server/src/modules/messaging/message-participant-manager/jobs/message-participant-match-participant.job.ts @@ -0,0 +1,35 @@ +import { Scope } from '@nestjs/common'; + +import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator'; +import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator'; +import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; +import { MessagingMessageParticipantService } from 'src/modules/messaging/message-participant-manager/services/messaging-message-participant.service'; + +export type MessageParticipantMatchParticipantJobData = { + workspaceId: string; + email: string; + personId?: string; + workspaceMemberId?: string; +}; + +@Processor({ + queueName: MessageQueue.messagingQueue, + scope: Scope.REQUEST, +}) +export class MessageParticipantMatchParticipantJob { + constructor( + private readonly messageParticipantService: MessagingMessageParticipantService, + ) {} + + @Process(MessageParticipantMatchParticipantJob.name) + async handle(data: MessageParticipantMatchParticipantJobData): Promise { + const { workspaceId, email, personId, workspaceMemberId } = data; + + await this.messageParticipantService.matchMessageParticipants( + workspaceId, + email, + personId, + workspaceMemberId, + ); + } +} diff --git a/packages/twenty-server/src/modules/messaging/message-participant-manager/jobs/message-participant-unmatch-participant.job.ts b/packages/twenty-server/src/modules/messaging/message-participant-manager/jobs/message-participant-unmatch-participant.job.ts new file mode 100644 index 000000000..2be146301 --- /dev/null +++ b/packages/twenty-server/src/modules/messaging/message-participant-manager/jobs/message-participant-unmatch-participant.job.ts @@ -0,0 +1,37 @@ +import { Scope } from '@nestjs/common'; + +import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator'; +import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; +import { MessagingMessageParticipantService } from 'src/modules/messaging/message-participant-manager/services/messaging-message-participant.service'; +import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator'; + +export type MessageParticipantUnmatchParticipantJobData = { + workspaceId: string; + email: string; + personId?: string; + workspaceMemberId?: string; +}; + +@Processor({ + queueName: MessageQueue.messagingQueue, + scope: Scope.REQUEST, +}) +export class MessageParticipantUnmatchParticipantJob { + constructor( + private readonly messageParticipantService: MessagingMessageParticipantService, + ) {} + + @Process(MessageParticipantUnmatchParticipantJob.name) + async handle( + data: MessageParticipantUnmatchParticipantJobData, + ): Promise { + const { workspaceId, email, personId, workspaceMemberId } = data; + + await this.messageParticipantService.unmatchMessageParticipants( + workspaceId, + email, + personId, + workspaceMemberId, + ); + } +} diff --git a/packages/twenty-server/src/modules/messaging/message-participants-manager/jobs/messaging-create-company-and-contact-after-sync.job.ts b/packages/twenty-server/src/modules/messaging/message-participant-manager/jobs/messaging-create-company-and-contact-after-sync.job.ts similarity index 100% rename from packages/twenty-server/src/modules/messaging/message-participants-manager/jobs/messaging-create-company-and-contact-after-sync.job.ts rename to packages/twenty-server/src/modules/messaging/message-participant-manager/jobs/messaging-create-company-and-contact-after-sync.job.ts diff --git a/packages/twenty-server/src/modules/calendar-messaging-participant/listeners/participant-person.listener.ts b/packages/twenty-server/src/modules/messaging/message-participant-manager/listeners/message-participant-person.listener.ts similarity index 70% rename from packages/twenty-server/src/modules/calendar-messaging-participant/listeners/participant-person.listener.ts rename to packages/twenty-server/src/modules/messaging/message-participant-manager/listeners/message-participant-person.listener.ts index 0435438a9..521c92585 100644 --- a/packages/twenty-server/src/modules/calendar-messaging-participant/listeners/participant-person.listener.ts +++ b/packages/twenty-server/src/modules/messaging/message-participant-manager/listeners/message-participant-person.listener.ts @@ -8,17 +8,17 @@ import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decora import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service'; import { - MatchParticipantJobData, - MatchParticipantJob, -} from 'src/modules/calendar-messaging-participant/jobs/match-participant.job'; + MessageParticipantMatchParticipantJobData, + MessageParticipantMatchParticipantJob, +} from 'src/modules/messaging/message-participant-manager/jobs/message-participant-match-participant.job'; import { - UnmatchParticipantJobData, - UnmatchParticipantJob, -} from 'src/modules/calendar-messaging-participant/jobs/unmatch-participant.job'; + MessageParticipantUnmatchParticipantJobData, + MessageParticipantUnmatchParticipantJob, +} from 'src/modules/messaging/message-participant-manager/jobs/message-participant-unmatch-participant.job'; import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity'; @Injectable() -export class ParticipantPersonListener { +export class MessageParticipantPersonListener { constructor( @InjectMessageQueue(MessageQueue.messagingQueue) private readonly messageQueueService: MessageQueueService, @@ -32,8 +32,8 @@ export class ParticipantPersonListener { return; } - await this.messageQueueService.add( - MatchParticipantJob.name, + await this.messageQueueService.add( + MessageParticipantMatchParticipantJob.name, { workspaceId: payload.workspaceId, email: payload.properties.after.email, @@ -52,8 +52,8 @@ export class ParticipantPersonListener { payload.properties.after, ).includes('email') ) { - await this.messageQueueService.add( - UnmatchParticipantJob.name, + await this.messageQueueService.add( + MessageParticipantUnmatchParticipantJob.name, { workspaceId: payload.workspaceId, email: payload.properties.before.email, @@ -61,8 +61,8 @@ export class ParticipantPersonListener { }, ); - await this.messageQueueService.add( - MatchParticipantJob.name, + await this.messageQueueService.add( + MessageParticipantMatchParticipantJob.name, { workspaceId: payload.workspaceId, email: payload.properties.after.email, diff --git a/packages/twenty-server/src/modules/calendar-messaging-participant/listeners/participant-workspace-member.listener.ts b/packages/twenty-server/src/modules/messaging/message-participant-manager/listeners/message-participant-workspace-member.listener.ts similarity index 71% rename from packages/twenty-server/src/modules/calendar-messaging-participant/listeners/participant-workspace-member.listener.ts rename to packages/twenty-server/src/modules/messaging/message-participant-manager/listeners/message-participant-workspace-member.listener.ts index 6cb0078c1..a7dde2e18 100644 --- a/packages/twenty-server/src/modules/calendar-messaging-participant/listeners/participant-workspace-member.listener.ts +++ b/packages/twenty-server/src/modules/messaging/message-participant-manager/listeners/message-participant-workspace-member.listener.ts @@ -8,17 +8,17 @@ import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decora import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service'; import { - MatchParticipantJobData, - MatchParticipantJob, -} from 'src/modules/calendar-messaging-participant/jobs/match-participant.job'; + MessageParticipantMatchParticipantJobData, + MessageParticipantMatchParticipantJob, +} from 'src/modules/messaging/message-participant-manager/jobs/message-participant-match-participant.job'; import { - UnmatchParticipantJobData, - UnmatchParticipantJob, -} from 'src/modules/calendar-messaging-participant/jobs/unmatch-participant.job'; + MessageParticipantUnmatchParticipantJobData, + MessageParticipantUnmatchParticipantJob, +} from 'src/modules/messaging/message-participant-manager/jobs/message-participant-unmatch-participant.job'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @Injectable() -export class ParticipantWorkspaceMemberListener { +export class MessageParticipantWorkspaceMemberListener { constructor( @InjectMessageQueue(MessageQueue.messagingQueue) private readonly messageQueueService: MessageQueueService, @@ -32,8 +32,8 @@ export class ParticipantWorkspaceMemberListener { return; } - await this.messageQueueService.add( - MatchParticipantJob.name, + await this.messageQueueService.add( + MessageParticipantMatchParticipantJob.name, { workspaceId: payload.workspaceId, email: payload.properties.after.userEmail, @@ -52,8 +52,8 @@ export class ParticipantWorkspaceMemberListener { payload.properties.after, ).includes('userEmail') ) { - await this.messageQueueService.add( - UnmatchParticipantJob.name, + await this.messageQueueService.add( + MessageParticipantUnmatchParticipantJob.name, { workspaceId: payload.workspaceId, email: payload.properties.before.userEmail, @@ -61,8 +61,8 @@ export class ParticipantWorkspaceMemberListener { }, ); - await this.messageQueueService.add( - MatchParticipantJob.name, + await this.messageQueueService.add( + MessageParticipantMatchParticipantJob.name, { workspaceId: payload.workspaceId, email: payload.properties.after.userEmail, diff --git a/packages/twenty-server/src/modules/messaging/message-participants-manager/listeners/message-participant.listener.ts b/packages/twenty-server/src/modules/messaging/message-participant-manager/listeners/message-participant.listener.ts similarity index 100% rename from packages/twenty-server/src/modules/messaging/message-participants-manager/listeners/message-participant.listener.ts rename to packages/twenty-server/src/modules/messaging/message-participant-manager/listeners/message-participant.listener.ts diff --git a/packages/twenty-server/src/modules/messaging/message-participants-manager/messaging-participants-manager.module.ts b/packages/twenty-server/src/modules/messaging/message-participant-manager/message-participant-manager.module.ts similarity index 51% rename from packages/twenty-server/src/modules/messaging/message-participants-manager/messaging-participants-manager.module.ts rename to packages/twenty-server/src/modules/messaging/message-participant-manager/message-participant-manager.module.ts index 5767093b9..b09af5ec6 100644 --- a/packages/twenty-server/src/modules/messaging/message-participants-manager/messaging-participants-manager.module.ts +++ b/packages/twenty-server/src/modules/messaging/message-participant-manager/message-participant-manager.module.ts @@ -7,19 +7,23 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; +import { AddPersonIdAndWorkspaceMemberIdService } from 'src/modules/calendar-messaging-participant-manager/services/add-person-id-and-workspace-member-id/add-person-id-and-workspace-member-id.service'; import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; 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 { CalendarCreateCompanyAndContactAfterSyncJob } from 'src/modules/messaging/message-participants-manager/jobs/calendar-create-company-and-contact-after-sync.job'; -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 { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module'; +import { MessageParticipantMatchParticipantJob } from 'src/modules/messaging/message-participant-manager/jobs/message-participant-match-participant.job'; +import { MessageParticipantUnmatchParticipantJob } from 'src/modules/messaging/message-participant-manager/jobs/message-participant-unmatch-participant.job'; +import { MessagingCreateCompanyAndContactAfterSyncJob } from 'src/modules/messaging/message-participant-manager/jobs/messaging-create-company-and-contact-after-sync.job'; +import { MessageParticipantPersonListener } from 'src/modules/messaging/message-participant-manager/listeners/message-participant-person.listener'; +import { MessageParticipantWorkspaceMemberListener } from 'src/modules/messaging/message-participant-manager/listeners/message-participant-workspace-member.listener'; +import { MessageParticipantListener } from 'src/modules/messaging/message-participant-manager/listeners/message-participant.listener'; +import { MessagingMessageParticipantService } from 'src/modules/messaging/message-participant-manager/services/messaging-message-participant.service'; import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity'; @Module({ imports: [ TypeOrmModule.forFeature([FeatureFlagEntity], 'core'), AnalyticsModule, - MessagingGmailDriverModule, AutoCompaniesAndContactsCreationModule, WorkspaceDataSourceModule, ObjectMetadataRepositoryModule.forFeature([ @@ -27,11 +31,18 @@ import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-o ]), TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'), TwentyORMModule.forFeature([CalendarChannelWorkspaceEntity]), + MessagingCommonModule, ], providers: [ + MessagingMessageParticipantService, + MessageParticipantMatchParticipantJob, + MessageParticipantUnmatchParticipantJob, MessagingCreateCompanyAndContactAfterSyncJob, - CalendarCreateCompanyAndContactAfterSyncJob, MessageParticipantListener, + MessageParticipantPersonListener, + MessageParticipantWorkspaceMemberListener, + AddPersonIdAndWorkspaceMemberIdService, ], + exports: [MessagingMessageParticipantService], }) -export class MessaginParticipantsManagerModule {} +export class MessageParticipantManagerModule {} diff --git a/packages/twenty-server/src/modules/messaging/common/services/messaging-message-participant.service.ts b/packages/twenty-server/src/modules/messaging/message-participant-manager/services/messaging-message-participant.service.ts similarity index 95% rename from packages/twenty-server/src/modules/messaging/common/services/messaging-message-participant.service.ts rename to packages/twenty-server/src/modules/messaging/message-participant-manager/services/messaging-message-participant.service.ts index 3d1da461e..24eaec161 100644 --- a/packages/twenty-server/src/modules/messaging/common/services/messaging-message-participant.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-participant-manager/services/messaging-message-participant.service.ts @@ -8,13 +8,11 @@ import { PersonRepository } from 'src/modules/person/repositories/person.reposit import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity'; import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; import { getFlattenedValuesAndValuesStringForBatchRawQuery } from 'src/modules/calendar/calendar-event-import-manager/utils/get-flattened-values-and-values-string-for-batch-raw-query.util'; -import { AddPersonIdAndWorkspaceMemberIdService } from 'src/modules/calendar-messaging-participant/services/add-person-id-and-workspace-member-id/add-person-id-and-workspace-member-id.service'; +import { AddPersonIdAndWorkspaceMemberIdService } from 'src/modules/calendar-messaging-participant-manager/services/add-person-id-and-workspace-member-id/add-person-id-and-workspace-member-id.service'; import { MessageParticipantRepository } from 'src/modules/messaging/common/repositories/message-participant.repository'; import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity'; import { ParticipantWithMessageId } from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-message'; -// Todo: this is not the right place for this file. The code needs to be refactored in term of business modules with a precise scope. -// Putting it here to avoid circular dependencies for now. @Injectable() export class MessagingMessageParticipantService { constructor( diff --git a/packages/twenty-server/src/modules/messaging/messaging.module.ts b/packages/twenty-server/src/modules/messaging/messaging.module.ts index 5e2afc037..24376567c 100644 --- a/packages/twenty-server/src/modules/messaging/messaging.module.ts +++ b/packages/twenty-server/src/modules/messaging/messaging.module.ts @@ -3,14 +3,14 @@ import { Module } from '@nestjs/common'; import { MessagingBlocklistManagerModule } from 'src/modules/messaging/blocklist-manager/messaging-blocklist-manager.module'; import { MessagingMessageCleanerModule } from 'src/modules/messaging/message-cleaner/messaging-message-cleaner.module'; import { MessagingImportManagerModule } from 'src/modules/messaging/message-import-manager/messaging-import-manager.module'; -import { MessaginParticipantsManagerModule } from 'src/modules/messaging/message-participants-manager/messaging-participants-manager.module'; +import { MessageParticipantManagerModule } from 'src/modules/messaging/message-participant-manager/message-participant-manager.module'; import { MessagingMonitoringModule } from 'src/modules/messaging/monitoring/messaging-monitoring.module'; @Module({ imports: [ MessagingImportManagerModule, MessagingMessageCleanerModule, - MessaginParticipantsManagerModule, + MessageParticipantManagerModule, MessagingBlocklistManagerModule, MessagingMonitoringModule, ],