mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 03:14:40 +03:00
UBERF-6094: preparing bot (#5061)
Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
parent
ace5692b91
commit
ada9a0bdde
@ -154,6 +154,7 @@ export async function connect (handler: (tx: Tx) => void): Promise<ClientConnect
|
||||
url: ''
|
||||
}
|
||||
},
|
||||
serviceAdapters: {},
|
||||
defaultContentAdapter: 'default',
|
||||
workspace: { ...getWorkspaceId(''), workspaceUrl: '', workspaceName: '' }
|
||||
}
|
||||
|
@ -73,6 +73,7 @@ export async function start (port: number, host?: string): Promise<void> {
|
||||
url: ''
|
||||
}
|
||||
},
|
||||
serviceAdapters: {},
|
||||
defaultContentAdapter: 'default',
|
||||
workspace: workspaceId
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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" />
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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 })
|
||||
|
@ -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>,
|
||||
|
@ -357,6 +357,7 @@ export function start (
|
||||
url: ''
|
||||
}
|
||||
},
|
||||
serviceAdapters: {},
|
||||
defaultContentAdapter: 'Rekoni',
|
||||
storageFactory: () =>
|
||||
new MinioService({
|
||||
|
@ -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: {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
54
server/core/src/service.ts
Normal file
54
server/core/src/service.ts
Normal 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)
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -157,6 +157,7 @@ describe('mongo operations', () => {
|
||||
url: ''
|
||||
}
|
||||
},
|
||||
serviceAdapters: {},
|
||||
defaultContentAdapter: 'default',
|
||||
workspace: { ...getWorkspaceId(dbId, ''), workspaceName: '', workspaceUrl: '' },
|
||||
storageFactory: () => createNullStorageFactory()
|
||||
|
Loading…
Reference in New Issue
Block a user