mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-29 21:15:06 +02:00
getCommonNotificationTxes() passed a hardcoded empty array as the `types` argument to pushInboxNotifications(), so every notification created through it was stored with `types: []`. NotificationsHandler resolves the allowed providers per notification type. With no types it matches no provider, logs "no notifications with email provider, skipping" and returns, so these notifications can never be delivered by email or any non-inbox provider. This affects everything routed through this helper - ToDos, process approvals (process:ids:ApproveRequestCreated), HR requests - while activity-message notifications are unaffected because that path passes its types through. The types are already computed: isShouldNotifyTx() returns a NotifyResult mapping each provider to the NotificationType docs that matched, and the result is passed in as notifyResult. Forward the inbox provider's types instead of discarding them.
1677 lines
48 KiB
TypeScript
1677 lines
48 KiB
TypeScript
//
|
|
// 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 { Analytics } from '@hcengineering/analytics'
|
|
import chunter, { ChatMessage } from '@hcengineering/chunter'
|
|
import contact, { Employee, type Person } from '@hcengineering/contact'
|
|
import core, {
|
|
AccountUuid,
|
|
AnyAttribute,
|
|
ArrOf,
|
|
AttachedDoc,
|
|
Class,
|
|
ClassCollaborators,
|
|
Collaborator,
|
|
Collection,
|
|
combineAttributes,
|
|
Data,
|
|
Doc,
|
|
DocumentUpdate,
|
|
MeasureContext,
|
|
MixinUpdate,
|
|
notEmpty,
|
|
PersonId,
|
|
readOnlyGuestAccountUuid,
|
|
Ref,
|
|
RefTo,
|
|
SortingOrder,
|
|
Space,
|
|
Timestamp,
|
|
Tx,
|
|
TxCreateDoc,
|
|
TxCUD,
|
|
TxMixin,
|
|
TxProcessor,
|
|
TxRemoveDoc,
|
|
TxUpdateDoc,
|
|
getClassCollaborators
|
|
} from '@hcengineering/core'
|
|
import notification, {
|
|
ActivityInboxNotification,
|
|
CommonInboxNotification,
|
|
DocNotifyContext,
|
|
InboxNotification,
|
|
MentionInboxNotification,
|
|
NotificationType
|
|
} from '@hcengineering/notification'
|
|
import { getResource, translate } from '@hcengineering/platform'
|
|
import { getAccountBySocialId, getEmployeesBySocialIds } from '@hcengineering/server-contact'
|
|
import { type TriggerControl } from '@hcengineering/server-core'
|
|
import { NOTIFICATION_BODY_SIZE, ReceiverInfo, SenderInfo } from '@hcengineering/server-notification'
|
|
import { markupToText, stripTags } from '@hcengineering/text-core'
|
|
|
|
import { PushNotificationsHandler } from './push'
|
|
import {
|
|
AvailableProvidersCache,
|
|
AvailableProvidersCacheKey,
|
|
Content,
|
|
ContextsCache,
|
|
ContextsCacheKey,
|
|
NotifyParams,
|
|
NotifyResult
|
|
} from './types'
|
|
import {
|
|
getHTMLPresenter,
|
|
getNotificationContent,
|
|
getNotificationLink,
|
|
getNotificationProviderControl,
|
|
getObjectSpace,
|
|
getReceiversInfo,
|
|
getSenderInfo,
|
|
getTextPresenter,
|
|
isMixinTx,
|
|
isShouldNotifyTx,
|
|
isUserEmployeeInFieldValueTypeMatch,
|
|
mentionTypeMatch,
|
|
messageToMarkup,
|
|
type NotificationProviderControl,
|
|
replaceAll,
|
|
updateNotifyContextsSpace
|
|
} from './utils'
|
|
|
|
export async function getCommonNotificationTxes (
|
|
ctx: MeasureContext,
|
|
control: TriggerControl,
|
|
doc: Doc,
|
|
data: Partial<Data<CommonInboxNotification>>,
|
|
receiver: ReceiverInfo,
|
|
sender: SenderInfo,
|
|
attachedTo: Ref<Doc>,
|
|
attachedToClass: Ref<Class<Doc>>,
|
|
space: Ref<Space>,
|
|
modifiedOn: Timestamp,
|
|
notifyResult: NotifyResult,
|
|
_class = notification.class.CommonInboxNotification,
|
|
tx?: TxCUD<Doc>
|
|
): Promise<Tx[]> {
|
|
if (notifyResult.size === 0 || !notifyResult.has(notification.providers.InboxNotificationProvider)) {
|
|
return []
|
|
}
|
|
|
|
const res: Tx[] = []
|
|
const notifyContexts = await control.findAll(ctx, notification.class.DocNotifyContext, { objectId: attachedTo })
|
|
|
|
await pushInboxNotifications(
|
|
ctx,
|
|
control,
|
|
res,
|
|
receiver,
|
|
sender,
|
|
attachedTo,
|
|
attachedToClass,
|
|
space,
|
|
notifyContexts,
|
|
data,
|
|
_class,
|
|
modifiedOn,
|
|
(notifyResult.get(notification.providers.InboxNotificationProvider) ?? []).map((t) => t._id),
|
|
true,
|
|
tx
|
|
)
|
|
|
|
return res
|
|
}
|
|
|
|
async function getTextPart (doc: Doc, control: TriggerControl): Promise<string | undefined> {
|
|
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<string | undefined> {
|
|
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, string> = {}
|
|
): 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<NotificationType>,
|
|
control: TriggerControl,
|
|
data: string,
|
|
notificationData?: InboxNotification,
|
|
message?: ActivityMessage
|
|
): Promise<Content | undefined> {
|
|
if (doc === undefined) return
|
|
const notificationType = control.modelDb.getObject(type)
|
|
if (notificationType.templates === undefined) return
|
|
|
|
const params: Record<string, string> =
|
|
notificationData !== undefined
|
|
? await getTranslatedNotificationContent(notificationData, notificationData._class, control)
|
|
: {}
|
|
|
|
let textPart = await getTextPart(doc, control)
|
|
if (textPart === undefined) {
|
|
if (
|
|
notificationData !== undefined &&
|
|
control.hierarchy.isDerived(notificationData._class, notification.class.CommonInboxNotification)
|
|
) {
|
|
textPart = params.title ?? params.body ?? ''
|
|
}
|
|
if (textPart === undefined || textPart === '') return
|
|
}
|
|
|
|
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 = `<a href='${link}'>${linkText}</a>`
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
async function getValueCollaborators (value: any, attr: AnyAttribute, control: TriggerControl): Promise<AccountUuid[]> {
|
|
const hierarchy = control.hierarchy
|
|
if (attr.type._class === core.class.RefTo) {
|
|
const to = (attr.type as RefTo<Doc>).to
|
|
|
|
if (hierarchy.isDerived(to, contact.class.Person)) {
|
|
const employee = await control.findAll(
|
|
control.ctx,
|
|
contact.mixin.Employee,
|
|
{
|
|
_id: value,
|
|
active: true
|
|
},
|
|
{ limit: 1 }
|
|
)
|
|
|
|
return employee[0]?.personUuid != null ? [employee[0].personUuid] : []
|
|
}
|
|
} else if (attr.type._class === core.class.TypeAccountUuid) {
|
|
return [value]
|
|
} else if (attr.type._class === core.class.TypePersonId) {
|
|
const acc = await getAccountBySocialId(control, value)
|
|
return acc == null ? [] : [acc]
|
|
} else if (attr.type._class === core.class.ArrOf) {
|
|
const arrOf = (attr.type as ArrOf<RefTo<Doc>>).of
|
|
|
|
if (arrOf._class === core.class.RefTo) {
|
|
const to = (arrOf as RefTo<Doc>).to
|
|
if (hierarchy.isDerived(to, contact.class.Person)) {
|
|
if (!Array.isArray(value)) {
|
|
control.ctx.error('Expected array but got non-array value when getting value collaborators', { attr, value })
|
|
return []
|
|
}
|
|
if (value.length === 0) return []
|
|
if (value.every((it) => it === null)) {
|
|
control.ctx.error('Null-values array of person refs when getting value collaborators', { attr, value })
|
|
}
|
|
|
|
const employees = await control.findAll(control.ctx, contact.mixin.Employee, {
|
|
_id: { $in: value },
|
|
active: true
|
|
})
|
|
|
|
return employees.map((e) => e.personUuid).filter(notEmpty)
|
|
}
|
|
} else if (arrOf._class === core.class.TypeAccountUuid) {
|
|
return Array.isArray(value) ? value : [value]
|
|
} else if (arrOf._class === core.class.TypePersonId) {
|
|
const socialIds = Array.isArray(value) ? value : [value]
|
|
const personsBySocialIds = await getEmployeesBySocialIds(control, socialIds)
|
|
|
|
return Object.values(personsBySocialIds)
|
|
.map((p) => p?.personUuid)
|
|
.filter(notEmpty)
|
|
}
|
|
}
|
|
return []
|
|
}
|
|
|
|
async function getKeyCollaborators (
|
|
docClass: Ref<Class<Doc>>,
|
|
value: any,
|
|
field: string,
|
|
control: TriggerControl
|
|
): Promise<AccountUuid[] | undefined> {
|
|
if (value !== undefined && value !== null) {
|
|
const attr = control.hierarchy.findAttribute(docClass, field)
|
|
if (attr !== undefined) {
|
|
const val = await getValueCollaborators(value, attr, control)
|
|
return val
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
export async function getDocCollaborators (
|
|
ctx: MeasureContext,
|
|
doc: Doc,
|
|
mixin: ClassCollaborators<Doc>,
|
|
control: TriggerControl
|
|
): Promise<AccountUuid[]> {
|
|
const collaborators = new Set<AccountUuid>()
|
|
if (mixin.allFields !== true) {
|
|
for (const field of mixin.fields) {
|
|
const value = (doc as any)[field]
|
|
const newCollaborators = await ctx.with('getKeyCollaborators', {}, (ctx) =>
|
|
getKeyCollaborators(doc._class, value, field, control)
|
|
)
|
|
if (newCollaborators !== undefined) {
|
|
for (const newCollaborator of newCollaborators) {
|
|
collaborators.add(newCollaborator)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
const attrs = control.hierarchy.getAllAttributes(doc._class)
|
|
// attr type will be checked into getKeyCollaborators
|
|
for (const [field] of attrs) {
|
|
const value = (doc as any)[field]
|
|
const newCollaborators = await ctx.with('getKeyCollaborators', {}, (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<Doc>,
|
|
objectClass: Ref<Class<Doc>>,
|
|
objectSpace: Ref<Space>,
|
|
contexts: DocNotifyContext[],
|
|
data: Partial<Data<InboxNotification>>,
|
|
_class: Ref<Class<InboxNotification>>,
|
|
modifiedOn: Timestamp,
|
|
types: Ref<NotificationType>[],
|
|
shouldUpdateTimestamp = true,
|
|
tx?: TxCUD<Doc>
|
|
): Promise<TxCreateDoc<InboxNotification> | undefined> {
|
|
const context = getDocNotifyContext(control, contexts, objectId, receiver.account)
|
|
let docNotifyContextId: Ref<DocNotifyContext>
|
|
|
|
if (context === undefined) {
|
|
docNotifyContextId = await createNotifyContext(
|
|
ctx,
|
|
control,
|
|
objectId,
|
|
objectClass,
|
|
objectSpace,
|
|
receiver,
|
|
sender.socialId,
|
|
shouldUpdateTimestamp ? modifiedOn : undefined,
|
|
tx
|
|
)
|
|
} else {
|
|
docNotifyContextId = context._id
|
|
}
|
|
|
|
const notificationData = {
|
|
user: receiver.account,
|
|
isViewed: receiver.role === 'GUEST' && receiver.account === readOnlyGuestAccountUuid,
|
|
docNotifyContext: docNotifyContextId,
|
|
archived: false,
|
|
objectId,
|
|
objectClass,
|
|
types,
|
|
...data
|
|
}
|
|
const notificationTx = control.txFactory.createTxCreateDoc(_class, receiver.space, notificationData)
|
|
res.push(notificationTx)
|
|
|
|
return notificationTx
|
|
}
|
|
|
|
async function activityInboxNotificationToText (
|
|
doc: Data<ActivityInboxNotification>
|
|
): 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<CommonInboxNotification>
|
|
): 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<MentionInboxNotification>,
|
|
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<InboxNotification>,
|
|
_class: Ref<Class<InboxNotification>>,
|
|
control: TriggerControl
|
|
): Promise<{ title: string, body: string, [key: string]: string }> {
|
|
if (control.hierarchy.isDerived(_class, notification.class.ActivityInboxNotification)) {
|
|
return await activityInboxNotificationToText(data as Data<ActivityInboxNotification>)
|
|
} else if (control.hierarchy.isDerived(_class, notification.class.MentionInboxNotification)) {
|
|
return await mentionInboxNotificationToText(data as Data<MentionInboxNotification>, control)
|
|
} else if (control.hierarchy.isDerived(_class, notification.class.CommonInboxNotification)) {
|
|
return await commonInboxNotificationToText(data as Data<CommonInboxNotification>)
|
|
}
|
|
|
|
return { title: '', body: '' }
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
export async function pushActivityInboxNotifications (
|
|
ctx: MeasureContext,
|
|
originTx: TxCUD<Doc>,
|
|
control: TriggerControl,
|
|
res: Tx[],
|
|
receiver: ReceiverInfo,
|
|
sender: SenderInfo,
|
|
object: Doc,
|
|
docNotifyContexts: DocNotifyContext[],
|
|
activityMessage: ActivityMessage,
|
|
types: Ref<NotificationType>[],
|
|
shouldUpdateTimestamp: boolean
|
|
): Promise<TxCreateDoc<InboxNotification> | undefined> {
|
|
const content = await getNotificationContent(originTx, receiver.employee, sender, object, control)
|
|
const data: Partial<Data<ActivityInboxNotification>> = {
|
|
...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,
|
|
types,
|
|
shouldUpdateTimestamp,
|
|
originTx
|
|
)
|
|
}
|
|
|
|
async function createNotifyContext (
|
|
ctx: MeasureContext,
|
|
control: TriggerControl,
|
|
objectId: Ref<Doc>,
|
|
objectClass: Ref<Class<Doc>>,
|
|
objectSpace: Ref<Space>,
|
|
receiver: ReceiverInfo,
|
|
sender: PersonId,
|
|
updateTimestamp?: Timestamp,
|
|
tx?: TxCUD<Doc>
|
|
): Promise<Ref<DocNotifyContext>> {
|
|
const contextsCache: ContextsCache = control.cache.get(ContextsCacheKey) ?? {
|
|
contexts: new Map<string, Ref<DocNotifyContext>>()
|
|
}
|
|
const cacheKey = `${objectId}_${receiver.account}`
|
|
const cachedId = contextsCache.contexts.get(cacheKey)
|
|
|
|
if (cachedId !== undefined) {
|
|
if (control.removedMap.has(cachedId)) {
|
|
contextsCache.contexts.delete(cacheKey)
|
|
} else {
|
|
return cachedId
|
|
}
|
|
}
|
|
|
|
const lastViewedTimestamp =
|
|
receiver.role === 'GUEST' && receiver.account === readOnlyGuestAccountUuid
|
|
? Number.MAX_VALUE
|
|
: receiver.socialIds.some((it) => it === sender)
|
|
? updateTimestamp
|
|
: undefined
|
|
|
|
const createTx = control.txFactory.createTxCreateDoc(notification.class.DocNotifyContext, receiver.space, {
|
|
user: receiver.account,
|
|
objectId,
|
|
objectClass,
|
|
objectSpace,
|
|
isPinned: false,
|
|
hidden: false,
|
|
tx: tx?._id,
|
|
lastUpdateTimestamp: updateTimestamp,
|
|
lastViewedTimestamp
|
|
})
|
|
|
|
contextsCache.contexts.set(cacheKey, createTx.objectId)
|
|
control.cache.set(ContextsCacheKey, contextsCache)
|
|
await ctx.with('apply', {}, () => control.apply(control.ctx, [createTx]))
|
|
return createTx.objectId
|
|
}
|
|
|
|
export async function getNotificationTxes (
|
|
ctx: MeasureContext,
|
|
control: TriggerControl,
|
|
object: Doc,
|
|
tx: TxCUD<Doc>,
|
|
receiver: ReceiverInfo,
|
|
sender: SenderInfo,
|
|
params: NotifyParams,
|
|
docNotifyContexts: DocNotifyContext[],
|
|
activityMessages: ActivityMessage[],
|
|
settings: NotificationProviderControl
|
|
): Promise<Tx[]> {
|
|
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,
|
|
object,
|
|
receiver,
|
|
params.isOwn,
|
|
params.isSpace,
|
|
settings,
|
|
docMessage
|
|
)
|
|
|
|
if (notifyResult.has(notification.providers.InboxNotificationProvider)) {
|
|
const types = (notifyResult.get(notification.providers.InboxNotificationProvider) ?? []).map((it) => it._id)
|
|
const notificationTx = await pushActivityInboxNotifications(
|
|
ctx,
|
|
tx,
|
|
control,
|
|
res,
|
|
receiver,
|
|
sender,
|
|
object,
|
|
docNotifyContexts,
|
|
message,
|
|
types,
|
|
params.shouldUpdateTimestamp
|
|
)
|
|
|
|
if (notificationTx !== undefined) {
|
|
const current: AvailableProvidersCache = control.contextCache.get(AvailableProvidersCacheKey) ?? new Map()
|
|
const providers = Array.from(notifyResult.keys())
|
|
if (providers.length > 0) {
|
|
current.set(notificationTx.objectId, providers)
|
|
control.contextCache.set('AvailableNotificationProviders', current)
|
|
}
|
|
}
|
|
} else {
|
|
const context = getDocNotifyContext(control, docNotifyContexts, message.attachedTo, receiver.account)
|
|
|
|
if (context === undefined) {
|
|
await createNotifyContext(
|
|
ctx,
|
|
control,
|
|
message.attachedTo,
|
|
message.attachedToClass,
|
|
object.space,
|
|
receiver,
|
|
sender.socialId,
|
|
params.shouldUpdateTimestamp ? tx.modifiedOn : undefined,
|
|
tx
|
|
)
|
|
}
|
|
}
|
|
}
|
|
return res
|
|
}
|
|
|
|
async function updateContextsTimestamp (
|
|
ctx: MeasureContext,
|
|
contexts: DocNotifyContext[],
|
|
timestamp: Timestamp,
|
|
control: TriggerControl,
|
|
modifiedBy: PersonId
|
|
): Promise<void> {
|
|
if (contexts.length === 0) return
|
|
const res: Tx[] = []
|
|
const modifiedByAccount = await getAccountBySocialId(control, modifiedBy)
|
|
|
|
for (const context of contexts) {
|
|
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 && context.user === modifiedByAccount
|
|
? {
|
|
lastViewedTimestamp: timestamp
|
|
}
|
|
: {})
|
|
})
|
|
|
|
res.push(updateTx)
|
|
}
|
|
|
|
if (res.length > 0) {
|
|
await ctx.with('apply', {}, () => control.apply(ctx, res))
|
|
}
|
|
}
|
|
|
|
async function removeContexts (
|
|
ctx: MeasureContext,
|
|
contexts: DocNotifyContext[],
|
|
unsubscribe: AccountUuid[],
|
|
control: TriggerControl
|
|
): Promise<void> {
|
|
if (contexts.length === 0) return
|
|
if (unsubscribe.length === 0) return
|
|
|
|
const res: Tx[] = []
|
|
|
|
for (const context of contexts) {
|
|
if (!unsubscribe.includes(context.user)) {
|
|
continue
|
|
}
|
|
|
|
const removeTx = control.txFactory.createTxRemoveDoc(context._class, context.space, context._id)
|
|
|
|
res.push(removeTx)
|
|
}
|
|
|
|
await control.apply(control.ctx, res)
|
|
}
|
|
|
|
export async function createCollabDocInfo (
|
|
ctx: MeasureContext,
|
|
currentRes: Tx[],
|
|
collaborators: AccountUuid[],
|
|
control: TriggerControl,
|
|
tx: TxCUD<Doc>,
|
|
object: Doc,
|
|
activityMessages: ActivityMessage[],
|
|
params: NotifyParams,
|
|
unsubscribe: AccountUuid[] = [],
|
|
cache: Map<Ref<Doc>, Doc> = new Map<Ref<Doc>, Doc>()
|
|
): Promise<Tx[]> {
|
|
let res: Tx[] = []
|
|
|
|
if (tx.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, tx.modifiedOn, control, tx.modifiedBy)
|
|
}
|
|
if (notifyContexts.length > 0 && unsubscribe.length > 0) {
|
|
await removeContexts(ctx, notifyContexts, unsubscribe, control)
|
|
}
|
|
|
|
const space = await getObjectSpace(control, object, cache)
|
|
|
|
if (space === undefined) {
|
|
control.ctx.error('Cannot find space for object', object)
|
|
Analytics.handleError(
|
|
new Error(`Cannot find space ${object.space} for objectId ${object._id}, objectClass ${object._class}`)
|
|
)
|
|
return res
|
|
}
|
|
|
|
cache.set(space._id, space)
|
|
|
|
const filteredCollaborators = !space.private
|
|
? collaborators
|
|
: collaborators.filter(
|
|
(it) =>
|
|
space.members.includes(it) ||
|
|
currentRes.some((tx) => {
|
|
if (tx._class === core.class.TxUpdateDoc) {
|
|
const updateTx = tx as TxUpdateDoc<Space>
|
|
if (updateTx.objectId === space._id) {
|
|
const added = combineAttributes([updateTx.operations], 'members', '$push', '$each')
|
|
return added.includes(it)
|
|
}
|
|
}
|
|
return false
|
|
})
|
|
)
|
|
const targets = new Set(filteredCollaborators)
|
|
|
|
// 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.mixin.Employee)) {
|
|
const account = (object as Employee).personUuid
|
|
|
|
if (account != null) {
|
|
targets.add(account)
|
|
}
|
|
}
|
|
|
|
if (targets.size === 0) {
|
|
return res
|
|
}
|
|
|
|
const receivers = await getReceiversInfo(ctx, Array.from(targets), control)
|
|
const sender: SenderInfo = await getSenderInfo(ctx, tx.modifiedBy, control)
|
|
const settings = await getNotificationProviderControl(ctx, control)
|
|
|
|
for (const receiver of receivers) {
|
|
const targetRes = await getNotificationTxes(
|
|
ctx,
|
|
control,
|
|
object,
|
|
tx,
|
|
receiver,
|
|
sender,
|
|
params,
|
|
notifyContexts,
|
|
docMessages,
|
|
settings
|
|
)
|
|
|
|
res = res.concat(targetRes)
|
|
}
|
|
return res
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
export function getAddCollaboratTxes (
|
|
objectId: Ref<Doc>,
|
|
objectClass: Ref<Class<Doc>>,
|
|
objectSpace: Ref<Space>,
|
|
control: TriggerControl,
|
|
collaborators: AccountUuid[]
|
|
): TxCreateDoc<Collaborator>[] {
|
|
const res: TxCreateDoc<Collaborator>[] = []
|
|
for (const collaborator of collaborators) {
|
|
const tx = control.txFactory.createTxCreateDoc(core.class.Collaborator, objectSpace, {
|
|
attachedTo: objectId,
|
|
attachedToClass: objectClass,
|
|
collaborator,
|
|
collection: 'collaborators'
|
|
})
|
|
res.push(tx)
|
|
}
|
|
return res
|
|
}
|
|
|
|
async function getTxCollabs (
|
|
ctx: MeasureContext,
|
|
tx: TxCUD<Doc>,
|
|
control: TriggerControl,
|
|
cache: Map<Ref<Doc>, Collaborator[]>,
|
|
doc: Doc
|
|
): Promise<{
|
|
added: AccountUuid[]
|
|
removed: AccountUuid[]
|
|
result: AccountUuid[]
|
|
}> {
|
|
const { hierarchy } = control
|
|
const mixin = getClassCollaborators(control.modelDb, hierarchy, doc._class)
|
|
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) {
|
|
return { added: [], removed: [], result: [] }
|
|
}
|
|
|
|
if ([core.class.TxUpdateDoc, core.class.TxMixin].includes(tx._class)) {
|
|
const collaborators =
|
|
cache.get(doc._id) ??
|
|
(await control.findAll(ctx, core.class.Collaborator, {
|
|
attachedTo: doc._id
|
|
}))
|
|
cache.set(doc._id, collaborators)
|
|
const collabs = collaborators.map((c) => c.collaborator)
|
|
const ops = isMixinTx(tx) ? tx.attributes : (tx as TxUpdateDoc<Doc>).operations
|
|
const newCollaborators = (await getNewCollaborators(ops, mixin, doc._class, control)).filter(
|
|
(p) => !collabs.includes(p)
|
|
)
|
|
const isSpace = control.hierarchy.isDerived(doc._class, core.class.Space)
|
|
const removedCollabs = isSpace ? await 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<Doc>,
|
|
activityMessages: ActivityMessage[],
|
|
cache: Map<Ref<Doc>, Collaborator[]>,
|
|
docCache: Map<Ref<Doc>, Doc>
|
|
): Promise<Tx[]> {
|
|
if (doc.space === core.space.Space) {
|
|
return []
|
|
}
|
|
|
|
const space = await getObjectSpace(control, doc, docCache)
|
|
if (space === undefined) return []
|
|
|
|
docCache.set(space._id, space)
|
|
|
|
const mixin = getClassCollaborators(control.modelDb, control.hierarchy, space._class)
|
|
if (mixin !== undefined) {
|
|
const collaborators =
|
|
cache.get(space._id) ??
|
|
(await control.findAll(ctx, core.class.Collaborator, {
|
|
attachedTo: space._id
|
|
}))
|
|
cache.set(space._id, collaborators)
|
|
const collabs = collaborators.map((c) => c.collaborator)
|
|
return await createCollabDocInfo(
|
|
ctx,
|
|
[],
|
|
collabs,
|
|
control,
|
|
tx,
|
|
doc,
|
|
activityMessages,
|
|
{ isSpace: true, isOwn: false, shouldUpdateTimestamp: true },
|
|
[],
|
|
docCache
|
|
)
|
|
}
|
|
return []
|
|
}
|
|
|
|
async function pushCollaboratorsToPublicSpace (
|
|
control: TriggerControl,
|
|
doc: Doc,
|
|
collaborators: AccountUuid[],
|
|
cache: Map<Ref<Doc>, Doc>
|
|
): Promise<Tx[]> {
|
|
const space = await getObjectSpace(control, doc, cache)
|
|
if (space === undefined) return []
|
|
|
|
cache.set(space._id, space)
|
|
|
|
if (control.hierarchy.isDerived(space._class, core.class.SystemSpace)) {
|
|
return []
|
|
}
|
|
|
|
if (space.private) {
|
|
return []
|
|
}
|
|
|
|
return collaborators
|
|
.filter((it) => !space.members.includes(it))
|
|
.map((it) => control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, { $push: { members: it } }))
|
|
}
|
|
|
|
async function createCollaboratorDoc (
|
|
ctx: MeasureContext,
|
|
tx: TxCreateDoc<Doc>,
|
|
control: TriggerControl,
|
|
activityMessage: ActivityMessage[],
|
|
cache: Map<Ref<Doc>, Collaborator[]>,
|
|
docCache: Map<Ref<Doc>, Doc>
|
|
): Promise<Tx[]> {
|
|
const res: Tx[] = []
|
|
const hierarchy = control.hierarchy
|
|
const mixin = getClassCollaborators(control.modelDb, hierarchy, tx.objectClass)
|
|
if (mixin === undefined) {
|
|
return res
|
|
}
|
|
|
|
const doc = TxProcessor.createDoc2Doc(tx)
|
|
const collaborators = await ctx.with('get-collaborators', {}, (ctx) => getDocCollaborators(ctx, doc, mixin, control))
|
|
const mixinTx = getAddCollaboratTxes(tx.objectId, tx.objectClass, tx.objectSpace, control, collaborators)
|
|
|
|
res.push(...mixinTx)
|
|
|
|
res.push(
|
|
...(await ctx.with('get-space-collabtxes', {}, (ctx) =>
|
|
getSpaceCollabTxes(ctx, control, doc, tx, activityMessage, cache, docCache)
|
|
))
|
|
)
|
|
|
|
res.push(...(await pushCollaboratorsToPublicSpace(control, doc, collaborators, docCache)))
|
|
|
|
const notificationTxes = await ctx.with('create-collabdocinfo', {}, (ctx) =>
|
|
createCollabDocInfo(
|
|
ctx,
|
|
res,
|
|
collaborators,
|
|
control,
|
|
tx,
|
|
doc,
|
|
activityMessage,
|
|
{
|
|
isOwn: true,
|
|
isSpace: false,
|
|
shouldUpdateTimestamp: true
|
|
},
|
|
[],
|
|
docCache
|
|
)
|
|
)
|
|
|
|
res.push(...notificationTxes)
|
|
|
|
return res
|
|
}
|
|
|
|
async function collectionCollabDoc (
|
|
ctx: MeasureContext,
|
|
tx: TxCUD<AttachedDoc>,
|
|
control: TriggerControl,
|
|
activityMessages: ActivityMessage[],
|
|
cache: Map<Ref<Doc>, Collaborator[]>,
|
|
docCache: Map<Ref<Doc>, Doc>,
|
|
ignoreCollection: boolean = false
|
|
): Promise<Tx[]> {
|
|
let res = await createCollaboratorNotifications(
|
|
ctx,
|
|
tx,
|
|
control,
|
|
activityMessages,
|
|
tx,
|
|
cache,
|
|
docCache,
|
|
ignoreCollection
|
|
)
|
|
|
|
if (![core.class.TxCreateDoc, core.class.TxRemoveDoc, core.class.TxUpdateDoc].includes(tx._class)) {
|
|
return res
|
|
}
|
|
|
|
const { attachedTo, attachedToClass } = tx
|
|
|
|
if (attachedTo === undefined || attachedToClass === undefined) {
|
|
return res
|
|
}
|
|
|
|
const mixin = getClassCollaborators(control.modelDb, control.hierarchy, attachedToClass)
|
|
|
|
if (mixin === undefined) {
|
|
return res
|
|
}
|
|
|
|
const doc = await ctx.with(
|
|
'get-doc',
|
|
{},
|
|
async (ctx) =>
|
|
docCache.get(attachedTo) ?? (await control.findAll(ctx, attachedToClass, { _id: attachedTo }, { limit: 1 }))[0]
|
|
)
|
|
|
|
if (doc === undefined) {
|
|
return res
|
|
}
|
|
|
|
docCache.set(doc._id, doc)
|
|
|
|
const collaborators = await ctx.with('get-collaborators', {}, (ctx) => getCollaborators(ctx, doc, control, tx, res))
|
|
|
|
res = res.concat(
|
|
await ctx.with('create-collab-doc-info', {}, (ctx) =>
|
|
createCollabDocInfo(
|
|
ctx,
|
|
res,
|
|
collaborators,
|
|
control,
|
|
tx,
|
|
doc,
|
|
activityMessages,
|
|
{
|
|
isOwn: false,
|
|
isSpace: false,
|
|
shouldUpdateTimestamp: true
|
|
},
|
|
[],
|
|
docCache
|
|
)
|
|
)
|
|
)
|
|
|
|
return res
|
|
}
|
|
|
|
async function removeContextNotifications (
|
|
control: TriggerControl,
|
|
notifyContextRefs: Ref<DocNotifyContext>[]
|
|
): Promise<Tx[]> {
|
|
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<Doc>, control: TriggerControl): Promise<Tx[]> {
|
|
const mixin = getClassCollaborators(control.modelDb, control.hierarchy, tx.objectClass)
|
|
|
|
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
|
|
}
|
|
|
|
async function getNewCollaborators (
|
|
ops: DocumentUpdate<Doc> | MixinUpdate<Doc, Doc>,
|
|
mixin: ClassCollaborators<Doc>,
|
|
docClass: Ref<Class<Doc>>,
|
|
control: TriggerControl
|
|
): Promise<AccountUuid[]> {
|
|
const newCollaborators = new Set<AccountUuid>()
|
|
if (ops.$push !== undefined) {
|
|
for (const key in ops.$push) {
|
|
if (mixin.fields.includes(key as any) || mixin.allFields === true) {
|
|
let value = (ops.$push as any)[key]
|
|
if (typeof value !== 'string') {
|
|
value = value.$each
|
|
}
|
|
const newCollabs = await 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 as any) || mixin.allFields === true) {
|
|
const value = (ops as any)[key]
|
|
const newCollabs = await getKeyCollaborators(docClass, value, key, control)
|
|
if (newCollabs !== undefined) {
|
|
for (const newCollab of newCollabs) {
|
|
newCollaborators.add(newCollab)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return Array.from(newCollaborators)
|
|
}
|
|
|
|
async function getRemovedMembers (
|
|
ops: DocumentUpdate<Space> | MixinUpdate<Space, Space>,
|
|
mixin: ClassCollaborators<Doc>,
|
|
docClass: Ref<Class<Space>>,
|
|
control: TriggerControl
|
|
): Promise<AccountUuid[]> {
|
|
const removedCollaborators: AccountUuid[] = []
|
|
if (ops.$pull !== undefined && 'members' in ops.$pull) {
|
|
const key = 'members'
|
|
if (mixin.fields.includes(key as any) || mixin.allFields === true) {
|
|
let value = (ops.$pull as any)[key]
|
|
if (typeof value !== 'string') {
|
|
value = value.$in
|
|
}
|
|
const collabs = await getKeyCollaborators(docClass, value, key, control)
|
|
if (collabs !== undefined) {
|
|
removedCollaborators.push(...collabs)
|
|
}
|
|
}
|
|
}
|
|
|
|
return Array.from(new Set(removedCollaborators))
|
|
}
|
|
|
|
async function createSyncCollaboratorsTxes (
|
|
ctx: MeasureContext,
|
|
control: TriggerControl,
|
|
cache: Map<Ref<Doc>, Collaborator[]>,
|
|
objectId: Ref<Doc>,
|
|
objectClass: Ref<Class<Doc>>,
|
|
objectSpace: Ref<Space>,
|
|
added: AccountUuid[],
|
|
removed: AccountUuid[]
|
|
): Promise<{ txes: Tx[], toAdd: AccountUuid[] }> {
|
|
const res: Tx[] = []
|
|
let currentCollaborators =
|
|
cache.get(objectId) ??
|
|
(await control.findAll(ctx, core.class.Collaborator, {
|
|
attachedTo: objectId
|
|
}))
|
|
|
|
const toAdd = added.filter((p) => currentCollaborators.find((c) => c.collaborator === p) === undefined)
|
|
|
|
if (toAdd.length === 0 && removed.length === 0) return { txes: res, toAdd: [] }
|
|
|
|
if (toAdd.length > 0) {
|
|
const txes = getAddCollaboratTxes(objectId, objectClass, objectSpace, control, toAdd)
|
|
res.push(...txes)
|
|
txes.forEach((tx) => {
|
|
const collab = TxProcessor.createDoc2Doc(tx)
|
|
currentCollaborators.push(collab)
|
|
})
|
|
}
|
|
|
|
if (removed.length > 0) {
|
|
const toRemove: Collaborator[] = []
|
|
const collabs: Collaborator[] = []
|
|
for (const collab of currentCollaborators) {
|
|
if (removed.includes(collab.collaborator)) {
|
|
toRemove.push(collab)
|
|
} else {
|
|
collabs.push(collab)
|
|
}
|
|
}
|
|
for (const removedCollab of toRemove) {
|
|
res.push(control.txFactory.createTxRemoveDoc(core.class.Collaborator, removedCollab.space, removedCollab._id))
|
|
}
|
|
currentCollaborators = collabs
|
|
}
|
|
|
|
cache.set(objectId, currentCollaborators)
|
|
|
|
return { txes: res, toAdd }
|
|
}
|
|
|
|
async function updateCollaboratorDoc (
|
|
ctx: MeasureContext,
|
|
tx: TxUpdateDoc<Doc> | TxMixin<Doc, Doc>,
|
|
control: TriggerControl,
|
|
activityMessages: ActivityMessage[],
|
|
cache: Map<Ref<Doc>, Collaborator[]>,
|
|
docCache: Map<Ref<Doc>, Doc>
|
|
): Promise<Tx[]> {
|
|
const hierarchy = control.hierarchy
|
|
let res: Tx[] = []
|
|
const mixin = getClassCollaborators(control.modelDb, hierarchy, tx.objectClass)
|
|
if (mixin === undefined) return []
|
|
const doc = await ctx.with(
|
|
'find-doc',
|
|
{ _class: tx.objectClass },
|
|
async (ctx) =>
|
|
docCache.get(tx.objectId) ?? (await control.findAll(ctx, tx.objectClass, { _id: tx.objectId }, { limit: 1 }))[0]
|
|
)
|
|
if (doc === undefined) return []
|
|
docCache.set(doc._id, doc)
|
|
const params: NotifyParams = { isOwn: true, isSpace: false, shouldUpdateTimestamp: true }
|
|
// we should handle change field and subscribe new collaborators
|
|
const collabsInfo = await ctx.with('get-tx-collaborators', {}, (ctx) => getTxCollabs(ctx, tx, control, cache, doc))
|
|
const sync = await createSyncCollaboratorsTxes(
|
|
ctx,
|
|
control,
|
|
cache,
|
|
doc._id,
|
|
doc._class,
|
|
doc.space,
|
|
collabsInfo.added,
|
|
collabsInfo.removed
|
|
)
|
|
res.push(...sync.txes)
|
|
|
|
res = res.concat(
|
|
await ctx.with('create-collab-docinfo', {}, (ctx) =>
|
|
createCollabDocInfo(
|
|
ctx,
|
|
res,
|
|
collabsInfo.result,
|
|
control,
|
|
tx,
|
|
doc,
|
|
activityMessages,
|
|
params,
|
|
collabsInfo.removed,
|
|
docCache
|
|
)
|
|
)
|
|
)
|
|
|
|
res = res.concat(
|
|
await ctx.with('get-space-collabtxes', {}, (ctx) =>
|
|
getSpaceCollabTxes(ctx, control, doc, tx, activityMessages, cache, docCache)
|
|
)
|
|
)
|
|
res = res.concat(
|
|
await ctx.with('update-notify-context-space', {}, (ctx) => updateNotifyContextsSpace(ctx, control, tx))
|
|
)
|
|
|
|
return res
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
export async function OnAttributeCreate (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
|
|
const result: Tx[] = []
|
|
for (const tx of txes) {
|
|
const attribute = TxProcessor.createDoc2Doc(tx as TxCreateDoc<AnyAttribute>)
|
|
const group = (
|
|
await control.modelDb.findAll(notification.class.NotificationGroup, { objectClass: attribute.attributeOf })
|
|
)[0]
|
|
if (group === undefined) {
|
|
continue
|
|
}
|
|
const isCollection: boolean = core.class.Collection === attribute.type._class
|
|
const objectClass = !isCollection ? attribute.attributeOf : (attribute.type as Collection<AttachedDoc>).of
|
|
const txClasses = !isCollection
|
|
? [control.hierarchy.isMixin(attribute.attributeOf) ? core.class.TxMixin : core.class.TxUpdateDoc]
|
|
: [core.class.TxCreateDoc, core.class.TxRemoveDoc]
|
|
const data: Data<NotificationType> = {
|
|
attribute: attribute._id,
|
|
group: group._id,
|
|
field: attribute.name,
|
|
generated: true,
|
|
objectClass,
|
|
txClasses,
|
|
hidden: false,
|
|
defaultEnabled: false,
|
|
templates: {
|
|
textTemplate: '{body}',
|
|
htmlTemplate: '<p>{body}</p><p>{link}</p>',
|
|
subjectTemplate: '{doc} updated'
|
|
},
|
|
label: attribute.label
|
|
}
|
|
if (isCollection) {
|
|
data.attachedToClass = attribute.attributeOf
|
|
}
|
|
const id =
|
|
`${notification.class.NotificationType}_${attribute.attributeOf}_${attribute.name}` as Ref<NotificationType>
|
|
result.push(control.txFactory.createTxCreateDoc(notification.class.NotificationType, core.space.Model, data, id))
|
|
}
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
export async function OnAttributeUpdate (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
|
|
const result: Tx[] = []
|
|
for (const tx of txes) {
|
|
const ctx = tx as TxUpdateDoc<AnyAttribute>
|
|
if (ctx.operations.hidden === undefined) {
|
|
continue
|
|
}
|
|
const type = (
|
|
await control.findAll(control.ctx, notification.class.NotificationType, { attribute: ctx.objectId })
|
|
)[0]
|
|
if (type === undefined) {
|
|
continue
|
|
}
|
|
result.push(
|
|
control.txFactory.createTxUpdateDoc(type._class, type.space, type._id, {
|
|
hidden: ctx.operations.hidden
|
|
})
|
|
)
|
|
}
|
|
return result
|
|
}
|
|
|
|
async function updateCollaborators (
|
|
ctx: MeasureContext,
|
|
control: TriggerControl,
|
|
tx: TxCUD<Doc>,
|
|
cache: Map<Ref<Doc>, Collaborator[]>
|
|
): Promise<Tx[]> {
|
|
if (tx._class !== core.class.TxUpdateDoc && tx._class !== core.class.TxMixin) return []
|
|
|
|
const hierarchy = control.hierarchy
|
|
|
|
if (hierarchy.classHierarchyMixin(tx.objectClass, activity.mixin.ActivityDoc) === undefined) return []
|
|
|
|
const mixin = getClassCollaborators(control.modelDb, hierarchy, tx.objectClass)
|
|
if (mixin === undefined) return []
|
|
|
|
const doc = (await control.findAll(ctx, tx.objectClass, { _id: tx.objectId }, { limit: 1 }))[0]
|
|
if (doc === undefined) return []
|
|
|
|
const collabsResult = await getTxCollabs(ctx, tx, control, cache, doc)
|
|
if (collabsResult.added.length === 0 && collabsResult.removed.length === 0) return []
|
|
|
|
const res: Tx[] = []
|
|
|
|
const { txes: collabTxes } = await createSyncCollaboratorsTxes(
|
|
ctx,
|
|
control,
|
|
cache,
|
|
doc._id,
|
|
doc._class,
|
|
doc.space,
|
|
collabsResult.added,
|
|
collabsResult.removed
|
|
)
|
|
|
|
res.push(...collabTxes)
|
|
|
|
const contexts = await control.findAll(control.ctx, notification.class.DocNotifyContext, { objectId: tx.attachedTo })
|
|
const addedInfo = collabsResult.added.length > 0 ? await getReceiversInfo(ctx, collabsResult.added, control) : []
|
|
|
|
for (const info of addedInfo) {
|
|
const context = getDocNotifyContext(control, contexts, doc._id, info.account)
|
|
if (context !== undefined) {
|
|
if (context.hidden) {
|
|
res.push(control.txFactory.createTxUpdateDoc(context._class, context.space, context._id, { hidden: false }))
|
|
}
|
|
}
|
|
await createNotifyContext(ctx, control, doc._id, doc._class, doc.space, info, tx.modifiedBy, undefined, tx)
|
|
}
|
|
|
|
if (collabsResult.removed.length > 0) {
|
|
await removeContexts(ctx, contexts, collabsResult.removed, control)
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
export async function createCollaboratorNotifications (
|
|
ctx: MeasureContext,
|
|
tx: TxCUD<Doc>,
|
|
control: TriggerControl,
|
|
activityMessages: ActivityMessage[],
|
|
originTx?: TxCUD<Doc>,
|
|
cache: Map<Ref<Doc>, Collaborator[]> = new Map<Ref<Doc>, Collaborator[]>(),
|
|
docCache: Map<Ref<Doc>, Doc> = new Map<Ref<Doc>, Doc>(),
|
|
ignoreCollection: boolean = false
|
|
): Promise<Tx[]> {
|
|
if (tx.space === core.space.DerivedTx) {
|
|
// do not forgot update collaborators for derived tx
|
|
return await ctx.with('updateDerivedCollaborators', {}, (ctx) => updateCollaborators(ctx, control, tx, cache))
|
|
}
|
|
|
|
if (activityMessages.length === 0) {
|
|
return []
|
|
}
|
|
|
|
if (tx.attachedTo !== undefined && !ignoreCollection) {
|
|
return await ctx.with('collectionCollabDoc', {}, (ctx) =>
|
|
collectionCollabDoc(ctx, tx as TxCUD<AttachedDoc>, control, activityMessages, cache, docCache, true)
|
|
)
|
|
}
|
|
|
|
switch (tx._class) {
|
|
case core.class.TxCreateDoc: {
|
|
return await ctx.with('createCollaboratorDoc', {}, (ctx) =>
|
|
createCollaboratorDoc(ctx, tx as TxCreateDoc<Doc>, control, activityMessages, cache, docCache)
|
|
)
|
|
}
|
|
case core.class.TxUpdateDoc:
|
|
case core.class.TxMixin: {
|
|
const res = await ctx.with('updateCollaboratorDoc', {}, (ctx) =>
|
|
updateCollaboratorDoc(ctx, tx as TxUpdateDoc<Doc>, control, activityMessages, cache, docCache)
|
|
)
|
|
return res
|
|
}
|
|
}
|
|
|
|
return []
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
export async function removeDocInboxNotifications (_id: Ref<ActivityMessage>, control: TriggerControl): Promise<Tx[]> {
|
|
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<Doc>,
|
|
res: Tx[]
|
|
): Promise<AccountUuid[]> {
|
|
const mixin = getClassCollaborators(control.modelDb, control.hierarchy, doc._class)
|
|
|
|
if (mixin === undefined) {
|
|
return []
|
|
}
|
|
|
|
const collaborators = await control.findAll(ctx, core.class.Collaborator, {
|
|
attachedTo: doc._id
|
|
})
|
|
|
|
if (collaborators.length > 0) {
|
|
return collaborators.map((p) => p.collaborator)
|
|
} else {
|
|
const collaborators = await getDocCollaborators(ctx, doc, mixin, control)
|
|
|
|
res.push(...getAddCollaboratTxes(tx.objectId, tx.objectClass, tx.objectSpace, control, collaborators))
|
|
return collaborators
|
|
}
|
|
}
|
|
|
|
function getDocNotifyContext (
|
|
control: TriggerControl,
|
|
contexts: DocNotifyContext[],
|
|
objectId: Ref<Doc>,
|
|
user: AccountUuid
|
|
): 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<Doc>[]
|
|
|
|
for (const tx of txes) {
|
|
if (tx._class === core.class.TxCreateDoc && tx.objectClass === notification.class.DocNotifyContext) {
|
|
const doc = TxProcessor.createDoc2Doc(tx as TxCreateDoc<DocNotifyContext>)
|
|
if (doc.objectId === objectId && doc.user === user) {
|
|
return doc
|
|
}
|
|
}
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
async function OnActivityMessageRemove (message: ActivityMessage, control: TriggerControl): Promise<Tx[]> {
|
|
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 OnEmployeeDeactivate (txes: TxCUD<Doc>[], control: TriggerControl): Promise<Tx[]> {
|
|
const result: Tx[] = []
|
|
for (const tx of txes) {
|
|
const actualTx = tx
|
|
if (core.class.TxMixin !== actualTx._class) {
|
|
return []
|
|
}
|
|
const ctx = actualTx as TxMixin<Person, Employee>
|
|
if (ctx.mixin !== contact.mixin.Employee || ctx.attributes.active !== false) {
|
|
return []
|
|
}
|
|
const person = (await control.findAll(control.ctx, contact.class.Person, { _id: ctx.objectId }))[0]
|
|
if (person?.personUuid === undefined) return []
|
|
|
|
const res: Tx[] = []
|
|
const subscriptions = await control.findAll(control.ctx, notification.class.PushSubscription, {
|
|
user: person.personUuid as AccountUuid
|
|
})
|
|
for (const sub of subscriptions) {
|
|
res.push(control.txFactory.createTxRemoveDoc(sub._class, sub.space, sub._id))
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
async function OnDocRemove (txes: TxCUD<Doc>[], control: TriggerControl): Promise<Tx[]> {
|
|
const ltxes = txes.filter((it) => it._class === core.class.TxRemoveDoc) as TxRemoveDoc<Doc>[]
|
|
const res: Tx[] = []
|
|
for (const tx of ltxes) {
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
res.push(...(await removeContextNotifications(control, [tx.objectId as Ref<DocNotifyContext>])))
|
|
}
|
|
|
|
res.push(...(await removeCollaboratorDoc(tx, control)))
|
|
}
|
|
return res
|
|
}
|
|
|
|
export * from './push'
|
|
export * from './types'
|
|
export * from './utils'
|
|
|
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
export default async () => ({
|
|
trigger: {
|
|
OnAttributeCreate,
|
|
OnAttributeUpdate,
|
|
OnDocRemove,
|
|
OnEmployeeDeactivate,
|
|
PushNotificationsHandler
|
|
},
|
|
function: {
|
|
IsUserEmployeeInFieldValueTypeMatch: isUserEmployeeInFieldValueTypeMatch,
|
|
MentionTypeMatch: mentionTypeMatch
|
|
}
|
|
})
|