mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-29 21:15:06 +02:00
UBERF-5811: rework backlinks (#4887)
Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import chunter, { Backlink, ChatMessage } from '@hcengineering/chunter'
|
||||
import chunter, { ChatMessage } from '@hcengineering/chunter'
|
||||
import contact, { Employee, formatName, Person, PersonAccount } from '@hcengineering/contact'
|
||||
import core, {
|
||||
Account,
|
||||
@@ -27,7 +27,6 @@ import core, {
|
||||
Data,
|
||||
Doc,
|
||||
DocumentUpdate,
|
||||
Hierarchy,
|
||||
MeasureContext,
|
||||
MixinUpdate,
|
||||
Ref,
|
||||
@@ -45,11 +44,12 @@ import core, {
|
||||
} from '@hcengineering/core'
|
||||
import notification, {
|
||||
ActivityInboxNotification,
|
||||
BaseNotificationType,
|
||||
ClassCollaborators,
|
||||
Collaborators,
|
||||
CommonInboxNotification,
|
||||
DocNotifyContext,
|
||||
InboxNotification,
|
||||
NotificationProvider,
|
||||
NotificationType
|
||||
} from '@hcengineering/notification'
|
||||
import { getMetadata, getResource } from '@hcengineering/platform'
|
||||
@@ -57,177 +57,103 @@ import type { TriggerControl } from '@hcengineering/server-core'
|
||||
import serverNotification, {
|
||||
getEmployee,
|
||||
getPersonAccount,
|
||||
getPersonAccountById,
|
||||
HTMLPresenter,
|
||||
TextPresenter
|
||||
getPersonAccountById
|
||||
} from '@hcengineering/server-notification'
|
||||
import activity, { ActivityMessage } from '@hcengineering/activity'
|
||||
|
||||
import { Content } from './types'
|
||||
import { Content, NotifyResult } from './types'
|
||||
import {
|
||||
getHTMLPresenter,
|
||||
getNotificationContent,
|
||||
getTextPresenter,
|
||||
isAllowed,
|
||||
isMixinTx,
|
||||
isShouldNotify,
|
||||
isShouldNotifyTx,
|
||||
isUserEmployeeInFieldValue,
|
||||
isUserInFieldValue,
|
||||
replaceAll,
|
||||
updateNotifyContextsSpace
|
||||
} from './utils'
|
||||
|
||||
export async function OnBacklinkCreate (
|
||||
originTx: TxCollectionCUD<Doc, AttachedDoc>,
|
||||
control: TriggerControl
|
||||
): Promise<Tx[]> {
|
||||
const hierarchy = control.hierarchy
|
||||
const isTxCorrect = await isBacklinkCreated(originTx, hierarchy, control)
|
||||
export function getPushCollaboratorTx (
|
||||
control: TriggerControl,
|
||||
user: Ref<Account>,
|
||||
doc: Doc
|
||||
): TxMixin<Doc, Doc> | undefined {
|
||||
const mixin = control.hierarchy.as(doc, notification.mixin.Collaborators)
|
||||
|
||||
if (!isTxCorrect) {
|
||||
return []
|
||||
}
|
||||
|
||||
const tx = originTx as TxCollectionCUD<Doc, Backlink>
|
||||
const receiver = await getPersonAccount(tx.objectId as Ref<Employee>, control)
|
||||
|
||||
if (receiver === undefined) {
|
||||
return []
|
||||
}
|
||||
|
||||
const sender = await getPersonAccountById(tx.modifiedBy, control)
|
||||
|
||||
if (sender === undefined) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (sender === receiver) {
|
||||
return []
|
||||
}
|
||||
|
||||
const backlink = TxProcessor.createDoc2Doc(tx.tx as TxCreateDoc<Backlink>)
|
||||
|
||||
if (!hierarchy.isDerived(backlink.backlinkClass, activity.class.ActivityMessage)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const message = (
|
||||
await control.findAll<ActivityMessage>(
|
||||
backlink.backlinkClass,
|
||||
{
|
||||
_id: backlink.backlinkId as Ref<ActivityMessage>
|
||||
},
|
||||
{ limit: 1 }
|
||||
)
|
||||
)[0]
|
||||
|
||||
if (message === undefined) {
|
||||
return []
|
||||
}
|
||||
|
||||
const res: Tx[] = []
|
||||
const collabMixin = hierarchy.as(message as Doc, notification.mixin.Collaborators)
|
||||
|
||||
if (collabMixin.collaborators === undefined || !collabMixin.collaborators.includes(receiver._id)) {
|
||||
const collabTx = control.txFactory.createTxMixin(
|
||||
message._id,
|
||||
message._class,
|
||||
message.space,
|
||||
notification.mixin.Collaborators,
|
||||
{
|
||||
$push: {
|
||||
collaborators: receiver._id
|
||||
}
|
||||
if (mixin.collaborators === undefined || !mixin.collaborators.includes(user)) {
|
||||
return control.txFactory.createTxMixin(doc._id, doc._class, doc.space, notification.mixin.Collaborators, {
|
||||
$push: {
|
||||
collaborators: user
|
||||
}
|
||||
)
|
||||
res.push(collabTx)
|
||||
})
|
||||
}
|
||||
|
||||
return res
|
||||
return undefined
|
||||
}
|
||||
|
||||
async function isBacklinkNotified (tx: TxCollectionCUD<Doc, Backlink>, control: TriggerControl): Promise<boolean> {
|
||||
const receiver = await getPersonAccount(tx.objectId as Ref<Employee>, control)
|
||||
|
||||
if (receiver === undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
const { hierarchy } = control
|
||||
const backlink = TxProcessor.createDoc2Doc(tx.tx as TxCreateDoc<Backlink>)
|
||||
|
||||
if (!hierarchy.isDerived(backlink.backlinkClass, activity.class.ActivityMessage)) {
|
||||
return false
|
||||
}
|
||||
|
||||
export async function isMessageAlreadyNotified (
|
||||
_id: Ref<ActivityMessage>,
|
||||
user: Ref<Account>,
|
||||
control: TriggerControl
|
||||
): Promise<boolean> {
|
||||
const exists = await control.findAll(
|
||||
notification.class.ActivityInboxNotification,
|
||||
{ attachedTo: backlink.backlinkId as Ref<ActivityMessage>, user: receiver._id },
|
||||
{ limit: 1 }
|
||||
{ attachedTo: _id, user },
|
||||
{ limit: 1, projection: { _id: 1 } }
|
||||
)
|
||||
|
||||
return exists.length > 0
|
||||
}
|
||||
|
||||
async function isAlreadyNotified (originTx: TxCUD<Doc>, control: TriggerControl): Promise<boolean> {
|
||||
if (originTx._class !== core.class.TxCollectionCUD) {
|
||||
return false
|
||||
}
|
||||
|
||||
const hierarchy = control.hierarchy
|
||||
const isBacklink = await isBacklinkCreated(originTx as TxCollectionCUD<Doc, AttachedDoc>, hierarchy, control)
|
||||
|
||||
if (!isBacklink) {
|
||||
return false
|
||||
}
|
||||
|
||||
return await isBacklinkNotified(originTx as TxCollectionCUD<Doc, Backlink>, control)
|
||||
}
|
||||
|
||||
async function isBacklinkCreated (
|
||||
ptx: TxCollectionCUD<Doc, AttachedDoc>,
|
||||
hierarchy: Hierarchy,
|
||||
control: TriggerControl
|
||||
): Promise<boolean> {
|
||||
if (ptx.tx._class !== core.class.TxCreateDoc || !hierarchy.isDerived(ptx.tx.objectClass, chunter.class.Backlink)) {
|
||||
return false
|
||||
}
|
||||
if (ptx.objectClass === contact.class.Person) {
|
||||
// We need to check if person is employee.
|
||||
const [person] = await control.findAll(contact.class.Person, { _id: ptx.objectId as Ref<Person> })
|
||||
return person !== undefined ? hierarchy.hasMixin(person, contact.mixin.Employee) : false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function isAllowed (
|
||||
export async function getCommonNotificationTxes (
|
||||
control: TriggerControl,
|
||||
receiver: Ref<PersonAccount>,
|
||||
typeId: Ref<NotificationType>,
|
||||
providerId: Ref<NotificationProvider>
|
||||
): Promise<boolean> {
|
||||
const setting = (
|
||||
await control.findAll(
|
||||
notification.class.NotificationSetting,
|
||||
{
|
||||
attachedTo: providerId,
|
||||
type: typeId,
|
||||
modifiedBy: receiver
|
||||
},
|
||||
{ limit: 1 }
|
||||
doc: Doc,
|
||||
data: Partial<Data<CommonInboxNotification>>,
|
||||
receiver: Ref<Account>,
|
||||
sender: Ref<Account>,
|
||||
attachedTo: Ref<Doc>,
|
||||
attachedToClass: Ref<Class<Doc>>,
|
||||
space: Ref<Space>,
|
||||
modifiedOn: Timestamp,
|
||||
notifyResult: NotifyResult
|
||||
): Promise<Tx[]> {
|
||||
const res: Tx[] = []
|
||||
|
||||
if (notifyResult.allowed) {
|
||||
const notifyContexts = await control.findAll(notification.class.DocNotifyContext, { attachedTo })
|
||||
|
||||
await pushInboxNotifications(
|
||||
control,
|
||||
res,
|
||||
receiver,
|
||||
attachedTo,
|
||||
attachedToClass,
|
||||
space,
|
||||
notifyContexts,
|
||||
data,
|
||||
notification.class.CommonInboxNotification,
|
||||
modifiedOn
|
||||
)
|
||||
)[0]
|
||||
if (setting !== undefined) {
|
||||
return setting.enabled
|
||||
}
|
||||
const type = (
|
||||
await control.modelDb.findAll(notification.class.NotificationType, {
|
||||
_id: typeId
|
||||
})
|
||||
)[0]
|
||||
if (type === undefined) return false
|
||||
return type.providers[providerId] ?? false
|
||||
|
||||
if (notifyResult.emails.length === 0) {
|
||||
return res
|
||||
}
|
||||
|
||||
const receiverAccount = await getPersonAccountById(receiver, control)
|
||||
if (receiverAccount === undefined) {
|
||||
return res
|
||||
}
|
||||
const emp = await getEmployee(receiverAccount.person as Ref<Employee>, control)
|
||||
if (emp?.active === true) {
|
||||
for (const type of notifyResult.emails) {
|
||||
await notifyByEmail(control, type._id, doc, sender as Ref<PersonAccount>, receiverAccount._id)
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
async function getTextPart (doc: Doc, control: TriggerControl): Promise<string | undefined> {
|
||||
@@ -243,20 +169,6 @@ async function getHtmlPart (doc: Doc, control: TriggerControl): Promise<string |
|
||||
return HTMLPresenter != null ? await (await getResource(HTMLPresenter.presenter))(doc, control) : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function getHTMLPresenter (_class: Ref<Class<Doc>>, hierarchy: Hierarchy): HTMLPresenter | undefined {
|
||||
return hierarchy.classHierarchyMixin(_class, serverNotification.mixin.HTMLPresenter)
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function getTextPresenter (_class: Ref<Class<Doc>>, hierarchy: Hierarchy): TextPresenter | undefined {
|
||||
return hierarchy.classHierarchyMixin(_class, serverNotification.mixin.TextPresenter)
|
||||
}
|
||||
|
||||
function fillTemplate (template: string, sender: string, doc: string, data: string): string {
|
||||
let res = replaceAll(template, '{sender}', sender)
|
||||
res = replaceAll(res, '{doc}', doc)
|
||||
@@ -270,7 +182,7 @@ function fillTemplate (template: string, sender: string, doc: string, data: stri
|
||||
export async function getContent (
|
||||
doc: Doc | undefined,
|
||||
sender: string,
|
||||
type: Ref<NotificationType>,
|
||||
type: Ref<BaseNotificationType>,
|
||||
control: TriggerControl,
|
||||
data: string
|
||||
): Promise<Content | undefined> {
|
||||
@@ -293,7 +205,7 @@ export async function getContent (
|
||||
|
||||
async function notifyByEmail (
|
||||
control: TriggerControl,
|
||||
type: Ref<NotificationType>,
|
||||
type: Ref<BaseNotificationType>,
|
||||
doc: Doc | undefined,
|
||||
senderId: Ref<PersonAccount>,
|
||||
receiverId: Ref<PersonAccount>,
|
||||
@@ -553,7 +465,7 @@ export async function getNotificationTxes (
|
||||
shouldUpdateTimestamp = true
|
||||
): Promise<Tx[]> {
|
||||
const res: Tx[] = []
|
||||
const notifyResult = await isShouldNotify(control, tx, originTx, object, target, isOwn, isSpace)
|
||||
const notifyResult = await isShouldNotifyTx(control, tx, originTx, object, target, isOwn, isSpace)
|
||||
|
||||
if (notifyResult.allowed) {
|
||||
await pushActivityInboxNotifications(
|
||||
@@ -834,11 +746,37 @@ async function removeCollaboratorDoc (tx: TxRemoveDoc<Doc>, control: TriggerCont
|
||||
}
|
||||
|
||||
const res: Tx[] = []
|
||||
const notifyContexts = await control.findAll(notification.class.DocNotifyContext, { attachedTo: tx.objectId })
|
||||
const notifyContexts = await control.findAll(
|
||||
notification.class.DocNotifyContext,
|
||||
{ attachedTo: tx.objectId },
|
||||
{
|
||||
projection: {
|
||||
_id: 1,
|
||||
_class: 1,
|
||||
space: 1
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (notifyContexts.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const notifyContextRefs = notifyContexts.map(({ _id }) => _id)
|
||||
const inboxNotifications = await control.findAll(notification.class.InboxNotification, {
|
||||
docNotifyContext: { $in: notifyContextRefs }
|
||||
})
|
||||
|
||||
const inboxNotifications = await control.findAll(
|
||||
notification.class.InboxNotification,
|
||||
{
|
||||
docNotifyContext: { $in: notifyContextRefs }
|
||||
},
|
||||
{
|
||||
projection: {
|
||||
_id: 1,
|
||||
_class: 1,
|
||||
space: 1
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
inboxNotifications.forEach((notification) => {
|
||||
res.push(control.txFactory.createTxRemoveDoc(notification._class, notification.space, notification._id))
|
||||
@@ -1010,10 +948,6 @@ export async function createCollaboratorNotifications (
|
||||
return []
|
||||
}
|
||||
|
||||
if (await isAlreadyNotified(originTx ?? tx, control)) {
|
||||
return []
|
||||
}
|
||||
|
||||
switch (tx._class) {
|
||||
case core.class.TxCreateDoc:
|
||||
return await createCollaboratorDoc(tx as TxCreateDoc<Doc>, control, activityMessages, originTx ?? tx)
|
||||
@@ -1025,8 +959,6 @@ export async function createCollaboratorNotifications (
|
||||
)
|
||||
return res
|
||||
}
|
||||
case core.class.TxRemoveDoc:
|
||||
return await removeCollaboratorDoc(tx as TxRemoveDoc<Doc>, control)
|
||||
case core.class.TxCollectionCUD:
|
||||
return await collectionCollabDoc(tx as TxCollectionCUD<Doc, AttachedDoc>, control, activityMessages)
|
||||
}
|
||||
@@ -1118,6 +1050,14 @@ export async function getCollaborators (
|
||||
}
|
||||
}
|
||||
|
||||
async function OnDocRemove (tx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> {
|
||||
const etx = TxProcessor.extractTx(tx)
|
||||
|
||||
if (etx._class !== core.class.TxRemoveDoc) return []
|
||||
|
||||
return await removeCollaboratorDoc(tx as TxRemoveDoc<Doc>, control)
|
||||
}
|
||||
|
||||
export * from './types'
|
||||
export * from './utils'
|
||||
|
||||
@@ -1127,8 +1067,8 @@ export default async () => ({
|
||||
OnChatMessageCreate,
|
||||
OnAttributeCreate,
|
||||
OnAttributeUpdate,
|
||||
OnBacklinkCreate,
|
||||
OnActivityNotificationViewed
|
||||
OnActivityNotificationViewed,
|
||||
OnDocRemove
|
||||
},
|
||||
function: {
|
||||
IsUserInFieldValue: isUserInFieldValue,
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
import { NotificationType } from '@hcengineering/notification'
|
||||
import { BaseNotificationType } from '@hcengineering/notification'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@@ -28,5 +28,5 @@ export interface Content {
|
||||
*/
|
||||
export interface NotifyResult {
|
||||
allowed: boolean
|
||||
emails: NotificationType[]
|
||||
emails: BaseNotificationType[]
|
||||
}
|
||||
|
||||
@@ -12,7 +12,13 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
import notification, { NotificationContent, NotificationType } from '@hcengineering/notification'
|
||||
import notification, {
|
||||
BaseNotificationType,
|
||||
CommonNotificationType,
|
||||
NotificationContent,
|
||||
NotificationProvider,
|
||||
NotificationType
|
||||
} from '@hcengineering/notification'
|
||||
import type { TriggerControl } from '@hcengineering/server-core'
|
||||
import core, {
|
||||
Account,
|
||||
@@ -28,11 +34,16 @@ import core, {
|
||||
TxMixin,
|
||||
TxUpdateDoc
|
||||
} from '@hcengineering/core'
|
||||
import serverNotification, { getPersonAccountById, NotificationPresenter } from '@hcengineering/server-notification'
|
||||
import serverNotification, {
|
||||
getPersonAccountById,
|
||||
HTMLPresenter,
|
||||
NotificationPresenter,
|
||||
TextPresenter
|
||||
} from '@hcengineering/server-notification'
|
||||
import { getResource, IntlString } from '@hcengineering/platform'
|
||||
import contact, { formatName, Person, PersonAccount } from '@hcengineering/contact'
|
||||
import { getTextPresenter, isAllowed, NotifyResult } from './index'
|
||||
import { DocUpdateMessage } from '@hcengineering/activity'
|
||||
import { NotifyResult } from './types'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@@ -94,7 +105,60 @@ async function findPersonForAccount (control: TriggerControl, personId: Ref<Pers
|
||||
return undefined
|
||||
}
|
||||
|
||||
export async function isShouldNotify (
|
||||
export async function shouldNotifyCommon (
|
||||
control: TriggerControl,
|
||||
user: Ref<Account>,
|
||||
typeId: Ref<CommonNotificationType>
|
||||
): Promise<NotifyResult> {
|
||||
const type = (await control.modelDb.findAll(notification.class.CommonNotificationType, { _id: typeId }))[0]
|
||||
|
||||
const emailTypes: BaseNotificationType[] = []
|
||||
let allowed = false
|
||||
|
||||
if (type === undefined) {
|
||||
return { allowed, emails: emailTypes }
|
||||
}
|
||||
|
||||
if (await isAllowed(control, user as Ref<PersonAccount>, type._id, notification.providers.PlatformNotification)) {
|
||||
allowed = true
|
||||
}
|
||||
if (await isAllowed(control, user as Ref<PersonAccount>, type._id, notification.providers.EmailNotification)) {
|
||||
emailTypes.push(type)
|
||||
}
|
||||
|
||||
return { allowed, emails: emailTypes }
|
||||
}
|
||||
|
||||
export async function isAllowed (
|
||||
control: TriggerControl,
|
||||
receiver: Ref<PersonAccount>,
|
||||
typeId: Ref<BaseNotificationType>,
|
||||
providerId: Ref<NotificationProvider>
|
||||
): Promise<boolean> {
|
||||
const setting = (
|
||||
await control.findAll(
|
||||
notification.class.NotificationSetting,
|
||||
{
|
||||
attachedTo: providerId,
|
||||
type: typeId,
|
||||
modifiedBy: receiver
|
||||
},
|
||||
{ limit: 1 }
|
||||
)
|
||||
)[0]
|
||||
if (setting !== undefined) {
|
||||
return setting.enabled
|
||||
}
|
||||
const type = (
|
||||
await control.modelDb.findAll(notification.class.BaseNotificationType, {
|
||||
_id: typeId
|
||||
})
|
||||
)[0]
|
||||
if (type === undefined) return false
|
||||
return type.providers[providerId] ?? false
|
||||
}
|
||||
|
||||
export async function isShouldNotifyTx (
|
||||
control: TriggerControl,
|
||||
tx: TxCUD<Doc>,
|
||||
originTx: TxCUD<Doc>,
|
||||
@@ -233,6 +297,14 @@ export function isMixinTx (tx: TxCUD<Doc>): tx is TxMixin<Doc, Doc> {
|
||||
return tx._class === core.class.TxMixin
|
||||
}
|
||||
|
||||
export function getHTMLPresenter (_class: Ref<Class<Doc>>, hierarchy: Hierarchy): HTMLPresenter | undefined {
|
||||
return hierarchy.classHierarchyMixin(_class, serverNotification.mixin.HTMLPresenter)
|
||||
}
|
||||
|
||||
export function getTextPresenter (_class: Ref<Class<Doc>>, hierarchy: Hierarchy): TextPresenter | undefined {
|
||||
return hierarchy.classHierarchyMixin(_class, serverNotification.mixin.TextPresenter)
|
||||
}
|
||||
|
||||
async function getFallbackNotificationFullfillment (
|
||||
object: Doc,
|
||||
originTx: TxCUD<Doc>,
|
||||
|
||||
Reference in New Issue
Block a user