Refactor server side notifications (#2327)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov
2022-10-26 17:24:39 +07:00
committed by GitHub
parent 70e00dbb5f
commit 3a8d1e2bcb
58 changed files with 539 additions and 400 deletions
+221 -186
View File
@@ -36,14 +36,24 @@ import core, {
} from '@hcengineering/core'
import notification, {
EmailNotification,
NotificationAction,
Notification,
NotificationProvider,
NotificationStatus
NotificationStatus,
NotificationType
} from '@hcengineering/notification'
import { getResource } from '@hcengineering/platform'
import type { TriggerControl } from '@hcengineering/server-core'
import { createLastViewTx, getUpdateLastViewTx } from '@hcengineering/server-notification'
import view, { HTMLPresenter, TextPresenter } from '@hcengineering/view'
import serverNotification, {
HTMLPresenter,
TextPresenter,
createLastViewTx,
getEmployeeAccount,
getEmployeeAccountById,
getUpdateLastViewTx
} from '@hcengineering/server-notification'
import { replaceAll } from './utils'
import { Content } from './types'
/**
* @public
@@ -54,33 +64,21 @@ export async function OnBacklinkCreate (tx: Tx, control: TriggerControl): Promis
if (!checkTx(ptx, hierarchy)) return []
const result: Tx[] = []
const receiver = await getReceiver(ptx, control)
const receiver = await getEmployeeAccount(ptx.objectId as Ref<Employee>, control)
if (receiver === undefined) return []
const sender = await getSender(ptx, control)
const sender = await getEmployeeAccountById(ptx.modifiedBy, control)
if (sender === undefined) return []
const backlink = getBacklink(ptx)
const doc = await getBacklinkDoc(backlink, control)
const textPart = doc !== undefined ? await getTextPart(doc, hierarchy) : undefined
const htmlPart = doc !== undefined ? await getHtmlPart(doc, hierarchy) : undefined
const createNotificationTx = await getPlatformNotificationTx(ptx, backlink, textPart, sender)
if (createNotificationTx !== undefined) {
result.push(createNotificationTx)
}
if (
sender !== undefined &&
textPart !== undefined &&
(await isAllowed(control, receiver, notification.ids.EmailNotification))
) {
const emailTx = await getEmailTx(ptx, backlink, sender, textPart, htmlPart, receiver)
if (emailTx !== undefined) {
result.push(emailTx)
}
}
return result
return await createNotificationTxes(
control,
ptx,
notification.ids.MentionNotification,
doc,
sender,
receiver,
backlink.message
)
}
function checkTx (ptx: TxCollectionCUD<Doc, Backlink>, hierarchy: Hierarchy): boolean {
@@ -98,6 +96,202 @@ function checkTx (ptx: TxCollectionCUD<Doc, Backlink>, hierarchy: Hierarchy): bo
return true
}
async function isAllowed (
control: TriggerControl,
receiver: EmployeeAccount,
providerId: Ref<NotificationProvider>
): Promise<boolean> {
const setting = (
await control.findAll(
notification.class.NotificationSetting,
{
provider: providerId,
type: notification.ids.MentionNotification,
space: receiver._id as unknown as Ref<Space>
},
{ limit: 1 }
)
)[0]
if (setting !== undefined) {
return setting.enabled
}
const provider = (
await control.modelDb.findAll(notification.class.NotificationProvider, {
_id: providerId
})
)[0]
if (provider === undefined) return false
return provider.default
}
async function getTextPart (doc: Doc, control: TriggerControl): Promise<string | undefined> {
const TextPresenter = getTextPresenter(doc._class, control.hierarchy)
if (TextPresenter === undefined) return
return await (
await getResource(TextPresenter.presenter)
)(doc, control)
}
async function getHtmlPart (doc: Doc, control: TriggerControl): Promise<string | undefined> {
const HTMLPresenter = getHTMLPresenter(doc._class, control.hierarchy)
const htmlPart =
HTMLPresenter !== undefined ? await (await getResource(HTMLPresenter.presenter))(doc, control) : undefined
return htmlPart
}
function getHTMLPresenter (_class: Ref<Class<Doc>>, hierarchy: Hierarchy): HTMLPresenter | undefined {
let clazz: Ref<Class<Obj>> | undefined = _class
while (clazz !== undefined) {
const _class = hierarchy.getClass(clazz)
const presenter = hierarchy.as(_class, serverNotification.mixin.HTMLPresenter)
if (presenter.presenter != null) return presenter
clazz = _class.extends
}
}
function getTextPresenter (_class: Ref<Class<Doc>>, hierarchy: Hierarchy): TextPresenter | undefined {
let clazz: Ref<Class<Obj>> | undefined = _class
while (clazz !== undefined) {
const _class = hierarchy.getClass(clazz)
const presenter = hierarchy.as(_class, serverNotification.mixin.TextPresenter)
if (presenter.presenter != null) return presenter
clazz = _class.extends
}
}
function fillTemplate (template: string, sender: string, doc: string, data: string): string {
let res = replaceAll(template, '{sender}', sender)
res = replaceAll(res, '{doc}', doc)
res = replaceAll(res, '{data}', data)
return res
}
async function getContent (
doc: Doc | undefined,
sender: string,
type: Ref<NotificationType>,
control: TriggerControl,
data: string
): Promise<Content | undefined> {
if (doc === undefined) return
const notificationType = control.modelDb.getObject(type)
const textPart = await getTextPart(doc, control)
if (textPart === undefined) return
const text = fillTemplate(notificationType.textTemplate, sender, textPart, data)
const htmlPart = await getHtmlPart(doc, control)
const html = fillTemplate(notificationType.htmlTemplate, sender, htmlPart ?? textPart, data)
const subject = fillTemplate(notificationType.subjectTemplate, sender, textPart, data)
return {
text,
html,
subject
}
}
/**
* @public
*/
export async function createNotificationTxes (
control: TriggerControl,
ptx: TxCollectionCUD<Doc, AttachedDoc>,
type: Ref<NotificationType>,
doc: Doc | undefined,
sender: EmployeeAccount,
receiver: EmployeeAccount,
data: string = '',
action?: NotificationAction
): Promise<Tx[]> {
const res: Tx[] = []
const senderName = formatName(sender.name)
const content = await getContent(doc, senderName, type, control, data)
if (await isAllowed(control, receiver, notification.ids.PlatformNotification)) {
const createNotificationTx = await getPlatformNotificationTx(ptx, type, content?.text, action)
res.push(createNotificationTx)
}
if (content !== undefined && (await isAllowed(control, receiver, notification.ids.EmailNotification))) {
const emailTx = await getEmailNotificationTx(ptx, senderName, content.text, content.html, content.subject, receiver)
if (emailTx !== undefined) {
res.push(emailTx)
}
}
return res
}
async function getPlatformNotificationTx (
ptx: TxCollectionCUD<Doc, AttachedDoc>,
type: Ref<NotificationType>,
text?: string,
action?: NotificationAction
): Promise<TxCollectionCUD<Doc, Notification>> {
const createTx: TxCreateDoc<Notification> = {
objectClass: notification.class.Notification,
objectSpace: notification.space.Notifications,
objectId: generateId(),
modifiedOn: ptx.modifiedOn,
modifiedBy: ptx.modifiedBy,
space: ptx.space,
_id: generateId(),
_class: core.class.TxCreateDoc,
attributes: {
tx: ptx._id,
status: NotificationStatus.New,
type
} as unknown as Data<Notification>
}
if (text !== undefined) {
createTx.attributes.text = text
}
if (action !== undefined) {
createTx.attributes.action = action
}
const createNotificationTx: TxCollectionCUD<Doc, Notification> = {
...ptx,
_id: generateId(),
collection: 'notifications',
tx: createTx
}
return createNotificationTx
}
async function getEmailNotificationTx (
ptx: TxCollectionCUD<Doc, AttachedDoc>,
sender: string,
text: string,
html: string,
subject: string,
receiver: EmployeeAccount
): Promise<TxCreateDoc<EmailNotification> | undefined> {
return {
_id: generateId(),
objectId: generateId(),
_class: core.class.TxCreateDoc,
space: core.space.DerivedTx,
objectClass: notification.class.EmailNotification,
objectSpace: notification.space.Notifications,
modifiedOn: ptx.modifiedOn,
modifiedBy: ptx.modifiedBy,
attributes: {
status: 'new',
sender,
receivers: [receiver.email],
subject,
text,
html
}
}
}
async function getUpdateLastViewTxes (
doc: Doc,
_id: Ref<Doc>,
@@ -197,86 +391,6 @@ export async function UpdateLastView (tx: Tx, control: TriggerControl): Promise<
return result
}
async function getReceiver (
ptx: TxCollectionCUD<Doc, Backlink>,
control: TriggerControl
): Promise<EmployeeAccount | undefined> {
return (
await control.modelDb.findAll(
contact.class.EmployeeAccount,
{
employee: ptx.objectId as Ref<Employee>
},
{ limit: 1 }
)
)[0]
}
async function isAllowed (
control: TriggerControl,
receiver: EmployeeAccount,
providerId: Ref<NotificationProvider>
): Promise<boolean> {
const setting = (
await control.findAll(
notification.class.NotificationSetting,
{
provider: providerId,
type: notification.ids.MentionNotification,
space: receiver._id as unknown as Ref<Space>
},
{ limit: 1 }
)
)[0]
if (setting !== undefined) {
return setting.enabled
}
const provider = (
await control.modelDb.findAll(notification.class.NotificationProvider, {
_id: providerId
})
)[0]
if (provider === undefined) return false
return provider.default
}
async function getPlatformNotificationTx (
ptx: TxCollectionCUD<Doc, Backlink>,
backlink: Backlink,
textPart: string | undefined,
sender: string | undefined
): Promise<TxCollectionCUD<Doc, Notification> | undefined> {
const createTx: TxCreateDoc<Notification> = {
objectClass: notification.class.Notification,
objectSpace: notification.space.Notifications,
objectId: generateId(),
modifiedOn: ptx.modifiedOn,
modifiedBy: ptx.modifiedBy,
space: ptx.space,
_id: generateId(),
_class: core.class.TxCreateDoc,
attributes: {
tx: ptx._id,
status: NotificationStatus.New,
type: notification.ids.MentionNotification
} as unknown as Data<Notification>
}
if (sender !== undefined && textPart !== undefined) {
const text = `${sender} mentioned you in ${textPart} ${backlink.message}`
createTx.attributes.text = text
}
const createNotificationTx: TxCollectionCUD<Doc, Notification> = {
...ptx,
_id: generateId(),
collection: 'notifications',
tx: createTx
}
return createNotificationTx
}
function getBacklink (ptx: TxCollectionCUD<Doc, Backlink>): Backlink {
return TxProcessor.createDoc2Doc(ptx.tx as TxCreateDoc<Backlink>)
}
@@ -293,85 +407,6 @@ async function getBacklinkDoc (backlink: Backlink, control: TriggerControl): Pro
)[0]
}
async function getTextPart (doc: Doc, hierarchy: Hierarchy): Promise<string | undefined> {
const TextPresenter = getTextPresenter(doc._class, hierarchy)
if (TextPresenter === undefined) return
return (await getResource(TextPresenter.presenter))(doc)
}
async function getHtmlPart (doc: Doc, hierarchy: Hierarchy): Promise<string | undefined> {
const HTMLPresenter = getHTMLPresenter(doc._class, hierarchy)
const htmlPart = HTMLPresenter !== undefined ? (await getResource(HTMLPresenter.presenter))(doc) : undefined
return htmlPart
}
async function getSender (ptx: TxCollectionCUD<Doc, Backlink>, control: TriggerControl): Promise<string | undefined> {
const account = (
await control.modelDb.findAll(
contact.class.EmployeeAccount,
{
_id: ptx.modifiedBy as Ref<EmployeeAccount>
},
{ limit: 1 }
)
)[0]
if (account === undefined) return undefined
return formatName(account.name)
}
async function getEmailTx (
ptx: TxCollectionCUD<Doc, Backlink>,
backlink: Backlink,
sender: string,
textPart: string,
htmlPart: string | undefined,
receiver: EmployeeAccount
): Promise<TxCreateDoc<EmailNotification> | undefined> {
const html = `<p><b>${sender}</b> mentioned you in ${htmlPart !== undefined ? htmlPart : textPart}</p> ${
backlink.message
}`
const text = `${sender} mentioned you in ${textPart}`
return {
_id: generateId(),
objectId: generateId(),
_class: core.class.TxCreateDoc,
space: core.space.DerivedTx,
objectClass: notification.class.EmailNotification,
objectSpace: notification.space.Notifications,
modifiedOn: ptx.modifiedOn,
modifiedBy: ptx.modifiedBy,
attributes: {
status: 'new',
sender,
receivers: [receiver.email],
subject: `You was mentioned in ${textPart}`,
text,
html
}
}
}
function getHTMLPresenter (_class: Ref<Class<Doc>>, hierarchy: Hierarchy): HTMLPresenter | undefined {
let clazz: Ref<Class<Obj>> | undefined = _class
while (clazz !== undefined) {
const _class = hierarchy.getClass(clazz)
const presenter = hierarchy.as(_class, view.mixin.HTMLPresenter)
if (presenter.presenter != null) return presenter
clazz = _class.extends
}
}
function getTextPresenter (_class: Ref<Class<Doc>>, hierarchy: Hierarchy): TextPresenter | undefined {
let clazz: Ref<Class<Obj>> | undefined = _class
while (clazz !== undefined) {
const _class = hierarchy.getClass(clazz)
const presenter = hierarchy.as(_class, view.mixin.TextPresenter)
if (presenter.presenter != null) return presenter
clazz = _class.extends
}
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export default async () => ({
trigger: {
@@ -0,0 +1,5 @@
export interface Content {
text: string
html: string
subject: string
}
@@ -0,0 +1,7 @@
function escapeRegExp (str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
export function replaceAll (str: string, find: string, replace: string): string {
return str.replace(new RegExp(escapeRegExp(find), 'g'), replace)
}