// // 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, DocUpdateMessage, Reaction } from '@hcengineering/activity' import core, { Account, AttachedDoc, Class, Data, Doc, MeasureContext, Ref, Tx, TxCUD, TxCollectionCUD, TxCreateDoc, TxProcessor, matchQuery, Hierarchy, Space } from '@hcengineering/core' 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 { PersonAccount } from '@hcengineering/contact' import { NotificationContent } from '@hcengineering/notification' import { getResource, translate } from '@hcengineering/platform' import { getDocUpdateAction, getTxAttributesUpdates } from './utils' import { ReferenceTrigger } from './references' export async function OnReactionChanged (originTx: Tx, control: TriggerControl): Promise { const tx = originTx as TxCollectionCUD const innerTx = TxProcessor.extractTx(tx) as TxCUD if (innerTx._class === core.class.TxCreateDoc) { const txes = await createReactionNotifications(tx, control) await control.apply(txes, true) return txes } if (innerTx._class === core.class.TxRemoveDoc) { const txes = await removeReactionNotifications(tx, control) await control.apply(txes, true) return txes } return [] } export async function removeReactionNotifications ( tx: TxCollectionCUD, control: TriggerControl ): Promise { const message = ( await control.findAll( 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, control: TriggerControl ): Promise { const createTx = TxProcessor.extractTx(tx) as TxCreateDoc const parentMessage = (await control.findAll(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 = { 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) res = res.concat( await createCollabDocInfo( [user] as Ref[], control, tx.tx, tx, parentMessage, [docUpdateMessage], { isOwn: true, isSpace: false, shouldUpdateTimestamp: false }, new Map() ) ) return res } function isActivityDoc (_class: Ref>, 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, object: Doc, rawMessage: Data, modifiedBy?: Ref ): TxCollectionCUD { 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 | undefined, control: ActivityControl, res: TxCollectionCUD[], object: Doc | undefined, originTx: TxCUD, modifiedBy?: Ref, objectCache?: DocObjectCache, controlRules?: ActivityMessageControl[] ): Promise[]> { if (object === undefined) { return res } if (!isActivityDoc(object._class, control.hierarchy)) { return res } const tx = originTx._class === core.class.TxCollectionCUD ? (originTx as TxCollectionCUD).tx : originTx const rawMessage: Data = { 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).collection : undefined } const attributesUpdates = await getTxAttributesUpdates(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, control: ActivityControl, res: TxCollectionCUD[] = [], originTx?: TxCUD, objectCache?: DocObjectCache ): Promise[]> { const { hierarchy } = control if (tx.space === core.space.DerivedTx) { return res } const etx = TxProcessor.extractTx(tx) as TxCUD 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(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) 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(tx.objectClass, { _id: tx.objectId }, { limit: 1 }))[0] } 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 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(tx.objectClass, { _id: tx.objectId }, { limit: 1 }))[0] } 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, control: TriggerControl): Promise { if (tx.space === core.space.DerivedTx) { return [] } if (control.hierarchy.isDerived(tx.objectClass, activity.class.ActivityMessage)) { return [] } const txes = await control.ctx.with( 'generateDocUpdateMessages', {}, async (ctx) => await generateDocUpdateMessages(ctx, tx, control) ) if (txes.length === 0) { return [] } const messages = txes.map((messageTx) => TxProcessor.createDoc2Doc(messageTx.tx as TxCreateDoc)) const notificationTxes = await control.ctx.with( 'createNotificationTxes', {}, async (ctx) => await createCollaboratorNotifications(ctx, tx, control, messages) ) return [...txes, ...notificationTxes] } async function OnDocRemoved (originTx: TxCUD, control: TriggerControl): Promise { const tx = TxProcessor.extractTx(originTx) as TxCUD 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( 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, _: Ref, control: TriggerControl ): Promise { const tx = TxProcessor.extractTx(originTx) as TxCreateDoc 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, intlParams: { title: text, reaction: reaction.emoji } } } export * from './references' // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export default async () => ({ trigger: { ReferenceTrigger, ActivityMessagesHandler, OnDocRemoved, OnReactionChanged }, function: { ReactionNotificationContentProvider } })