platform/server-plugins/activity-resources/src/index.ts

466 lines
13 KiB
TypeScript
Raw Normal View History

//
// 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<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(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<ActivityMessage, Reaction>,
control: TriggerControl
): Promise<Tx[]> {
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<ActivityMessage, Reaction>,
control: TriggerControl
): Promise<Tx[]> {
const createTx = TxProcessor.extractTx(tx) as TxCreateDoc<Reaction>
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<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(
[user] as Ref<PersonAccount>[],
control,
tx.tx,
tx,
parentMessage,
[docUpdateMessage],
{ isOwn: true, isSpace: false, shouldUpdateTimestamp: false },
new Map()
)
)
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 | undefined,
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(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>[]> {
const { hierarchy } = control
if (tx.space === core.space.DerivedTx) {
return res
}
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(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<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(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<Doc>, control: TriggerControl): Promise<Tx[]> {
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<DocUpdateMessage>))
const notificationTxes = await control.ctx.with(
'createNotificationTxes',
{},
async (ctx) => await createCollaboratorNotifications(ctx, tx, control, messages)
)
return [...txes, ...notificationTxes]
}
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(
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,
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
}
})