diff --git a/packages/basic.gblib/services/GBVMService.ts b/packages/basic.gblib/services/GBVMService.ts index 90461b79..4db47605 100644 --- a/packages/basic.gblib/services/GBVMService.ts +++ b/packages/basic.gblib/services/GBVMService.ts @@ -1028,7 +1028,7 @@ export class GBVMService extends GBService { let result; try { - if (!GBConfigService.get('GBVM')) { + if (GBConfigService.get('GBVM') !== false) { return await (async () => { return await new Promise((resolve) => { sandbox['resolve'] = resolve; diff --git a/packages/core.gbapp/services/GBConfigService.ts b/packages/core.gbapp/services/GBConfigService.ts index 3187bcda..4102e1b3 100644 --- a/packages/core.gbapp/services/GBConfigService.ts +++ b/packages/core.gbapp/services/GBConfigService.ts @@ -86,7 +86,7 @@ export class GBConfigService { value = this.getServerPort(); break; case 'GBVM': - value = false; + value = true; break; case 'STORAGE_NAME': value = null; diff --git a/packages/core.gbapp/services/GBCoreService.ts b/packages/core.gbapp/services/GBCoreService.ts index be4aaac6..1f2e81d6 100644 --- a/packages/core.gbapp/services/GBCoreService.ts +++ b/packages/core.gbapp/services/GBCoreService.ts @@ -113,7 +113,7 @@ export class GBCoreService implements IGBCoreService { constructor() { this.adminService = new GBAdminService(this); } - public async ensureInstances(instances: IGBInstance[], bootInstance: any, core: IGBCoreService) {} + public async ensureInstances(instances: IGBInstance[], bootInstance: any, core: IGBCoreService) { } /** * Gets database config and connect to storage. Currently two databases @@ -121,60 +121,60 @@ export class GBCoreService implements IGBCoreService { */ public async initStorage(): Promise { this.dialect = GBConfigService.get('STORAGE_DIALECT'); - + let port: number | undefined; let host: string | undefined; let database: string | undefined; let username: string | undefined; let password: string | undefined; let storage: string | undefined; - - + + if (!['mssql', 'postgres', 'sqlite'].includes(this.dialect)) { throw new Error(`Unknown or unsupported dialect: ${this.dialect}.`); } - - + + if (this.dialect === 'mssql' || this.dialect === 'postgres') { host = GBConfigService.get('STORAGE_SERVER'); database = GBConfigService.get('STORAGE_NAME'); username = GBConfigService.get('STORAGE_USERNAME'); password = GBConfigService.get('STORAGE_PASSWORD'); - + const portStr = GBConfigService.get('STORAGE_PORT'); port = portStr ? parseInt(portStr, 10) : undefined; - + if (!host || !database || !username || !password || !port) { throw new Error(`Missing required configuration for ${this.dialect}.`); } } else if (this.dialect === 'sqlite') { storage = GBConfigService.get('STORAGE_FILE'); - + if (!storage) { throw new Error('STORAGE_FILE is required for SQLite.'); } - + if (!(await GBUtil.exists(storage))) { process.env.STORAGE_SYNC = 'true'; } } - - + + const logging: boolean | Function = GBConfigService.get('STORAGE_LOGGING') === 'true' ? (str: string): void => { - GBLogEx.info(0, str); - } + GBLogEx.info(0, str); + } : false; - - + + const encrypt: boolean = GBConfigService.get('STORAGE_ENCRYPT') === 'true'; - - + + const acquireStr = GBConfigService.get('STORAGE_ACQUIRE_TIMEOUT'); const acquire = acquireStr ? parseInt(acquireStr, 10) : 10000; // Valor padrĂ£o de 10 segundos - - + + const sequelizeOptions: SequelizeOptions = { define: { freezeTableName: true, @@ -185,7 +185,7 @@ export class GBCoreService implements IGBCoreService { logging: logging as boolean, dialect: this.dialect as Dialect, storage: storage, - quoteIdentifiers: this.dialect === 'postgres', + quoteIdentifiers: this.dialect === 'postgres', dialectOptions: this.dialect === 'mssql' ? { options: { trustServerCertificate: true, @@ -200,11 +200,11 @@ export class GBCoreService implements IGBCoreService { acquire: acquire, }, }; - - + + this.sequelize = new Sequelize(database, username, password, sequelizeOptions); } - + /** * Checks wheather storage is acessible or not and opens firewall * in case of any connection block. @@ -338,7 +338,7 @@ STORAGE_SYNC_ALTER=true ENDPOINT_UPDATE=true `; -await fs.writeFile('.env', env); + await fs.writeFile('.env', env); } /** @@ -548,7 +548,7 @@ await fs.writeFile('.env', env); * Verifies that an complex global password has been specified * before starting the server. */ - public ensureAdminIsSecured() {} + public ensureAdminIsSecured() { } public async createBootInstance( core: GBCoreService, @@ -703,27 +703,27 @@ await fs.writeFile('.env', env); if (GBConfigService.get('GB_MODE') === 'legacy') { // Handles calls for BASIC persistence on sheet files. GBLog.info(`Defining Config.xlsx variable ${name}= '${value}'...`); - + let { baseUrl, client } = await GBDeployer.internalGetDriveClient(min); const maxLines = 512; const file = 'Config.xlsx'; const packagePath = GBUtil.getGBAIPath(min.botId, `gbot`); - + let document = await new SystemKeywords().internalGetDocument(client, baseUrl, packagePath, file); - + // Creates book session that will be discarded. let sheets = await client.api(`${baseUrl}/drive/items/${document.id}/workbook/worksheets`).get(); - + // Get the current rows in column A let results = await client .api(`${baseUrl}/drive/items/${document.id}/workbook/worksheets('${sheets.value[0].name}')/range(address='A1:A${maxLines}')`) .get(); - + const rows = results.values; let address = ''; let lastEmptyRow = -1; let isEdit = false; - + // Loop through column A to find the row where name matches, or find the next empty row for (let i = 7; i <= rows.length; i++) { let result = rows[i - 1][0]; @@ -732,27 +732,27 @@ await fs.writeFile('.env', env); isEdit = true; // We are in editing mode break; } else if (!result && lastEmptyRow === -1) { - lastEmptyRow = i ; // Store the first empty row if no match is found + lastEmptyRow = i; // Store the first empty row if no match is found } } - + // If no match was found and there's an empty row, add a new entry if (!isEdit && lastEmptyRow !== -1) { address = `A${lastEmptyRow}:B${lastEmptyRow}`; // Add new entry in columns A and B } - + // Prepare the request body based on whether it's an edit or add operation let body = { values: isEdit ? [[value]] : [[name, value]] }; - + // Update or add the new value in the found address await client .api(`${baseUrl}/drive/items/${document.id}/workbook/worksheets('${sheets.value[0].name}')/range(address='${address}')`) .patch(body); - + } else { let packagePath = GBUtil.getGBAIPath(min.botId, `gbot`); const config = path.join(GBConfigService.get('STORAGE_LIBRARY'), packagePath, 'config.csv'); - + const db = await csvdb(config, ['name', 'value'], ','); if (await db.get({ name: name })) { await db.edit({ name: name }, { name, value }); @@ -761,7 +761,7 @@ await fs.writeFile('.env', env); } } } - + /** * Get a dynamic param from instance. Dynamic params are defined in Config.xlsx * and loaded into the work folder from comida command. @@ -863,7 +863,7 @@ await fs.writeFile('.env', env); public async ensureFolders(instances, deployer: GBDeployer) { const storageMode = process.env.GB_MODE; let libraryPath = GBConfigService.get('STORAGE_LIBRARY'); - + if (storageMode === 'gbcluster') { const minioClient = new Client({ endPoint: process.env.DRIVE_SERVER, @@ -872,54 +872,58 @@ await fs.writeFile('.env', env); accessKey: process.env.DRIVE_ACCESSKEY, secretKey: process.env.DRIVE_SECRET, }); - + await this.syncBotStorage(instances, 'default', deployer, libraryPath); - + const bucketStream = await minioClient.listBuckets(); - + for await (const bucket of bucketStream) { if (bucket.name.endsWith('.gbai') && bucket.name.startsWith(process.env.DRIVE_ORG_PREFIX)) { const botId = bucket.name.replace('.gbai', '').replace(process.env.DRIVE_ORG_PREFIX, ''); await this.syncBotStorage(instances, botId, deployer, libraryPath); - + } } } else { if (!(await GBUtil.exists(libraryPath))) { mkdirp.sync(libraryPath); } - + await this.syncBotStorage(instances, 'default', deployer, libraryPath); - + const files = await fs.readdir(libraryPath); await CollectionUtil.asyncForEach(files, async (file) => { if (file.trim().toLowerCase() !== 'default.gbai' && file.charAt(0) !== '_') { let botId = file.replace(/\.gbai/, ''); await this.syncBotStorage(instances, botId, deployer, libraryPath); - + } }); } } - + private async syncBotStorage(instances: any, botId: any, deployer: GBDeployer, libraryPath: string) { + + let instance = instances.find(p => p.botId.toLowerCase().trim() === botId.toLowerCase().trim()); - if (!instance) { - GBLog.info(`Importing package ${botId}.gbai...`); + if (process.env.GB_MODE === 'local') { + if (!instance) { + GBLog.info(`Importing package ${botId}.gbai...`); - // Creates a bot. + // Creates a bot. - let mobile = null, - email = null; + let mobile = null, + email = null; - instance = await deployer.deployBlankBot(botId, mobile, email); - instances.push(instance); - const gbaiPath = path.join(libraryPath, `${botId}.gbai`); + instance = await deployer.deployBlankBot(botId, mobile, email); + instances.push(instance); + const gbaiPath = path.join(libraryPath, `${botId}.gbai`); - if (!(await GBUtil.exists(gbaiPath))) { - fs.mkdir(gbaiPath, { recursive: true }); + if (!(await GBUtil.exists(gbaiPath))) { + fs.mkdir(gbaiPath, { recursive: true }); + } } } return instance; diff --git a/packages/core.gbapp/services/GBDeployer.ts b/packages/core.gbapp/services/GBDeployer.ts index a0696c8e..1b75a8f3 100644 --- a/packages/core.gbapp/services/GBDeployer.ts +++ b/packages/core.gbapp/services/GBDeployer.ts @@ -522,7 +522,7 @@ export class GBDeployer implements IGBDeployer { secretKey: process.env.DRIVE_SECRET, }); - const bucketName = process.env.DRIVE_BUCKETPREFIX + min.botId + '.gbai'; + const bucketName = process.env.DRIVE_ORG_PREFIX + min.botId + '.gbai'; if (!(await GBUtil.exists(localPath))) { await fs.mkdir(localPath, { recursive: true }); @@ -670,7 +670,7 @@ export class GBDeployer implements IGBDeployer { await this.cleanupPackage(min.instance, packageName); } - if (GBConfigService.get('GB_MODE') !== 'legacy') { + if (GBConfigService.get('GB_MODE') === 'local') { const filePath = path.join(GBConfigService.get('STORAGE_LIBRARY'), gbai, packageName); await GBUtil.copyIfNewerRecursive(filePath, packageWorkFolder); } else { diff --git a/packages/saas.gbapp/dialog/NewUserDialog.ts b/packages/saas.gbapp/dialog/NewUserDialog.ts index d5b625c5..30ea055f 100755 --- a/packages/saas.gbapp/dialog/NewUserDialog.ts +++ b/packages/saas.gbapp/dialog/NewUserDialog.ts @@ -116,7 +116,7 @@ export class NewUserDialog extends IGBDialog { return await step.replaceDialog('/welcome_saas_bottemplate', step.activeDialog.state.options); } else { step.activeDialog.state.options.templateName = template; - debugger; + await NewUserDialog.createBot(step, min, true); return await step.replaceDialog('/ask', { isReturning: true }); @@ -252,7 +252,7 @@ export class NewUserDialog extends IGBDialog { '12', '99', '1234', - 'crawlergbot.gbai', + 'ai-search.gbai', true ); diff --git a/packages/saas.gbapp/service/GBOService.ts b/packages/saas.gbapp/service/GBOService.ts index 599061e7..e9a51b32 100755 --- a/packages/saas.gbapp/service/GBOService.ts +++ b/packages/saas.gbapp/service/GBOService.ts @@ -37,8 +37,11 @@ import MicrosoftGraph from "@microsoft/microsoft-graph-client"; import sgMail from '@sendgrid/mail'; import { default as PasswordGenerator } from 'strict-password-generator'; import { promises as fs } from 'fs'; +import { existsSync } from 'fs'; + import { GBConfigService } from "../../core.gbapp/services/GBConfigService.js"; import path from "path"; +import { Client } from "minio"; export class GBOService { @@ -75,54 +78,6 @@ export class GBOService { }); } - public async createSubFolderAtRoot(token: string, name: string, - siteId: string, libraryId: string) { - return new Promise((resolve, reject) => { - let client = MicrosoftGraph.Client.init({ - authProvider: done => { - done(null, token); - } - }); - const body = { - "name": name, - "folder": {}, - "@microsoft.graph.conflictBehavior": "rename" - } - client.api(`https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${libraryId}/drive/root/children`) - .post(body, (err, res) => { - if (err) { - reject(err) - } - else { - resolve(res); - } - }); - }); - } - public async createSubFolderAt(token: string, parentPath: string, name: string, - siteId: string, libraryId: string) { - return new Promise((resolve, reject) => { - let client = MicrosoftGraph.Client.init({ - authProvider: done => { - done(null, token); - } - }); - const body = { - "name": name, - "folder": {}, - "@microsoft.graph.conflictBehavior": "rename" - } - client.api(`https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${libraryId}/drive/root:/${parentPath}:/children`) - .post(body, (err, res) => { - if (err) { - reject(err) - } - else { - resolve(res); - } - }); - }); - } public async listTemplates(min: GBMinInstance) { if (GBConfigService.get('GB_MODE') === 'legacy') { @@ -146,7 +101,7 @@ export class GBOService { } else { - const templatesDir = path.join(process.env.PWD,'templates'); + const templatesDir = path.join(process.env.PWD, 'templates'); const gbaiDirectories = []; // Read all entries in the templates directory @@ -161,67 +116,109 @@ export class GBOService { return gbaiDirectories; } } + public async copyTemplates(min: GBMinInstance, gbaiDest: any, templateName: string, kind: string, botName: string): Promise { + const storageMode = process.env.GB_MODE; - public async copyTemplates(min: GBMinInstance, gbaiDest, templateName: string, kind: string, botName: string) { + if (storageMode === 'legacy') { + // Microsoft Graph (SharePoint) Implementation + const token = await (min.adminService as any).acquireElevatedToken(min.instance.instanceId, true); + const siteId = process.env.STORAGE_SITE_ID; + const templateLibraryId = process.env.SAAS_TEMPLATE_LIBRARY; + const libraryId = process.env.STORAGE_LIBRARY; - let token = - await (min.adminService as any).acquireElevatedToken(min.instance.instanceId, true); + const client = MicrosoftGraph.Client.init({ + authProvider: (done) => done(null, token) + }); - let siteId = process.env.STORAGE_SITE_ID; - let templateLibraryId = process.env.SAAS_TEMPLATE_LIBRARY; - let libraryId = process.env.STORAGE_LIBRARY; + const packageName = `${templateName.split('.')[0]}.${kind}`; + const destinationName = `${botName}.${kind}`; - let client = MicrosoftGraph.Client.init({ - authProvider: done => { - done(null, token); - } - }); + try { + // Try direct copy first + const src = await client.api( + `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${templateLibraryId}/drive/root:/${templateName}/${packageName}` + ).get(); - const body = - { - "parentReference": { driveId: gbaiDest.parentReference.driveId, id: gbaiDest.id }, - "name": `${botName}.${kind}` - } - - const packageName = `${templateName.split('.')[0]}.${kind}`; - - try { - const src = await client.api( - `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${templateLibraryId}/drive/root:/${templateName}/${packageName}`) - .get(); - - return await client.api( - `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${templateLibraryId}/drive/items/${src.id}/copy`) - .post(body); - - } catch (error) { - - if (error.code === "itemNotFound") { - - } else if (error.code === "nameAlreadyExists") { - - let src = await client.api( - `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${templateLibraryId}/drive/root:/${templateName}/${packageName}:/children`) - .get(); - const dstName = `${botName}.gbai/${botName}.${kind}`; - let dst = await client.api(`https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${libraryId}/drive/root:/${dstName}`) - .get(); - - await CollectionUtil.asyncForEach(src.value, async item => { - - const body = - { - "parentReference": { driveId: dst.parentReference.driveId, id: dst.id } - } - await client.api( - `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${templateLibraryId}/drive/items/${item.id}/copy`) - .post(body); + await client.api( + `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${templateLibraryId}/drive/items/${src.id}/copy` + ).post({ + parentReference: { + driveId: gbaiDest.parentReference.driveId, + id: gbaiDest.id + }, + name: destinationName }); + } catch (error) { + if (error.code === "nameAlreadyExists") { + // Handle existing destination by copying contents individually + const srcItems = await client.api( + `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${templateLibraryId}/drive/root:/${templateName}/${packageName}:/children` + ).get(); + + const dstPath = `${botName}.gbai/${destinationName}`; + const dst = await client.api( + `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${libraryId}/drive/root:/${dstPath}` + ).get(); + + await CollectionUtil.asyncForEach(srcItems.value, async (item) => { + await client.api( + `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${templateLibraryId}/drive/items/${item.id}/copy` + ).post({ + parentReference: { + driveId: dst.parentReference.driveId, + id: dst.id + } + }); + }); + } else { + GBLog.error(`Failed to copy templates: ${error.message}`); + throw error; + } } - else { - GBLog.error(error); - throw error; - } + } + else if (storageMode === 'gbcluster') { + + // MinIO Implementation + const minioClient = new Client({ + endPoint: process.env.DRIVE_SERVER, + port: parseInt(process.env.DRIVE_PORT), + useSSL: process.env.DRIVE_USE_SSL === 'true', + accessKey: process.env.DRIVE_ACCESSKEY, + secretKey: process.env.DRIVE_SECRET, + }); + + + const bucketName = `${process.env.DRIVE_ORG_PREFIX}${botName}.gbai`; + const packageName = `${templateName.split('.')[0]}.${kind}`; + const localTemplatePath = path.join(process.env.PWD, 'templates', templateName, packageName); + const minioDestinationPath = `${botName}.${kind}`; + + const uploadDirectory = async (localPath: string, minioPath: string = '') => { + + // Ensure the bucket exists in local file system + if (existsSync(localPath)) { + + const entries = await fs.readdir(localPath, { withFileTypes: true }); + + for (const entry of entries) { + const fullLocalPath = path.join(localPath, entry.name); + const objectName = path.posix.join(minioPath, entry.name); + + if (entry.isDirectory()) { + await uploadDirectory(fullLocalPath, objectName); + } else { + const fileContent = await fs.readFile(fullLocalPath); + await minioClient.putObject(bucketName, objectName, fileContent); + GBLog.info(`Uploaded ${objectName} to MinIO bucket ${bucketName}`); + } + } + } + else { + GBLog.verbose(`Package ${localPath} does not exist on templates.`); + } + }; + + await uploadDirectory(localTemplatePath, minioDestinationPath); } } @@ -365,4 +362,56 @@ export class GBOService { return documents[0]; } + + public async createRootFolder(token: string, name: string, + siteId: string, libraryId: string) { + const storageMode = process.env.GB_MODE; + + if (storageMode === 'gbcluster') { + // Minio implementation + const minioClient = new Client({ + endPoint: process.env.DRIVE_SERVER, + port: parseInt(process.env.DRIVE_PORT), + useSSL: process.env.DRIVE_USE_SSL === 'true', + accessKey: process.env.DRIVE_ACCESSKEY, + secretKey: process.env.DRIVE_SECRET, + }); + + + // Ensure bucket exists + + name = `${process.env.DRIVE_ORG_PREFIX}${name}`; + const bucketExists = await minioClient.bucketExists(name); + if (!bucketExists) { + await minioClient.makeBucket(name); + } + return { name: name, folder: {} }; // Return similar structure to MS Graph + + } else { + // Original MS Graph implementation + return new Promise((resolve, reject) => { + let client = MicrosoftGraph.Client.init({ + authProvider: done => { + done(null, token); + } + }); + const body = { + "name": name, + "folder": {}, + "@microsoft.graph.conflictBehavior": "rename" + } + client.api(`https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${libraryId}/drive/root/children`) + .post(body, (err, res) => { + if (err) { + reject(err) + } + else { + resolve(res); + } + }); + }); + } + } + + } diff --git a/packages/saas.gbapp/service/MainService.ts b/packages/saas.gbapp/service/MainService.ts index 2e1de4f4..747ca02f 100755 --- a/packages/saas.gbapp/service/MainService.ts +++ b/packages/saas.gbapp/service/MainService.ts @@ -35,6 +35,7 @@ import { GBMinInstance, GBLog } from 'botlib'; import { CollectionUtil } from 'pragmatismo-io-framework'; import urlJoin from 'url-join'; import { GBOService } from './GBOService.js'; +import { GBConfigService } from '../../core.gbapp/services/GBConfigService.js'; export class MainService { async createSubscriptionMSFT(email: string, plan: string, offer: string, quantity: number, additionalData: any) { } @@ -105,7 +106,10 @@ export class MainService { subscription.externalSubscriptionId = externalSubscriptionId; await subscription.save(); - let token = await (min.adminService.acquireElevatedToken as any)(min.instance.instanceId, true); + let token = + GBConfigService.get('GB_MODE') === 'legacy'? + await (min.adminService.acquireElevatedToken as any)(min.instance.instanceId, true) : + null; let siteId = process.env.STORAGE_SITE_ID; let libraryId = process.env.STORAGE_LIBRARY; @@ -119,15 +123,15 @@ export class MainService { GBLog.info( 'Creating .gbai folder ...'); - let item = await gboService.createSubFolderAtRoot(token, `${botName}.gbai`, siteId, libraryId); - await sleep(1000); + let item = await gboService.createRootFolder(token, `${botName}.gbai`, siteId, libraryId); + GBLog.info( 'Copying Templates...'); - await gboService.copyTemplates(min, item, 'Shared.gbai', 'gbkb', botName); - await gboService.copyTemplates(min, item, 'Shared.gbai', 'gbot', botName); - await gboService.copyTemplates(min, item, 'Shared.gbai', 'gbtheme', botName); - await gboService.copyTemplates(min, item, 'Shared.gbai', 'gbdialog', botName); - await gboService.copyTemplates(min, item, 'Shared.gbai', 'gbdata', botName); + // await gboService.copyTemplates(min, item, 'shared.gbai', 'gbkb', botName); + // await gboService.copyTemplates(min, item, 'shared.gbai', 'gbot', botName); + // await gboService.copyTemplates(min, item, 'shared.gbai', 'gbtheme', botName); + // await gboService.copyTemplates(min, item, 'shared.gbai', 'gbdialog', botName); + // await gboService.copyTemplates(min, item, 'shared.gbai', 'gbdata', botName); await gboService.copyTemplates(min, item, templateName, 'gbkb', botName); await gboService.copyTemplates(min, item, templateName, 'gbot', botName); await gboService.copyTemplates(min, item, templateName, 'gbtheme', botName);