// // Copyright © 2023 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, ActivityMessageControl, DocAttributeUpdates, DocUpdateMessage, Reaction } from '@hcengineering/activity' import { PersonAccount } from '@hcengineering/contact' import core, { Account, AttachedDoc, Class, Collection, Data, Doc, Hierarchy, matchQuery, MeasureContext, Ref, Space, Tx, TxCollectionCUD, TxCreateDoc, TxCUD, TxProcessor } from '@hcengineering/core' import notification, { NotificationContent } from '@hcengineering/notification' import { getResource, translate } from '@hcengineering/platform' import { ActivityControl, DocObjectCache } from '@hcengineering/server-activity' import type { TriggerControl } from '@hcengineering/server-core' import { createCollabDocInfo, createCollaboratorNotifications, getTextPresenter, removeDocInboxNotifications } from '@hcengineering/server-notification-resources' import { ReferenceTrigger } from './references' import { getAttrName, getCollectionAttribute, getDocUpdateAction, getTxAttributesUpdates } from './utils' export async function OnReactionChanged (originTx: Tx, control: TriggerControl): Promise<Tx[]> { const tx = originTx as TxCollectionCUD<ActivityMessage, Reaction> const innerTx = TxProcessor.extractTx(tx) as TxCUD<Reaction> if (innerTx._class === core.class.TxCreateDoc) { const txes = await createReactionNotifications(tx, control) await control.apply(control.ctx, txes) return [] } if (innerTx._class === core.class.TxRemoveDoc) { const txes = await removeReactionNotifications(tx, control) await control.apply(control.ctx, txes) return [] } return [] } export async function removeReactionNotifications ( tx: TxCollectionCUD<ActivityMessage, Reaction>, control: TriggerControl ): Promise<Tx[]> { const message = ( await control.findAll( control.ctx, activity.class.ActivityMessage, { objectId: tx.tx.objectId }, { projection: { _id: 1, _class: 1, space: 1 } } ) )[0] if (message === undefined) { return [] } const res: Tx[] = [] const txes = await removeDocInboxNotifications(message._id, control) const removeTx = control.txFactory.createTxRemoveDoc(message._class, message.space, message._id) res.push(removeTx) res.push(...txes) return res } export async function createReactionNotifications ( tx: TxCollectionCUD<ActivityMessage, Reaction>, control: TriggerControl ): Promise<Tx[]> { const createTx = TxProcessor.extractTx(tx) as TxCreateDoc<Reaction> const parentMessage = (await control.findAll(control.ctx, activity.class.ActivityMessage, { _id: tx.objectId }))[0] if (parentMessage === undefined) { return [] } const user = parentMessage.createdBy if (user === undefined || user === core.account.System || user === tx.modifiedBy) { return [] } let res: Tx[] = [] const rawMessage: Data<DocUpdateMessage> = { txId: tx._id, attachedTo: parentMessage._id, attachedToClass: parentMessage._class, objectId: createTx.objectId, objectClass: createTx.objectClass, action: 'create', collection: 'docUpdateMessages', updateCollection: tx.collection } const messageTx = getDocUpdateMessageTx(control, tx, parentMessage, rawMessage, tx.modifiedBy) if (messageTx === undefined) { return [] } res.push(messageTx) const docUpdateMessage = TxProcessor.createDoc2Doc(messageTx.tx as TxCreateDoc<DocUpdateMessage>) res = res.concat( await createCollabDocInfo( control.ctx, [user] as Ref<PersonAccount>[], control, tx.tx, tx, parentMessage, [docUpdateMessage], { isOwn: true, isSpace: false, shouldUpdateTimestamp: false } ) ) return res } function isActivityDoc (_class: Ref<Class<Doc>>, hierarchy: Hierarchy): boolean { const mixin = hierarchy.classHierarchyMixin(_class, activity.mixin.ActivityDoc) return mixin !== undefined } function isSpace (space: Doc, hierarchy: Hierarchy): space is Space { return hierarchy.isDerived(space._class, core.class.Space) } function getDocUpdateMessageTx ( control: ActivityControl, originTx: TxCUD<Doc>, object: Doc, rawMessage: Data<DocUpdateMessage>, modifiedBy?: Ref<Account> ): TxCollectionCUD<Doc, DocUpdateMessage> { const { hierarchy } = control const space = isSpace(object, hierarchy) ? object._id : object.space const innerTx = control.txFactory.createTxCreateDoc( activity.class.DocUpdateMessage, space, rawMessage, undefined, originTx.modifiedOn, modifiedBy ?? originTx.modifiedBy ) return control.txFactory.createTxCollectionCUD( rawMessage.attachedToClass, rawMessage.attachedTo, space, rawMessage.collection, innerTx, originTx.modifiedOn, modifiedBy ?? originTx.modifiedBy ) } export async function pushDocUpdateMessages ( ctx: MeasureContext, control: ActivityControl, res: TxCollectionCUD<Doc, DocUpdateMessage>[], object: Doc | undefined, originTx: TxCUD<Doc>, modifiedBy?: Ref<Account>, objectCache?: DocObjectCache, controlRules?: ActivityMessageControl[] ): Promise<TxCollectionCUD<Doc, DocUpdateMessage>[]> { if (object === undefined) { return res } if (!isActivityDoc(object._class, control.hierarchy)) { return res } const tx = originTx._class === core.class.TxCollectionCUD ? (originTx as TxCollectionCUD<Doc, AttachedDoc>).tx : originTx const rawMessage: Data<DocUpdateMessage> = { txId: originTx._id, attachedTo: object._id, attachedToClass: object._class, objectId: tx.objectId, objectClass: tx.objectClass, action: getDocUpdateAction(control, tx), collection: 'docUpdateMessages', updateCollection: originTx._class === core.class.TxCollectionCUD ? (originTx as TxCollectionCUD<Doc, AttachedDoc>).collection : undefined } const attributesUpdates = await getTxAttributesUpdates(ctx, control, originTx, tx, object, objectCache, controlRules) for (const attributeUpdates of attributesUpdates) { res.push( getDocUpdateMessageTx( control, originTx, object, { ...rawMessage, attributeUpdates }, modifiedBy ) ) } if (attributesUpdates.length === 0 && rawMessage.action !== 'update') { res.push(getDocUpdateMessageTx(control, originTx, object, rawMessage, modifiedBy)) } return res } export async function generateDocUpdateMessages ( ctx: MeasureContext, tx: TxCUD<Doc>, control: ActivityControl, res: TxCollectionCUD<Doc, DocUpdateMessage>[] = [], originTx?: TxCUD<Doc>, objectCache?: DocObjectCache ): Promise<TxCollectionCUD<Doc, DocUpdateMessage>[]> { if (tx.space === core.space.DerivedTx) { return res } const { hierarchy } = control const etx = TxProcessor.extractTx(tx) as TxCUD<Doc> if ( hierarchy.isDerived(tx.objectClass, activity.class.ActivityMessage) || hierarchy.isDerived(etx.objectClass, activity.class.ActivityMessage) ) { return res } if ( hierarchy.classHierarchyMixin(tx.objectClass, activity.mixin.IgnoreActivity) !== undefined || hierarchy.classHierarchyMixin(etx.objectClass, activity.mixin.IgnoreActivity) !== undefined ) { return res } // Check if we have override control over transaction => activity mappings const controlRules = control.modelDb.findAllSync<ActivityMessageControl>(activity.class.ActivityMessageControl, { objectClass: { $in: hierarchy.getAncestors(tx.objectClass) } }) if (controlRules.length > 0) { for (const r of controlRules) { for (const s of r.skip) { const otx = originTx ?? etx if (matchQuery(otx !== undefined ? [tx, otx] : [tx], s, r.objectClass, hierarchy).length > 0) { // Match found, we need to skip return res } } } } switch (tx._class) { case core.class.TxCreateDoc: { const doc = TxProcessor.createDoc2Doc(tx as TxCreateDoc<Doc>) return await ctx.with( 'pushDocUpdateMessages', {}, async (ctx) => await pushDocUpdateMessages(ctx, control, res, doc, originTx ?? tx, undefined, objectCache, controlRules) ) } case core.class.TxMixin: case core.class.TxUpdateDoc: { if (!isActivityDoc(tx.objectClass, control.hierarchy)) { return res } let doc = objectCache?.docs?.get(tx.objectId) if (doc === undefined) { doc = (await control.findAll(ctx, tx.objectClass, { _id: tx.objectId }, { limit: 1 }))[0] objectCache?.docs?.set(tx.objectId, doc) } return await ctx.with( 'pushDocUpdateMessages', {}, async (ctx) => await pushDocUpdateMessages( ctx, control, res, doc ?? undefined, originTx ?? tx, undefined, objectCache, controlRules ) ) } case core.class.TxCollectionCUD: { const actualTx = TxProcessor.extractTx(tx) as TxCUD<Doc> res = await generateDocUpdateMessages(ctx, actualTx, control, res, tx, objectCache) if ([core.class.TxCreateDoc, core.class.TxRemoveDoc].includes(actualTx._class)) { if (!isActivityDoc(tx.objectClass, control.hierarchy)) { return res } let doc = objectCache?.docs?.get(tx.objectId) if (doc === undefined) { doc = (await control.findAll(ctx, tx.objectClass, { _id: tx.objectId }, { limit: 1 }))[0] objectCache?.docs?.set(tx.objectId, doc) } if (doc !== undefined) { return await ctx.with( 'pushDocUpdateMessages', {}, async (ctx) => await pushDocUpdateMessages( ctx, control, res, doc ?? undefined, originTx ?? tx, undefined, objectCache, controlRules ) ) } } return res } } return res } async function ActivityMessagesHandler (tx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> { if ( control.hierarchy.isDerived(tx.objectClass, activity.class.ActivityMessage) || control.hierarchy.isDerived(tx.objectClass, notification.class.DocNotifyContext) || control.hierarchy.isDerived(tx.objectClass, notification.class.ActivityInboxNotification) || control.hierarchy.isDerived(tx.objectClass, notification.class.BrowserNotification) ) { return [] } const cache: DocObjectCache = control.contextCache.get('ActivityMessagesHandler') ?? { docs: new Map(), transactions: new Map() } control.contextCache.set('ActivityMessagesHandler', cache) const txes = tx.space === core.space.DerivedTx ? [] : await control.ctx.with( 'generateDocUpdateMessages', {}, async (ctx) => await generateDocUpdateMessages(ctx, tx, control, [], undefined, cache) ) const messages = txes.map((messageTx) => TxProcessor.createDoc2Doc(messageTx.tx as TxCreateDoc<DocUpdateMessage>)) const notificationTxes = await control.ctx.with( 'createCollaboratorNotifications', {}, async (ctx) => await createCollaboratorNotifications(ctx, tx, control, messages, undefined, cache.docs as Map<Ref<Doc>, Doc>) ) const result = [...txes, ...notificationTxes] if (result.length > 0) { await control.apply(control.ctx, result) } return [] } async function OnDocRemoved (originTx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> { const tx = TxProcessor.extractTx(originTx) as TxCUD<Doc> if (tx._class !== core.class.TxRemoveDoc) { return [] } const activityDocMixin = control.hierarchy.classHierarchyMixin(tx.objectClass, activity.mixin.ActivityDoc) if (activityDocMixin === undefined) { return [] } const messages = await control.findAll( control.ctx, activity.class.ActivityMessage, { attachedTo: tx.objectId }, { projection: { _id: 1, _class: 1, space: 1 } } ) return messages.map((message) => control.txFactory.createTxRemoveDoc(message._class, message.space, message._id)) } async function ReactionNotificationContentProvider ( doc: ActivityMessage, originTx: TxCUD<Doc>, _: Ref<Account>, control: TriggerControl ): Promise<NotificationContent> { const tx = TxProcessor.extractTx(originTx) as TxCreateDoc<Reaction> const presenter = getTextPresenter(doc._class, control.hierarchy) const reaction = TxProcessor.createDoc2Doc(tx) let text = '' if (presenter !== undefined) { const fn = await getResource(presenter.presenter) text = await fn(doc, control) } else { text = await translate(activity.string.Message, {}) } return { title: activity.string.ReactionNotificationTitle, body: activity.string.ReactionNotificationBody, data: reaction.emoji, intlParams: { title: text, reaction: reaction.emoji } } } async function getAttributesUpdatesText ( attributeUpdates: DocAttributeUpdates, objectClass: Ref<Class<Doc>>, hierarchy: Hierarchy ): Promise<string | undefined> { const attrName = await getAttrName(attributeUpdates, objectClass, hierarchy) if (attrName === undefined) { return undefined } if (attributeUpdates.added.length > 0) { return await translate(activity.string.NewObject, { object: attrName }) } if (attributeUpdates.removed.length > 0) { return await translate(activity.string.RemovedObject, { object: attrName }) } if (attributeUpdates.set.length > 0) { const values = attributeUpdates.set const isUnset = values.length > 0 && !values.some((value) => value !== null && value !== '') if (isUnset) { return await translate(activity.string.UnsetObject, { object: attrName }) } else { return await translate(activity.string.ChangedObject, { object: attrName }) } } return undefined } export async function DocUpdateMessageTextPresenter (doc: DocUpdateMessage, control: TriggerControl): Promise<string> { const { hierarchy } = control const { attachedTo, attachedToClass, objectClass, objectId, action, updateCollection, attributeUpdates } = doc const isOwn = attachedTo === objectId const collectionAttribute = getCollectionAttribute(hierarchy, attachedToClass, updateCollection) const clazz = hierarchy.getClass(objectClass) const objectName = (collectionAttribute?.type as Collection<AttachedDoc>)?.itemLabel ?? clazz.label const collectionName = collectionAttribute?.label const name = isOwn || collectionName === undefined ? await translate(objectName, {}) : await translate(collectionName, {}) if (action === 'create') { return await translate(activity.string.NewObject, { object: name }) } if (action === 'remove') { return await translate(activity.string.RemovedObject, { object: name }) } if (action === 'update' && attributeUpdates !== undefined) { const text = await getAttributesUpdatesText(attributeUpdates, objectClass, hierarchy) if (text !== undefined) { return text } } return await translate(activity.string.UpdatedObject, { object: name }) } export * from './references' // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export default async () => ({ trigger: { ReferenceTrigger, ActivityMessagesHandler, OnDocRemoved, OnReactionChanged }, function: { ReactionNotificationContentProvider, DocUpdateMessageTextPresenter } })