import { type ActivityMessageControl, type DocAttributeUpdates, type DocUpdateAction } from '@hcengineering/activity' import cardPlugin, { type Card, type Tag } from '@hcengineering/card' import { type ActivityUpdate, ActivityUpdateType } from '@hcengineering/communication-types' import core, { type AttachedDoc, type Attribute, type Class, type Collection, type Doc, type Hierarchy, type MeasureContext, type Mixin, type Ref, type RefTo, type TxCreateDoc, type TxCUD, type TxMixin, TxProcessor, type TxUpdateDoc, combineAttributes, type ArrOf, type AccountUuid } from '@hcengineering/core' import notification from '@hcengineering/notification' import { translate } from '@hcengineering/platform' import { type ActivityControl, type DocObjectCache, getAllObjectTransactions } from '@hcengineering/server-activity' import { type TriggerControl } from '@hcengineering/server-core' import { getDocCollaborators } from '@hcengineering/server-notification-resources' function getAvailableAttributesKeys (tx: TxCUD, hierarchy: Hierarchy): string[] { if (hierarchy.isDerived(tx._class, core.class.TxUpdateDoc)) { const updateTx = tx as TxUpdateDoc const _class = updateTx.objectClass try { hierarchy.getClass(_class) } catch (err: any) { // class is deleted return [] } const hiddenAttrs = getHiddenAttrs(hierarchy, _class) return Object.entries(updateTx.operations) .flatMap(([id, val]) => { if (['$push', '$pull', '$unset'].includes(id)) { return Object.keys(val) } return id }) .filter((id) => !id.startsWith('$') && !hiddenAttrs.has(id)) } if (hierarchy.isDerived(tx._class, core.class.TxMixin)) { const mixinTx = tx as TxMixin const _class = mixinTx.mixin try { hierarchy.getClass(_class) } catch (err: any) { // mixin is deleted return [] } const hiddenAttrs = getHiddenAttrs(hierarchy, _class) return Object.keys(mixinTx.attributes) .filter((id) => !id.startsWith('$')) .filter((key) => !hiddenAttrs.has(key)) } return [] } function getModifiedAttributes (tx: TxCUD, hierarchy: Hierarchy): Record { if (hierarchy.isDerived(tx._class, core.class.TxUpdateDoc)) { const updateTx = tx as TxUpdateDoc return updateTx.operations as Record } if (hierarchy.isDerived(tx._class, core.class.TxMixin)) { const mixinTx = tx as TxMixin return mixinTx.attributes as Record } return {} } export function getDocUpdateAction (control: ActivityControl, tx: TxCUD): DocUpdateAction { const hierarchy = control.hierarchy if (hierarchy.isDerived(tx._class, core.class.TxCreateDoc)) { return 'create' } if (hierarchy.isDerived(tx._class, core.class.TxRemoveDoc)) { return 'remove' } return 'update' } export async function getDocDiff ( control: ActivityControl, _class: Ref>, objectId: Ref, lastTxId: Ref>, mixin?: Ref>, objectCache?: DocObjectCache ): Promise<{ doc?: Doc, prevDoc?: Doc }> { const hierarchy = control.hierarchy const objectTxes = objectCache?.transactions.get(objectId) ?? (await getAllObjectTransactions(control, _class, [objectId], mixin)).get(objectId) ?? [] const createTx = objectTxes.find((tx) => tx._class === core.class.TxCreateDoc) if (createTx === undefined) { return {} } let doc: Doc | undefined let prevDoc: Doc | undefined doc = TxProcessor.createDoc2Doc(createTx as TxCreateDoc) for (const actualTx of objectTxes) { if (actualTx._class === core.class.TxUpdateDoc) { prevDoc = hierarchy.clone(doc) doc = TxProcessor.updateDoc2Doc(doc, actualTx as TxUpdateDoc) } if (actualTx._class === core.class.TxMixin) { prevDoc = hierarchy.clone(doc) doc = TxProcessor.updateMixin4Doc(doc, actualTx as TxMixin) } if (actualTx._id === lastTxId) { break } } return { doc, prevDoc } } interface AttributeDiff { added: DocAttributeUpdates['added'] removed: DocAttributeUpdates['removed'] } async function getCollaboratorsDiff ( control: ActivityControl, doc: Doc, prevDoc: Doc | undefined ): Promise { const { hierarchy } = control const value = hierarchy.as(doc, notification.mixin.Collaborators).collaborators ?? [] let prevValue: AccountUuid[] = [] if (prevDoc !== undefined && hierarchy.hasMixin(prevDoc, notification.mixin.Collaborators)) { prevValue = hierarchy.as(prevDoc, notification.mixin.Collaborators).collaborators ?? [] } else if (prevDoc !== undefined) { const mixin = hierarchy.classHierarchyMixin(prevDoc._class, notification.mixin.ClassCollaborators) prevValue = mixin !== undefined ? await getDocCollaborators((control as TriggerControl).ctx, prevDoc, mixin, control as TriggerControl) : [] } const added = value.filter((item) => !prevValue.includes(item)) as DocAttributeUpdates['added'] const removed = prevValue.filter((item) => !value.includes(item)) as DocAttributeUpdates['removed'] return { added, removed } } export async function getAttributeDiff ( control: ActivityControl, doc: Doc, prevDoc: Doc | undefined, attrKey: string, attrClass: Ref>, mixin?: Ref> ): Promise { const { hierarchy } = control let actualDoc: Doc | undefined = doc let actualPrevDoc: Doc | undefined = prevDoc if (mixin != null && hierarchy.isDerived(attrClass, notification.mixin.Collaborators)) { return await getCollaboratorsDiff(control, doc, prevDoc) } if (mixin != null) { actualDoc = hierarchy.as(doc, mixin) actualPrevDoc = prevDoc === undefined ? undefined : hierarchy.as(prevDoc, mixin) } const value = (actualDoc as any)[attrKey] ?? [] const prevValue = (actualPrevDoc as any)?.[attrKey] ?? [] if (!Array.isArray(value) || !Array.isArray(prevValue)) { return { added: [], removed: [] } } const added = value.filter((item) => !prevValue.includes(item)) as DocAttributeUpdates['added'] const removed = prevValue.filter((item) => !value.includes(item)) as DocAttributeUpdates['removed'] return { added, removed } } export async function getTxAttributesUpdates ( ctx: MeasureContext, control: ActivityControl, tx: TxCUD, object: Doc, objectCache?: DocObjectCache, controlRules?: ActivityMessageControl[] ): Promise { if (![core.class.TxMixin, core.class.TxUpdateDoc].includes(tx._class)) { return [] } let updateObject = object if (updateObject._id !== tx.objectId) { updateObject = objectCache?.docs?.get(tx.objectId) ?? (await control.findAll(ctx, tx.objectClass, { _id: tx.objectId }))[0] } if (updateObject === undefined) { return [] } const hierarchy = control.hierarchy const allowedFields = new Set(controlRules?.flatMap((it) => it.allowedFields ?? []) ?? []) const skipFields = new Set(controlRules?.flatMap((it) => it.skipFields ?? []) ?? []) const keys = getAvailableAttributesKeys(tx, hierarchy).filter( (it) => !skipFields.has(it) && (allowedFields.size === 0 || allowedFields.has(it)) ) if (keys.length === 0) { return [] } const result: DocAttributeUpdates[] = [] const modifiedAttributes = getModifiedAttributes(tx, hierarchy) const isMixin = hierarchy.isDerived(tx._class, core.class.TxMixin) const mixin = isMixin ? (tx as TxMixin).mixin : undefined let docDiff: { doc?: Doc, prevDoc?: Doc } | undefined for (const key of keys) { let attrValue = modifiedAttributes[key] let prevValue const added = combineAttributes([modifiedAttributes], key, '$push', '$each') const removed = combineAttributes([modifiedAttributes], key, '$pull', '$in') let attrClass: Ref> | undefined = mixin const clazz = hierarchy.findAttribute(updateObject._class, key) if (clazz !== undefined && 'to' in clazz.type) { attrClass = clazz.type.to as Ref> } else if (clazz !== undefined && hierarchy.isDerived(clazz.type._class, core.class.ArrOf)) { attrClass = (clazz.type as ArrOf).of._class } else if (clazz !== undefined && 'of' in clazz?.type) { attrClass = (clazz.type.of as RefTo).to } if (attrClass == null && clazz?.type?._class !== undefined) { attrClass = clazz.type._class } if (attrClass === undefined) { continue } if (attrClass === core.class.TypeCollaborativeDoc) { // collaborative documents activity is handled by collaborator continue } if (hierarchy.isDerived(attrClass, core.class.TypeMarkup) || mixin === notification.mixin.Collaborators) { if (docDiff === undefined) { docDiff = await getDocDiff(control, updateObject._class, updateObject._id, tx._id, mixin, objectCache) } } if (Array.isArray(attrValue) && docDiff?.doc !== undefined) { const diff = await getAttributeDiff(control, docDiff.doc, docDiff.prevDoc, key, attrClass, mixin) added.push(...diff.added) removed.push(...diff.removed) attrValue = [] } if (docDiff?.prevDoc !== undefined) { const { prevDoc } = docDiff const rawPrevValue = isMixin ? (hierarchy.as(prevDoc, attrClass) as any)[key] : (prevDoc as any)[key] if (Array.isArray(rawPrevValue)) { prevValue = rawPrevValue } else if (rawPrevValue !== undefined && rawPrevValue !== null && typeof rawPrevValue === 'object') { prevValue = rawPrevValue._id } else { prevValue = rawPrevValue } } let setAttr = [] if (Array.isArray(attrValue)) { setAttr = attrValue } else if (key in modifiedAttributes) { setAttr = [attrValue] } result.push({ attrKey: key, attrClass, set: setAttr, added, removed, prevValue, isMixin }) } return result } function getHiddenAttrs (hierarchy: Hierarchy, _class: Ref>): Set { return new Set( [...hierarchy.getAllAttributes(_class).entries()].filter(([, attr]) => attr.hidden === true).map(([k]) => k) ) } export async function getAttrName ( attributeUpdates: DocAttributeUpdates, objectClass: Ref>, hierarchy: Hierarchy ): Promise { const { attrKey, attrClass, isMixin } = attributeUpdates let attrObjectClass = objectClass try { if (isMixin) { const keyedAttribute = [...hierarchy.getAllAttributes(attrClass).entries()] .filter(([, value]) => value.hidden !== true) .map(([key, attr]) => ({ key, attr })) .find(({ key }) => key === attrKey) if (keyedAttribute === undefined) { return undefined } attrObjectClass = keyedAttribute.attr.attributeOf } const attribute = hierarchy.getAttribute(attrObjectClass, attrKey) const label = attribute.shortLabel ?? attribute.label if (label === undefined) { return undefined } return await translate(label, {}) } catch (e) { console.error(e) return undefined } } export function getCollectionAttribute ( hierarchy: Hierarchy, objectClass: Ref>, collection?: string ): Attribute> | undefined { if (collection === undefined) { return undefined } const descendants = hierarchy.getDescendants(objectClass) for (const descendant of descendants) { const collectionAttribute = hierarchy.findAttribute(descendant, collection) if (collectionAttribute !== undefined) { return collectionAttribute } } return undefined } function getAttrClass ( hierarchy: Hierarchy, objectClass: Ref>, attrKey: string ): Ref> | undefined { const clazz = hierarchy.findAttribute(objectClass, attrKey) if (clazz === undefined) return undefined if (hierarchy.isDerived(clazz.type._class, core.class.RefTo)) { return (clazz.type as RefTo).to } else if (hierarchy.isDerived(clazz.type._class, core.class.ArrOf)) { const of = (clazz.type as ArrOf).of return of._class === core.class.RefTo ? (of as RefTo).to : of._class } return clazz.type._class } export async function getNewActivityUpdates ( control: TriggerControl, originTx: TxCUD, card: Card ): Promise { if (![core.class.TxMixin, core.class.TxUpdateDoc].includes(originTx._class)) { return [] } const tx = originTx as TxUpdateDoc | TxMixin const { hierarchy } = control const keys = getAvailableAttributesKeys(tx, hierarchy) const result: ActivityUpdate[] = [] const mixin = hierarchy.isDerived(tx._class, core.class.TxMixin) ? (tx as TxMixin).mixin : undefined if (mixin != null && Object.keys((tx as TxMixin).attributes).length === 0) { const clazz = hierarchy.getClass(mixin) if (hierarchy.isDerived(clazz._class, cardPlugin.class.Tag)) { result.push({ type: ActivityUpdateType.Tag, tag: mixin, action: 'add' }) } } if (keys.length === 0) return result const modifiedAttributes = getModifiedAttributes(tx, hierarchy) for (const key of keys) { const attrValue = modifiedAttributes[key] const added = combineAttributes([modifiedAttributes], key, '$push', '$each') const removed = combineAttributes([modifiedAttributes], key, '$pull', '$in') const isUnset = combineAttributes([modifiedAttributes], key, '$unset')[0] === true if (isUnset && hierarchy.isMixin(key as any)) { const tag = key as Ref const clazz = hierarchy.getClass(tag) if (hierarchy.isDerived(clazz._class, cardPlugin.class.Tag)) { result.push({ type: ActivityUpdateType.Tag, tag, action: 'remove' }) } } const attrClass: Ref> | undefined = getAttrClass(hierarchy, mixin ?? card._class, key) if (attrClass === undefined) continue if ( hierarchy.isDerived(attrClass, core.class.TypeMarkup) || hierarchy.isDerived(attrClass, core.class.TypeCollaborativeDoc) ) { continue } if (Array.isArray(attrValue)) { const diff = await getAttributeDiff(control, card, undefined, key, attrClass, mixin) added.push(...diff.added) removed.push(...diff.removed) } result.push({ type: ActivityUpdateType.Attribute, attrKey: key, attrClass, set: attrValue, added, removed, mixin }) } return result }