// // Copyright © 2025 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 attachment, { type Attachment } from '@hcengineering/attachment' import drive, { FileVersion } from '@hcengineering/drive' import core, { BlobMetadata, Class, Client, Doc, DocumentQuery, DomainParams, DomainRequestOptions, DomainResult, FindOptions, FindResult, Hierarchy, MeasureContext, ModelDb, OperationDomain, Ref, SearchOptions, SearchQuery, SearchResult, systemAccountUuid, Tx, TxOperations, TxResult, WithLookup, WorkspaceUuid } from '@hcengineering/core' import { type RestClient, createRestClient } from '@hcengineering/api-client' import { getTransactorEndpoint } from '@hcengineering/server-client' import { generateToken } from '@hcengineering/server-token' import { AttachmentPatchEvent, MessageEventType } from '@hcengineering/communication-sdk-types' import { BlobSourceType, type VideoTranscodeResult } from './types' import { AttachmentID } from '@hcengineering/communication-types' async function getClient (workspace: WorkspaceUuid, token: string): Promise { const endpoint = await getTransactorEndpoint(token) const client = createRestClient(toHttpUrl(endpoint), workspace, token) const { model, hierarchy } = await client.getModel() return new RestClientAdapter(client, hierarchy, model) } /** * @public */ export class Controller { private readonly workspaces: Map = new Map() async get (workspace: WorkspaceUuid): Promise { let client = this.workspaces.get(workspace) if (client === undefined) { client = await WorkspaceClient.create(workspace) this.workspaces.set(workspace, client) } return client } async close (): Promise { for (const workspace of this.workspaces.values()) { await workspace.close() } this.workspaces.clear() } } /** * @public */ export class WorkspaceClient { private constructor ( readonly workspace: WorkspaceUuid, readonly client: Client ) {} static async create (workspace: WorkspaceUuid): Promise { const token = generateToken(systemAccountUuid, workspace, { service: 'media' }) const client = await getClient(workspace, token) return new WorkspaceClient(workspace, client) } async close (): Promise { await this.client.close() } async updateBlobMetadata (ctx: MeasureContext, result: VideoTranscodeResult, metadata: BlobMetadata): Promise { if (result.source.source !== BlobSourceType.Doc) { return } const { objectClass, objectId } = result.source const hierarchy = this.client.getHierarchy() const txOps = new TxOperations(this.client, core.account.System) if (hierarchy.isDerived(objectClass, attachment.class.Attachment)) { const doc = await txOps.findOne(attachment.class.Attachment, { _id: objectId as Ref }) if (doc !== undefined) { await txOps.update(doc, { ...doc.metadata, ...metadata }) } } if (hierarchy.isDerived(objectClass, drive.class.FileVersion)) { const doc = await txOps.findOne(drive.class.FileVersion, { _id: objectId as Ref }) if (doc !== undefined) { await txOps.update(doc, { ...doc.metadata, ...metadata }) } } } async updateCommMetadata (ctx: MeasureContext, result: VideoTranscodeResult, metadata: BlobMetadata): Promise { if (result.source.source !== BlobSourceType.Message) { return } const event: AttachmentPatchEvent = { type: MessageEventType.AttachmentPatch, cardId: result.source.cardId, messageId: result.source.messageId, socialId: core.account.System, operations: [ { opcode: 'update', attachments: [ { id: result.blobId as AttachmentID, params: { metadata } } ] } ] } const txOps = new TxOperations(this.client, core.account.System) await txOps.domainRequest('communication' as OperationDomain, { event }) } } function toHttpUrl (url: string): string { return url.replace('ws://', 'http://').replace('wss://', 'https://') } class RestClientAdapter implements Client { constructor ( private readonly client: RestClient, private readonly hierarchy: Hierarchy | undefined, private readonly model: ModelDb | undefined ) {} async domainRequest( domain: OperationDomain, params: DomainParams, options?: DomainRequestOptions ): Promise> { return await this.client.domainRequest(domain, params, options) } async findAll( _class: Ref>, query: DocumentQuery, options?: FindOptions ): Promise> { return await this.client.findAll(_class, query, options) } async tx (tx: Tx): Promise { return await this.client.tx(tx) } async findOne( _class: Ref>, query: DocumentQuery, options?: FindOptions ): Promise | undefined> { return await this.client.findOne(_class, query, options) } async searchFulltext (query: SearchQuery, options: SearchOptions): Promise { return await this.client.searchFulltext(query, options) } async close (): Promise { // No ned to close the REST client } getHierarchy (): Hierarchy { if (this.hierarchy === undefined) { throw new Error('Hierarchy is not defined') } return this.hierarchy } getModel (): ModelDb { if (this.model === undefined) { throw new Error('Model is not defined') } return this.model } }