UBERF-6094: preparing bot (#5061)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina 2024-03-25 21:31:13 +04:00 committed by GitHub
parent ace5692b91
commit ada9a0bdde
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 184 additions and 28 deletions

View File

@ -154,6 +154,7 @@ export async function connect (handler: (tx: Tx) => void): Promise<ClientConnect
url: ''
}
},
serviceAdapters: {},
defaultContentAdapter: 'default',
workspace: { ...getWorkspaceId(''), workspaceUrl: '', workspaceName: '' }
}

View File

@ -73,6 +73,7 @@ export async function start (port: number, host?: string): Promise<void> {
url: ''
}
},
serviceAdapters: {},
defaultContentAdapter: 'default',
workspace: workspaceId
}

View File

@ -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<string, any>
}
@Model(notification.class.MentionInboxNotification, notification.class.CommonInboxNotification)
export class TMentionInboxNotification extends TCommonInboxNotification implements MentionInboxNotification {
@Prop(TypeRef(core.class.Doc), core.string.Object)
mentionedIn!: Ref<Doc>
@Prop(TypeRef(core.class.Doc), core.string.Class)
mentionedInClass!: Ref<Class<Doc>>
}
@Model(notification.class.ActivityNotificationViewlet, core.class.Doc, DOMAIN_MODEL)
export class TActivityNotificationViewlet extends TDoc implements ActivityNotificationViewlet {
messageMatch!: DocumentQuery<Doc>
@ -324,7 +334,8 @@ export function createModel (builder: Builder): void {
TNotificationContextPresenter,
TActivityNotificationViewlet,
TBaseNotificationType,
TCommonNotificationType
TCommonNotificationType,
TMentionInboxNotification
)
// Temporarily disabled, we should think about it

View File

@ -162,12 +162,14 @@
{/if}
{#if !skipLabel && showDatePreposition}
<span class="text-sm lower mr-1">
<span class="text-sm lower">
<Label label={activity.string.At} />
</span>
{/if}
<MessageTimestamp date={message.createdOn ?? message.modifiedOn} />
<span class="text-sm lower">
<MessageTimestamp date={message.createdOn ?? message.modifiedOn} />
</span>
</div>
<slot name="content" />

View File

@ -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
}

View File

@ -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 })

View File

@ -249,6 +249,11 @@ export interface CommonInboxNotification extends InboxNotification {
iconProps?: Record<string, any>
}
export interface MentionInboxNotification extends CommonInboxNotification {
mentionedIn: Ref<Doc>
mentionedInClass: Ref<Class<Doc>>
}
export interface DisplayActivityInboxNotification extends ActivityInboxNotification {
combinedIds: Ref<ActivityInboxNotification>[]
}
@ -332,7 +337,8 @@ const notification = plugin(notificationId, {
InboxNotification: '' as Ref<Class<InboxNotification>>,
ActivityInboxNotification: '' as Ref<Class<ActivityInboxNotification>>,
CommonInboxNotification: '' as Ref<Class<CommonInboxNotification>>,
ActivityNotificationViewlet: '' as Ref<Class<ActivityNotificationViewlet>>
ActivityNotificationViewlet: '' as Ref<Class<ActivityNotificationViewlet>>,
MentionInboxNotification: '' as Ref<Class<MentionInboxNotification>>
},
ids: {
NotificationSettings: '' as Ref<Doc>,

View File

@ -357,6 +357,7 @@ export function start (
url: ''
}
},
serviceAdapters: {},
defaultContentAdapter: 'Rekoni',
storageFactory: () =>
new MinioService({

View File

@ -394,6 +394,8 @@ async function OnDocRemoved (originTx: TxCUD<Doc>, 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: {

View File

@ -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<Doc>, 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<ActivityReference>,
control: TriggerControl,
@ -93,9 +115,11 @@ export async function getPersonNotificationTxes (
return res
}
const data: Partial<Data<CommonInboxNotification>> = {
const data: Partial<Data<MentionInboxNotification>> = {
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)

View File

@ -117,7 +117,8 @@ export async function getCommonNotificationTxes (
attachedToClass: Ref<Class<Doc>>,
space: Ref<Space>,
modifiedOn: Timestamp,
notifyResult: NotifyResult
notifyResult: NotifyResult,
_class = notification.class.CommonInboxNotification
): Promise<Tx[]> {
const res: Tx[] = []
@ -133,7 +134,7 @@ export async function getCommonNotificationTxes (
space,
notifyContexts,
data,
notification.class.CommonInboxNotification,
_class,
modifiedOn
)
}

View File

@ -1077,7 +1077,8 @@ export async function assignWorkspace (
_email: string,
workspaceId: string,
shouldReplaceAccount: boolean = false,
client?: Client
client?: Client,
personAccountId?: Ref<PersonAccount>
): Promise<Workspace> {
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<PersonAccount>
): Promise<void> {
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<Employee> })
if (employee === undefined) {

View File

@ -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<string, ContentTextAdapterConfiguration>
serviceAdapters: Record<string, ServiceAdapterConfig>
defaultContentAdapter: string
storageFactory?: () => StorageAdapter
}

View File

@ -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,

View File

@ -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,

View File

@ -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<string, ServiceAdapter>,
private readonly context: MeasureContext
) {}
getAdapter (adapterId: string): ServiceAdapter | undefined {
return this.adapters.get(adapterId)
}
async close (): Promise<void> {
for (const adapter of this.adapters.values()) {
await adapter.close()
}
}
metrics (): MeasureContext {
return this.context
}
}
export async function createServiceAdaptersManager (
serviceAdapters: Record<string, ServiceAdapterConfig>,
context: MeasureContext
): Promise<ServiceAdaptersManager> {
const adapters = new Map<string, ServiceAdapter>()
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)
}

View File

@ -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>) => void
fx: (f: () => Promise<void>) => void
serviceFx: (f: (adapter: ServiceAdaptersManager) => Promise<void>) => void
// Bulk operations in case trigger require some
apply: (tx: Tx[], broadcast: boolean, target?: string[]) => Promise<TxResult>
applyCtx: (ctx: MeasureContext, tx: Tx[], broadcast: boolean, target?: string[]) => Promise<TxResult>
@ -411,3 +412,16 @@ export interface ServerStorageOptions {
broadcast?: BroadcastFunc
}
export interface ServiceAdapter {
close: () => Promise<void>
metrics: () => MeasureContext
}
export type ServiceAdapterFactory = (url: string, db: string, context: MeasureContext) => Promise<ServiceAdapter>
export interface ServiceAdapterConfig {
factory: ServiceAdapterFactory
db: string
url: string
}

View File

@ -157,6 +157,7 @@ describe('mongo operations', () => {
url: ''
}
},
serviceAdapters: {},
defaultContentAdapter: 'default',
workspace: { ...getWorkspaceId(dbId, ''), workspaceName: '', workspaceUrl: '' },
storageFactory: () => createNullStorageFactory()