/** * * Agora Real Time Engagement * Created by Liu Loulou in 2022-06. * Copyright (c) 2022 Agora IO. All rights reserved. * */ import * as path from 'path'; import * as fs from '../common/fs'; import * as log from '../common/logger'; import { Cmd, StatusCmd } from '../msg/cmd/cmd'; import { RTE } from '../rte/rte'; import { Value } from '../value/value'; import { Extension } from './extension'; export abstract class UiBaseExtension extends Extension { constructor(name: string, resourceFolder: string, defaultResource: string) { super(name); this.resourceFolder = resourceFolder; this.defaultResource = defaultResource; } override async onStart(rte: RTE): Promise { // Check if the UI resource exists or not. if (!fs.fileExistsSync(this.resourceFolder)) { log.error(`The resource folder: ${this.resourceFolder} does not exist, \ current dir: ${path.resolve('.')}`); throw new Error( `Resource folder: ${this.resourceFolder} does not exist.`, ); } const widgetProps = rte.getProp('widget_props'); if (widgetProps) { // RTE defines the type of 'widget_props' a JS object, therefore, using // 'getReference' below to get it. const widgetPropsJson = widgetProps.getReference(); if (typeof widgetPropsJson === 'object') { this.widgetProps = widgetPropsJson; } else { log.warn( `The type of 'widget_props' property must be a JavaScript object, \ ${JSON.stringify(widgetPropsJson)}`, ); this.widgetProps = {}; } } } override async onCmd(rte: RTE, cmd: Cmd): Promise { const request = cmd.toJson(); log.info(`The ui extension receives cmd: ${JSON.stringify(request)}`); if (request.command === 'get_widget_props') { // The ui widget needs the extension's related 'RTE' object to communicate // with the extension. But it can not get through 'getExportedRte()', as // it does not know about the name of the extension, and the name of the // extension group of the extension. So we should return the 'RTE' object // to the ui widget before rendering. // TODO(Liu): The RTE runtime does not support returning a js object now, // so we hack here. Remove if the runtime supports 'returnCmd()'. const additionalProp = cmd.getProperty('additional'); if (additionalProp) { const additional = additionalProp.getReference() as { cb: (rte: RTE) => void; }; const { cb } = additional; cb(rte); } rte.returnJson('ok', this.widgetProps, cmd); return; } if (request.command === 'read_widget_content') { let { indexResource } = request; if (!indexResource || indexResource === '' || indexResource === '/') { indexResource = this.defaultResource; } const requestFile = `${this.resourceFolder}/${indexResource}`; if (await fs.fileExists(requestFile)) { rte.returnJson( 'ok', { basePath: await fs.realpath(this.resourceFolder), path: await fs.realpath(requestFile), content: await fs.readFile(requestFile), }, cmd, ); } else { rte.returnText('error', 'Not found', cmd); } return; } // Check if the command comes through HTTP::GET, and if yes, the extension // will return UI resources according to the URL. if (request.properties.method && request.properties.method === 1) { // HTTP GET let { url } = request.properties; if (url === '/' || url === '') { url = this.defaultResource; } // TODO(Lyuge): should restrict path inside root (white-list is best) const requestFile = this.resourceFolder + url; if (await fs.fileExists(requestFile)) { const status = new StatusCmd('ok', requestFile); // The 'rte_status_detail_type' property declares that the http protocol // should serve the status cmd as a file. status.setProperty( 'rte_status_detail_type', new Value('application/octet-stream'), ); rte.returnCmd(status, cmd); } else { const status = new StatusCmd('error', 'Not found'); // The http protocol treats the 'error' status code as 500, you can set // a customized http status code with the 'code' property. status.setProperty('code', new Value(404)); rte.returnCmd(status, cmd); } return; } } private _resourceFolder: string; public get resourceFolder(): string { if (this.addonFolder) { return `${path.resolve(this.addonFolder)}/${this._resourceFolder}/`; } return this._resourceFolder; } public set resourceFolder(v: string) { this._resourceFolder = v; } protected defaultResource: string; protected widgetProps: Record = {}; }