fix: add sending out email campaigns

This commit is contained in:
Karol Sójko
2022-06-27 10:43:23 +02:00
parent 6f88a96c3e
commit bf14ec05f9
15 changed files with 330 additions and 17 deletions
Generated
+53 -3
View File
@@ -2499,6 +2499,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["jsonwebtoken", "npm:8.5.1"]\
],\
"linkType": "HARD"\
}],\
["npm:3.19.4", {\
"packageLocation": "./.yarn/cache/@standardnotes-auth-npm-3.19.4-29b35c1352-84ac9e669e.zip/node_modules/@standardnotes/auth/",\
"packageDependencies": [\
["@standardnotes/auth", "npm:3.19.4"],\
["@standardnotes/common", "npm:1.23.1"],\
["jsonwebtoken", "npm:8.5.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["@standardnotes/auth-server", [\
@@ -2511,13 +2520,13 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["@standardnotes/analytics", "npm:1.6.0"],\
["@standardnotes/api", "npm:1.1.13"],\
["@standardnotes/auth", "npm:3.19.3"],\
["@standardnotes/common", "npm:1.23.0"],\
["@standardnotes/domain-events", "npm:2.32.2"],\
["@standardnotes/common", "npm:1.23.1"],\
["@standardnotes/domain-events", "npm:2.32.4"],\
["@standardnotes/domain-events-infra", "npm:1.5.2"],\
["@standardnotes/features", "npm:1.45.5"],\
["@standardnotes/responses", "npm:1.6.36"],\
["@standardnotes/scheduler", "npm:1.1.1"],\
["@standardnotes/settings", "npm:1.14.3"],\
["@standardnotes/settings", "npm:1.15.0"],\
["@standardnotes/sncrypto-common", "npm:1.9.0"],\
["@standardnotes/sncrypto-node", "npm:1.8.3"],\
["@standardnotes/time", "npm:1.7.0"],\
@@ -2568,6 +2577,13 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["@standardnotes/common", "npm:1.23.0"]\
],\
"linkType": "HARD"\
}],\
["npm:1.23.1", {\
"packageLocation": "./.yarn/cache/@standardnotes-common-npm-1.23.1-ed73dbb679-f498f4c469.zip/node_modules/@standardnotes/common/",\
"packageDependencies": [\
["@standardnotes/common", "npm:1.23.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["@standardnotes/config", [\
@@ -2610,6 +2626,16 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["@standardnotes/scheduler", "npm:1.1.1"]\
],\
"linkType": "HARD"\
}],\
["npm:2.32.4", {\
"packageLocation": "./.yarn/cache/@standardnotes-domain-events-npm-2.32.4-0b0cc85196-9f8a1637aa.zip/node_modules/@standardnotes/domain-events/",\
"packageDependencies": [\
["@standardnotes/domain-events", "npm:2.32.4"],\
["@standardnotes/auth", "npm:3.19.4"],\
["@standardnotes/features", "npm:1.45.6"],\
["@standardnotes/scheduler", "npm:1.1.2"]\
],\
"linkType": "HARD"\
}]\
]],\
["@standardnotes/domain-events-infra", [\
@@ -2663,6 +2689,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["@standardnotes/common", "npm:1.23.0"]\
],\
"linkType": "HARD"\
}],\
["npm:1.45.6", {\
"packageLocation": "./.yarn/cache/@standardnotes-features-npm-1.45.6-6f88b4fd2a-572780d6e2.zip/node_modules/@standardnotes/features/",\
"packageDependencies": [\
["@standardnotes/features", "npm:1.45.6"],\
["@standardnotes/auth", "npm:3.19.4"],\
["@standardnotes/common", "npm:1.23.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["@standardnotes/files-server", [\
@@ -2760,6 +2795,14 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["@standardnotes/common", "npm:1.23.0"]\
],\
"linkType": "HARD"\
}],\
["npm:1.1.2", {\
"packageLocation": "./.yarn/cache/@standardnotes-scheduler-npm-1.1.2-78f89314fb-68642a0874.zip/node_modules/@standardnotes/scheduler/",\
"packageDependencies": [\
["@standardnotes/scheduler", "npm:1.1.2"],\
["@standardnotes/common", "npm:1.23.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["@standardnotes/scheduler-server", [\
@@ -2842,6 +2885,13 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["@standardnotes/settings", "npm:1.14.3"]\
],\
"linkType": "HARD"\
}],\
["npm:1.15.0", {\
"packageLocation": "./.yarn/cache/@standardnotes-settings-npm-1.15.0-bfec86ee49-4397d453a1.zip/node_modules/@standardnotes/settings/",\
"packageDependencies": [\
["@standardnotes/settings", "npm:1.15.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["@standardnotes/sncrypto-common", [\
+139
View File
@@ -0,0 +1,139 @@
import 'reflect-metadata'
import 'newrelic'
import { Stream } from 'stream'
import { Logger } from 'winston'
import * as dayjs from 'dayjs'
import * as utc from 'dayjs/plugin/utc'
import { UserRepositoryInterface } from '../src/Domain/User/UserRepositoryInterface'
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
import TYPES from '../src/Bootstrap/Types'
import { Env } from '../src/Bootstrap/Env'
import { SettingServiceInterface } from '../src/Domain/Setting/SettingServiceInterface'
import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
import { UserSubscriptionRepositoryInterface } from '../src/Domain/Subscription/UserSubscriptionRepositoryInterface'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { MuteMarketingEmailsOption, SettingName } from '@standardnotes/settings'
import { EmailMessageIdentifier } from '@standardnotes/common'
import { User } from '../src/Domain/User/User'
import { EncryptionVersion } from '../src/Domain/Encryption/EncryptionVersion'
import { TimerInterface } from '@standardnotes/time'
const inputArgs = process.argv.slice(2)
const emailMessageIdentifier = inputArgs[0]
const sendEmailCampaign = async (
userRepository: UserRepositoryInterface,
settingService: SettingServiceInterface,
userSubscriptionRepository: UserSubscriptionRepositoryInterface,
timer: TimerInterface,
domainEventFactory: DomainEventFactoryInterface,
domainEventPublisher: DomainEventPublisherInterface,
): Promise<void> => {
const stream = await userRepository.streamAll()
return new Promise((resolve, reject) => {
stream
.pipe(
new Stream.Transform({
objectMode: true,
transform: async (rawUserData, _encoding, callback) => {
const emailsMutedSetting = await settingService.findSettingWithDecryptedValue({
userUuid: rawUserData.user_uuid,
settingName: SettingName.MuteMarketingEmails,
})
if (emailsMutedSetting === null) {
const user = (await userRepository.findOneByUuid(rawUserData.user_uuid)) as User
await settingService.createOrReplace({
user,
props: {
name: SettingName.MuteMarketingEmails,
unencryptedValue: MuteMarketingEmailsOption.NotMuted,
serverEncryptionVersion: EncryptionVersion.Default,
sensitive: false,
},
})
}
if (emailsMutedSetting?.value === MuteMarketingEmailsOption.Muted) {
callback()
return
}
let activeSubscription = false
let subscriptionPlanName = null
const userSubscription = await userSubscriptionRepository.findOneByUserUuid(rawUserData.user_uuid)
if (userSubscription !== null) {
activeSubscription =
!userSubscription.cancelled && userSubscription.endsAt > timer.getTimestampInMicroseconds()
subscriptionPlanName = userSubscription.planName
}
await domainEventPublisher.publish(
domainEventFactory.createEmailMessageRequestedEvent({
userEmail: rawUserData.user_email,
messageIdentifier: emailMessageIdentifier as EmailMessageIdentifier,
context: {
activeSubscription,
subscriptionPlanName,
},
}),
)
callback()
},
}),
)
.on('finish', resolve)
.on('error', reject)
})
}
const container = new ContainerConfigLoader()
void container.load().then((container) => {
dayjs.extend(utc)
const env: Env = new Env()
env.load()
const logger: Logger = container.get(TYPES.Logger)
logger.info(`Starting email campaign for email ${emailMessageIdentifier} ...`)
const userRepository: UserRepositoryInterface = container.get(TYPES.UserRepository)
const settingService: SettingServiceInterface = container.get(TYPES.SettingService)
const userSubscriptionRepository: UserSubscriptionRepositoryInterface = container.get(
TYPES.UserSubscriptionRepository,
)
const timer: TimerInterface = container.get(TYPES.Timer)
const domainEventFactory: DomainEventFactoryInterface = container.get(TYPES.DomainEventFactory)
const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.DomainEventPublisher)
Promise.resolve(
sendEmailCampaign(
userRepository,
settingService,
userSubscriptionRepository,
timer,
domainEventFactory,
domainEventPublisher,
),
)
.then(() => {
logger.info(`${emailMessageIdentifier} email campaign complete.`)
process.exit(0)
})
.catch((error) => {
logger.error(`Could not finish ${emailMessageIdentifier} email campaign: ${error.message}`)
process.exit(1)
})
})
+3 -3
View File
@@ -33,13 +33,13 @@
"@standardnotes/analytics": "^1.6.0",
"@standardnotes/api": "^1.1.13",
"@standardnotes/auth": "^3.19.2",
"@standardnotes/common": "^1.23.0",
"@standardnotes/domain-events": "^2.31.1",
"@standardnotes/common": "^1.23.1",
"@standardnotes/domain-events": "^2.32.4",
"@standardnotes/domain-events-infra": "^1.4.135",
"@standardnotes/features": "^1.45.2",
"@standardnotes/responses": "^1.6.15",
"@standardnotes/scheduler": "^1.1.1",
"@standardnotes/settings": "^1.14.2",
"@standardnotes/settings": "^1.15.0",
"@standardnotes/sncrypto-common": "^1.8.1",
"@standardnotes/sncrypto-node": "^1.8.1",
"@standardnotes/time": "^1.6.8",
@@ -1,6 +1,6 @@
import 'reflect-metadata'
import { RoleName } from '@standardnotes/common'
import { EmailMessageIdentifier, RoleName } from '@standardnotes/common'
import { PredicateName, PredicateAuthority, PredicateVerificationResult } from '@standardnotes/scheduler'
import { TimerInterface } from '@standardnotes/time'
@@ -18,6 +18,35 @@ describe('DomainEventFactory', () => {
timer.getUTCDate = jest.fn().mockReturnValue(new Date(1))
})
it('should create a EMAIL_MESSAGE_REQUESTED event', () => {
expect(
createFactory().createEmailMessageRequestedEvent({
userEmail: '[email protected]',
messageIdentifier: EmailMessageIdentifier.ENCOURAGE_EMAIL_BACKUPS,
context: {
foo: 'bar',
},
}),
).toEqual({
createdAt: expect.any(Date),
meta: {
correlation: {
userIdentifier: '[email protected]',
userIdentifierType: 'email',
},
origin: 'auth',
},
payload: {
messageIdentifier: 'ENCOURAGE_EMAIL_BACKUPS',
userEmail: '[email protected]',
context: {
foo: 'bar',
},
},
type: 'EMAIL_MESSAGE_REQUESTED',
})
})
it('should create a PREDICATE_VERIFIED event', () => {
expect(
createFactory().createPredicateVerifiedEvent({
@@ -1,4 +1,4 @@
import { RoleName, Uuid } from '@standardnotes/common'
import { EmailMessageIdentifier, RoleName, Uuid } from '@standardnotes/common'
import {
AccountDeletionRequestedEvent,
UserEmailChangedEvent,
@@ -14,6 +14,7 @@ import {
SharedSubscriptionInvitationCanceledEvent,
PredicateVerifiedEvent,
DomainEventService,
EmailMessageRequestedEvent,
} from '@standardnotes/domain-events'
import { Predicate, PredicateVerificationResult } from '@standardnotes/scheduler'
import { TimerInterface } from '@standardnotes/time'
@@ -26,6 +27,25 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
export class DomainEventFactory implements DomainEventFactoryInterface {
constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
createEmailMessageRequestedEvent(dto: {
userEmail: string
messageIdentifier: EmailMessageIdentifier
context: Record<string, unknown>
}): EmailMessageRequestedEvent {
return {
type: 'EMAIL_MESSAGE_REQUESTED',
createdAt: this.timer.getUTCDate(),
meta: {
correlation: {
userIdentifier: dto.userEmail,
userIdentifierType: 'email',
},
origin: DomainEventService.Auth,
},
payload: dto,
}
}
createPredicateVerifiedEvent(dto: {
userUuid: Uuid
predicate: Predicate
@@ -1,4 +1,4 @@
import { Uuid, RoleName } from '@standardnotes/common'
import { Uuid, RoleName, EmailMessageIdentifier } from '@standardnotes/common'
import { Predicate, PredicateVerificationResult } from '@standardnotes/scheduler'
import {
AccountDeletionRequestedEvent,
@@ -14,10 +14,16 @@ import {
SharedSubscriptionInvitationCreatedEvent,
SharedSubscriptionInvitationCanceledEvent,
PredicateVerifiedEvent,
EmailMessageRequestedEvent,
} from '@standardnotes/domain-events'
import { InviteeIdentifierType } from '../SharedSubscription/InviteeIdentifierType'
export interface DomainEventFactoryInterface {
createEmailMessageRequestedEvent(dto: {
userEmail: string
messageIdentifier: EmailMessageIdentifier
context: Record<string, unknown>
}): EmailMessageRequestedEvent
createUserSignedInEvent(dto: {
userUuid: string
userEmail: string
@@ -39,13 +39,13 @@ describe('SettingsAssociationService', () => {
it('should return the default set of settings for a newly registered user', () => {
const settings = createService().getDefaultSettingsAndValuesForNewUser()
const flatSettings = [...(settings as Map<SettingName, SettingDescription>).keys()]
expect(flatSettings).toEqual(['MUTE_SIGN_IN_EMAILS', 'LOG_SESSION_USER_AGENT'])
expect(flatSettings).toEqual(['MUTE_SIGN_IN_EMAILS', 'MUTE_MARKETING_EMAILS', 'LOG_SESSION_USER_AGENT'])
})
it('should return the default set of settings for a newly registered vault account', () => {
const settings = createService().getDefaultSettingsAndValuesForNewVaultAccount()
const flatSettings = [...(settings as Map<SettingName, SettingDescription>).keys()]
expect(flatSettings).toEqual(['MUTE_SIGN_IN_EMAILS', 'LOG_SESSION_USER_AGENT'])
expect(flatSettings).toEqual(['MUTE_SIGN_IN_EMAILS', 'MUTE_MARKETING_EMAILS', 'LOG_SESSION_USER_AGENT'])
expect(settings.get(SettingName.LogSessionUserAgent)?.value).toEqual('disabled')
})
@@ -1,5 +1,10 @@
import { PermissionName } from '@standardnotes/features'
import { LogSessionUserAgentOption, MuteSignInEmailsOption, SettingName } from '@standardnotes/settings'
import {
LogSessionUserAgentOption,
MuteMarketingEmailsOption,
MuteSignInEmailsOption,
SettingName,
} from '@standardnotes/settings'
import { injectable } from 'inversify'
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
@@ -14,6 +19,7 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt
SettingName.MuteFailedBackupsEmails,
SettingName.MuteFailedCloudBackupsEmails,
SettingName.MuteSignInEmails,
SettingName.MuteMarketingEmails,
SettingName.DropboxBackupFrequency,
SettingName.GoogleDriveBackupFrequency,
SettingName.OneDriveBackupFrequency,
@@ -28,6 +34,7 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt
SettingName.MuteFailedBackupsEmails,
SettingName.MuteFailedCloudBackupsEmails,
SettingName.MuteSignInEmails,
SettingName.MuteMarketingEmails,
SettingName.ListedAuthorSecrets,
SettingName.LogSessionUserAgent,
]
@@ -47,6 +54,14 @@ export class SettingsAssociationService implements SettingsAssociationServiceInt
value: MuteSignInEmailsOption.NotMuted,
},
],
[
SettingName.MuteMarketingEmails,
{
sensitive: false,
serverEncryptionVersion: EncryptionVersion.Unencrypted,
value: MuteMarketingEmailsOption.NotMuted,
},
],
[
SettingName.LogSessionUserAgent,
{
+59 -5
View File
@@ -1820,13 +1820,13 @@ __metadata:
"@standardnotes/analytics": ^1.6.0
"@standardnotes/api": ^1.1.13
"@standardnotes/auth": ^3.19.2
"@standardnotes/common": ^1.23.0
"@standardnotes/domain-events": ^2.31.1
"@standardnotes/common": ^1.23.1
"@standardnotes/domain-events": ^2.32.4
"@standardnotes/domain-events-infra": ^1.4.135
"@standardnotes/features": ^1.45.2
"@standardnotes/responses": ^1.6.15
"@standardnotes/scheduler": ^1.1.1
"@standardnotes/settings": ^1.14.2
"@standardnotes/settings": ^1.15.0
"@standardnotes/sncrypto-common": ^1.8.1
"@standardnotes/sncrypto-node": ^1.8.1
"@standardnotes/time": ^1.6.8
@@ -1889,6 +1889,16 @@ __metadata:
languageName: node
linkType: hard
"@standardnotes/auth@npm:^3.19.4":
version: 3.19.4
resolution: "@standardnotes/auth@npm:3.19.4"
dependencies:
"@standardnotes/common": ^1.23.1
jsonwebtoken: ^8.5.1
checksum: 84ac9e669ee393a8ac04c49729cd732280ab7ad9c6ad780e1f8cc7e02bdd1d15b8446d84134e775d805e6fa04e9500d9e62d7f857e6b1f5bb36f10a0c31640ff
languageName: node
linkType: hard
"@standardnotes/common@npm:^1.19.1, @standardnotes/common@npm:^1.19.4, @standardnotes/common@npm:^1.22.0, @standardnotes/common@npm:^1.23.0":
version: 1.23.0
resolution: "@standardnotes/common@npm:1.23.0"
@@ -1896,6 +1906,13 @@ __metadata:
languageName: node
linkType: hard
"@standardnotes/common@npm:^1.23.1":
version: 1.23.1
resolution: "@standardnotes/common@npm:1.23.1"
checksum: f498f4c469f7b9cdfc08b7648077832a2a214d844f012d02d73c2c418fab221af3302570cc6b1957b5ed481b55237f95bc24fc5c231162bfcdf8730333d38405
languageName: node
linkType: hard
"@standardnotes/config@npm:2.0.1":
version: 2.0.1
resolution: "@standardnotes/config@npm:2.0.1"
@@ -1946,7 +1963,7 @@ __metadata:
languageName: node
linkType: hard
"@standardnotes/domain-events@npm:^2.27.6, @standardnotes/domain-events@npm:^2.29.0, @standardnotes/domain-events@npm:^2.31.1, @standardnotes/domain-events@npm:^2.32.2":
"@standardnotes/domain-events@npm:^2.27.6, @standardnotes/domain-events@npm:^2.29.0, @standardnotes/domain-events@npm:^2.32.2":
version: 2.32.2
resolution: "@standardnotes/domain-events@npm:2.32.2"
dependencies:
@@ -1968,6 +1985,17 @@ __metadata:
languageName: node
linkType: hard
"@standardnotes/domain-events@npm:^2.32.4":
version: 2.32.4
resolution: "@standardnotes/domain-events@npm:2.32.4"
dependencies:
"@standardnotes/auth": ^3.19.4
"@standardnotes/features": ^1.45.6
"@standardnotes/scheduler": ^1.1.2
checksum: 9f8a1637aa63d04f2c60e1a4c75e5fc5f4013f4a5e39b9cd55d56a894220cee47390e8b37422b4c128db05d231ab371e8a1caa4e1af3395bf04319697dae7303
languageName: node
linkType: hard
"@standardnotes/encryption@npm:^1.8.19":
version: 1.8.19
resolution: "@standardnotes/encryption@npm:1.8.19"
@@ -1989,6 +2017,16 @@ __metadata:
languageName: node
linkType: hard
"@standardnotes/features@npm:^1.45.6":
version: 1.45.6
resolution: "@standardnotes/features@npm:1.45.6"
dependencies:
"@standardnotes/auth": ^3.19.4
"@standardnotes/common": ^1.23.1
checksum: 572780d6e24efa664f316f6cf56c01907e00263db6204fee17f7ce9e43fa376411f373863dda59f8fb039250a224b7f8637fbd0bcbd7241112e061accbc05360
languageName: node
linkType: hard
"@standardnotes/files-server@workspace:packages/files":
version: 0.0.0-use.local
resolution: "@standardnotes/files-server@workspace:packages/files"
@@ -2112,6 +2150,15 @@ __metadata:
languageName: node
linkType: hard
"@standardnotes/scheduler@npm:^1.1.2":
version: 1.1.2
resolution: "@standardnotes/scheduler@npm:1.1.2"
dependencies:
"@standardnotes/common": ^1.23.1
checksum: 68642a08741aeb4936e4a69e43e2b85e5cbfa3ce60006d292cee517d33093edaf35a6a7af61343d85ab3474d6e980a073a2779f6e0b1f11805ab06a407b15ce7
languageName: node
linkType: hard
"@standardnotes/server-monorepo@workspace:.":
version: 0.0.0-use.local
resolution: "@standardnotes/server-monorepo@workspace:."
@@ -2148,13 +2195,20 @@ __metadata:
languageName: node
linkType: hard
"@standardnotes/settings@npm:1.14.3, @standardnotes/settings@npm:^1.14.2":
"@standardnotes/settings@npm:1.14.3":
version: 1.14.3
resolution: "@standardnotes/settings@npm:1.14.3"
checksum: 60fbb2ca856083b1afdf3a93cfb2729151c8a4a34f02564a8a814a7caad9982285303ad79ffb1da7643046061a6efe39410e57e8e4703a8963be8dc0ffabeaa6
languageName: node
linkType: hard
"@standardnotes/settings@npm:^1.15.0":
version: 1.15.0
resolution: "@standardnotes/settings@npm:1.15.0"
checksum: 4397d453a1a0cd5554012a5bf5ee2025c102ffc6d742841eecd2ab9cd46b0fbfe78da61b401451646274887d92801cafcef4c283bbfc6074b4d152103e15f861
languageName: node
linkType: hard
"@standardnotes/sncrypto-common@npm:^1.3.0, @standardnotes/sncrypto-common@npm:^1.8.1, @standardnotes/sncrypto-common@npm:^1.9.0":
version: 1.9.0
resolution: "@standardnotes/sncrypto-common@npm:1.9.0"