@ -47,6 +47,7 @@ import { NotFoundError } from 'src/engine/utils/graphql-errors.util';
|
|||||||
import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory';
|
import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory';
|
||||||
import { QueryResultGettersFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters.factory';
|
import { QueryResultGettersFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters.factory';
|
||||||
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
|
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
|
||||||
|
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||||
|
|
||||||
import { WorkspaceQueryRunnerOptions } from './interfaces/query-runner-option.interface';
|
import { WorkspaceQueryRunnerOptions } from './interfaces/query-runner-option.interface';
|
||||||
import {
|
import {
|
||||||
@ -432,6 +433,12 @@ export class WorkspaceQueryRunnerService {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const deletedBlocklistItem = await this.handleDeleteBlocklistItem(
|
||||||
|
args.id,
|
||||||
|
workspaceId,
|
||||||
|
objectMetadataItem,
|
||||||
|
);
|
||||||
// TODO END
|
// TODO END
|
||||||
|
|
||||||
const result = await this.execute(query, workspaceId);
|
const result = await this.execute(query, workspaceId);
|
||||||
@ -459,6 +466,7 @@ export class WorkspaceQueryRunnerService {
|
|||||||
properties: {
|
properties: {
|
||||||
before: {
|
before: {
|
||||||
...(deletedWorkspaceMember ?? {}),
|
...(deletedWorkspaceMember ?? {}),
|
||||||
|
...(deletedBlocklistItem ?? {}),
|
||||||
...this.removeNestedProperties(parsedResults?.[0]),
|
...this.removeNestedProperties(parsedResults?.[0]),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -615,4 +623,36 @@ export class WorkspaceQueryRunnerService {
|
|||||||
|
|
||||||
return workspaceMemberResult.edges?.[0]?.node;
|
return workspaceMemberResult.edges?.[0]?.node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async handleDeleteBlocklistItem(
|
||||||
|
id: string,
|
||||||
|
workspaceId: string,
|
||||||
|
objectMetadataItem: ObjectMetadataInterface,
|
||||||
|
) {
|
||||||
|
if (objectMetadataItem.standardId !== STANDARD_OBJECT_IDS.blocklist) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blocklistItemResult = await this.executeAndParse<IRecord>(
|
||||||
|
`
|
||||||
|
query {
|
||||||
|
blocklistCollection(filter: {id: {eq: "${id}"}}) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
handle
|
||||||
|
workspaceMember {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
objectMetadataItem,
|
||||||
|
'',
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return blocklistItemResult.edges?.[0]?.node;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,6 +44,7 @@ import { WorkspaceGoogleCalendarSyncModule } from 'src/modules/calendar/services
|
|||||||
import { AutoCompaniesAndContactsCreationModule } from 'src/modules/connected-account/auto-companies-and-contacts-creation/auto-companies-and-contacts-creation.module';
|
import { AutoCompaniesAndContactsCreationModule } from 'src/modules/connected-account/auto-companies-and-contacts-creation/auto-companies-and-contacts-creation.module';
|
||||||
import { GmailFetchMessagesFromCacheCronJob } from 'src/modules/messaging/crons/jobs/gmail-fetch-messages-from-cache.cron.job';
|
import { GmailFetchMessagesFromCacheCronJob } from 'src/modules/messaging/crons/jobs/gmail-fetch-messages-from-cache.cron.job';
|
||||||
import { GmailPartialSyncCronJob } from 'src/modules/messaging/crons/jobs/gmail-partial-sync.cron.job';
|
import { GmailPartialSyncCronJob } from 'src/modules/messaging/crons/jobs/gmail-partial-sync.cron.job';
|
||||||
|
import { BlocklistReimportMessagesJob } from 'src/modules/messaging/jobs/blocklist-reimport-messages.job';
|
||||||
import { DeleteConnectedAccountAssociatedMessagingDataJob } from 'src/modules/messaging/jobs/delete-connected-account-associated-messaging-data.job';
|
import { DeleteConnectedAccountAssociatedMessagingDataJob } from 'src/modules/messaging/jobs/delete-connected-account-associated-messaging-data.job';
|
||||||
import { BlocklistItemDeleteMessagesJob } from 'src/modules/messaging/jobs/blocklist-item-delete-messages.job';
|
import { BlocklistItemDeleteMessagesJob } from 'src/modules/messaging/jobs/blocklist-item-delete-messages.job';
|
||||||
import { GmailFullSyncJob } from 'src/modules/messaging/jobs/gmail-full-sync.job';
|
import { GmailFullSyncJob } from 'src/modules/messaging/jobs/gmail-full-sync.job';
|
||||||
@ -59,6 +60,7 @@ import { TimelineActivityModule } from 'src/modules/timeline/timeline-activity.m
|
|||||||
import { MessageChannelMessageAssociationObjectMetadata } from 'src/modules/messaging/standard-objects/message-channel-message-association.object-metadata';
|
import { MessageChannelMessageAssociationObjectMetadata } from 'src/modules/messaging/standard-objects/message-channel-message-association.object-metadata';
|
||||||
import { MessageChannelObjectMetadata } from 'src/modules/messaging/standard-objects/message-channel.object-metadata';
|
import { MessageChannelObjectMetadata } from 'src/modules/messaging/standard-objects/message-channel.object-metadata';
|
||||||
import { BlocklistItemDeleteCalendarEventsJob } from 'src/modules/calendar/jobs/blocklist-item-delete-calendar-events.job';
|
import { BlocklistItemDeleteCalendarEventsJob } from 'src/modules/calendar/jobs/blocklist-item-delete-calendar-events.job';
|
||||||
|
import { BlocklistReimportCalendarEventsJob } from 'src/modules/calendar/jobs/blocklist-reimport-calendar-events.job';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -191,6 +193,14 @@ import { BlocklistItemDeleteCalendarEventsJob } from 'src/modules/calendar/jobs/
|
|||||||
provide: BlocklistItemDeleteCalendarEventsJob.name,
|
provide: BlocklistItemDeleteCalendarEventsJob.name,
|
||||||
useClass: BlocklistItemDeleteCalendarEventsJob,
|
useClass: BlocklistItemDeleteCalendarEventsJob,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: BlocklistReimportMessagesJob.name,
|
||||||
|
useClass: BlocklistReimportMessagesJob,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: BlocklistReimportCalendarEventsJob.name,
|
||||||
|
useClass: BlocklistReimportCalendarEventsJob,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class JobsModule {
|
export class JobsModule {
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { FieldMetadataInterface } from './field-metadata.interface';
|
|||||||
|
|
||||||
export interface ObjectMetadataInterface {
|
export interface ObjectMetadataInterface {
|
||||||
id: string;
|
id: string;
|
||||||
|
standardId?: string | null;
|
||||||
nameSingular: string;
|
nameSingular: string;
|
||||||
namePlural: string;
|
namePlural: string;
|
||||||
labelSingular: string;
|
labelSingular: string;
|
||||||
|
|||||||
@ -0,0 +1,59 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { MessageQueueJob } from 'src/engine/integrations/message-queue/interfaces/message-queue-job.interface';
|
||||||
|
|
||||||
|
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||||
|
import { GoogleCalendarSyncService } from 'src/modules/calendar/services/google-calendar-sync/google-calendar-sync.service';
|
||||||
|
import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository';
|
||||||
|
import { ConnectedAccountObjectMetadata } from 'src/modules/connected-account/standard-objects/connected-account.object-metadata';
|
||||||
|
|
||||||
|
export type BlocklistReimportCalendarEventsJobData = {
|
||||||
|
workspaceId: string;
|
||||||
|
workspaceMemberId: string;
|
||||||
|
handle: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BlocklistReimportCalendarEventsJob
|
||||||
|
implements MessageQueueJob<BlocklistReimportCalendarEventsJobData>
|
||||||
|
{
|
||||||
|
private readonly logger = new Logger(BlocklistReimportCalendarEventsJob.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectObjectMetadataRepository(ConnectedAccountObjectMetadata)
|
||||||
|
private readonly connectedAccountRepository: ConnectedAccountRepository,
|
||||||
|
private readonly googleCalendarSyncService: GoogleCalendarSyncService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async handle(data: BlocklistReimportCalendarEventsJobData): Promise<void> {
|
||||||
|
const { workspaceId, workspaceMemberId, handle } = data;
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`Reimporting calendar events from handle ${handle} in workspace ${workspaceId} for workspace member ${workspaceMemberId}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const connectedAccount =
|
||||||
|
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}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.googleCalendarSyncService.startGoogleCalendarSync(
|
||||||
|
workspaceId,
|
||||||
|
connectedAccount[0].id,
|
||||||
|
handle,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`Reimporting calendar events from ${handle} in workspace ${workspaceId} for workspace member ${workspaceMemberId} done`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,12 +2,17 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||||||
import { OnEvent } from '@nestjs/event-emitter';
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
|
|
||||||
import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event';
|
import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event';
|
||||||
|
import { ObjectRecordDeleteEvent } from 'src/engine/integrations/event-emitter/types/object-record-delete.event';
|
||||||
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
|
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
|
||||||
import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service';
|
import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service';
|
||||||
import {
|
import {
|
||||||
BlocklistItemDeleteCalendarEventsJobData,
|
BlocklistItemDeleteCalendarEventsJobData,
|
||||||
BlocklistItemDeleteCalendarEventsJob,
|
BlocklistItemDeleteCalendarEventsJob,
|
||||||
} from 'src/modules/calendar/jobs/blocklist-item-delete-calendar-events.job';
|
} from 'src/modules/calendar/jobs/blocklist-item-delete-calendar-events.job';
|
||||||
|
import {
|
||||||
|
BlocklistReimportCalendarEventsJobData,
|
||||||
|
BlocklistReimportCalendarEventsJob,
|
||||||
|
} from 'src/modules/calendar/jobs/blocklist-reimport-calendar-events.job';
|
||||||
import { BlocklistObjectMetadata } from 'src/modules/connected-account/standard-objects/blocklist.object-metadata';
|
import { BlocklistObjectMetadata } from 'src/modules/connected-account/standard-objects/blocklist.object-metadata';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -29,4 +34,18 @@ export class CalendarBlocklistListener {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OnEvent('blocklist.deleted')
|
||||||
|
async handleDeletedEvent(
|
||||||
|
payload: ObjectRecordDeleteEvent<BlocklistObjectMetadata>,
|
||||||
|
) {
|
||||||
|
await this.messageQueueService.add<BlocklistReimportCalendarEventsJobData>(
|
||||||
|
BlocklistReimportCalendarEventsJob.name,
|
||||||
|
{
|
||||||
|
workspaceId: payload.workspaceId,
|
||||||
|
workspaceMemberId: payload.properties.before.workspaceMember.id,
|
||||||
|
handle: payload.properties.before.handle,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -68,6 +68,7 @@ export class GoogleCalendarSyncService {
|
|||||||
public async startGoogleCalendarSync(
|
public async startGoogleCalendarSync(
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
connectedAccountId: string,
|
connectedAccountId: string,
|
||||||
|
emailOrDomainToReimport?: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const connectedAccount = await this.connectedAccountRepository.getById(
|
const connectedAccount = await this.connectedAccountRepository.getById(
|
||||||
connectedAccountId,
|
connectedAccountId,
|
||||||
@ -137,8 +138,9 @@ export class GoogleCalendarSyncService {
|
|||||||
const googleCalendarEvents = await googleCalendarClient.events.list({
|
const googleCalendarEvents = await googleCalendarClient.events.list({
|
||||||
calendarId: 'primary',
|
calendarId: 'primary',
|
||||||
maxResults: 500,
|
maxResults: 500,
|
||||||
syncToken,
|
syncToken: emailOrDomainToReimport ? undefined : syncToken,
|
||||||
pageToken: nextPageToken,
|
pageToken: nextPageToken,
|
||||||
|
q: emailOrDomainToReimport,
|
||||||
showDeleted: true,
|
showDeleted: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -174,10 +176,19 @@ export class GoogleCalendarSyncService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredEvents = filterOutBlocklistedEvents(
|
let filteredEvents = filterOutBlocklistedEvents(events, blocklistedEmails);
|
||||||
events,
|
|
||||||
blocklistedEmails,
|
if (emailOrDomainToReimport) {
|
||||||
);
|
// We still need to filter the events to only keep the ones that have the email or domain we want to reimport
|
||||||
|
// because the q parameter in the list method also filters the events that have the email or domain in their summary, description ...
|
||||||
|
// The q parameter allows us to narrow down the events
|
||||||
|
filteredEvents = filteredEvents.filter(
|
||||||
|
(event) =>
|
||||||
|
event.attendees?.some(
|
||||||
|
(attendee) => attendee.email?.endsWith(emailOrDomainToReimport),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const eventExternalIds = filteredEvents.map((event) => event.id as string);
|
const eventExternalIds = filteredEvents.map((event) => event.id as string);
|
||||||
|
|
||||||
|
|||||||
@ -43,6 +43,25 @@ export class ConnectedAccountRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getAllByWorkspaceMemberId(
|
||||||
|
workspaceMemberId: string,
|
||||||
|
workspaceId: string,
|
||||||
|
transactionManager?: EntityManager,
|
||||||
|
): Promise<ObjectRecord<ConnectedAccountObjectMetadata>[] | undefined> {
|
||||||
|
const dataSourceSchema =
|
||||||
|
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||||
|
|
||||||
|
const connectedAccounts =
|
||||||
|
await this.workspaceDataSourceService.executeRawQuery(
|
||||||
|
`SELECT * FROM ${dataSourceSchema}."connectedAccount" WHERE "accountOwnerId" = $1`,
|
||||||
|
[workspaceMemberId],
|
||||||
|
workspaceId,
|
||||||
|
transactionManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
return connectedAccounts;
|
||||||
|
}
|
||||||
|
|
||||||
public async getAllByHandleAndWorkspaceMemberId(
|
public async getAllByHandleAndWorkspaceMemberId(
|
||||||
handle: string,
|
handle: string,
|
||||||
workspaceMemberId: string,
|
workspaceMemberId: string,
|
||||||
|
|||||||
@ -0,0 +1,59 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { MessageQueueJob } from 'src/engine/integrations/message-queue/interfaces/message-queue-job.interface';
|
||||||
|
|
||||||
|
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||||
|
import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository';
|
||||||
|
import { ConnectedAccountObjectMetadata } from 'src/modules/connected-account/standard-objects/connected-account.object-metadata';
|
||||||
|
import { GmailFullSyncService } from 'src/modules/messaging/services/gmail-full-sync/gmail-full-sync.service';
|
||||||
|
|
||||||
|
export type BlocklistReimportMessagesJobData = {
|
||||||
|
workspaceId: string;
|
||||||
|
workspaceMemberId: string;
|
||||||
|
handle: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BlocklistReimportMessagesJob
|
||||||
|
implements MessageQueueJob<BlocklistReimportMessagesJobData>
|
||||||
|
{
|
||||||
|
private readonly logger = new Logger(BlocklistReimportMessagesJob.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectObjectMetadataRepository(ConnectedAccountObjectMetadata)
|
||||||
|
private readonly connectedAccountRepository: ConnectedAccountRepository,
|
||||||
|
private readonly gmailFullSyncService: GmailFullSyncService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async handle(data: BlocklistReimportMessagesJobData): Promise<void> {
|
||||||
|
const { workspaceId, workspaceMemberId, handle } = data;
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`Reimporting messages from handle ${handle} in workspace ${workspaceId} for workspace member ${workspaceMemberId}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const connectedAccount =
|
||||||
|
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}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.gmailFullSyncService.fetchConnectedAccountThreads(
|
||||||
|
workspaceId,
|
||||||
|
connectedAccount[0].id,
|
||||||
|
[handle],
|
||||||
|
);
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`Reimporting messages from ${handle} in workspace ${workspaceId} for workspace member ${workspaceMemberId} done`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,9 +2,14 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||||||
import { OnEvent } from '@nestjs/event-emitter';
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
|
|
||||||
import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event';
|
import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event';
|
||||||
|
import { ObjectRecordDeleteEvent } from 'src/engine/integrations/event-emitter/types/object-record-delete.event';
|
||||||
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
|
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
|
||||||
import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service';
|
import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service';
|
||||||
import { BlocklistObjectMetadata } from 'src/modules/connected-account/standard-objects/blocklist.object-metadata';
|
import { BlocklistObjectMetadata } from 'src/modules/connected-account/standard-objects/blocklist.object-metadata';
|
||||||
|
import {
|
||||||
|
BlocklistReimportMessagesJob,
|
||||||
|
BlocklistReimportMessagesJobData,
|
||||||
|
} from 'src/modules/messaging/jobs/blocklist-reimport-messages.job';
|
||||||
import {
|
import {
|
||||||
BlocklistItemDeleteMessagesJobData,
|
BlocklistItemDeleteMessagesJobData,
|
||||||
BlocklistItemDeleteMessagesJob,
|
BlocklistItemDeleteMessagesJob,
|
||||||
@ -18,10 +23,10 @@ export class MessagingBlocklistListener {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
@OnEvent('blocklist.created')
|
@OnEvent('blocklist.created')
|
||||||
handleCreatedEvent(
|
async handleCreatedEvent(
|
||||||
payload: ObjectRecordCreateEvent<BlocklistObjectMetadata>,
|
payload: ObjectRecordCreateEvent<BlocklistObjectMetadata>,
|
||||||
) {
|
) {
|
||||||
this.messageQueueService.add<BlocklistItemDeleteMessagesJobData>(
|
await this.messageQueueService.add<BlocklistItemDeleteMessagesJobData>(
|
||||||
BlocklistItemDeleteMessagesJob.name,
|
BlocklistItemDeleteMessagesJob.name,
|
||||||
{
|
{
|
||||||
workspaceId: payload.workspaceId,
|
workspaceId: payload.workspaceId,
|
||||||
@ -29,4 +34,18 @@ export class MessagingBlocklistListener {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OnEvent('blocklist.deleted')
|
||||||
|
async handleDeletedEvent(
|
||||||
|
payload: ObjectRecordDeleteEvent<BlocklistObjectMetadata>,
|
||||||
|
) {
|
||||||
|
await this.messageQueueService.add<BlocklistReimportMessagesJobData>(
|
||||||
|
BlocklistReimportMessagesJob.name,
|
||||||
|
{
|
||||||
|
workspaceId: payload.workspaceId,
|
||||||
|
workspaceMemberId: payload.properties.before.workspaceMember.id,
|
||||||
|
handle: payload.properties.before.handle,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,8 +25,8 @@ import {
|
|||||||
MessageChannelObjectMetadata,
|
MessageChannelObjectMetadata,
|
||||||
MessageChannelSyncStatus,
|
MessageChannelSyncStatus,
|
||||||
} from 'src/modules/messaging/standard-objects/message-channel.object-metadata';
|
} from 'src/modules/messaging/standard-objects/message-channel.object-metadata';
|
||||||
import { gmailSearchFilterExcludeEmails } from 'src/modules/messaging/utils/gmail-search-filter.util';
|
|
||||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||||
|
import { gmailSearchFilterEmailAdresses } from 'src/modules/messaging/utils/gmail-search-filter.util';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GmailFullSyncService {
|
export class GmailFullSyncService {
|
||||||
@ -54,6 +54,7 @@ export class GmailFullSyncService {
|
|||||||
public async fetchConnectedAccountThreads(
|
public async fetchConnectedAccountThreads(
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
connectedAccountId: string,
|
connectedAccountId: string,
|
||||||
|
includedEmails?: string[],
|
||||||
) {
|
) {
|
||||||
const connectedAccount = await this.connectedAccountRepository.getById(
|
const connectedAccount = await this.connectedAccountRepository.getById(
|
||||||
connectedAccountId,
|
connectedAccountId,
|
||||||
@ -109,19 +110,20 @@ export class GmailFullSyncService {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const gmailClient: gmail_v1.Gmail =
|
||||||
|
await this.gmailClientProvider.getGmailClient(refreshToken);
|
||||||
|
|
||||||
|
const blocklistedEmails = await this.fetchBlocklistEmails(
|
||||||
|
connectedAccount.accountOwnerId,
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
await workspaceDataSource
|
await workspaceDataSource
|
||||||
?.transaction(async (transactionManager) => {
|
?.transaction(async (transactionManager) => {
|
||||||
const gmailClient: gmail_v1.Gmail =
|
|
||||||
await this.gmailClientProvider.getGmailClient(refreshToken);
|
|
||||||
|
|
||||||
const blocklistedEmails = await this.fetchBlocklistEmails(
|
|
||||||
connectedAccount.accountOwnerId,
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.fetchAllMessageIdsFromGmailAndStoreInCache(
|
await this.fetchAllMessageIdsFromGmailAndStoreInCache(
|
||||||
gmailClient,
|
gmailClient,
|
||||||
gmailMessageChannel.id,
|
gmailMessageChannel.id,
|
||||||
|
includedEmails || [],
|
||||||
blocklistedEmails,
|
blocklistedEmails,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
transactionManager,
|
transactionManager,
|
||||||
@ -150,6 +152,7 @@ export class GmailFullSyncService {
|
|||||||
public async fetchAllMessageIdsFromGmailAndStoreInCache(
|
public async fetchAllMessageIdsFromGmailAndStoreInCache(
|
||||||
gmailClient: gmail_v1.Gmail,
|
gmailClient: gmail_v1.Gmail,
|
||||||
messageChannelId: string,
|
messageChannelId: string,
|
||||||
|
includedEmails: string[],
|
||||||
blocklistedEmails: string[],
|
blocklistedEmails: string[],
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
transactionManager?: EntityManager,
|
transactionManager?: EntityManager,
|
||||||
@ -164,7 +167,7 @@ export class GmailFullSyncService {
|
|||||||
userId: 'me',
|
userId: 'me',
|
||||||
maxResults: GMAIL_USERS_MESSAGES_LIST_MAX_RESULT,
|
maxResults: GMAIL_USERS_MESSAGES_LIST_MAX_RESULT,
|
||||||
pageToken,
|
pageToken,
|
||||||
q: gmailSearchFilterExcludeEmails(blocklistedEmails),
|
q: gmailSearchFilterEmailAdresses(includedEmails, blocklistedEmails),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.data?.messages) {
|
if (response.data?.messages) {
|
||||||
|
|||||||
@ -0,0 +1,58 @@
|
|||||||
|
import {
|
||||||
|
excludedCategoriesAndFileTypesString,
|
||||||
|
gmailSearchFilterEmailAdresses,
|
||||||
|
gmailSearchFilterExcludeEmailAdresses,
|
||||||
|
gmailSearchFilterIncludeOnlyEmailAdresses,
|
||||||
|
gmailSearchFilterNonPersonalEmails,
|
||||||
|
} from 'src/modules/messaging/utils/gmail-search-filter.util';
|
||||||
|
|
||||||
|
describe('gmailSearchFilterExcludeEmailAdresses', () => {
|
||||||
|
it('should return correct search filter for excluding emails', () => {
|
||||||
|
const emails = ['hello@twenty.com', 'hey@twenty.com'];
|
||||||
|
const result = gmailSearchFilterExcludeEmailAdresses(emails);
|
||||||
|
|
||||||
|
expect(result).toBe(
|
||||||
|
`(in:inbox from:-(${gmailSearchFilterNonPersonalEmails}|hello@twenty.com|hey@twenty.com)|(in:sent to:-(${gmailSearchFilterNonPersonalEmails}|hello@twenty.com|hey@twenty.com)) ${excludedCategoriesAndFileTypesString}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('should return correct search filter for excluding emails when no emails are provided', () => {
|
||||||
|
const result = gmailSearchFilterExcludeEmailAdresses();
|
||||||
|
|
||||||
|
expect(result).toBe(
|
||||||
|
`from:-(${gmailSearchFilterNonPersonalEmails}) ${excludedCategoriesAndFileTypesString}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('gmailSearchFilterIncludeOnlyEmailAdresses', () => {
|
||||||
|
it('should return correct search filter for including emails', () => {
|
||||||
|
const emails = ['hello@twenty.com', 'hey@twenty.com'];
|
||||||
|
const result = gmailSearchFilterIncludeOnlyEmailAdresses(emails);
|
||||||
|
|
||||||
|
expect(result).toBe(
|
||||||
|
`(in:inbox from:(hello@twenty.com|hey@twenty.com)|(in:sent to:(hello@twenty.com|hey@twenty.com)) ${excludedCategoriesAndFileTypesString}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('should return undefined when no emails are provided', () => {
|
||||||
|
const result = gmailSearchFilterIncludeOnlyEmailAdresses();
|
||||||
|
|
||||||
|
expect(result).toBe(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('gmailSearchFilterEmailAdresses', () => {
|
||||||
|
it('should return correct search filter for including emails and excluding emails', () => {
|
||||||
|
const includedEmails = ['hello@twenty.com', 'hey@twenty.com'];
|
||||||
|
|
||||||
|
const excludedEmails = ['noreply@twenty.com', 'no-reply@twenty.com'];
|
||||||
|
|
||||||
|
const result = gmailSearchFilterEmailAdresses(
|
||||||
|
includedEmails,
|
||||||
|
excludedEmails,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe(
|
||||||
|
`(in:inbox from:((hello@twenty.com|hey@twenty.com) -(${gmailSearchFilterNonPersonalEmails}|noreply@twenty.com|no-reply@twenty.com))|(in:sent to:((hello@twenty.com|hey@twenty.com) -(${gmailSearchFilterNonPersonalEmails}|noreply@twenty.com|no-reply@twenty.com)) ${excludedCategoriesAndFileTypesString}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,14 +1,59 @@
|
|||||||
export const gmailSearchFilterNonPersonalEmails =
|
export const gmailSearchFilterNonPersonalEmails =
|
||||||
'*noreply@|*no-reply@|*do_not_reply@|*no.reply@|*info@|*contact@|*hello@|*support@|*feedback@|*service@|*help@';
|
'*noreply@|*no-reply@|*do_not_reply@|*no.reply@|*info@|*contact@|*hello@|*support@|*feedback@|*service@|*help@';
|
||||||
|
|
||||||
export const gmailSearchFilterExcludeEmails = (emails: string[]): string => {
|
export const excludedCategories = ['promotions', 'social', 'forums'];
|
||||||
if (emails.length === 0) {
|
|
||||||
return `from:-(${gmailSearchFilterNonPersonalEmails}) -category:promotions -category:social -category:forums -filename:.ics`;
|
export const excludedFileTypes = ['.ics'];
|
||||||
|
|
||||||
|
export const excludedCategoriesAndFileTypesString = `-category:${excludedCategories.join(
|
||||||
|
' -category:',
|
||||||
|
)} -filename:${excludedFileTypes.join(' -filename:')}`;
|
||||||
|
|
||||||
|
export const gmailSearchFilterExcludeEmailAdresses = (
|
||||||
|
emails?: string[],
|
||||||
|
): string => {
|
||||||
|
if (!emails || emails.length === 0) {
|
||||||
|
return `from:-(${gmailSearchFilterNonPersonalEmails}) ${excludedCategoriesAndFileTypesString}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `(in:inbox from:-(${gmailSearchFilterNonPersonalEmails}|${emails.join(
|
return `(in:inbox from:-(${gmailSearchFilterNonPersonalEmails}|${emails.join(
|
||||||
'|',
|
'|',
|
||||||
)})|(in:sent to:-(${gmailSearchFilterNonPersonalEmails}|${emails.join(
|
)})|(in:sent to:-(${gmailSearchFilterNonPersonalEmails}|${emails.join(
|
||||||
'|',
|
'|',
|
||||||
)})) -category:promotions -category:social -category:forums -filename:.ics`;
|
)})) ${excludedCategoriesAndFileTypesString}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const gmailSearchFilterIncludeOnlyEmailAdresses = (
|
||||||
|
emails?: string[],
|
||||||
|
): string | undefined => {
|
||||||
|
if (!emails || emails.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `(in:inbox from:(${emails.join('|')})|(in:sent to:(${emails.join(
|
||||||
|
'|',
|
||||||
|
)})) ${excludedCategoriesAndFileTypesString}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const gmailSearchFilterEmailAdresses = (
|
||||||
|
includedEmails?: string[] | undefined,
|
||||||
|
excludedEmails?: string[] | undefined,
|
||||||
|
): string | undefined => {
|
||||||
|
if (!includedEmails || includedEmails.length === 0) {
|
||||||
|
return gmailSearchFilterExcludeEmailAdresses(excludedEmails);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!excludedEmails || excludedEmails.length === 0) {
|
||||||
|
return gmailSearchFilterIncludeOnlyEmailAdresses(includedEmails);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `(in:inbox from:((${includedEmails.join(
|
||||||
|
'|',
|
||||||
|
)}) -(${gmailSearchFilterNonPersonalEmails}|${excludedEmails.join(
|
||||||
|
'|',
|
||||||
|
)}))|(in:sent to:((${includedEmails.join(
|
||||||
|
'|',
|
||||||
|
)}) -(${gmailSearchFilterNonPersonalEmails}|${excludedEmails.join(
|
||||||
|
'|',
|
||||||
|
)})) ${excludedCategoriesAndFileTypesString}`;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user