From ada9a0bddec3069e498e56b2677e0d2df6491a1a Mon Sep 17 00:00:00 2001 From: Kristina Date: Mon, 25 Mar 2024 21:31:13 +0400 Subject: [PATCH] UBERF-6094: preparing bot (#5061) Signed-off-by: Kristina Fefelova --- dev/client-resources/src/connection.ts | 1 + dev/server/src/server.ts | 1 + models/notification/src/index.ts | 15 +++++- .../ActivityMessageTemplate.svelte | 6 ++- .../src/components/ChannelScrollView.svelte | 8 ++- .../src/inboxNotificationsClient.ts | 8 ++- plugins/notification/src/index.ts | 8 ++- pods/server/src/server.ts | 1 + .../activity-resources/src/index.ts | 2 + .../activity-resources/src/references.ts | 33 ++++++++++-- .../notification-resources/src/index.ts | 5 +- server/account/src/index.ts | 30 ++++++++--- server/core/src/configuration.ts | 9 +++- server/core/src/server/index.ts | 7 +++ server/core/src/server/storage.ts | 7 +++ server/core/src/service.ts | 54 +++++++++++++++++++ server/core/src/types.ts | 16 +++++- server/mongo/src/__tests__/storage.test.ts | 1 + 18 files changed, 184 insertions(+), 28 deletions(-) create mode 100644 server/core/src/service.ts diff --git a/dev/client-resources/src/connection.ts b/dev/client-resources/src/connection.ts index 62c704348c..bf1c73be98 100644 --- a/dev/client-resources/src/connection.ts +++ b/dev/client-resources/src/connection.ts @@ -154,6 +154,7 @@ export async function connect (handler: (tx: Tx) => void): Promise { url: '' } }, + serviceAdapters: {}, defaultContentAdapter: 'default', workspace: workspaceId } diff --git a/models/notification/src/index.ts b/models/notification/src/index.ts index 975ef80b36..e8cd4ed4e0 100644 --- a/models/notification/src/index.ts +++ b/models/notification/src/index.ts @@ -72,7 +72,8 @@ import { type ActivityNotificationViewlet, type BaseNotificationType, type CommonNotificationType, - notificationId + notificationId, + type MentionInboxNotification } from '@hcengineering/notification' import { type Asset, type IntlString } from '@hcengineering/platform' import setting from '@hcengineering/setting' @@ -274,6 +275,15 @@ export class TCommonInboxNotification extends TInboxNotification implements Comm iconProps?: Record } +@Model(notification.class.MentionInboxNotification, notification.class.CommonInboxNotification) +export class TMentionInboxNotification extends TCommonInboxNotification implements MentionInboxNotification { + @Prop(TypeRef(core.class.Doc), core.string.Object) + mentionedIn!: Ref + + @Prop(TypeRef(core.class.Doc), core.string.Class) + mentionedInClass!: Ref> +} + @Model(notification.class.ActivityNotificationViewlet, core.class.Doc, DOMAIN_MODEL) export class TActivityNotificationViewlet extends TDoc implements ActivityNotificationViewlet { messageMatch!: DocumentQuery @@ -324,7 +334,8 @@ export function createModel (builder: Builder): void { TNotificationContextPresenter, TActivityNotificationViewlet, TBaseNotificationType, - TCommonNotificationType + TCommonNotificationType, + TMentionInboxNotification ) // Temporarily disabled, we should think about it diff --git a/plugins/activity-resources/src/components/activity-message/ActivityMessageTemplate.svelte b/plugins/activity-resources/src/components/activity-message/ActivityMessageTemplate.svelte index 96fd7a8036..bc8ade35f1 100644 --- a/plugins/activity-resources/src/components/activity-message/ActivityMessageTemplate.svelte +++ b/plugins/activity-resources/src/components/activity-message/ActivityMessageTemplate.svelte @@ -162,12 +162,14 @@ {/if} {#if !skipLabel && showDatePreposition} - + {/if} - + + + diff --git a/plugins/chunter-resources/src/components/ChannelScrollView.svelte b/plugins/chunter-resources/src/components/ChannelScrollView.svelte index 1d2bd2c411..d02ac3be88 100644 --- a/plugins/chunter-resources/src/components/ChannelScrollView.svelte +++ b/plugins/chunter-resources/src/components/ChannelScrollView.svelte @@ -97,6 +97,10 @@ displayMessages = filteredMessages }) + inboxClient.inboxNotificationsByContext.subscribe(() => { + readViewportMessages() + }) + function scrollToBottom (afterScrollFn?: () => void) { if (scroller !== undefined && scrollElement !== undefined) { scroller.scrollBy(scrollElement.scrollHeight) @@ -275,7 +279,7 @@ } function readViewportMessages () { - if (scrollElement === undefined || scrollContentBox === undefined) { + if (!scrollElement || !scrollContentBox) { return } @@ -304,7 +308,7 @@ return } - if (scrollContentBox === undefined || scrollElement === undefined) { + if (!scrollContentBox || !scrollElement) { return } diff --git a/plugins/notification-resources/src/inboxNotificationsClient.ts b/plugins/notification-resources/src/inboxNotificationsClient.ts index b82588c080..6a5839cfd5 100644 --- a/plugins/notification-resources/src/inboxNotificationsClient.ts +++ b/plugins/notification-resources/src/inboxNotificationsClient.ts @@ -224,11 +224,9 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient { return } - const notificationsToRead = await client.findAll(notification.class.ActivityInboxNotification, { - user: getCurrentAccount()._id, - attachedTo: { $in: toReadIds }, - isViewed: { $ne: true } - }) + const notificationsToRead = get(this.activityInboxNotifications).filter(({ attachedTo }) => + toReadIds.includes(attachedTo) + ) for (const notification of notificationsToRead) { await client.update(notification, { isViewed: true }) diff --git a/plugins/notification/src/index.ts b/plugins/notification/src/index.ts index 9595aeda79..13da62f9ea 100644 --- a/plugins/notification/src/index.ts +++ b/plugins/notification/src/index.ts @@ -249,6 +249,11 @@ export interface CommonInboxNotification extends InboxNotification { iconProps?: Record } +export interface MentionInboxNotification extends CommonInboxNotification { + mentionedIn: Ref + mentionedInClass: Ref> +} + export interface DisplayActivityInboxNotification extends ActivityInboxNotification { combinedIds: Ref[] } @@ -332,7 +337,8 @@ const notification = plugin(notificationId, { InboxNotification: '' as Ref>, ActivityInboxNotification: '' as Ref>, CommonInboxNotification: '' as Ref>, - ActivityNotificationViewlet: '' as Ref> + ActivityNotificationViewlet: '' as Ref>, + MentionInboxNotification: '' as Ref> }, ids: { NotificationSettings: '' as Ref, diff --git a/pods/server/src/server.ts b/pods/server/src/server.ts index bf96bbc75e..8aede09260 100644 --- a/pods/server/src/server.ts +++ b/pods/server/src/server.ts @@ -357,6 +357,7 @@ export function start ( url: '' } }, + serviceAdapters: {}, defaultContentAdapter: 'Rekoni', storageFactory: () => new MinioService({ diff --git a/server-plugins/activity-resources/src/index.ts b/server-plugins/activity-resources/src/index.ts index 724755271a..698c8c6975 100644 --- a/server-plugins/activity-resources/src/index.ts +++ b/server-plugins/activity-resources/src/index.ts @@ -394,6 +394,8 @@ async function OnDocRemoved (originTx: TxCUD, control: TriggerControl): Pro return messages.map((message) => control.txFactory.createTxRemoveDoc(message._class, message.space, message._id)) } +export * from './references' + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export default async () => ({ trigger: { diff --git a/server-plugins/activity-resources/src/references.ts b/server-plugins/activity-resources/src/references.ts index bac00b5aba..6fb6c2e1ff 100644 --- a/server-plugins/activity-resources/src/references.ts +++ b/server-plugins/activity-resources/src/references.ts @@ -35,7 +35,7 @@ import core, { TxUpdateDoc, Type } from '@hcengineering/core' -import notification, { CommonInboxNotification } from '@hcengineering/notification' +import notification, { MentionInboxNotification } from '@hcengineering/notification' import { ServerKit, extractReferences, getHTML, parseHTML, yDocContentToNodes } from '@hcengineering/text' import { StorageAdapter, TriggerControl } from '@hcengineering/server-core' import activity, { ActivityMessage, ActivityReference } from '@hcengineering/activity' @@ -49,6 +49,28 @@ import { const extensions = [ServerKit] +export function isDocMentioned (doc: Ref, content: string | Buffer): boolean { + const references = [] + + if (content instanceof Buffer) { + const nodes = yDocContentToNodes(extensions, content) + for (const node of nodes) { + references.push(...extractReferences(node)) + } + } else { + const doc = parseHTML(content, extensions) + references.push(...extractReferences(doc)) + } + + for (const ref of references) { + if (ref.objectId === doc) { + return true + } + } + + return false +} + export async function getPersonNotificationTxes ( reference: Data, control: TriggerControl, @@ -93,9 +115,11 @@ export async function getPersonNotificationTxes ( return res } - const data: Partial> = { + const data: Partial> = { header: activity.string.MentionedYouIn, - messageHtml: reference.message + messageHtml: reference.message, + mentionedIn: reference.attachedDocId, + mentionedInClass: reference.attachedDocClass } const notifyResult = await shouldNotifyCommon(control, receiver._id, notification.ids.MentionCommonNotificationType) @@ -114,7 +138,8 @@ export async function getPersonNotificationTxes ( reference.srcDocClass, space, originTx.modifiedOn, - notifyResult + notifyResult, + notification.class.MentionInboxNotification ) res.push(...texes) diff --git a/server-plugins/notification-resources/src/index.ts b/server-plugins/notification-resources/src/index.ts index 45aff0910a..87d9cb4066 100644 --- a/server-plugins/notification-resources/src/index.ts +++ b/server-plugins/notification-resources/src/index.ts @@ -117,7 +117,8 @@ export async function getCommonNotificationTxes ( attachedToClass: Ref>, space: Ref, modifiedOn: Timestamp, - notifyResult: NotifyResult + notifyResult: NotifyResult, + _class = notification.class.CommonInboxNotification ): Promise { const res: Tx[] = [] @@ -133,7 +134,7 @@ export async function getCommonNotificationTxes ( space, notifyContexts, data, - notification.class.CommonInboxNotification, + _class, modifiedOn ) } diff --git a/server/account/src/index.ts b/server/account/src/index.ts index ab4b387a45..8e1c0ba071 100644 --- a/server/account/src/index.ts +++ b/server/account/src/index.ts @@ -1077,7 +1077,8 @@ export async function assignWorkspace ( _email: string, workspaceId: string, shouldReplaceAccount: boolean = false, - client?: Client + client?: Client, + personAccountId?: Ref ): Promise { const email = cleanEmail(_email) const initWS = getMetadata(toolPlugin.metadata.InitWorkspace) @@ -1087,7 +1088,14 @@ export async function assignWorkspace ( const workspaceInfo = await getWorkspaceAndAccount(db, productId, email, workspaceId) if (workspaceInfo.account !== null) { - await createPersonAccount(workspaceInfo.account, productId, workspaceId, shouldReplaceAccount, client) + await createPersonAccount( + workspaceInfo.account, + productId, + workspaceId, + shouldReplaceAccount, + client, + personAccountId + ) } // Add account into workspace. @@ -1191,7 +1199,8 @@ async function createPersonAccount ( productId: string, workspace: string, shouldReplaceCurrent: boolean = false, - client?: Client + client?: Client, + personAccountId?: Ref ): Promise { const connection = client ?? (await connect(getTransactor(), getWorkspaceId(workspace, productId))) try { @@ -1210,11 +1219,16 @@ async function createPersonAccount ( if (existingAccount === undefined) { const employee = await createEmployee(ops, name, account.email) - await ops.createDoc(contact.class.PersonAccount, core.space.Model, { - email: account.email, - person: employee, - role: AccountRole.User - }) + await ops.createDoc( + contact.class.PersonAccount, + core.space.Model, + { + email: account.email, + person: employee, + role: AccountRole.User + }, + personAccountId + ) } else { const employee = await ops.findOne(contact.mixin.Employee, { _id: existingAccount.person as Ref }) if (employee === undefined) { diff --git a/server/core/src/configuration.ts b/server/core/src/configuration.ts index d268fb15a9..6caaa53cd3 100644 --- a/server/core/src/configuration.ts +++ b/server/core/src/configuration.ts @@ -18,7 +18,13 @@ import { type MeasureContext, type ServerStorage, type WorkspaceIdWithUrl } from import { type DbAdapterFactory } from './adapter' import { type FullTextPipelineStage } from './indexer/types' import { type StorageAdapter } from './storage' -import type { ContentTextAdapter, ContentTextAdapterFactory, FullTextAdapter, FullTextAdapterFactory } from './types' +import type { + ContentTextAdapter, + ContentTextAdapterFactory, + FullTextAdapter, + FullTextAdapterFactory, + ServiceAdapterConfig +} from './types' /** * @public @@ -61,6 +67,7 @@ export interface DbConfiguration { stages: FullTextPipelineStageFactory } contentAdapters: Record + serviceAdapters: Record defaultContentAdapter: string storageFactory?: () => StorageAdapter } diff --git a/server/core/src/server/index.ts b/server/core/src/server/index.ts index 7d218457fa..e862f2abae 100644 --- a/server/core/src/server/index.ts +++ b/server/core/src/server/index.ts @@ -39,6 +39,7 @@ import { type StorageAdapter } from '../storage' import { Triggers } from '../triggers' import { type ServerStorageOptions } from '../types' import { TServerStorage } from './storage' +import { createServiceAdaptersManager } from '../service' /** * @public @@ -118,6 +119,11 @@ export async function createServerStorage ( throw new Error(`No Adapter for ${DOMAIN_DOC_INDEX_STATE}`) } + const serviceAdaptersManager = await createServiceAdaptersManager( + conf.serviceAdapters, + conf.metrics.newChild('🔌 service adapters', {}) + ) + const indexFactory = (storage: ServerStorage): FullTextIndex => { if (storageAdapter === undefined) { throw new Error('No storage adapter') @@ -166,6 +172,7 @@ export async function createServerStorage ( triggers, fulltextAdapter, storageAdapter, + serviceAdaptersManager, modelDb, conf.workspace, indexFactory, diff --git a/server/core/src/server/storage.ts b/server/core/src/server/storage.ts index 8186fdbcc8..2808f42951 100644 --- a/server/core/src/server/storage.ts +++ b/server/core/src/server/storage.ts @@ -63,6 +63,7 @@ import serverCore from '../plugin' import { type Triggers } from '../triggers' import type { FullTextAdapter, ObjectDDParticipant, ServerStorageOptions, TriggerControl } from '../types' import { type StorageAdapter } from '../storage' +import { type ServiceAdaptersManager } from '../service' export class TServerStorage implements ServerStorage { private readonly fulltext: FullTextIndex @@ -84,6 +85,7 @@ export class TServerStorage implements ServerStorage { private readonly triggers: Triggers, private readonly fulltextAdapter: FullTextAdapter, readonly storageAdapter: StorageAdapter | undefined, + private readonly serviceAdaptersManager: ServiceAdaptersManager, readonly modelDb: ModelDb, private readonly workspace: WorkspaceIdWithUrl, readonly indexFactory: (storage: ServerStorage) => FullTextIndex, @@ -143,6 +145,8 @@ export class TServerStorage implements ServerStorage { } console.timeLog(this.workspace.name, 'closing fulltext') await this.fulltextAdapter.close() + console.timeLog(this.workspace.name, 'closing service adapters') + await this.serviceAdaptersManager.close() } private getAdapter (domain: Domain): DbAdapter { @@ -590,6 +594,9 @@ export class TServerStorage implements ServerStorage { triggerFx.fx(() => f(adapter, this.workspace)) }, + serviceFx: (f) => { + triggerFx.fx(() => f(this.serviceAdaptersManager)) + }, findAll: fAll(ctx), findAllCtx: findAll, modelDb: this.modelDb, diff --git a/server/core/src/service.ts b/server/core/src/service.ts new file mode 100644 index 0000000000..5cfe7cb971 --- /dev/null +++ b/server/core/src/service.ts @@ -0,0 +1,54 @@ +// +// Copyright © 2024 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 { type MeasureContext } from '@hcengineering/core' + +import { type ServiceAdapter, type ServiceAdapterConfig } from './types' + +export class ServiceAdaptersManager { + constructor ( + private readonly adapters: Map, + private readonly context: MeasureContext + ) {} + + getAdapter (adapterId: string): ServiceAdapter | undefined { + return this.adapters.get(adapterId) + } + + async close (): Promise { + for (const adapter of this.adapters.values()) { + await adapter.close() + } + } + + metrics (): MeasureContext { + return this.context + } +} + +export async function createServiceAdaptersManager ( + serviceAdapters: Record, + context: MeasureContext +): Promise { + const adapters = new Map() + + for (const key in serviceAdapters) { + const adapterConf = serviceAdapters[key] + const adapter = await adapterConf.factory(adapterConf.url, adapterConf.db, context.newChild(key, {})) + + adapters.set(key, adapter) + } + return new ServiceAdaptersManager(adapters, context) +} diff --git a/server/core/src/types.ts b/server/core/src/types.ts index 760fbdc15b..fbb4917617 100644 --- a/server/core/src/types.ts +++ b/server/core/src/types.ts @@ -43,6 +43,7 @@ import { import type { Asset, Resource } from '@hcengineering/platform' import { type StorageAdapter } from './storage' import { type Readable } from 'stream' +import { type ServiceAdaptersManager } from './service' /** * @public @@ -133,7 +134,7 @@ export interface TriggerControl { // Later can be replaced with generic one with bucket encapsulated inside. storageFx: (f: (adapter: StorageAdapter, workspaceId: WorkspaceId) => Promise) => void fx: (f: () => Promise) => void - + serviceFx: (f: (adapter: ServiceAdaptersManager) => Promise) => void // Bulk operations in case trigger require some apply: (tx: Tx[], broadcast: boolean, target?: string[]) => Promise applyCtx: (ctx: MeasureContext, tx: Tx[], broadcast: boolean, target?: string[]) => Promise @@ -411,3 +412,16 @@ export interface ServerStorageOptions { broadcast?: BroadcastFunc } + +export interface ServiceAdapter { + close: () => Promise + metrics: () => MeasureContext +} + +export type ServiceAdapterFactory = (url: string, db: string, context: MeasureContext) => Promise + +export interface ServiceAdapterConfig { + factory: ServiceAdapterFactory + db: string + url: string +} diff --git a/server/mongo/src/__tests__/storage.test.ts b/server/mongo/src/__tests__/storage.test.ts index ee828d5a36..5e5b2fc0a8 100644 --- a/server/mongo/src/__tests__/storage.test.ts +++ b/server/mongo/src/__tests__/storage.test.ts @@ -157,6 +157,7 @@ describe('mongo operations', () => { url: '' } }, + serviceAdapters: {}, defaultContentAdapter: 'default', workspace: { ...getWorkspaceId(dbId, ''), workspaceName: '', workspaceUrl: '' }, storageFactory: () => createNullStorageFactory()