new(all): Alpha Word Debugger for 3.0.
This commit is contained in:
parent
f21c699b54
commit
c954786efb
7 changed files with 456 additions and 223 deletions
|
@ -136,3 +136,8 @@ CREATE TABLE [dbo].[GuaribasSchedule]
|
|||
[updatedAt] [datetimeoffset](7) NULL
|
||||
|
||||
GO
|
||||
|
||||
|
||||
# 3.0.0
|
||||
|
||||
ALTER TABLE dbo.GuaribasInstance ADD botKey nvarchar(64) NULL;
|
||||
|
|
204
packages/basic.gblib/services/DebuggerService.ts
Normal file
204
packages/basic.gblib/services/DebuggerService.ts
Normal file
|
@ -0,0 +1,204 @@
|
|||
/*****************************************************************************\
|
||||
| ( )_ _ |
|
||||
| _ _ _ __ _ _ __ ___ ___ _ _ | ,_)(_) ___ ___ _ |
|
||||
| ( '_`\ ( '__)/'_` ) /'_ `\/' _ ` _ `\ /'_` )| | | |/',__)/' v `\ /'_`\ |
|
||||
| | (_) )| | ( (_| |( (_) || ( ) ( ) |( (_| || |_ | |\__,\| (˅) |( (_) ) |
|
||||
| | ,__/'(_) `\__,_)`\__ |(_) (_) (_)`\__,_)`\__)(_)(____/(_) (_)`\___/' |
|
||||
| | | ( )_) | |
|
||||
| (_) \___/' |
|
||||
| |
|
||||
| General Bots Copyright (c) Pragmatismo.io. All rights reserved. |
|
||||
| Licensed under the AGPL-3.0. |
|
||||
| |
|
||||
| According to our dual licensing model,this program can be used either |
|
||||
| under the terms of the GNU Affero General Public License,version 3, |
|
||||
| or under a proprietary license. |
|
||||
| |
|
||||
| The texts of the GNU Affero General Public License with an additional |
|
||||
| permission and of our proprietary license can be found at and |
|
||||
| in the LICENSE file you have received along with this program. |
|
||||
| |
|
||||
| This program is distributed in the hope that it will be useful, |
|
||||
| but WITHOUT ANY WARRANTY,without even the implied warranty of |
|
||||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
||||
| GNU Affero General Public License for more details. |
|
||||
| |
|
||||
| "General Bots" is a registered trademark of Pragmatismo.io. |
|
||||
| The licensing of the program under the AGPLv3 does not imply a |
|
||||
| trademark license. Therefore any rights,title and interest in |
|
||||
| our trademarks remain entirely with us. |
|
||||
| |
|
||||
\*****************************************************************************/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { GBLog, GBMinInstance } from 'botlib';
|
||||
import { GBServer } from '../../../src/app';
|
||||
import { GBAdminService } from '../../admin.gbapp/services/GBAdminService';
|
||||
import { GuaribasUser } from '../../security.gbapp/models';
|
||||
import { DialogKeywords } from './DialogKeywords';
|
||||
import { GBDeployer } from '../../core.gbapp/services/GBDeployer';
|
||||
const Swagger = require('swagger-client');
|
||||
const fs = require('fs');
|
||||
import { CollectionUtil } from 'pragmatismo-io-framework';
|
||||
import * as request from 'request-promise-native';
|
||||
|
||||
const urlJoin = require('url-join');
|
||||
const Path = require('path');
|
||||
const Fs = require('fs');
|
||||
const url = require('url');
|
||||
|
||||
/**
|
||||
* Web Automation services of conversation to be called by BASIC.
|
||||
*/
|
||||
export class DebuggerService {
|
||||
|
||||
/**
|
||||
* Reference to minimal bot instance.
|
||||
*/
|
||||
public min: GBMinInstance;
|
||||
|
||||
/**
|
||||
* Reference to the base system keywords functions to be called.
|
||||
*/
|
||||
public dk: DialogKeywords;
|
||||
|
||||
/**
|
||||
* Current user object to get BASIC properties read.
|
||||
*/
|
||||
public user;
|
||||
|
||||
/**
|
||||
* HTML browser for conversation over page interaction.
|
||||
*/
|
||||
browser: any;
|
||||
|
||||
sys: any;
|
||||
|
||||
/**
|
||||
* The number used in this execution for HEAR calls (useful for SET SCHEDULE).
|
||||
*/
|
||||
hrOn: string;
|
||||
|
||||
userId: GuaribasUser;
|
||||
debugWeb: boolean;
|
||||
lastDebugWeb: Date;
|
||||
|
||||
/**
|
||||
* SYSTEM account maxLines,when used with impersonated contexts (eg. running in SET SCHEDULE).
|
||||
*/
|
||||
maxLines: number = 2000;
|
||||
|
||||
pageMap = {};
|
||||
|
||||
/**
|
||||
* When creating this keyword facade,a bot instance is
|
||||
* specified among the deployer service.
|
||||
*/
|
||||
constructor(min: GBMinInstance, user, dk) {
|
||||
this.min = min;
|
||||
this.user = user;
|
||||
this.dk = dk;
|
||||
|
||||
this.debugWeb = this.min.core.getParam<boolean>(
|
||||
this.min.instance,
|
||||
'Debug Web Automation',
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
private client;
|
||||
|
||||
public async setBreakPoint({ botId, botApiKey, line }) {
|
||||
|
||||
const client = GBServer.globals.debuggers[botId];
|
||||
|
||||
async function mainScript({ Debugger }) {
|
||||
return new Promise((fulfill, reject) => {
|
||||
Debugger.scriptParsed((params) => {
|
||||
const { scriptId, url } = params;
|
||||
fulfill(scriptId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const scriptId = await mainScript(client);
|
||||
const { breakpointId } = await await client.Debugger.setBreakpoint({
|
||||
location: {
|
||||
scriptId,
|
||||
lineNumber: 6 - 1 // (zero-based)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async removeBreakPoint({ botId, botApiKey, line }) {
|
||||
|
||||
}
|
||||
|
||||
public async continue({ botId, botApiKey, force }) {
|
||||
const client = GBServer.globals.debuggers[botId];
|
||||
client.Debugger.resume();
|
||||
}
|
||||
|
||||
public async stopDebug({ botId, botApiKey, force }) {
|
||||
const client = GBServer.globals.debuggers[botId];
|
||||
client.close();
|
||||
}
|
||||
|
||||
public async stepOver({ botId, botApiKey, force }) {
|
||||
const client = GBServer.globals.debuggers[botId];
|
||||
client.stepOver();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public async startDebug({ botId, botApiKey, scriptName }) {
|
||||
|
||||
// TODO : Map this.
|
||||
let webchatKey = null;
|
||||
|
||||
this.client = await new Swagger({
|
||||
spec: JSON.parse(fs.readFileSync('directline-3.0.json', 'utf8')), usePromise: true
|
||||
});
|
||||
this.client.clientAuthorizations.add(
|
||||
'AuthorizationBotConnector',
|
||||
new Swagger.ApiKeyAuthorization('Authorization', `Bearer ${webchatKey}`, 'header')
|
||||
);
|
||||
const response = await this.client.Conversations.Conversations_StartConversation();
|
||||
const conversationId = response.obj.conversationId;
|
||||
GBServer.globals.debugConversationId = conversationId;
|
||||
|
||||
this.client.Conversations.Conversations_PostActivity({
|
||||
conversationId: conversationId,
|
||||
activity: {
|
||||
textFormat: 'plain',
|
||||
text: `/call ${scriptName}`,
|
||||
type: 'message',
|
||||
from: {
|
||||
id: 'test',
|
||||
name: 'test'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Setup debugger.
|
||||
|
||||
const client = GBServer.globals.debuggers[botId];
|
||||
|
||||
client.Debugger.paused(({ callFrames, reason, hitBreakpoints }) => {
|
||||
|
||||
if (hitBreakpoints.length>1)
|
||||
{
|
||||
GBLog.info(`.gbdialog break at line ${callFrames[0].location.lineNumber + 1}`); // (zero-based)
|
||||
}
|
||||
else (reason === ''){
|
||||
GBLog.info(`.gbdialog ${reason} at line ${callFrames[0].location.lineNumber + 1}`); // (zero-based)
|
||||
}
|
||||
});
|
||||
|
||||
await client.Runtime.runIfWaitingForDebugger();
|
||||
await client.Debugger.enable();
|
||||
await client.Debugger.setPauseOnExceptions('all');
|
||||
|
||||
}
|
||||
}
|
|
@ -223,7 +223,7 @@ export class GBVMService extends GBService {
|
|||
if (fs.existsSync(jsfile)) {
|
||||
let code: string = fs.readFileSync(jsfile, 'utf8');
|
||||
|
||||
code = code.replace(/^.*exports.*$/gm, '');
|
||||
code.replace(/^.*exports.*$/gm, '');
|
||||
|
||||
code = `
|
||||
|
||||
|
@ -313,6 +313,38 @@ export class GBVMService extends GBService {
|
|||
});
|
||||
}
|
||||
|
||||
private getParams = (text, names) => {
|
||||
|
||||
let ret = {};
|
||||
const splitParamsButIgnoreCommasInDoublequotes = (str) => {
|
||||
return str.split(',').reduce((accum, curr) => {
|
||||
if (accum.isConcatting) {
|
||||
accum.soFar[accum.soFar.length - 1] += ',' + curr
|
||||
} else {
|
||||
accum.soFar.push(curr)
|
||||
}
|
||||
if (curr.split('"').length % 2 == 0) {
|
||||
accum.isConcatting = !accum.isConcatting
|
||||
}
|
||||
return accum;
|
||||
}, { soFar: [], isConcatting: false }).soFar
|
||||
}
|
||||
|
||||
const items = splitParamsButIgnoreCommasInDoublequotes(text);
|
||||
|
||||
let i = 0;
|
||||
let json = '{';
|
||||
names.forEach(name => {
|
||||
let value = items[i];
|
||||
i++;
|
||||
json = `${json} "${name}": ${value} ${names.length == i ? '' : ','}`;
|
||||
});
|
||||
json = `${json}}`
|
||||
|
||||
return json;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Converts General Bots BASIC
|
||||
*
|
||||
|
@ -325,180 +357,170 @@ export class GBVMService extends GBService {
|
|||
|
||||
code = `<%\n
|
||||
|
||||
|
||||
${process.env.ENABLE_AUTH ? `hear gbLogin as login` : ``}
|
||||
|
||||
${code}
|
||||
|
||||
`;
|
||||
|
||||
// Split all params by comma, not inside strings.
|
||||
var matchingLines = [];
|
||||
var allLines = code.split("\n");
|
||||
|
||||
const getParams = (text, names) => {
|
||||
|
||||
let ret = {};
|
||||
const splitParamsButIgnoreCommasInDoublequotes = (str) => {
|
||||
return str.split(',').reduce((accum, curr) => {
|
||||
if (accum.isConcatting) {
|
||||
accum.soFar[accum.soFar.length - 1] += ',' + curr
|
||||
} else {
|
||||
accum.soFar.push(curr)
|
||||
}
|
||||
if (curr.split('"').length % 2 == 0) {
|
||||
accum.isConcatting = !accum.isConcatting
|
||||
}
|
||||
return accum;
|
||||
}, { soFar: [], isConcatting: false }).soFar
|
||||
for (var i = 0; i < allLines.length; i++) {
|
||||
if (allLines[i].match(pattern)) {
|
||||
matchingLines.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
const items = splitParamsButIgnoreCommasInDoublequotes(text);
|
||||
return matchingLines;
|
||||
|
||||
let i = 0;
|
||||
let json = '{';
|
||||
names.forEach(name => {
|
||||
let value = items[i];
|
||||
i++;
|
||||
json = `${json} "${name}": ${value} ${names.length == i ? '' : ','}`;
|
||||
});
|
||||
json = `${json}}`
|
||||
|
||||
return json;
|
||||
};
|
||||
|
||||
code = `${code}\n%>`;
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
private getKeywords() {
|
||||
|
||||
// Keywords from General Bots BASIC.
|
||||
|
||||
code = code.replace(/^\s*(\w+)\s*\=\s*SELECT\s*(.*)/gim, ($0, $1, $2) => {
|
||||
let keywords = [];
|
||||
let i = 0;
|
||||
|
||||
keywords[i++] = [/^\s*(\w+)\s*\=\s*SELECT\s*(.*)/gim, ($0, $1, $2) => {
|
||||
let tableName = /\sFROM\s(\w+)/.exec($2)[1];
|
||||
let sql = `SELECT ${$2}`.replace(tableName, '?');
|
||||
return `${$1} = await sys.executeSQL({data:${$1}, sql:"${sql}", tableName:"${tableName}"})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*open\s*(.*)/gim, ($0, $1, $2) => {
|
||||
keywords[i++] = [/^\s*open\s*(.*)/gim, ($0, $1, $2) => {
|
||||
|
||||
if (!$1.startsWith("\"") && !$1.startsWith("\'")) {
|
||||
$1 = `"${$1}"`;
|
||||
}
|
||||
const params = getParams($1, ['url', 'username', 'password']);
|
||||
const params = this.getParams($1, ['url', 'username', 'password']);
|
||||
|
||||
return `page = await wa.getPage(${params})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(set hear on)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
keywords[i++] = [/^\s*(set hear on)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
return `hrOn = ${$3}\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\shear (\w+) as login/gim, ($0, $1) => {
|
||||
keywords[i++] = [/^\shear (\w+) as login/gim, ($0, $1) => {
|
||||
return `${$1} = await dk.getHear({kind:"login"})`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\shear (\w+) as email/gim, ($0, $1) => {
|
||||
keywords[i++] = [/^\shear (\w+) as email/gim, ($0, $1) => {
|
||||
return `${$1} = await dk.getHear({kind:"email"})`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\shear (\w+) as integer/gim, ($0, $1) => {
|
||||
keywords[i++] = [/^\shear (\w+) as integer/gim, ($0, $1) => {
|
||||
return `${$1} = await dk.getHear({kind:"integer"})`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\shear (\w+) as file/gim, ($0, $1) => {
|
||||
keywords[i++] = [/^\shear (\w+) as file/gim, ($0, $1) => {
|
||||
return `${$1} = await dk.getHear({kind:"file"})`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\shear (\w+) as boolean/gim, ($0, $1) => {
|
||||
keywords[i++] = [/^\shear (\w+) as boolean/gim, ($0, $1) => {
|
||||
return `${$1} = await dk.getHear({kind:"boolean"})`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\shear (\w+) as name/gim, ($0, $1) => {
|
||||
keywords[i++] = [/^\shear (\w+) as name/gim, ($0, $1) => {
|
||||
return `${$1} = await dk.getHear({kind:"name"})`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\shear (\w+) as date/gim, ($0, $1) => {
|
||||
keywords[i++] = [/^\shear (\w+) as date/gim, ($0, $1) => {
|
||||
return `${$1} = await dk.getHear({kind:"date"})`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\shear (\w+) as hour/gim, ($0, $1) => {
|
||||
keywords[i++] = [/^\shear (\w+) as hour/gim, ($0, $1) => {
|
||||
return `${$1} = await dk.getHear({kind:"hour"})`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\shear (\w+) as phone/gim, ($0, $1) => {
|
||||
keywords[i++] = [/^\shear (\w+) as phone/gim, ($0, $1) => {
|
||||
return `${$1} = await dk.getHear({kind:"phone"})`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\shear (\w+) as money/gim, ($0, $1) => {
|
||||
keywords[i++] = [/^\shear (\w+) as money/gim, ($0, $1) => {
|
||||
return `${$1} = await dk.getHear({kind:"money")}`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\shear (\w+) as language/gim, ($0, $1) => {
|
||||
keywords[i++] = [/^\shear (\w+) as language/gim, ($0, $1) => {
|
||||
return `${$1} = await dk.getHear({kind:"language")}`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\shear (\w+) as zipcode/gim, ($0, $1) => {
|
||||
keywords[i++] = [/^\shear (\w+) as zipcode/gim, ($0, $1) => {
|
||||
return `${$1} = await dk.getHear({kind:"zipcode")}`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\shear (\w+) as (.*)/gim, ($0, $1, $2) => {
|
||||
keywords[i++] = [/^\shear (\w+) as (.*)/gim, ($0, $1, $2) => {
|
||||
return `${$1} = await dk.getHear({kind:"menu", args: [${$2}])}`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(hear)\s*(\w+)/gim, ($0, $1, $2) => {
|
||||
keywords[i++] = [/^\s*(hear)\s*(\w+)/gim, ($0, $1, $2) => {
|
||||
return `${$2} = await dk.getHear({})`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(\w+)\s*\=\s*find contact\s*(.*)/gim, ($0, $1, $2, $3) => {
|
||||
keywords[i++] = [/^\s*(\w+)\s*\=\s*find contact\s*(.*)/gim, ($0, $1, $2, $3) => {
|
||||
return `${$1} = await dk.fndContact({${$2})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(\w+)\s*=\s*find\s*(.*)\s*or talk\s*(.*)/gim, ($0, $1, $2, $3) => {
|
||||
keywords[i++] = [/^\s*(\w+)\s*=\s*find\s*(.*)\s*or talk\s*(.*)/gim, ($0, $1, $2, $3) => {
|
||||
return `${$1} = await sys.find({args:[${$2}])\n
|
||||
if (!${$1}) {
|
||||
await dk.talk ({${$3}})\n;
|
||||
return -1;
|
||||
}
|
||||
`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\sCALL\s*(.*)/gim, ($0, $1) => {
|
||||
keywords[i++] = [/^\sCALL\s*(.*)/gim, ($0, $1) => {
|
||||
return `await ${$1}\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(\w+)\s*\=\s*find\s*(.*)/gim, ($0, $1, $2, $3) => {
|
||||
return `${$1} = await sys.find({args: [${$2}]})\n`;
|
||||
});
|
||||
keywords[i++] = [/^\s*(\w+)\s*\=\s*find\s*(.*)/gim, ($0, $1, $2, $3) => {
|
||||
return `
|
||||
${$1} = await sys.find({args: [${$2}]})\n`;
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(\w+)\s*\=\s*create deal(\s)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
const params = getParams($3, ['dealName', 'contact', 'company', 'amount']);
|
||||
keywords[i++] = [/^\s*(\w+)\s*\=\s*create deal(\s)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
const params = this.getParams($3, ['dealName', 'contact', 'company', 'amount']);
|
||||
|
||||
return `${$1} = await dk.createDeal(${params})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(\w+)\s*\=\s*active tasks/gim, ($0, $1) => {
|
||||
keywords[i++] = [/^\s*(\w+)\s*\=\s*active tasks/gim, ($0, $1) => {
|
||||
return `${$1} = await dk.getActiveTasks({})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(\w+)\s*\=\s*append\s*(.*)/gim, ($0, $1, $2, $3) => {
|
||||
keywords[i++] = [/^\s*(\w+)\s*\=\s*append\s*(.*)/gim, ($0, $1, $2, $3) => {
|
||||
return `${$1} = await sys.append({args:[${$2}]})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(\w+)\s*\=\s*sort\s*(\w+)\s*by(.*)/gim, ($0, $1, $2, $3) => {
|
||||
keywords[i++] = [/^\s*(\w+)\s*\=\s*sort\s*(\w+)\s*by(.*)/gim, ($0, $1, $2, $3) => {
|
||||
return `${$1} = await sys.sortBy({array: ${$2}, memberName: "${$3}"})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\ssee\s*text\s*of\s*(\w+)\s*as\s*(\w+)\s*/gim, ($0, $1, $2, $3) => {
|
||||
keywords[i++] = [/^\ssee\s*text\s*of\s*(\w+)\s*as\s*(\w+)\s*/gim, ($0, $1, $2, $3) => {
|
||||
return `${$2} = await sys.seeText({url: ${$1})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\ssee\s*caption\s*of\s*(\w+)\s*as(.*)/gim, ($0, $1, $2, $3) => {
|
||||
keywords[i++] = [/^\ssee\s*caption\s*of\s*(\w+)\s*as(.*)/gim, ($0, $1, $2, $3) => {
|
||||
return `${$2} = await sys.seeCaption({url: ${$1})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(wait)\s*(\d+)/gim, ($0, $1, $2) => {
|
||||
keywords[i++] = [/^\s*(wait)\s*(\d+)/gim, ($0, $1, $2) => {
|
||||
return `await sys.wait({seconds:${$2})`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(get stock for )(.*)/gim, ($0, $1, $2) => {
|
||||
keywords[i++] = [/^\s*(get stock for )(.*)/gim, ($0, $1, $2) => {
|
||||
return `stock = await sys.getStock({symbol: ${$2})`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(\w+)\s*\=\s*get\s(.*)/gim, ($0, $1, $2, $3) => {
|
||||
keywords[i++] = [/^\s*(\w+)\s*\=\s*get\s(.*)/gim, ($0, $1, $2, $3) => {
|
||||
|
||||
const count = ($2.match(/\,/g) || []).length;
|
||||
const values = $2.split(',');
|
||||
|
@ -523,225 +545,223 @@ export class GBVMService extends GBService {
|
|||
return `${$1} = await sys.get ({file: ${$2}, addressOrHeaders: headers, httpUsername, httpPs})`;
|
||||
}
|
||||
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/\= NEW OBJECT/gi, ($0, $1, $2, $3) => {
|
||||
keywords[i++] = [/\= NEW OBJECT/gi, ($0, $1, $2, $3) => {
|
||||
return ` = {}`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/\= NEW ARRAY/gi, ($0, $1, $2, $3) => {
|
||||
keywords[i++] = [/\= NEW ARRAY/gi, ($0, $1, $2, $3) => {
|
||||
return ` = []`;
|
||||
});
|
||||
}];
|
||||
|
||||
|
||||
code = code.replace(/^\s*(go to)(\s)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
const params = getParams($3, ['fromOrDialogName', 'dialogName']);
|
||||
keywords[i++] = [/^\s*(go to)(\s)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
const params = this.getParams($3, ['fromOrDialogName', 'dialogName']);
|
||||
return `await dk.gotoDialog(${params})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(set language)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
keywords[i++] = [/^\s*(set language)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
return `await dk.setLanguage ({${$3}})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*set header\s*(.*)\sas\s(.*)/gim, ($0, $1, $2) => {
|
||||
keywords[i++] = [/^\s*set header\s*(.*)\sas\s(.*)/gim, ($0, $1, $2) => {
|
||||
return `headers[${$1}]=${$2})`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*set http username\s*\=\s*(.*)/gim, ($0, $1) => {
|
||||
keywords[i++] = [/^\s*set http username\s*\=\s*(.*)/gim, ($0, $1) => {
|
||||
return `httpUsername = ${$1}`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\sset http password\s*\=\s*(.*)/gim, ($0, $1) => {
|
||||
keywords[i++] = [/^\sset http password\s*\=\s*(.*)/gim, ($0, $1) => {
|
||||
return `httpPs = ${$1}`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(datediff)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
const params = getParams($3, ['date1', 'date2', 'mode']);
|
||||
keywords[i++] = [/^\s*(datediff)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
const params = this.getParams($3, ['date1', 'date2', 'mode']);
|
||||
return `await dk.dateDiff (${params}})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(dateadd)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
const params = getParams($3, ['date', 'mode', 'units']);
|
||||
keywords[i++] = [/^\s*(dateadd)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
const params = this.getParams($3, ['date', 'mode', 'units']);
|
||||
return `await dk.dateAdd (${$3})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(set max lines)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
keywords[i++] = [/^\s*(set max lines)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
return `await dk.setMaxLines ({count: ${$3}})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(set max columns)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
keywords[i++] = [/^\s*(set max columns)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
return `await dk.setMaxColumns ({count: ${$3}})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(set translator)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
keywords[i++] = [/^\s*(set translator)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
return `await dk.setTranslatorOn ({on: "${$3.toLowerCase()}"})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(set theme)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
keywords[i++] = [/^\s*(set theme)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
return `await dk.setTheme ({theme: "${$3.toLowerCase()}"})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(set whole word)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
keywords[i++] = [/^\s*(set whole word)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
return `await dk.setWholeWord ({on: "${$3.toLowerCase()}"})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(\w+)\s*\=\s*post\s*(.*),\s*(.*)/gim, ($0, $1, $2, $3) => {
|
||||
keywords[i++] = [/^\s*(\w+)\s*\=\s*post\s*(.*),\s*(.*)/gim, ($0, $1, $2, $3) => {
|
||||
return `${$1} = await sys.postByHttp ({url:${$2}, data:${$3}, headers})`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(\w+)\s*\=\s*put\s*(.*),\s*(.*)/gim, ($0, $1, $2, $3) => {
|
||||
keywords[i++] = [/^\s*(\w+)\s*\=\s*put\s*(.*),\s*(.*)/gim, ($0, $1, $2, $3) => {
|
||||
return `${$1} = await sys.putByHttp ({url:${$2}, data:${$3}, headers})`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(\w+)\s*\=\s*download\s*(.*),\s*(.*)/gim, ($0, $1, $2, $3) => {
|
||||
keywords[i++] = [/^\s*(\w+)\s*\=\s*download\s*(.*),\s*(.*)/gim, ($0, $1, $2, $3) => {
|
||||
return `${$1} = await sys.download ({handle:page, selector: ${$2}, folder:${$3}})`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(\w+)\s*\=\s*CREATE FOLDER\s*(.*)/gim, ($0, $1, $2) => {
|
||||
keywords[i++] = [/^\s*(\w+)\s*\=\s*CREATE FOLDER\s*(.*)/gim, ($0, $1, $2) => {
|
||||
return `${$1} = await sys.createFolder ({name:${$2}})`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\sSHARE FOLDER\s*(.*)/gim, ($0, $1) => {
|
||||
keywords[i++] = [/^\sSHARE FOLDER\s*(.*)/gim, ($0, $1) => {
|
||||
return `await sys.shareFolder ({name: ${$1}})`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(create a bot farm using)(\s)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
keywords[i++] = [/^\s*(create a bot farm using)(\s)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
return `await sys.createABotFarmUsing ({${$3}})`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(transfer to)(\s)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
keywords[i++] = [/^\s*(transfer to)(\s)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
return `await dk.transferTo ({to:${$3}})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(\btransfer\b)(?=(?:[^"]|"[^"]*")*$)/gim, () => {
|
||||
keywords[i++] = [/^\s*(\btransfer\b)(?=(?:[^"]|"[^"]*")*$)/gim, () => {
|
||||
return `await dk.transferTo ({})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(exit)/gim, () => {
|
||||
keywords[i++] = [/^\s*(exit)/gim, () => {
|
||||
return ``;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(show menu)/gim, () => {
|
||||
keywords[i++] = [/^\s*(show menu)/gim, () => {
|
||||
return `await dk.showMenu ({})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(talk to)(\s)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
const params = getParams($3, ['mobile', 'message']);
|
||||
keywords[i++] = [/^\s*(talk to)(\s)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
const params = this.getParams($3, ['mobile', 'message']);
|
||||
return `await sys.talkTo(${params})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(talk)(\s)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
keywords[i++] = [/^\s*(talk)(\s)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
if ($3.substr(0, 1) !== "\"") {
|
||||
$3 = `"${$3}"`;
|
||||
}
|
||||
return `await dk.talk ({text: ${$3}})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(send sms to)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
const params = getParams($3, ['mobile', 'message']);
|
||||
keywords[i++] = [/^\s*(send sms to)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
const params = this.getParams($3, ['mobile', 'message']);
|
||||
return `await sys.sendSmsTo(${params})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(send email)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
const params = getParams($3, ['to', 'subject', 'body']);
|
||||
keywords[i++] = [/^\s*(send email)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
const params = this.getParams($3, ['to', 'subject', 'body']);
|
||||
return `await dk.sendEmail(${params})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(send mail)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
const params = getParams($3, ['to', 'subject', 'body']);
|
||||
keywords[i++] = [/^\s*(send mail)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
const params = this.getParams($3, ['to', 'subject', 'body']);
|
||||
return `await dk.sendEmail(${params})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(send file to)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
const params = getParams($3, ['mobile', 'filename', 'caption']);
|
||||
keywords[i++] = [/^\s*(send file to)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
const params = this.getParams($3, ['mobile', 'filename', 'caption']);
|
||||
return `await dk.sendFileTo(${params})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(hover)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
const params = getParams($3, ['handle', 'selector']);
|
||||
keywords[i++] = [/^\s*(hover)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
const params = this.getParams($3, ['handle', 'selector']);
|
||||
return `await wa.hover (${params})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(click link text)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
const params = getParams('page,' + $3, ['handle', 'text', 'index']);
|
||||
keywords[i++] = [/^\s*(click link text)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
const params = this.getParams('page,' + $3, ['handle', 'text', 'index']);
|
||||
return `await wa.linkByText (${params})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(click)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
keywords[i++] = [/^\s*(click)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
// TODO: page is not string.
|
||||
const params = getParams('page,' + $3, ['handle', 'frameOrSelector', 'selector']);
|
||||
const params = this.getParams('page,' + $3, ['handle', 'frameOrSelector', 'selector']);
|
||||
return `await wa.click (${params})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(send file)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
const params = getParams($3, ['filename', 'caption']);
|
||||
keywords[i++] = [/^\s*(send file)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
const params = this.getParams($3, ['filename', 'caption']);
|
||||
return `await dk.sendFile(${params})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(copy)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
const params = getParams($3, ['src', 'dst']);
|
||||
keywords[i++] = [/^\s*(copy)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
const params = this.getParams($3, ['src', 'dst']);
|
||||
return `await sys.copyFile (${params})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(convert)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
const params = getParams($3, ['src', 'dst']);
|
||||
keywords[i++] = [/^\s*(convert)(\s*)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
const params = this.getParams($3, ['src', 'dst']);
|
||||
return `await sys.convert (${params})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(\w+)\s*\=\s*(.*)\s*as chart/gim, ($0, $1, $2) => {
|
||||
keywords[i++] = [/^\s*(\w+)\s*\=\s*(.*)\s*as chart/gim, ($0, $1, $2) => {
|
||||
return `await dk.chart ({type:'bar', data: ${2}, legends:null, transpose: false})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(chart)(\s)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
const params = getParams($3, ['type', 'data', 'legends', 'transpose']);
|
||||
keywords[i++] = [/^\s*(chart)(\s)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
const params = this.getParams($3, ['type', 'data', 'legends', 'transpose']);
|
||||
return `await dk.chart (${params})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\sMERGE\s(.*)\sWITH\s(.*)BY\s(.*)/gim, ($0, $1, $2, $3) => {
|
||||
keywords[i++] = [/^\sMERGE\s(.*)\sWITH\s(.*)BY\s(.*)/gim, ($0, $1, $2, $3) => {
|
||||
return `await sys.merge({file: ${$1}, data: ${$2}, key1: ${$3}})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\sPRESS\s(.*)/gim, ($0, $1, $2) => {
|
||||
keywords[i++] = [/^\sPRESS\s(.*)/gim, ($0, $1, $2) => {
|
||||
return `await wa.pressKey({handle: page, char: ${$1})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\sSCREENSHOT\s(.*)/gim, ($0, $1, $2) => {
|
||||
keywords[i++] = [/^\sSCREENSHOT\s(.*)/gim, ($0, $1, $2) => {
|
||||
return `await wa.screenshot({handle: page, selector: ${$1}})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\sTWEET\s(.*)/gim, ($0, $1, $2) => {
|
||||
keywords[i++] = [/^\sTWEET\s(.*)/gim, ($0, $1, $2) => {
|
||||
return `await sys.tweet({text: ${$1})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(\w+)\s*\=\s*(.*)\s*as image/gim, ($0, $1, $2) => {
|
||||
keywords[i++] = [/^\s*(\w+)\s*\=\s*(.*)\s*as image/gim, ($0, $1, $2) => {
|
||||
return `${$1} = await sys.asImage({data: ${$2}})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(\w+)\s*\=\s*(.*)\s*as pdf/gim, ($0, $1, $2) => {
|
||||
keywords[i++] = [/^\s*(\w+)\s*\=\s*(.*)\s*as pdf/gim, ($0, $1, $2) => {
|
||||
return `${$1} = await sys.asPdf({data: ${$2})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\s*(\w+)\s*\=\s*FILL\s(.*)\sWITH\s(.*)/gim, ($0, $1, $2, $3) => {
|
||||
keywords[i++] = [/^\s*(\w+)\s*\=\s*FILL\s(.*)\sWITH\s(.*)/gim, ($0, $1, $2, $3) => {
|
||||
return `${$1} = await sys.fill({templateName: ${$2}, data: ${$3}})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\ssave\s(.*)\sas\s(.*)/gim, ($0, $1, $2, $3) => {
|
||||
keywords[i++] = [/^\ssave\s(.*)\sas\s(.*)/gim, ($0, $1, $2, $3) => {
|
||||
return `await sys.saveFile({file: ${$2}, data: ${$1})\n`;
|
||||
});
|
||||
code = code.replace(/^\s*(save)(\s)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
}];
|
||||
keywords[i++] = [/^\s*(save)(\s)(.*)/gim, ($0, $1, $2, $3) => {
|
||||
return `await sys.save({args: [${$3}]})\n`;
|
||||
});
|
||||
}];
|
||||
|
||||
code = code.replace(/^\sset\s(.*)/gim, ($0, $1, $2) => {
|
||||
const params = getParams($1, ['file', 'address', 'value']);
|
||||
keywords[i++] = [/^\sset\s(.*)/gim, ($0, $1, $2) => {
|
||||
const params = this.getParams($1, ['file', 'address', 'value']);
|
||||
return `await sys.set (${params})`;
|
||||
});
|
||||
|
||||
code = `${code}\n%>`;
|
||||
|
||||
return code;
|
||||
}];
|
||||
return keywords;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Executes the converted JavaScript from BASIC code inside execution context.
|
||||
*/
|
||||
|
@ -810,6 +830,7 @@ export class GBVMService extends GBService {
|
|||
min: 1,
|
||||
max: 1,
|
||||
debuggerPort: 9222,
|
||||
botId: botId,
|
||||
cpu: 100,
|
||||
memory: 50000,
|
||||
time: 60 * 60 * 24 * 14,
|
||||
|
|
|
@ -7,7 +7,8 @@ const { } = require('child_process');
|
|||
const { dirname } = require('path');
|
||||
const { fileURLToPath } = require('url');
|
||||
const net = require('net');
|
||||
|
||||
import { GBLog } from 'botlib';
|
||||
import { GBServer } from '../../../../src/app';
|
||||
const genericPool = require('generic-pool');
|
||||
const finalStream = require('final-stream');
|
||||
|
||||
|
@ -50,7 +51,7 @@ const createVm2Pool = ({ min, max, ...limits }) => {
|
|||
const runner = spawn('cpulimit', [
|
||||
'-ql', limits.cpu,
|
||||
'--',
|
||||
'node', `--inspect-brk=${limits.debuggerPort} --experimental-fetch`, `--max-old-space-size=${limits.memory}`,
|
||||
'node', `--inspect-brk=${limits.debuggerPort}`, `--experimental-fetch`, `--max-old-space-size=${limits.memory}`,
|
||||
limits.script
|
||||
, ref
|
||||
], { cwd: limits.cwd, shell: false });
|
||||
|
@ -61,6 +62,10 @@ const createVm2Pool = ({ min, max, ...limits }) => {
|
|||
|
||||
runner.stderr.on('data', (data) => {
|
||||
stderrCache = stderrCache + data.toString();
|
||||
if (stderrCache.includes('failed: address already in use'))
|
||||
{
|
||||
limitError = stderrCache;
|
||||
}
|
||||
if (stderrCache.includes('FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory')) {
|
||||
limitError = 'code execution exceeed allowed memory';
|
||||
}
|
||||
|
@ -79,17 +84,16 @@ const createVm2Pool = ({ min, max, ...limits }) => {
|
|||
const run = async (code, scope) => {
|
||||
const childProcess = await pool.acquire();
|
||||
|
||||
await waitUntil(() => childProcess.socket);
|
||||
|
||||
const socket = net.createConnection(childProcess.socket);
|
||||
|
||||
CDP(async (client) => {
|
||||
const { Debugger, Runtime } = client;
|
||||
try {
|
||||
client.Debugger.paused(() => {
|
||||
client.Debugger.resume();
|
||||
client.close();
|
||||
});
|
||||
await client.Runtime.runIfWaitingForDebugger();
|
||||
await client.Debugger.enable();
|
||||
GBServer.globals.debuggers[scope.botId] = client;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
GBLog.error(err);
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
|
@ -97,11 +101,6 @@ const createVm2Pool = ({ min, max, ...limits }) => {
|
|||
console.error(err);
|
||||
});
|
||||
|
||||
|
||||
await waitUntil(() => childProcess.socket);
|
||||
|
||||
const socket = net.createConnection(childProcess.socket);
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
limitError = 'code execution took too long and was killed';
|
||||
kill(childProcess);
|
||||
|
|
|
@ -86,6 +86,9 @@ export class GuaribasInstance extends Model<GuaribasInstance>
|
|||
|
||||
public version: string;
|
||||
|
||||
@Column(DataType.STRING(64))
|
||||
public botKey: string;
|
||||
|
||||
@Column(DataType.STRING(255))
|
||||
public enabledAdmin: boolean;
|
||||
|
||||
|
|
|
@ -365,7 +365,7 @@ export class GBMinService {
|
|||
}
|
||||
});
|
||||
|
||||
await sleep(15000);
|
||||
await sleep(5000);
|
||||
|
||||
|
||||
});
|
||||
|
|
|
@ -70,6 +70,7 @@ export class RootData {
|
|||
public wwwroot: string; // .gbui or a static webapp.
|
||||
public entryPointDialog: string; // To replace default welcome dialog.
|
||||
public debugConversationId: any; // Used to self-message during debug.
|
||||
public debuggers: any []; // Client of attached Debugger instances by botId.
|
||||
}
|
||||
/**
|
||||
* General Bots open-core entry point.
|
||||
|
|
Loading…
Add table
Reference in a new issue