// // Copyright © 2020, 2021 Anticrm Platform Contributors. // Copyright © 2021, 2022 Hardcore Engineering Inc. // // Licensed under the Eclipse Public License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. You may // obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // // See the License for the specific language governing permissions and // limitations under the License. // import activity, { ActivityMessage, DocUpdateMessage } from '@hcengineering/activity' import chunter, { ChatMessage } from '@hcengineering/chunter' import contact, { getAvatarProviderId, getGravatarUrl, Person, PersonAccount, type AvatarInfo } from '@hcengineering/contact' import core, { Account, AnyAttribute, ArrOf, AttachedDoc, Class, Collection, concatLink, Data, Doc, DocumentUpdate, generateId, MeasureContext, MixinUpdate, Ref, RefTo, SortingOrder, Space, Timestamp, toIdMap, Tx, TxCollectionCUD, TxCreateDoc, TxCUD, TxMixin, TxProcessor, TxRemoveDoc, TxUpdateDoc } from '@hcengineering/core' import notification, { ActivityInboxNotification, BaseNotificationType, BrowserNotification, ClassCollaborators, Collaborators, CommonInboxNotification, DocNotifyContext, InboxNotification, MentionInboxNotification, notificationId, NotificationStatus, NotificationType, PushData, PushSubscription } from '@hcengineering/notification' import { getMetadata, getResource, translate } from '@hcengineering/platform' import serverCore, { type TriggerControl } from '@hcengineering/server-core' import serverNotification, { getPersonAccountById, NOTIFICATION_BODY_SIZE, PUSH_NOTIFICATION_TITLE_SIZE, ReceiverInfo, SenderInfo } from '@hcengineering/server-notification' import serverView from '@hcengineering/server-view' import { markupToText, stripTags } from '@hcengineering/text' import { encodeObjectURI } from '@hcengineering/view' import { workbenchId } from '@hcengineering/workbench' import webpush, { WebPushError } from 'web-push' import { Content, ContextsCache, ContextsCacheKey, NotifyParams, NotifyResult } from './types' import { createPullCollaboratorsTx, createPushCollaboratorsTx, getHTMLPresenter, getNotificationContent, getNotificationLink, getNotificationProviderControl, getTextPresenter, getUsersInfo, isAllowed, isMixinTx, isShouldNotifyTx, isUserEmployeeInFieldValueTypeMatch, isUserInFieldValueTypeMatch, messageToMarkup, replaceAll, toReceiverInfo, updateNotifyContextsSpace, type NotificationProviderControl } from './utils' export function getPushCollaboratorTx ( control: TriggerControl, user: Ref, doc: Doc ): TxMixin | undefined { const mixin = control.hierarchy.as(doc, notification.mixin.Collaborators) if (mixin.collaborators === undefined || !mixin.collaborators.includes(user)) { return control.txFactory.createTxMixin(doc._id, doc._class, doc.space, notification.mixin.Collaborators, { $push: { collaborators: user } }) } return undefined } export async function getCommonNotificationTxes ( ctx: MeasureContext, control: TriggerControl, doc: Doc, data: Partial>, receiver: ReceiverInfo, sender: SenderInfo, attachedTo: Ref, attachedToClass: Ref>, space: Ref, modifiedOn: Timestamp, notifyResult: NotifyResult, _class = notification.class.CommonInboxNotification, tx?: TxCUD ): Promise { if (notifyResult.size === 0 || !notifyResult.has(notification.providers.InboxNotificationProvider)) { return [] } const res: Tx[] = [] const notifyContexts = await control.findAll(ctx, notification.class.DocNotifyContext, { objectId: attachedTo }) const notificationTx = await pushInboxNotifications( ctx, control, res, receiver, sender, attachedTo, attachedToClass, space, notifyContexts, data, _class, modifiedOn, true, tx ) if (notificationTx !== undefined) { const notificationData = TxProcessor.createDoc2Doc(notificationTx) const subscriptions = await control.findAll(ctx, notification.class.PushSubscription, { user: receiver._id }) await applyNotificationProviders( notificationData, notifyResult, attachedTo, attachedToClass, control, res, doc, receiver, sender, subscriptions, _class ) } return res } async function getTextPart (doc: Doc, control: TriggerControl): Promise { const TextPresenter = getTextPresenter(doc._class, control.hierarchy) if (TextPresenter === undefined) return return await ( await getResource(TextPresenter.presenter) )(doc, control) } async function getHtmlPart (doc: Doc, control: TriggerControl): Promise { const HTMLPresenter = getHTMLPresenter(doc._class, control.hierarchy) return HTMLPresenter != null ? await (await getResource(HTMLPresenter.presenter))(doc, control) : undefined } function fillTemplate ( template: string, sender: string, doc: string, data: string, params: Record = {} ): string { let res = replaceAll(template, '{sender}', sender) res = replaceAll(res, '{doc}', doc) res = replaceAll(res, '{data}', data) for (const key in params) { res = replaceAll(res, `{${key}}`, params[key]) } return res } /** * @public */ export async function getContentByTemplate ( doc: Doc | undefined, sender: string, type: Ref, control: TriggerControl, data: string, notificationData?: InboxNotification, message?: ActivityMessage ): Promise { if (doc === undefined) return const notificationType = control.modelDb.getObject(type) if (notificationType.templates === undefined) return const textPart = await getTextPart(doc, control) if (textPart === undefined) return const params: Record = notificationData !== undefined ? await getTranslatedNotificationContent(notificationData, notificationData._class, control) : {} if ( notificationData !== undefined && control.hierarchy.isDerived(notificationData._class, notification.class.MentionInboxNotification) ) { const messageContent = (notificationData as MentionInboxNotification).messageHtml const text = messageContent !== undefined ? markupToText(messageContent) : undefined params.body = text ?? params.body params.message = text ?? params.message } if (message !== undefined) { const markup = await messageToMarkup(control, message) params.message = markup !== undefined ? markupToText(markup) : params.message ?? '' } else if (params.message === undefined) { params.message = params.body ?? '' } const link = await getNotificationLink(control, doc, message?._id) const app = control.branding?.title ?? 'Huly' const linkText = await translate(notification.string.ViewIn, { app }) params.link = `${linkText}` const text = fillTemplate(notificationType.templates.textTemplate, sender, textPart, data, params) const htmlPart = await getHtmlPart(doc, control) const html = fillTemplate(notificationType.templates.htmlTemplate, sender, htmlPart ?? textPart, data, params) const subject = fillTemplate(notificationType.templates.subjectTemplate, sender, textPart, data, params) if (subject === '') return return { text, html, subject } } function getValueCollaborators (value: any, attr: AnyAttribute, control: TriggerControl): Ref[] { const hierarchy = control.hierarchy if (attr.type._class === core.class.RefTo) { const to = (attr.type as RefTo).to if (hierarchy.isDerived(to, contact.class.Person)) { return (control.modelDb.getAccountByPersonId(value) as PersonAccount[]).map((it) => it._id) } else if (hierarchy.isDerived(to, core.class.Account)) { const acc = getPersonAccountById(value, control) return acc !== undefined ? [acc._id] : [] } } else if (attr.type._class === core.class.ArrOf) { const arrOf = (attr.type as ArrOf>).of if (arrOf._class === core.class.RefTo) { const to = (arrOf as RefTo).to if (hierarchy.isDerived(to, contact.class.Person)) { const employeeAccounts = control.modelDb.findAllSync(contact.class.PersonAccount, { person: { $in: Array.isArray(value) ? value : [value] } }) return employeeAccounts.map((p) => p._id) } else if (hierarchy.isDerived(to, core.class.Account)) { const employeeAccounts = control.modelDb.findAllSync(contact.class.PersonAccount, { _id: { $in: Array.isArray(value) ? value : [value] } }) return employeeAccounts.map((p) => p._id) } } } return [] } function getKeyCollaborators ( docClass: Ref>, value: any, field: string, control: TriggerControl ): Ref[] | undefined { if (value !== undefined && value !== null) { const attr = control.hierarchy.findAttribute(docClass, field) if (attr !== undefined) { return getValueCollaborators(value, attr, control) } } } /** * @public */ export async function getDocCollaborators ( ctx: MeasureContext, doc: Doc, mixin: ClassCollaborators, control: TriggerControl ): Promise[]> { const collaborators = new Set>() for (const field of mixin.fields) { const value = (doc as any)[field] const newCollaborators = await ctx.with('getKeyCollaborators', {}, async (ctx) => getKeyCollaborators(doc._class, value, field, control) ) if (newCollaborators !== undefined) { for (const newCollaborator of newCollaborators) { collaborators.add(newCollaborator) } } } return Array.from(collaborators.values()) } export async function pushInboxNotifications ( ctx: MeasureContext, control: TriggerControl, res: Tx[], receiver: ReceiverInfo, sender: SenderInfo, objectId: Ref, objectClass: Ref>, objectSpace: Ref, contexts: DocNotifyContext[], data: Partial>, _class: Ref>, modifiedOn: Timestamp, shouldUpdateTimestamp = true, tx?: TxCUD ): Promise | undefined> { const context = getDocNotifyContext(control, contexts, objectId, receiver._id) let docNotifyContextId: Ref if (context === undefined) { docNotifyContextId = await createNotifyContext( ctx, control, objectId, objectClass, objectSpace, receiver, sender._id, shouldUpdateTimestamp ? modifiedOn : undefined, tx ) } else { docNotifyContextId = context._id } const notificationData = { user: receiver._id, isViewed: false, docNotifyContext: docNotifyContextId, archived: false, ...data } const notificationTx = control.txFactory.createTxCreateDoc(_class, receiver.space, notificationData) res.push(notificationTx) return notificationTx } async function activityInboxNotificationToText ( doc: Data ): Promise<{ title: string, body: string, [key: string]: string }> { let title: string = '' let body: string = '' const params = doc.intlParams ?? {} if (doc.intlParamsNotLocalized != null && Object.keys(doc.intlParamsNotLocalized).length > 0) { for (const key in doc.intlParamsNotLocalized) { const val = doc.intlParamsNotLocalized[key] params[key] = await translate(val, params) } } if (doc.title != null) { title = await translate(doc.title, params) } if (doc.body != null) { body = await translate(doc.body, params) } return { ...params, title, body } } async function commonInboxNotificationToText ( doc: Data ): Promise<{ title: string, body: string, [key: string]: string }> { let title: string = '' let body: string = '' let params = doc.intlParams ?? {} if (doc.props != null) { params = { ...params, ...doc.props } } if (doc.intlParamsNotLocalized != null && Object.keys(doc.intlParamsNotLocalized).length > 0) { for (const key in doc.intlParamsNotLocalized) { const val = doc.intlParamsNotLocalized[key] params[key] = await translate(val, params) } } if (doc.header != null) { title = await translate(doc.header, params) } if (doc.messageHtml != null) { body = stripTags(doc.messageHtml, NOTIFICATION_BODY_SIZE) } if (doc.message != null) { body = await translate(doc.message, params) } return { ...params, title, body } } async function mentionInboxNotificationToText ( doc: Data, control: TriggerControl ): Promise<{ title: string, body: string, [key: string]: string }> { let obj = (await control.findAll(control.ctx, doc.mentionedInClass, { _id: doc.mentionedIn }, { limit: 1 }))[0] if (obj !== undefined) { if (control.hierarchy.isDerived(obj._class, chunter.class.ChatMessage)) { obj = ( await control.findAll( control.ctx, (obj as ChatMessage).attachedToClass, { _id: (obj as ChatMessage).attachedTo }, { limit: 1 } ) )[0] } if (obj !== undefined) { const textPresenter = getTextPresenter(obj._class, control.hierarchy) if (textPresenter !== undefined) { const textPresenterFunc = await getResource(textPresenter.presenter) const title = await textPresenterFunc(obj, control) doc.intlParams = { ...doc.intlParams, title } } } } return await commonInboxNotificationToText(doc) } export async function getTranslatedNotificationContent ( data: Data, _class: Ref>, control: TriggerControl ): Promise<{ title: string, body: string, [key: string]: string }> { if (control.hierarchy.isDerived(_class, notification.class.ActivityInboxNotification)) { return await activityInboxNotificationToText(data as Data) } else if (control.hierarchy.isDerived(_class, notification.class.MentionInboxNotification)) { return await mentionInboxNotificationToText(data as Data, control) } else if (control.hierarchy.isDerived(_class, notification.class.CommonInboxNotification)) { return await commonInboxNotificationToText(data as Data) } return { title: '', body: '' } } function isReactionMessage (message?: ActivityMessage): boolean { return ( message !== undefined && message._class === activity.class.DocUpdateMessage && (message as DocUpdateMessage).objectClass === activity.class.Reaction ) } export async function createPushFromInbox ( control: TriggerControl, receiver: ReceiverInfo, attachedTo: Ref, attachedToClass: Ref>, data: Data, _class: Ref>, sender: SenderInfo, _id: Ref, subscriptions: PushSubscription[], message?: ActivityMessage ): Promise { let { title, body } = await getTranslatedNotificationContent(data, _class, control) if (title === '' || body === '') { return } title = title.slice(0, PUSH_NOTIFICATION_TITLE_SIZE) const senderPerson = sender.person const linkProviders = control.modelDb.findAllSync(serverView.mixin.ServerLinkIdProvider, {}) const provider = linkProviders.find(({ _id }) => _id === attachedToClass) let id: string = attachedTo if (provider !== undefined) { const encodeFn = await getResource(provider.encode) const doc = (await control.findAll(control.ctx, attachedToClass, { _id: attachedTo }))[0] if (doc === undefined) { return } id = await encodeFn(doc, control) } const path = [workbenchId, control.workspace.workspaceUrl, notificationId, encodeObjectURI(id, attachedToClass)] await createPushNotification( control, receiver._id as Ref, title, body, _id, subscriptions, senderPerson, path ) return control.txFactory.createTxCreateDoc(notification.class.BrowserNotification, receiver.space, { user: receiver._id, status: NotificationStatus.New, title, body, senderId: sender._id, tag: _id, objectId: attachedTo, objectClass: attachedToClass, messageId: isReactionMessage(message) ? (message?.attachedTo as Ref) : message?._id, messageClass: isReactionMessage(message) ? (message?.attachedToClass as Ref>) : message?._class, onClickLocation: { path } }) } export async function createPushNotification ( control: TriggerControl, target: Ref, title: string, body: string, _id: string, subscriptions: PushSubscription[], senderAvatar?: Data, path?: string[] ): Promise { const publicKey = getMetadata(notification.metadata.PushPublicKey) const privateKey = getMetadata(serverNotification.metadata.PushPrivateKey) const subject = getMetadata(serverNotification.metadata.PushSubject) ?? 'mailto:hey@huly.io' if (privateKey === undefined || publicKey === undefined) return const userSubscriptions = subscriptions.filter((it) => it.user === target) const data: PushData = { title, body } if (_id !== undefined) { data.tag = _id } const front = control.branding?.front ?? getMetadata(serverCore.metadata.FrontUrl) ?? '' const domainPath = `${workbenchId}/${control.workspace.workspaceUrl}` data.domain = concatLink(front, domainPath) if (path !== undefined) { data.url = concatLink(front, path.join('/')) } if (senderAvatar != null) { const provider = getAvatarProviderId(senderAvatar.avatarType) if (provider === contact.avatarProvider.Image) { if (senderAvatar.avatar != null) { const url = await control.storageAdapter.getUrl(control.ctx, control.workspace, senderAvatar.avatar) data.icon = url.includes('://') ? url : concatLink(front, url) } } else if (provider === contact.avatarProvider.Gravatar && senderAvatar.avatarProps?.url !== undefined) { data.icon = getGravatarUrl(senderAvatar.avatarProps?.url, 512) } } webpush.setVapidDetails(subject, publicKey, privateKey) for (const subscription of userSubscriptions) { void sendPushToSubscription(control, target, subscription, data) } } async function sendPushToSubscription ( control: TriggerControl, targetUser: Ref, subscription: PushSubscription, data: PushData ): Promise { try { await webpush.sendNotification(subscription, JSON.stringify(data)) } catch (err) { control.ctx.info('Cannot send push notification to', { user: targetUser, err }) if (err instanceof WebPushError && err.body.includes('expired')) { const tx = control.txFactory.createTxRemoveDoc(subscription._class, subscription.space, subscription._id) await control.apply(control.ctx, [tx]) } } } /** * @public */ export async function pushActivityInboxNotifications ( ctx: MeasureContext, originTx: TxCUD, control: TriggerControl, res: Tx[], receiver: ReceiverInfo, sender: SenderInfo, object: Doc, docNotifyContexts: DocNotifyContext[], activityMessage: ActivityMessage, shouldUpdateTimestamp: boolean ): Promise | undefined> { const content = await getNotificationContent(originTx, [receiver.account], sender, object, control) const data: Partial> = { ...content, attachedTo: activityMessage._id, attachedToClass: activityMessage._class } return await pushInboxNotifications( ctx, control, res, receiver, sender, activityMessage.attachedTo, activityMessage.attachedToClass, object.space, docNotifyContexts, data, notification.class.ActivityInboxNotification, activityMessage.modifiedOn, shouldUpdateTimestamp, originTx ) } export async function applyNotificationProviders ( data: InboxNotification, notifyResult: NotifyResult, attachedTo: Ref, attachedToClass: Ref>, control: TriggerControl, res: Tx[], object: Doc, receiver: ReceiverInfo, sender: SenderInfo, subscriptions: PushSubscription[], _class = notification.class.ActivityInboxNotification, message?: ActivityMessage ): Promise { const resources = control.modelDb.findAllSync(serverNotification.class.NotificationProviderResources, {}) for (const [provider, types] of notifyResult.entries()) { if (provider === notification.providers.PushNotificationProvider) { // const now = Date.now() const pushTx = await createPushFromInbox( control, receiver, attachedTo, attachedToClass, data, _class, sender, data._id, subscriptions, message ) if (pushTx !== undefined) { res.push(pushTx) } continue } const resource = resources.find((it) => it.provider === provider) if (resource === undefined) continue const fn = await getResource(resource.fn) const txes = await fn(control, types, object, data, receiver, sender, message) if (txes.length > 0) { res.push(...txes) } } } async function createNotifyContext ( ctx: MeasureContext, control: TriggerControl, objectId: Ref, objectClass: Ref>, objectSpace: Ref, receiver: ReceiverInfo, sender: Ref, updateTimestamp?: Timestamp, tx?: TxCUD ): Promise> { const contextsCache: ContextsCache = control.cache.get(ContextsCacheKey) ?? { contexts: new Map>() } const cacheKey = `${objectId}_${receiver._id}` const cachedId = contextsCache.contexts.get(cacheKey) if (cachedId !== undefined) { if (control.removedMap.has(cachedId)) { contextsCache.contexts.delete(cacheKey) } else { return cachedId } } const createTx = control.txFactory.createTxCreateDoc(notification.class.DocNotifyContext, receiver.space, { user: receiver._id, objectId, objectClass, objectSpace, isPinned: false, hidden: false, tx: tx?._id, lastUpdateTimestamp: updateTimestamp, lastViewedTimestamp: sender === receiver._id ? updateTimestamp : undefined }) contextsCache.contexts.set(cacheKey, createTx.objectId) control.cache.set(ContextsCacheKey, contextsCache) await ctx.with('apply', {}, () => control.apply(control.ctx, [createTx])) if (receiver.account?.email !== undefined) { control.ctx.contextData.broadcast.targets['docNotifyContext' + createTx._id] = (it) => { if (it._id === createTx._id) { return [receiver.account?.email] } } } return createTx.objectId } export async function getNotificationTxes ( ctx: MeasureContext, control: TriggerControl, object: Doc, tx: TxCUD, originTx: TxCUD, receiver: ReceiverInfo, sender: SenderInfo, params: NotifyParams, docNotifyContexts: DocNotifyContext[], activityMessages: ActivityMessage[], settings: NotificationProviderControl, subscriptions: PushSubscription[] ): Promise { if (receiver.account === undefined) { return [] } const res: Tx[] = [] for (const message of activityMessages) { const docMessage = message._class === activity.class.DocUpdateMessage ? (message as DocUpdateMessage) : undefined const notifyResult = await isShouldNotifyTx( control, tx, originTx, object, [receiver.account], params.isOwn, params.isSpace, settings, docMessage ) if (notifyResult.has(notification.providers.InboxNotificationProvider)) { const notificationTx = await pushActivityInboxNotifications( ctx, originTx, control, res, receiver, sender, object, docNotifyContexts, message, params.shouldUpdateTimestamp ) if (notificationTx !== undefined) { const notificationData = TxProcessor.createDoc2Doc(notificationTx) await applyNotificationProviders( notificationData, notifyResult, message.attachedTo, message.attachedToClass, control, res, object, receiver, sender, subscriptions, notificationData._class, message ) } } else { const context = getDocNotifyContext(control, docNotifyContexts, message.attachedTo, receiver.account._id) if (context === undefined) { await createNotifyContext( ctx, control, message.attachedTo, message.attachedToClass, object.space, receiver, sender._id, params.shouldUpdateTimestamp ? originTx.modifiedOn : undefined, tx ) } } } return res } async function updateContextsTimestamp ( ctx: MeasureContext, contexts: DocNotifyContext[], timestamp: Timestamp, control: TriggerControl, modifiedBy: Ref ): Promise { if (contexts.length === 0) return const res: Tx[] = [] for (const context of contexts) { const account = getPersonAccountById(context.user, control) const isViewed = context.lastViewedTimestamp !== undefined && (context.lastUpdateTimestamp ?? 0) <= context.lastViewedTimestamp const updateTx = control.txFactory.createTxUpdateDoc(context._class, context.space, context._id, { hidden: false, lastUpdateTimestamp: timestamp, ...(isViewed && modifiedBy === context.user ? { lastViewedTimestamp: timestamp } : {}) }) res.push(updateTx) if (account?.email !== undefined) { control.ctx.contextData.broadcast.targets['docNotifyContext' + updateTx._id] = (it) => { if (it._id === updateTx._id) { return [account.email] } } } } if (res.length > 0) { await ctx.with('apply', {}, () => control.apply(ctx, res)) } } async function removeContexts ( ctx: MeasureContext, contexts: DocNotifyContext[], unsubscribe: Ref[], control: TriggerControl ): Promise { if (contexts.length === 0) return if (unsubscribe.length === 0) return const res: Tx[] = [] for (const context of contexts) { if (!unsubscribe.includes(context.user as Ref)) { continue } const account = control.modelDb.getObject(context.user) if (account === undefined) continue const removeTx = control.txFactory.createTxRemoveDoc(context._class, context.space, context._id) res.push(removeTx) if (account.email !== undefined) { control.ctx.contextData.broadcast.targets['docNotifyContext' + removeTx._id] = (it) => { if (it._id === removeTx._id) { return [account.email] } } } } await control.apply(control.ctx, res) } export async function createCollabDocInfo ( ctx: MeasureContext, collaborators: Ref[], control: TriggerControl, tx: TxCUD, originTx: TxCUD, object: Doc, activityMessages: ActivityMessage[], params: NotifyParams, unsubscribe: Ref[] = [] ): Promise { let res: Tx[] = [] if (originTx.space === core.space.DerivedTx) { return res } const docMessages = activityMessages.filter((message) => message.attachedTo === object._id) if (docMessages.length === 0) { if (unsubscribe.length > 0) { const notifyContexts = await control.findAll(ctx, notification.class.DocNotifyContext, { objectId: object._id, user: { $in: unsubscribe } }) await removeContexts(ctx, notifyContexts, unsubscribe, control) } return res } let notifyContexts: DocNotifyContext[] = [] if (!(object._id === tx.objectId && tx._class === core.class.TxCreateDoc)) { notifyContexts = await control.findAll(ctx, notification.class.DocNotifyContext, { objectId: object._id }) } if (notifyContexts.length > 0) { await updateContextsTimestamp(ctx, notifyContexts, originTx.modifiedOn, control, originTx.modifiedBy) } if (notifyContexts.length > 0 && unsubscribe.length > 0) { await removeContexts(ctx, notifyContexts, unsubscribe, control) } const targets = new Set(collaborators) // user is not collaborator of himself, but we should notify user of changes related to users account (mentions, comments etc) if (control.hierarchy.isDerived(object._class, contact.class.Person)) { const acc = control.modelDb.getAccountByPersonId(object._id as Ref) as PersonAccount[] for (const a of acc.map((it) => it._id)) { targets.add(a) } } if (targets.size === 0) { return res } const usersInfo = await ctx.with( 'get-user-info', {}, async (ctx) => await getUsersInfo(ctx, [...Array.from(targets), originTx.modifiedBy as Ref], control) ) const sender: SenderInfo = usersInfo.get(originTx.modifiedBy) ?? { _id: originTx.modifiedBy } const settings = await getNotificationProviderControl(ctx, control) const subscriptions = await control.findAll(ctx, notification.class.PushSubscription, { user: { $in: Array.from(targets) } }) for (const target of targets) { const info: ReceiverInfo | undefined = toReceiverInfo(control.hierarchy, usersInfo.get(target)) if (info === undefined) continue const targetRes = await getNotificationTxes( ctx, control, object, tx, originTx, info, sender, params, notifyContexts, docMessages, settings, subscriptions ) const ids = new Set(targetRes.map((it) => it._id)) if (info.account?.email !== undefined) { const id = generateId() as string control.ctx.contextData.broadcast.targets[id] = (it) => { if (ids.has(it._id)) { return [info.account?.email] } } } res = res.concat(targetRes) } return res } /** * @public */ export function getMixinTx ( actualTx: TxCUD, control: TriggerControl, collaborators: Ref[] ): TxMixin { return control.txFactory.createTxMixin( actualTx.objectId, actualTx.objectClass, actualTx.objectSpace, notification.mixin.Collaborators, { collaborators } ) } async function getTxCollabs ( ctx: MeasureContext, tx: TxCUD, control: TriggerControl, doc: Doc ): Promise<{ added: Ref[] removed: Ref[] result: Ref[] }> { const { hierarchy } = control const mixin = hierarchy.classHierarchyMixin( doc._class, notification.mixin.ClassCollaborators ) if (mixin === undefined) return { added: [], removed: [], result: [] } if (tx._class === core.class.TxCreateDoc) { const collabs = await getDocCollaborators(ctx, doc, mixin, control) return { added: collabs, removed: [], result: collabs } } if (tx._class === core.class.TxRemoveDoc) { if (hierarchy.hasMixin(doc, notification.mixin.Collaborators)) { return { added: [], removed: [], result: hierarchy.as(doc, notification.mixin.Collaborators).collaborators ?? [] } } return { added: [], removed: [], result: [] } } if ([core.class.TxUpdateDoc, core.class.TxMixin].includes(tx._class)) { const collabs = new Set(hierarchy.as(doc, notification.mixin.Collaborators).collaborators ?? []) const ops = isMixinTx(tx) ? tx.attributes : (tx as TxUpdateDoc).operations const newCollaborators = getNewCollaborators(ops, mixin, doc._class, control).filter((p) => !collabs.has(p)) const isSpace = control.hierarchy.isDerived(doc._class, core.class.Space) const removedCollabs = isSpace ? getRemovedMembers(ops, mixin, (doc as Space)._class, control) : [] const result = [...collabs, ...newCollaborators].filter((p) => !removedCollabs.includes(p)) return { added: newCollaborators, removed: removedCollabs, result } } return { added: [], removed: [], result: [] } } async function getSpaceCollabTxes ( ctx: MeasureContext, control: TriggerControl, doc: Doc, tx: TxCUD, originTx: TxCUD, activityMessages: ActivityMessage[], cache: Map, Doc> ): Promise { if (doc.space === core.space.Space) { return [] } const space = cache.get(doc.space) ?? (await control.findAll(ctx, core.class.Space, { _id: doc.space }, { limit: 1 }))[0] if (space === undefined) return [] cache.set(space._id, space) const mixin = control.hierarchy.classHierarchyMixin( space._class, notification.mixin.ClassCollaborators ) if (mixin !== undefined) { const collabs = control.hierarchy.as(space, notification.mixin.Collaborators) if (collabs.collaborators !== undefined) { return await createCollabDocInfo( ctx, collabs.collaborators as Ref[], control, tx, originTx, doc, activityMessages, { isSpace: true, isOwn: false, shouldUpdateTimestamp: true } ) } } return [] } async function createCollaboratorDoc ( ctx: MeasureContext, tx: TxCreateDoc, control: TriggerControl, activityMessage: ActivityMessage[], originTx: TxCUD, cache: Map, Doc> ): Promise { const res: Tx[] = [] const hierarchy = control.hierarchy const mixin = hierarchy.classHierarchyMixin(tx.objectClass, notification.mixin.ClassCollaborators) if (mixin === undefined) { return res } const doc = TxProcessor.createDoc2Doc(tx) const collaborators = await ctx.with( 'get-collaborators', {}, async (ctx) => await getDocCollaborators(ctx, doc, mixin, control) ) const mixinTx = getMixinTx(tx, control, collaborators) const notificationTxes = await ctx.with( 'create-collabdocinfo', {}, async (ctx) => await createCollabDocInfo( ctx, collaborators as Ref[], control, tx, originTx, doc, activityMessage, { isOwn: true, isSpace: false, shouldUpdateTimestamp: true } ) ) res.push(mixinTx) res.push(...notificationTxes) res.push( ...(await ctx.with( 'get-space-collabtxes', {}, async (ctx) => await getSpaceCollabTxes(ctx, control, doc, tx, originTx, activityMessage, cache) )) ) return res } async function updateCollaboratorsMixin ( ctx: MeasureContext, tx: TxMixin, control: TriggerControl, activityMessages: ActivityMessage[], originTx: TxCUD ): Promise { const { hierarchy } = control if (tx._class !== core.class.TxMixin) return [] if (originTx.space === core.space.DerivedTx) return [] if (!hierarchy.isDerived(tx.mixin, notification.mixin.Collaborators)) return [] const res: Tx[] = [] const notificationControl = await getNotificationProviderControl(ctx, control) if (tx.attributes.collaborators !== undefined) { const createTx = hierarchy.isDerived(tx.objectClass, core.class.AttachedDoc) ? ( await control.findAll(ctx, core.class.TxCollectionCUD, { 'tx.objectId': tx.objectId, 'tx._class': core.class.TxCreateDoc }) )[0] : ( await control.findAll(ctx, core.class.TxCreateDoc, { objectId: tx.objectId }) )[0] const mixinTxes = await control.findAll(ctx, core.class.TxMixin, { objectId: tx.objectId }) const prevDoc = TxProcessor.buildDoc2Doc([createTx, ...mixinTxes].filter((t) => t._id !== tx._id)) as Doc const newCollabs: Ref[] = [] let prevCollabs: Set> if (hierarchy.hasMixin(prevDoc, notification.mixin.Collaborators)) { const prevDocMixin = control.hierarchy.as(prevDoc, notification.mixin.Collaborators) prevCollabs = new Set(prevDocMixin.collaborators ?? []) } else { const mixin = hierarchy.classHierarchyMixin(prevDoc._class, notification.mixin.ClassCollaborators) prevCollabs = mixin !== undefined ? new Set(await getDocCollaborators(ctx, prevDoc, mixin, control)) : new Set() } const type = await control.modelDb.findOne(notification.class.BaseNotificationType, { _id: notification.ids.CollaboratoAddNotification }) if (type === undefined) { return res } const providers = await control.modelDb.findAll(notification.class.NotificationProvider, {}) for (const collab of tx.attributes.collaborators) { if (!prevCollabs.has(collab) && tx.modifiedBy !== collab) { for (const provider of providers) { if (isAllowed(control, collab as Ref, type, provider, notificationControl)) { newCollabs.push(collab) break } } } } if (newCollabs.length > 0) { const docNotifyContexts = await control.findAll(ctx, notification.class.DocNotifyContext, { user: { $in: newCollabs }, objectId: tx.objectId }) const infos = await ctx.with( 'get-user-info', {}, async (ctx) => await getUsersInfo(ctx, [...newCollabs, originTx.modifiedBy] as Ref[], control) ) const sender: SenderInfo = infos.get(originTx.modifiedBy) ?? { _id: originTx.modifiedBy } for (const collab of newCollabs) { const target = toReceiverInfo(hierarchy, infos.get(collab)) if (target === undefined) continue for (const message of activityMessages) { await pushActivityInboxNotifications( ctx, originTx, control, res, target, sender, prevDoc, docNotifyContexts, message, true ) } } } } return res } async function collectionCollabDoc ( ctx: MeasureContext, tx: TxCollectionCUD, control: TriggerControl, activityMessages: ActivityMessage[], cache: Map, Doc> ): Promise { const actualTx = TxProcessor.extractTx(tx) as TxCUD let res = await createCollaboratorNotifications(ctx, actualTx, control, activityMessages, tx, cache) if (![core.class.TxCreateDoc, core.class.TxRemoveDoc, core.class.TxUpdateDoc].includes(actualTx._class)) { return res } const mixin = control.hierarchy.classHierarchyMixin(tx.objectClass, notification.mixin.ClassCollaborators) if (mixin === undefined) { return res } const doc = await ctx.with( 'get-doc', {}, async (ctx) => cache.get(tx.objectId) ?? (await control.findAll(ctx, tx.objectClass, { _id: tx.objectId }, { limit: 1 }))[0] ) if (doc === undefined) { return res } cache.set(doc._id, doc) const collaborators = await ctx.with( 'get-collaborators', {}, async (ctx) => await getCollaborators(ctx, doc, control, tx, res) ) res = res.concat( await ctx.with( 'create-collab-doc-info', {}, async (ctx) => await createCollabDocInfo( ctx, collaborators as Ref[], control, actualTx, tx, doc, activityMessages, { isOwn: false, isSpace: false, shouldUpdateTimestamp: true } ) ) ) return res } async function removeContextNotifications ( control: TriggerControl, notifyContextRefs: Ref[] ): Promise { const inboxNotifications = await control.findAll( control.ctx, notification.class.InboxNotification, { docNotifyContext: { $in: notifyContextRefs } }, { projection: { _id: 1, _class: 1, space: 1 } } ) return inboxNotifications.map((notification) => control.txFactory.createTxRemoveDoc(notification._class, notification.space, notification._id) ) } async function removeCollaboratorDoc (tx: TxRemoveDoc, control: TriggerControl): Promise { const hierarchy = control.hierarchy const mixin = hierarchy.classHierarchyMixin(tx.objectClass, notification.mixin.ClassCollaborators) if (mixin === undefined) { return [] } const res: Tx[] = [] const notifyContexts = await control.findAll( control.ctx, notification.class.DocNotifyContext, { objectId: tx.objectId }, { projection: { _id: 1, _class: 1, space: 1 } } ) if (notifyContexts.length === 0) { return [] } const notifyContextRefs = notifyContexts.map(({ _id }) => _id) const txes = await removeContextNotifications(control, notifyContextRefs) res.push(...txes) notifyContexts.forEach((context) => { res.push(control.txFactory.createTxRemoveDoc(context._class, context.space, context._id)) }) return res } function getNewCollaborators ( ops: DocumentUpdate | MixinUpdate, mixin: ClassCollaborators, docClass: Ref>, control: TriggerControl ): Ref[] { const newCollaborators = new Set>() if (ops.$push !== undefined) { for (const key in ops.$push) { if (mixin.fields.includes(key)) { let value = (ops.$push as any)[key] if (typeof value !== 'string') { value = value.$each } const newCollabs = getKeyCollaborators(docClass, value, key, control) if (newCollabs !== undefined) { for (const newCollab of newCollabs) { newCollaborators.add(newCollab) } } } } } for (const key in ops) { if (key.startsWith('$')) continue if (mixin.fields.includes(key)) { const value = (ops as any)[key] const newCollabs = getKeyCollaborators(docClass, value, key, control) if (newCollabs !== undefined) { for (const newCollab of newCollabs) { newCollaborators.add(newCollab) } } } } return Array.from(newCollaborators.values()) } function getRemovedMembers ( ops: DocumentUpdate | MixinUpdate, mixin: ClassCollaborators, docClass: Ref>, control: TriggerControl ): Ref[] { const removedCollaborators: Ref[] = [] if (ops.$pull !== undefined && 'members' in ops.$pull) { const key = 'members' if (mixin.fields.includes(key)) { let value = (ops.$pull as any)[key] if (typeof value !== 'string') { value = value.$in } const collabs = getKeyCollaborators(docClass, value, key, control) if (collabs !== undefined) { removedCollaborators.push(...collabs) } } } return Array.from(new Set(removedCollaborators).values()) } async function updateCollaboratorDoc ( ctx: MeasureContext, tx: TxUpdateDoc | TxMixin, control: TriggerControl, originTx: TxCUD, activityMessages: ActivityMessage[], cache: Map, Doc> ): Promise { const hierarchy = control.hierarchy let res: Tx[] = [] const mixin = hierarchy.classHierarchyMixin(tx.objectClass, notification.mixin.ClassCollaborators) if (mixin === undefined) return [] const doc = await ctx.with( 'find-doc', { _class: tx.objectClass }, async (ctx) => cache.get(tx.objectId) ?? (await control.findAll(ctx, tx.objectClass, { _id: tx.objectId }, { limit: 1 }))[0] ) if (doc === undefined) return [] cache.set(doc._id, doc) const params: NotifyParams = { isOwn: true, isSpace: false, shouldUpdateTimestamp: true } if (hierarchy.hasMixin(doc, notification.mixin.Collaborators)) { // we should handle change field and subscribe new collaborators const collabsInfo = await ctx.with( 'get-tx-collaborators', {}, async (ctx) => await getTxCollabs(ctx, tx, control, doc) ) if (collabsInfo.added.length > 0) { res.push(createPushCollaboratorsTx(control, tx.objectId, tx.objectClass, tx.objectSpace, collabsInfo.added)) } if (collabsInfo.removed.length > 0) { res.push(createPullCollaboratorsTx(control, tx.objectId, tx.objectClass, tx.objectSpace, collabsInfo.removed)) } res = res.concat( await ctx.with( 'create-collab-docinfo', {}, async (ctx) => await createCollabDocInfo( ctx, collabsInfo.result as Ref[], control, tx, originTx, doc, activityMessages, params, collabsInfo.removed as Ref[] ) ) ) } else { const collaborators = await ctx.with( 'get-doc-collaborators', {}, async (ctx) => await getDocCollaborators(ctx, doc, mixin, control) ) res.push(getMixinTx(tx, control, collaborators)) res = res.concat( await createCollabDocInfo( ctx, collaborators as Ref[], control, tx, originTx, doc, activityMessages, params ) ) } res = res.concat( await ctx.with( 'get-space-collabtxes', {}, async (ctx) => await getSpaceCollabTxes(ctx, control, doc, tx, originTx, activityMessages, cache) ) ) res = res.concat( await ctx.with('update-notify-context-space', {}, async (ctx) => await updateNotifyContextsSpace(ctx, control, tx)) ) return res } /** * @public */ export async function OnAttributeCreate (tx: Tx, control: TriggerControl): Promise { const attribute = TxProcessor.createDoc2Doc(tx as TxCreateDoc) const group = ( await control.modelDb.findAll(notification.class.NotificationGroup, { objectClass: attribute.attributeOf }) )[0] if (group === undefined) return [] const isCollection: boolean = core.class.Collection === attribute.type._class const objectClass = !isCollection ? attribute.attributeOf : (attribute.type as Collection).of const txClasses = !isCollection ? [control.hierarchy.isMixin(attribute.attributeOf) ? core.class.TxMixin : core.class.TxUpdateDoc] : [core.class.TxCreateDoc, core.class.TxRemoveDoc] const data: Data = { attribute: attribute._id, group: group._id, field: attribute.name, generated: true, objectClass, txClasses, hidden: false, defaultEnabled: false, templates: { textTemplate: '{body}', htmlTemplate: '

{body}

', subjectTemplate: '{doc} updated' }, label: attribute.label } if (isCollection) { data.attachedToClass = attribute.attributeOf } const id = `${notification.class.NotificationType}_${attribute.attributeOf}_${attribute.name}` as Ref const res = control.txFactory.createTxCreateDoc(notification.class.NotificationType, core.space.Model, data, id) return [res] } /** * @public */ export async function OnAttributeUpdate (tx: Tx, control: TriggerControl): Promise { const ctx = tx as TxUpdateDoc if (ctx.operations.hidden === undefined) return [] const type = (await control.findAll(control.ctx, notification.class.NotificationType, { attribute: ctx.objectId }))[0] if (type === undefined) return [] const res = control.txFactory.createTxUpdateDoc(type._class, type.space, type._id, { hidden: ctx.operations.hidden }) return [res] } async function applyUserTxes ( ctx: MeasureContext, control: TriggerControl, txes: Tx[], cache: Map, Doc> = new Map, Doc>() ): Promise { const map: Map, Tx[]> = new Map, Tx[]>() const res: Tx[] = [] for (const tx of txes) { const ttx = tx as TxCUD if ( control.hierarchy.isDerived(ttx.objectClass, notification.class.InboxNotification) && ttx._class === core.class.TxCreateDoc ) { const notification = TxProcessor.createDoc2Doc(ttx as TxCreateDoc) if (map.has(notification.user)) { map.get(notification.user)?.push(tx) } else { map.set(notification.user, [tx]) } } else if ( control.hierarchy.isDerived(ttx.objectClass, notification.class.BrowserNotification) && ttx._class === core.class.TxCreateDoc ) { const notification = TxProcessor.createDoc2Doc(ttx as TxCreateDoc) if (map.has(notification.user)) { map.get(notification.user)?.push(tx) } else { map.set(notification.user, [tx]) } } else { res.push(tx) } } for (const [user, txs] of map.entries()) { const account = (cache.get(user) as PersonAccount) ?? ctx.withSync('get-person-account', {}, () => getPersonAccountById(user, control)) if (account !== undefined) { cache.set(account._id, account) await control.apply(ctx, txs) const m1 = toIdMap(txs) control.ctx.contextData.broadcast.targets.docNotifyContext = (it) => { if (m1.has(it._id)) { return [account.email] } } } } return res } async function updateCollaborators ( ctx: MeasureContext, control: TriggerControl, tx: TxCUD, cache: Map, Doc> = new Map, Doc>() ): Promise { if (tx._class !== core.class.TxUpdateDoc && tx._class !== core.class.TxMixin) return [] const hierarchy = control.hierarchy const mixin = hierarchy.classHierarchyMixin(tx.objectClass, notification.mixin.ClassCollaborators) if (mixin === undefined) return [] const { objectClass, objectId, objectSpace } = tx const ops = isMixinTx(tx) ? tx.attributes : (tx as TxUpdateDoc).operations const addedCollaborators = getNewCollaborators(ops, mixin, objectClass, control) const isSpace = control.hierarchy.isDerived(objectClass, core.class.Space) const removedCollaborators = isSpace ? getRemovedMembers(ops, mixin, objectClass as Ref>, control) : [] if (removedCollaborators.length === 0 && addedCollaborators.length === 0) return [] const doc = cache.get(objectId) ?? (await control.findAll(control.ctx, objectClass, { _id: objectId }, { limit: 1 }))[0] cache.set(objectId, doc) if (doc === undefined) return [] const res: Tx[] = [] const currentCollaborators = new Set(hierarchy.as(doc, notification.mixin.Collaborators).collaborators ?? []) const toAdd = addedCollaborators.filter((p) => !currentCollaborators.has(p)) if (toAdd.length === 0 && removedCollaborators.length === 0) return [] if (toAdd.length > 0) { res.push(createPushCollaboratorsTx(control, objectId, objectClass, objectSpace, toAdd)) } if (removedCollaborators.length > 0) { res.push(createPullCollaboratorsTx(control, objectId, objectClass, objectSpace, removedCollaborators)) } if (hierarchy.classHierarchyMixin(objectClass, activity.mixin.ActivityDoc) === undefined) return res const contexts = await control.findAll(control.ctx, notification.class.DocNotifyContext, { objectId }) const addedInfo = await getUsersInfo(ctx, toAdd as Ref[], control) for (const addedUser of addedInfo.values()) { const info = toReceiverInfo(hierarchy, addedUser) if (info === undefined) continue const context = getDocNotifyContext(control, contexts, objectId, info._id) if (context !== undefined) { if (context.hidden) { res.push(control.txFactory.createTxUpdateDoc(context._class, context.space, context._id, { hidden: false })) } } await createNotifyContext(ctx, control, objectId, objectClass, objectSpace, info, tx.modifiedBy, undefined, tx) } await removeContexts(ctx, contexts, removedCollaborators as Ref[], control) return res } export async function createCollaboratorNotifications ( ctx: MeasureContext, tx: TxCUD, control: TriggerControl, activityMessages: ActivityMessage[], originTx?: TxCUD, cache: Map, Doc> = new Map, Doc>() ): Promise { if (tx.space === core.space.DerivedTx) { // do not forgot update collaborators for derived tx return await ctx.with( 'updateDerivedCollaborators', {}, async (ctx) => await updateCollaborators(ctx, control, TxProcessor.extractTx(tx) as TxCUD, cache) ) } if (activityMessages.length === 0) { return [] } switch (tx._class) { case core.class.TxCreateDoc: { const res = await ctx.with( 'createCollaboratorDoc', {}, async (ctx) => await createCollaboratorDoc(ctx, tx as TxCreateDoc, control, activityMessages, originTx ?? tx, cache) ) return await applyUserTxes(ctx, control, res) } case core.class.TxUpdateDoc: case core.class.TxMixin: { let res = await ctx.with( 'updateCollaboratorDoc', {}, async (ctx) => await updateCollaboratorDoc(ctx, tx as TxUpdateDoc, control, originTx ?? tx, activityMessages, cache) ) res = res.concat( await ctx.with( 'updateCollaboratorMixin', {}, async (ctx) => await updateCollaboratorsMixin( ctx, tx as TxMixin, control, activityMessages, originTx ?? tx ) ) ) return await applyUserTxes(ctx, control, res) } case core.class.TxCollectionCUD: { const res = await ctx.with( 'collectionCollabDoc', {}, async (ctx) => await collectionCollabDoc(ctx, tx as TxCollectionCUD, control, activityMessages, cache) ) return await applyUserTxes(ctx, control, res) } } return [] } /** * @public */ export async function removeDocInboxNotifications (_id: Ref, control: TriggerControl): Promise { const inboxNotifications = await control.findAll(control.ctx, notification.class.InboxNotification, { attachedTo: _id }) return inboxNotifications.map((inboxNotification) => control.txFactory.createTxRemoveDoc( notification.class.InboxNotification, inboxNotification.space, inboxNotification._id ) ) } export async function getCollaborators ( ctx: MeasureContext, doc: Doc, control: TriggerControl, tx: TxCUD, res: Tx[] ): Promise[]> { const mixin = control.hierarchy.classHierarchyMixin(doc._class, notification.mixin.ClassCollaborators) if (mixin === undefined) { return [] } if (control.hierarchy.hasMixin(doc, notification.mixin.Collaborators)) { return control.hierarchy.as(doc, notification.mixin.Collaborators).collaborators } else { const collaborators = await getDocCollaborators(ctx, doc, mixin, control) res.push(getMixinTx(tx, control, collaborators)) return collaborators } } function getDocNotifyContext ( control: TriggerControl, contexts: DocNotifyContext[], objectId: Ref, user: Ref ): DocNotifyContext | undefined { const context = contexts.find((it) => it.objectId === objectId && it.user === user) if (context !== undefined) { return context } const txes = [...control.txes, ...control.ctx.contextData.broadcast.txes] as TxCUD[] for (const tx of txes) { if (tx._class === core.class.TxCreateDoc && tx.objectClass === notification.class.DocNotifyContext) { const doc = TxProcessor.createDoc2Doc(tx as TxCreateDoc) if (doc.objectId === objectId && doc.user === user) { return doc } } } return undefined } async function OnActivityMessageRemove (message: ActivityMessage, control: TriggerControl): Promise { if (control.removedMap.has(message.attachedTo)) { return [] } const contexts = await control.findAll(control.ctx, notification.class.DocNotifyContext, { objectId: message.attachedTo, lastUpdateTimestamp: message.createdOn }) if (contexts.length === 0) return [] const lastMessage = ( await control.findAll( control.ctx, activity.class.ActivityMessage, { attachedTo: message.attachedTo, space: message.space }, { sort: { createdOn: SortingOrder.Descending }, limit: 1 } ) )[0] if (lastMessage === undefined) return [] const res: Tx[] = [] for (const context of contexts) { const tx = control.txFactory.createTxUpdateDoc(context._class, context.space, context._id, { lastUpdateTimestamp: lastMessage.createdOn ?? lastMessage.modifiedOn }) res.push(tx) } return res } async function OnDocRemove (originTx: TxCUD, control: TriggerControl): Promise { const tx = TxProcessor.extractTx(originTx) as TxRemoveDoc if (tx._class !== core.class.TxRemoveDoc) return [] const res: Tx[] = [] if (control.hierarchy.isDerived(tx.objectClass, activity.class.ActivityMessage)) { const message = control.removedMap.get(tx.objectId) as ActivityMessage | undefined if (message !== undefined) { const txes = await OnActivityMessageRemove(message, control) res.push(...txes) } } else if (control.hierarchy.isDerived(tx.objectClass, notification.class.DocNotifyContext)) { const contextsCache: ContextsCache | undefined = control.cache.get(ContextsCacheKey) if (contextsCache !== undefined) { for (const [key, value] of contextsCache.contexts.entries()) { if (value === tx.objectId) { contextsCache.contexts.delete(key) } } } return await removeContextNotifications(control, [tx.objectId as Ref]) } const txes = await removeCollaboratorDoc(tx, control) res.push(...txes) return res } export * from './types' export * from './utils' // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export default async () => ({ trigger: { OnAttributeCreate, OnAttributeUpdate, OnDocRemove }, function: { IsUserInFieldValueTypeMatch: isUserInFieldValueTypeMatch, IsUserEmployeeInFieldValueTypeMatch: isUserEmployeeInFieldValueTypeMatch } })