* FIX: Admin now is internationalized.

* FIX: Webchat now receives a private token.
* FIX: OAuth2 now has got revised and included state to avoid CSRF attacks.
* FIX: Now server will only start with a secure administration password.
This commit is contained in:
Rodrigo Rodriguez 2018-09-24 11:04:36 -03:00
parent 7375f179b2
commit 3832f27451
19 changed files with 678 additions and 461 deletions

View file

@ -187,7 +187,13 @@ here is a list of admin commands related to deploying .gb* files.
| deployPackage | Deploy a KB package. Usage **deployPackage** [package-name]. Then, you need to run rebuildIndex. | | deployPackage | Deploy a KB package. Usage **deployPackage** [package-name]. Then, you need to run rebuildIndex. |
| undeployPackage | Undeploy a KB. Usage **undeployPackage** [package-name]. | | undeployPackage | Undeploy a KB. Usage **undeployPackage** [package-name]. |
| redeployPackage | Undeploy and then deploys the KB. Usage **redeployPackage** [package-name]. Then, you need to run rebuildIndex. | | redeployPackage | Undeploy and then deploys the KB. Usage **redeployPackage** [package-name]. Then, you need to run rebuildIndex. |
| rebuildIndex | Rebuild Azure Search indexes, must be run after **deployPackage** or **redeployPackage**. | | setupSecurity | Setup connection to user directories. |
Discontinued commands:
| Command | Description |Reason |
|-----------------| -----------------------------------------------------------------------------------------------------------------|------|
| rebuildIndex | Rebuild Azure Search indexes, must be run after **deployPackage** or **redeployPackage**. | Now it is called automatically |
### Credits & Inspiration ### Credits & Inspiration

View file

@ -1,5 +1,12 @@
# Release History # Release History
## Version 0.1.3
* FIX: Admin now is internationalized.
* FIX: Webchat now receives a private token.
* FIX: OAuth2 now has got revised and included state to avoid CSRF attacks.
* FIX: Now server will only start with a secure administration password.
## Version 0.1.2 ## Version 0.1.2
* NEW: kb.gbapp now has a complete browser of excel articles. * NEW: kb.gbapp now has a complete browser of excel articles.

View file

@ -42,59 +42,32 @@ import { GBConfigService } from "../../core.gbapp/services/GBConfigService";
import { KBService } from "./../../kb.gbapp/services/KBService"; import { KBService } from "./../../kb.gbapp/services/KBService";
import { BotAdapter } from "botbuilder"; import { BotAdapter } from "botbuilder";
import { GBAdminService } from "../services/GBAdminService"; import { GBAdminService } from "../services/GBAdminService";
import { Messages } from "../strings";
/** /**
* Dialogs for administration tasks. * Dialogs for administration tasks.
*/ */
export class AdminDialog extends IGBDialog { export class AdminDialog extends IGBDialog {
static async undeployPackageCommand(text: any, min: GBMinInstance, dc) {
static async undeployPackageCommand(text: any, min: GBMinInstance) {
let packageName = text.split(" ")[1]; let packageName = text.split(" ")[1];
let importer = new GBImporter(min.core); let importer = new GBImporter(min.core);
let deployer = new GBDeployer(min.core, importer); let deployer = new GBDeployer(min.core, importer);
dc.context.sendActivity(`Undeploying package ${packageName}...`);
await deployer.undeployPackageFromLocalPath( await deployer.undeployPackageFromLocalPath(
min.instance, min.instance,
UrlJoin("deploy", packageName) UrlJoin("deploy", packageName)
); );
dc.context.sendActivity(`Package ${packageName} undeployed...`);
} }
static async deployPackageCommand( static async deployPackageCommand(text: string,
text: string, deployer: GBDeployer
dc,
deployer: GBDeployer,
min: GBMinInstance
) { ) {
let packageName = text.split(" ")[1]; let packageName = text.split(" ")[1];
await dc.context.sendActivity(
`Deploying package ${packageName}... (It may take a few seconds)`
);
let additionalPath = GBConfigService.get("ADDITIONAL_DEPLOY_PATH"); let additionalPath = GBConfigService.get("ADDITIONAL_DEPLOY_PATH");
await deployer.deployPackageFromLocalPath( await deployer.deployPackageFromLocalPath(
UrlJoin(additionalPath, packageName) UrlJoin(additionalPath, packageName)
); );
await dc.context.sendActivity(
`Package ${packageName} deployed... Please run rebuildIndex command.`
);
} }
static async rebuildIndexCommand(min: GBMinInstance, dc) {
let search = new AzureSearch(
min.instance.searchKey,
min.instance.searchHost,
min.instance.searchIndex,
min.instance.searchIndexer
);
dc.context.sendActivity("Rebuilding index...");
await search.deleteIndex();
let kbService = new KBService(min.core.sequelize);
await search.createIndex(
kbService.getSearchSchema(min.instance.searchIndex),
"gb"
);
await dc.context.sendActivity("Index rebuilt.");
}
/** /**
* Setup dialogs flows and define services call. * Setup dialogs flows and define services call.
* *
@ -107,101 +80,80 @@ export class AdminDialog extends IGBDialog {
let importer = new GBImporter(min.core); let importer = new GBImporter(min.core);
let deployer = new GBDeployer(min.core, importer); let deployer = new GBDeployer(min.core, importer);
min.dialogs.add("/adminRat", [
async dc => {
await AdminDialog.refreshAdminToken(min, dc);
// await dc.context.sendActivity(
// `Deploying package ... (It may take a few seconds)`
// );
// await AdminDialog.deployPackageCommand(
// "deployPackage ProjectOnline.gbkb",
// dc,
// deployer,
// min
// );
await dc.endAll();
}
]);
min.dialogs.add("/adminUpdateToken", [
async (dc, args, next) => {
await dc.endAll();
let service = new GBAdminService();
await service.saveValue("authenticatorToken", args.token)
await dc.context.sendActivities([
{ type: 'typing' },
{ type: 'message', text: "Token has been updated." },
{ type: 'message', text: "Please, log out now from the administration work account on next screen." },
{ type: 'delay', value: 4000 },
])
}
]);
min.dialogs.add("/admin", [ min.dialogs.add("/admin", [
async (dc, args) => { async dc => {
const prompt = "Please, authenticate:"; const locale = dc.context.activity.locale;
const prompt = Messages[locale].authenticate;
await dc.prompt("textPrompt", prompt); await dc.prompt("textPrompt", prompt);
}, },
async (dc, value) => { async (dc, password) => {
let text = value; const locale = dc.context.activity.locale;
const user = min.userState.get(dc.context); if (
password === GBConfigService.get("ADMIN_PASS") &&
GBAdminService.StrongRegex.test(password)
) {
if (!user.authenticated || text === GBConfigService.get("ADMIN_PASS")) {
user.authenticated = true;
await dc.context.sendActivity( await dc.context.sendActivity(
"Welcome to Pragmatismo.io GeneralBots Administration." Messages[locale].welcome
); );
await dc.prompt("textPrompt", "Which task do you wanna run now?"); await dc.prompt("textPrompt", Messages[locale].which_task);
} else { } else {
await dc.prompt("textPrompt", Messages[locale].wrong_password);
await dc.endAll(); await dc.endAll();
} }
}, },
async (dc, value) => { async (dc, value) => {
const locale = dc.context.activity.locale;
var text = value; var text = value;
const user = min.userState.get(dc.context); const user = min.userState.get(dc.context);
let cmdName = text.split(" ")[0];
dc.context.sendActivity(Messages[locale].working(cmdName))
if (text === "quit") { if (text === "quit") {
user.authenticated = false; user.authenticated = false;
await dc.replace("/"); await dc.replace("/");
} else if (text === "sync") { } else if (cmdName === "deployPackage") {
await min.core.syncDatabaseStructure(); await AdminDialog.deployPackageCommand(text, deployer);
await dc.context.sendActivity("Sync started...");
await dc.replace("/admin", { firstRun: false }); await dc.replace("/admin", { firstRun: false });
} else if (text.split(" ")[0] === "rebuildIndex") { } else if (cmdName === "redeployPackage") {
await AdminDialog.rebuildIndexCommand(min, dc); await AdminDialog.undeployPackageCommand(text, min);
await AdminDialog.deployPackageCommand(text, deployer);
await dc.context.sendActivity();
await dc.replace("/admin", { firstRun: false }); await dc.replace("/admin", { firstRun: false });
} else if (text.split(" ")[0] === "deployPackage") { } else if (cmdName === "undeployPackage") {
await AdminDialog.deployPackageCommand(text, dc, deployer, min); await AdminDialog.undeployPackageCommand(text, min);
await dc.replace("/admin", { firstRun: false }); await dc.replace("/admin", { firstRun: false });
} else if (text.split(" ")[0] === "redeployPackage") { } else if (cmdName === "setupSecurity") {
await AdminDialog.undeployPackageCommand(text, min, dc); await AdminDialog.setupSecurity(min, dc);
await AdminDialog.deployPackageCommand(text, dc, deployer, min); }
await dc.context.sendActivity("Redeploy done."); else{
await dc.replace("/admin", { firstRun: false }); await dc.context.sendActivity(Messages[locale].unknown_command);
} else if (text.split(" ")[0] === "undeployPackage") { dc.endAll()
await AdminDialog.undeployPackageCommand(text, min, dc); await dc.replace("/answer", { query: text });
await dc.replace("/admin", { firstRun: false });
} else if (text.split(" ")[0] === "applyPackage") {
await dc.context.sendActivity("Applying in progress...");
await min.core.loadInstance(text.split(" ")[1]);
await dc.context.sendActivity("Applying done...");
await dc.replace("/admin", { firstRun: false });
} else if (text.split(" ")[0] === "rat") {
await AdminDialog.refreshAdminToken(min, dc);
} }
} }
]); ]);
} }
private static async refreshAdminToken(min: any, dc: any) { private static async setupSecurity(min: any, dc: any) {
let config = { const locale = dc.context.activity.locale;
authenticatorTenant: min.instance.authenticatorTenant, let state = `${min.instance.instanceId}${Math.floor(
authenticatorClientID: min.instance.authenticatorClientID Math.random() * 1000000000
}; )}`;
await min.conversationalService.sendEvent(dc, "play", { await min.adminService.setValue(
playerType: "login", min.instance.instanceId,
data: config "AntiCSRFAttackState",
}); state
await dc.context.sendActivity("Update your Administrative token by Login..."); );
let url = `https://login.microsoftonline.com/${
min.instance.authenticatorTenant
}/oauth2/authorize?client_id=${
min.instance.authenticatorClientId
}&response_type=code&redirect_uri=${min.instance.botServerUrl}/${
min.instance.botId
}/token&state=${state}&response_mode=query`;
await dc.context.sendActivity(
Messages[locale].consent(url)
);
} }
} }

View file

@ -45,6 +45,9 @@ import {
export class GuaribasAdmin extends Model<GuaribasAdmin> export class GuaribasAdmin extends Model<GuaribasAdmin>
{ {
@Column
instanceId: number;
@Column @Column
key: string; key: string;

View file

@ -30,28 +30,97 @@
| | | |
\*****************************************************************************/ \*****************************************************************************/
"use strict" "use strict";
import { GuaribasAdmin } from "../models/AdminModel"; import { GuaribasAdmin } from "../models/AdminModel";
import { IGBCoreService } from "botlib";
import { AuthenticationContext, TokenResponse } from "adal-node";
const UrlJoin = require("url-join");
export class GBAdminService { export class GBAdminService {
public static StrongRegex = new RegExp(
"^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,})"
);
async saveValue(key: string, value: string): Promise<GuaribasAdmin> { core: IGBCoreService;
let options = { where: {} }
options.where = { key: key } constructor(core: IGBCoreService) {
this.core = core;
}
public async setValue(
instanceId: number,
key: string,
value: string
): Promise<GuaribasAdmin> {
let options = { where: {} };
options.where = { key: key };
let admin = await GuaribasAdmin.findOne(options); let admin = await GuaribasAdmin.findOne(options);
if (admin == null) { if (admin == null) {
admin = new GuaribasAdmin(); admin = new GuaribasAdmin();
admin.key = key; admin.key = key;
} }
admin.value = value; admin.value = value;
return admin.save() admin.instanceId = instanceId;
return admin.save();
} }
async getValue(key: string) { public async getValue(instanceId: number, key: string) {
let options = { where: {} } let options = { where: {} };
options.where = { key: key } options.where = { key: key, instanceId: instanceId };
let obj = await GuaribasAdmin.findOne(options); let obj = await GuaribasAdmin.findOne(options);
return Promise.resolve(obj.value); return Promise.resolve(obj.value);
} }
public async acquireElevatedToken(instanceId): Promise<string> {
return new Promise<string>(async (resolve, reject) => {
let instance = await this.core.loadInstanceById(instanceId);
let expiresOn = new Date(await this.getValue(instanceId, "expiresOn"));
if (expiresOn.getTime() > new Date().getTime()) {
let accessToken = await this.getValue(instanceId, "accessToken");
resolve(accessToken);
} else {
let authorizationUrl = UrlJoin(
instance.authenticatorAuthorityHostUrl,
instance.authenticatorTenant,
"/oauth2/authorize"
);
var authenticationContext = new AuthenticationContext(authorizationUrl);
let refreshToken = await this.getValue(instanceId, "refreshToken");
let resource = "https://graph.microsoft.com";
authenticationContext.acquireTokenWithRefreshToken(
refreshToken,
instance.authenticatorClientId,
instance.authenticatorClientSecret,
resource,
async (err, res) => {
if (err) {
reject(err);
} else {
let tokens = res as TokenResponse;
await this.setValue(
instanceId,
"accessToken",
tokens.accessToken
);
await this.setValue(
instanceId,
"refreshToken",
tokens.refreshToken
);
await this.setValue(
instanceId,
"expiresOn",
tokens.expiresOn.toString()
);
resolve(tokens.accessToken);
}
}
);
}
});
}
} }

View file

@ -0,0 +1,21 @@
export const Messages = {
"en-US": {
authenticate: "Please, authenticate:",
welcome: "Welcome to Pragmatismo.io GeneralBots Administration.",
which_task: "Which task do you wanna run now?",
working:(command)=> `I'm working on ${command}`,
unknown_command: text =>
`Well, but ${text} is not a administrative General Bots command, I will try to search for it.`,
hi: text => `Hello, ${text}.`,
undeployPackage: text => `Undeploying package ${text}...`,
deployPackage: text => `Deploying package ${text}...`,
redeployPackage: text => `Redeploying package ${text}...`,
packageUndeployed: text => `Package ${text} undeployed...`,
consent: (url)=>`Please, consent access to this app at: [Microsoft Online](${url}).`,
wrong_password: "Sorry, wrong password. Please, try again."
},
"pt-BR": {
show_video: "Vou te mostrar um vídeo. Por favor, aguarde...",
hi: msg => `Oi, ${msg}.`
}
};

View file

@ -67,7 +67,7 @@ export class GuaribasInstance extends Model<GuaribasInstance>
instanceId: number; instanceId: number;
@Column @Column
applicationPrincipal: string; botServerUrl:string;
@Column @Column
whoAmIVideo: string; whoAmIVideo: string;
@ -109,10 +109,21 @@ export class GuaribasInstance extends Model<GuaribasInstance>
@Column @Column
authenticatorTenant: string; authenticatorTenant: string;
@Column @Column
authenticatorSignUpSignInPolicy: string; authenticatorAuthorityHostUrl: string;
@Column @Column
authenticatorClientID: string; authenticatorClientId: string;
@Column
authenticatorClientSecret: string;
@Column
cloudSubscriptionId: string;
@Column
cloudRegion: string;
@Column @Column
whatsappBotKey: string; whatsappBotKey: string;

View file

@ -1,4 +1,4 @@
import { IGBInstance } from 'botlib'; import { IGBInstance } from "botlib";
/*****************************************************************************\ /*****************************************************************************\
| ( )_ _ | | ( )_ _ |
| _ _ _ __ _ _ __ ___ ___ _ _ | ,_)(_) ___ ___ _ | | _ _ _ __ _ _ __ ___ ___ _ _ | ,_)(_) ___ ___ _ |
@ -44,7 +44,6 @@ import { Messages } from "../strings";
import { AzureText } from "pragmatismo-io-framework"; import { AzureText } from "pragmatismo-io-framework";
const Nexmo = require("nexmo"); const Nexmo = require("nexmo");
export interface LanguagePickerSettings { export interface LanguagePickerSettings {
defaultLocale?: string; defaultLocale?: string;
supportedLocales?: string[]; supportedLocales?: string[];
@ -62,30 +61,40 @@ export class GBConversationalService implements IGBConversationalService {
} }
async sendEvent(dc: any, name: string, value: any): Promise<any> { async sendEvent(dc: any, name: string, value: any): Promise<any> {
const msg = MessageFactory.text(""); if (dc.context.activity.channelId === "webchat") {
msg.value = value; const msg = MessageFactory.text("");
msg.type = "event"; msg.value = value;
msg.name = name; msg.type = "event";
return dc.context.sendActivity(msg); msg.name = name;
return dc.context.sendActivity(msg);
}
} }
async sendSms(min: GBMinInstance, mobile: string, text: string): Promise<any> { async sendSms(
min: GBMinInstance,
mobile: string,
text: string
): Promise<any> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const nexmo = new Nexmo({ const nexmo = new Nexmo({
apiKey: min.instance.smsKey, apiKey: min.instance.smsKey,
apiSecret: min.instance.smsSecret, apiSecret: min.instance.smsSecret
}); });
nexmo.message.sendSms( nexmo.message.sendSms(
min.instance.smsServiceNumber, min.instance.smsServiceNumber,
mobile, mobile,
text, (err, data) => { text,
if (err) { reject(err) } else { resolve(data) } (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
} }
); );
}); });
} }
async routeNLP(dc: any, min: GBMinInstance, text: string): Promise<boolean> { async routeNLP(dc: any, min: GBMinInstance, text: string): Promise<boolean> {
// Invokes LUIS. // Invokes LUIS.

View file

@ -78,7 +78,7 @@ export class GBCoreService implements IGBCoreService {
*/ */
constructor() { constructor() {
this.dialect = GBConfigService.get("DATABASE_DIALECT") this.dialect = GBConfigService.get("DATABASE_DIALECT")
this.adminService = new GBAdminService(); this.adminService = new GBAdminService(this)
} }
/** /**
@ -269,6 +269,15 @@ export class GBCoreService implements IGBCoreService {
return GuaribasInstance.findAll({}); return GuaribasInstance.findAll({});
} }
/**
* Loads just one Bot instance by its internal Id.
*/
async loadInstanceById(instanceId: string): Promise<IGBInstance> {
let options = { where: {instanceId: instanceId} }
return GuaribasInstance.findOne(options);
}
/** /**
* Loads just one Bot instance. * Loads just one Bot instance.
*/ */

View file

@ -30,36 +30,37 @@
| | | |
\*****************************************************************************/ \*****************************************************************************/
"use strict" "use strict";
const logger = require("../../../src/logger") const logger = require("../../../src/logger");
const Path = require("path") const Path = require("path");
const UrlJoin = require("url-join") const UrlJoin = require("url-join");
const Fs = require("fs") const Fs = require("fs");
const WaitUntil = require("wait-until") const WaitUntil = require("wait-until");
const express = require("express") const express = require("express");
import { KBService } from "./../../kb.gbapp/services/KBService" import { KBService } from "./../../kb.gbapp/services/KBService";
import { GBImporter } from "./GBImporter" import { GBImporter } from "./GBImporter";
import { IGBCoreService, IGBInstance } from "botlib" import { IGBCoreService, IGBInstance } from "botlib";
import { GBConfigService } from "./GBConfigService" import { GBConfigService } from "./GBConfigService";
import { GBError } from "botlib" import { GBError } from "botlib";
import { GuaribasPackage } from "../models/GBModel" import { GuaribasPackage, GuaribasInstance } from "../models/GBModel";
import { IGBPackage } from "botlib" import { IGBPackage } from "botlib";
import { AzureSearch } from "pragmatismo-io-framework";
/** Deployer service for bots, themes, ai and more. */ /** Deployer service for bots, themes, ai and more. */
export class GBDeployer { export class GBDeployer {
core: IGBCoreService core: IGBCoreService;
importer: GBImporter importer: GBImporter;
workDir: string = "./work" workDir: string = "./work";
static deployFolder = "deploy" static deployFolder = "deploy";
constructor(core: IGBCoreService, importer: GBImporter) { constructor(core: IGBCoreService, importer: GBImporter) {
this.core = core this.core = core;
this.importer = importer this.importer = importer;
} }
/** /**
@ -72,103 +73,102 @@ export class GBDeployer {
server: any, server: any,
appPackages: Array<IGBPackage> appPackages: Array<IGBPackage>
) { ) {
let _this = this let _this = this;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
let totalPackages = 0 let totalPackages = 0;
let additionalPath = GBConfigService.get("ADDITIONAL_DEPLOY_PATH") let additionalPath = GBConfigService.get("ADDITIONAL_DEPLOY_PATH");
let paths = [GBDeployer.deployFolder] let paths = [GBDeployer.deployFolder];
if (additionalPath) { if (additionalPath) {
paths = paths.concat(additionalPath.toLowerCase().split(";")) paths = paths.concat(additionalPath.toLowerCase().split(";"));
} }
let botPackages = new Array<string>() let botPackages = new Array<string>();
let gbappPackages = new Array<string>() let gbappPackages = new Array<string>();
let generalPackages = new Array<string>() let generalPackages = new Array<string>();
function doIt(path) { function doIt(path) {
const isDirectory = source => Fs.lstatSync(source).isDirectory() const isDirectory = source => Fs.lstatSync(source).isDirectory();
const getDirectories = source => const getDirectories = source =>
Fs.readdirSync(source) Fs.readdirSync(source)
.map(name => Path.join(source, name)) .map(name => Path.join(source, name))
.filter(isDirectory) .filter(isDirectory);
let dirs = getDirectories(path) let dirs = getDirectories(path);
dirs.forEach(element => { dirs.forEach(element => {
if (element.startsWith(".")) { if (element.startsWith(".")) {
logger.info(`Ignoring ${element}...`) logger.info(`Ignoring ${element}...`);
} else { } else {
if (element.endsWith(".gbot")) { if (element.endsWith(".gbot")) {
botPackages.push(element) botPackages.push(element);
} else if (element.endsWith(".gbapp")) { } else if (element.endsWith(".gbapp")) {
gbappPackages.push(element) gbappPackages.push(element);
} else { } else {
generalPackages.push(element) generalPackages.push(element);
} }
} }
}) });
} }
logger.info( logger.info(
`Starting looking for packages (.gbot, .gbtheme, .gbkb, .gbapp)...` `Starting looking for packages (.gbot, .gbtheme, .gbkb, .gbapp)...`
) );
paths.forEach(e => { paths.forEach(e => {
logger.info(`Looking in: ${e}...`) logger.info(`Looking in: ${e}...`);
doIt(e) doIt(e);
}) });
/** Deploys all .gbapp files first. */ /** Deploys all .gbapp files first. */
let appPackagesProcessed = 0 let appPackagesProcessed = 0;
gbappPackages.forEach(e => { gbappPackages.forEach(e => {
logger.info(`Deploying app: ${e}...`)
// Skips .gbapp inside deploy folder. // Skips .gbapp inside deploy folder.
if (!e.startsWith("deploy")) { if (!e.startsWith("deploy")) {
logger.info(`Deploying app: ${e}...`);
import(e) import(e)
.then(m => { .then(m => {
let p = new m.Package() let p = new m.Package();
p.loadPackage(core, core.sequelize) p.loadPackage(core, core.sequelize);
appPackages.push(p) appPackages.push(p);
logger.info(`App (.gbapp) deployed: ${e}.`) logger.info(`App (.gbapp) deployed: ${e}.`);
appPackagesProcessed++ appPackagesProcessed++;
}) })
.catch(err => { .catch(err => {
logger.error(`Error deploying App (.gbapp): ${e}: ${err}`) logger.error(`Error deploying App (.gbapp): ${e}: ${err}`);
appPackagesProcessed++ appPackagesProcessed++;
}) });
} else { } else {
appPackagesProcessed++ appPackagesProcessed++;
} }
}) });
WaitUntil() WaitUntil()
.interval(1000) .interval(1000)
.times(10) .times(10)
.condition(function (cb) { .condition(function(cb) {
logger.info(`Waiting for app package deployment...`) logger.info(`Waiting for app package deployment...`);
cb(appPackagesProcessed == gbappPackages.length) cb(appPackagesProcessed == gbappPackages.length);
}) })
.done(function (result) { .done(async result => {
logger.info(`App Package deployment done.`); logger.info(`App Package deployment done.`);
(async () => { await core.syncDatabaseStructure();
await core.syncDatabaseStructure()
})()
/** Deploys all .gbot files first. */ /** Deploys all .gbot files first. */
botPackages.forEach(e => { botPackages.forEach(e => {
logger.info(`Deploying bot: ${e}...`) logger.info(`Deploying bot: ${e}...`);
_this.deployBot(e) _this.deployBot(e);
logger.info(`Bot: ${e} deployed...`) logger.info(`Bot: ${e} deployed...`);
}) });
/** Then all remaining generalPackages are loaded. */ /** Then all remaining generalPackages are loaded. */
generalPackages = generalPackages.filter(p => !p.endsWith(".git"));
generalPackages.forEach(filename => { generalPackages.forEach(filename => {
let filenameOnly = Path.basename(filename) let filenameOnly = Path.basename(filename);
logger.info(`Deploying package: ${filename}...`) logger.info(`Deploying package: ${filename}...`);
/** Handles apps for general bots - .gbapp must stay out of deploy folder. */ /** Handles apps for general bots - .gbapp must stay out of deploy folder. */
@ -178,57 +178,54 @@ export class GBDeployer {
) { ) {
/** Themes for bots. */ /** Themes for bots. */
} else if (Path.extname(filename) === ".gbtheme") { } else if (Path.extname(filename) === ".gbtheme") {
server.use("/themes/" + filenameOnly, express.static(filename)) server.use("/themes/" + filenameOnly, express.static(filename));
logger.info( logger.info(
`Theme (.gbtheme) assets accessible at: ${"/themes/" + `Theme (.gbtheme) assets accessible at: ${"/themes/" +
filenameOnly}.` filenameOnly}.`
) );
/** Knowledge base for bots. */ /** Knowledge base for bots. */
} else if (Path.extname(filename) === ".gbkb") { } else if (Path.extname(filename) === ".gbkb") {
server.use( server.use(
"/kb/" + filenameOnly + "/subjects", "/kb/" + filenameOnly + "/subjects",
express.static(UrlJoin(filename, "subjects")) express.static(UrlJoin(filename, "subjects"))
) );
logger.info( logger.info(
`KB (.gbkb) assets accessible at: ${"/kb/" + filenameOnly}.` `KB (.gbkb) assets accessible at: ${"/kb/" + filenameOnly}.`
) );
} else if ( } else if (Path.extname(filename) === ".gbui") {
Path.extname(filename) === ".gbui" ||
filename.endsWith(".git")
) {
// Already Handled // Already Handled
} else { } else {
/** Unknown package format. */ /** Unknown package format. */
let err = new Error(`Package type not handled: ${filename}.`) let err = new Error(`Package type not handled: ${filename}.`);
reject(err) reject(err);
} }
totalPackages++ totalPackages++;
}) });
WaitUntil() WaitUntil()
.interval(100) .interval(100)
.times(5) .times(5)
.condition(function (cb) { .condition(function(cb) {
logger.info(`Waiting for package deployment...`) logger.info(`Waiting for package deployment...`);
cb(totalPackages == generalPackages.length) cb(totalPackages == generalPackages.length);
}) })
.done(function (result) { .done(function(result) {
if (botPackages.length === 0) { if (botPackages.length === 0) {
logger.info( logger.info(
"The server is running with no bot instances, at least one .gbot file must be deployed." "The server is running with no bot instances, at least one .gbot file must be deployed."
) );
} else { } else {
logger.info(`Package deployment done.`) logger.info(`Package deployment done.`);
} }
resolve() resolve();
}) });
}) });
} catch (err) { } catch (err) {
logger.error(err) logger.error(err);
reject(err) reject(err);
} }
}) });
} }
/** /**
@ -236,13 +233,13 @@ export class GBDeployer {
*/ */
async deployBot(localPath: string): Promise<IGBInstance> { async deployBot(localPath: string): Promise<IGBInstance> {
let packageType = Path.extname(localPath) let packageType = Path.extname(localPath);
let packageName = Path.basename(localPath) let packageName = Path.basename(localPath);
let instance = await this.importer.importIfNotExistsBotPackage( let instance = await this.importer.importIfNotExistsBotPackage(
packageName, packageName,
localPath localPath
) );
return instance return instance;
} }
async deployPackageToStorage( async deployPackageToStorage(
@ -252,7 +249,7 @@ export class GBDeployer {
return GuaribasPackage.create({ return GuaribasPackage.create({
packageName: packageName, packageName: packageName,
instanceId: instanceId instanceId: instanceId
}) });
} }
deployTheme(localPath: string) { deployTheme(localPath: string) {
@ -268,71 +265,86 @@ export class GBDeployer {
} }
async deployPackageFromLocalPath(localPath: string) { async deployPackageFromLocalPath(localPath: string) {
let packageType = Path.extname(localPath) let packageType = Path.extname(localPath);
switch (packageType) { switch (packageType) {
case ".gbot": case ".gbot":
return this.deployBot(localPath) return this.deployBot(localPath);
case ".gbtheme": case ".gbtheme":
return this.deployTheme(localPath) return this.deployTheme(localPath);
// PACKAGE: Put in package logic. // PACKAGE: Put in package logic.
case ".gbkb": case ".gbkb":
let service = new KBService(this.core.sequelize) let service = new KBService(this.core.sequelize);
return service.deployKb(this.core, this, localPath) return service.deployKb(this.core, this, localPath);
case ".gbui": case ".gbui":
break break;
default: default:
var err = GBError.create( var err = GBError.create(
`GuaribasBusinessError: Unknow package type: ${packageType}.` `GuaribasBusinessError: Unknow package type: ${packageType}.`
) );
Promise.reject(err) Promise.reject(err);
break break;
} }
} }
async undeployPackageFromLocalPath(instance: IGBInstance, localPath: string) { async undeployPackageFromLocalPath(instance: IGBInstance, localPath: string) {
let packageType = Path.extname(localPath) let packageType = Path.extname(localPath);
let packageName = Path.basename(localPath) let packageName = Path.basename(localPath);
let p = await this.getPackageByName(instance.instanceId, packageName) let p = await this.getPackageByName(instance.instanceId, packageName);
switch (packageType) { switch (packageType) {
case ".gbot": case ".gbot":
// TODO: this.undeployBot(packageName, localPath) // TODO: this.undeployBot(packageName, localPath)
break break;
case ".gbtheme": case ".gbtheme":
// TODO: this.undeployTheme(packageName, localPath) // TODO: this.undeployTheme(packageName, localPath)
break break;
case ".gbkb": case ".gbkb":
let service = new KBService(this.core.sequelize) let service = new KBService(this.core.sequelize);
return service.undeployKbFromStorage(instance, p.packageId) return service.undeployKbFromStorage(instance, this, p.packageId);
case ".gbui": case ".gbui":
break break;
default: default:
var err = GBError.create( var err = GBError.create(
`GuaribasBusinessError: Unknown package type: ${packageType}.` `GuaribasBusinessError: Unknown package type: ${packageType}.`
) );
Promise.reject(err) Promise.reject(err);
break break;
} }
} }
public async rebuildIndex(instance: GuaribasInstance) {
let search = new AzureSearch(
instance.searchKey,
instance.searchHost,
instance.searchIndex,
instance.searchIndexer
);
await search.deleteIndex();
let kbService = new KBService(this.core.sequelize);
await search.createIndex(
kbService.getSearchSchema(instance.searchIndex),
"gb"
);
}
async getPackageByName( async getPackageByName(
instanceId: number, instanceId: number,
packageName: string packageName: string
): Promise<GuaribasPackage> { ): Promise<GuaribasPackage> {
var where = { packageName: packageName, instanceId: instanceId } var where = { packageName: packageName, instanceId: instanceId };
return GuaribasPackage.findOne({ return GuaribasPackage.findOne({
where: where where: where
}) });
} }
/** /**
@ -341,15 +353,15 @@ export class GBDeployer {
* *
*/ */
async scanBootPackage() { async scanBootPackage() {
const deployFolder = "deploy" const deployFolder = "deploy";
let bootPackage = GBConfigService.get("BOOT_PACKAGE") let bootPackage = GBConfigService.get("BOOT_PACKAGE");
if (bootPackage === "none") { if (bootPackage === "none") {
return Promise.resolve(true) return Promise.resolve(true);
} else { } else {
return this.deployPackageFromLocalPath( return this.deployPackageFromLocalPath(
UrlJoin(deployFolder, bootPackage) UrlJoin(deployFolder, bootPackage)
) );
} }
} }
} }

View file

@ -30,13 +30,15 @@
| | | |
\*****************************************************************************/ \*****************************************************************************/
"use strict" "use strict";
const { TextPrompt } = require("botbuilder-dialogs") const { TextPrompt } = require("botbuilder-dialogs");
const UrlJoin = require("url-join") const UrlJoin = require("url-join");
const express = require("express") const express = require("express");
const logger = require("../../../src/logger") const logger = require("../../../src/logger");
const request = require('request-promise-native') const request = require("request-promise-native");
var crypto = require("crypto");
var AuthenticationContext = require("adal-node").AuthenticationContext;
import { import {
BotFrameworkAdapter, BotFrameworkAdapter,
@ -44,28 +46,32 @@ import {
ConversationState, ConversationState,
MemoryStorage, MemoryStorage,
UserState UserState
} from "botbuilder" } from "botbuilder";
import { GBCoreService } from "./GBCoreService" import { GBMinInstance, IGBPackage } from "botlib";
import { GBConversationalService } from "./GBConversationalService" import { GBAnalyticsPackage } from "../../analytics.gblib";
import { GBMinInstance, IGBPackage } from "botlib" import { GBCorePackage } from "../../core.gbapp";
import { GBAnalyticsPackage } from "../../analytics.gblib" import { GBKBPackage } from "../../kb.gbapp";
import { GBCorePackage } from "../../core.gbapp" import { GBDeployer } from "./GBDeployer";
import { GBKBPackage } from "../../kb.gbapp" import { GBSecurityPackage } from "../../security.gblib";
import { GBDeployer } from "./GBDeployer" import { GBAdminPackage } from "./../../admin.gbapp/index";
import { GBSecurityPackage } from "../../security.gblib" import { GBCustomerSatisfactionPackage } from "../../customer-satisfaction.gbapp";
import { GBAdminPackage } from "./../../admin.gbapp/index" import { GBWhatsappPackage } from "../../whatsapp.gblib";
import { GBCustomerSatisfactionPackage } from "../../customer-satisfaction.gbapp" import {
import { GBWhatsappPackage } from "../../whatsapp.gblib" IGBAdminService,
IGBCoreService,
IGBConversationalService
} from "botlib";
/** Minimal service layer for a bot. */ /** Minimal service layer for a bot. */
export class GBMinService { export class GBMinService {
core: GBCoreService core: IGBCoreService;
conversationalService: GBConversationalService conversationalService: IGBConversationalService;
deployer: GBDeployer adminService: IGBAdminService;
deployer: GBDeployer;
corePackage = "core.gbai" corePackage = "core.gbai";
/** /**
* Static initialization of minimal instance. * Static initialization of minimal instance.
@ -73,13 +79,15 @@ export class GBMinService {
* @param core Basic database services to identify instance, for example. * @param core Basic database services to identify instance, for example.
*/ */
constructor( constructor(
core: GBCoreService, core: IGBCoreService,
conversationalService: GBConversationalService, conversationalService: IGBConversationalService,
adminService: IGBAdminService,
deployer: GBDeployer deployer: GBDeployer
) { ) {
this.core = core this.core = core;
this.conversationalService = conversationalService this.conversationalService = conversationalService;
this.deployer = deployer this.adminService = adminService;
this.deployer = deployer;
} }
/** /**
@ -97,37 +105,34 @@ export class GBMinService {
server: any, server: any,
appPackages: Array<IGBPackage> appPackages: Array<IGBPackage>
): Promise<GBMinInstance> { ): Promise<GBMinInstance> {
// Serves default UI on root address '/'. // Serves default UI on root address '/'.
let uiPackage = "default.gbui" let uiPackage = "default.gbui";
server.use( server.use(
"/", "/",
express.static(UrlJoin(GBDeployer.deployFolder, uiPackage, "build")) express.static(UrlJoin(GBDeployer.deployFolder, uiPackage, "build"))
) );
// Loads all bot instances from storage and starting loading them. // Loads all bot instances from storage and starting loading them.
let instances = await this.core.loadInstances() let instances = await this.core.loadInstances();
Promise.all( Promise.all(
instances.map(async instance => { instances.map(async instance => {
// Gets the authorization key for each instance from Bot Service. // Gets the authorization key for each instance from Bot Service.
let webchatToken = await this.getWebchatToken(instance) let webchatToken = await this.getWebchatToken(instance);
// Serves the bot information object via HTTP so clients can get // Serves the bot information object via HTTP so clients can get
// instance information stored on server. // instance information stored on server.
server.get("/instances/:botId", (req, res) => { server.get("/instances/:botId", (req, res) => {
(async () => { (async () => {
// Returns the instance object to clients requesting bot info. // Returns the instance object to clients requesting bot info.
let botId = req.params.botId let botId = req.params.botId;
let instance = await this.core.loadInstance(botId) let instance = await this.core.loadInstance(botId);
if (instance) { if (instance) {
let speechToken = await this.getSTSToken(instance) let speechToken = await this.getSTSToken(instance);
res.send( res.send(
JSON.stringify({ JSON.stringify({
@ -138,30 +143,30 @@ export class GBMinService {
speechToken: speechToken, speechToken: speechToken,
conversationId: webchatToken.conversationId, conversationId: webchatToken.conversationId,
authenticatorTenant: instance.authenticatorTenant, authenticatorTenant: instance.authenticatorTenant,
authenticatorClientID: instance.authenticatorClientID authenticatorClientId: instance.authenticatorClientId
}) })
) );
} else { } else {
let error = `Instance not found: ${botId}.` let error = `Instance not found: ${botId}.`;
res.sendStatus(error) res.sendStatus(error);
logger.error(error) logger.error(error);
} }
})() })();
}) });
// Build bot adapter. // Build bot adapter.
var { min, adapter, conversationState } = await this.buildBotAdapter( var { min, adapter, conversationState } = await this.buildBotAdapter(
instance instance
) );
// Call the loadBot context.activity for all packages. // Call the loadBot context.activity for all packages.
this.invokeLoadBot(appPackages, min, server) this.invokeLoadBot(appPackages, min, server);
// Serves individual URL for each bot conversational interface... // Serves individual URL for each bot conversational interface...
let url = `/api/messages/${instance.botId}` let url = `/api/messages/${instance.botId}`;
server.post(url, async (req, res) => { server.post(url, async (req, res) => {
return this.receiver( return this.receiver(
adapter, adapter,
@ -171,20 +176,107 @@ export class GBMinService {
min, min,
instance, instance,
appPackages appPackages
) );
}) });
logger.info( logger.info(
`GeneralBots(${instance.engineName}) listening on: ${url}.` `GeneralBots(${instance.engineName}) listening on: ${url}.`
) );
// Serves individual URL for each bot user interface. // Serves individual URL for each bot user interface.
let uiUrl = `/${instance.botId}` let uiUrl = `/${instance.botId}`;
server.use( server.use(
uiUrl, uiUrl,
express.static(UrlJoin(GBDeployer.deployFolder, uiPackage, "build")) express.static(UrlJoin(GBDeployer.deployFolder, uiPackage, "build"))
) );
logger.info(`Bot UI ${uiPackage} accessible at: ${uiUrl}.`)
logger.info(`Bot UI ${uiPackage} accessible at: ${uiUrl}.`);
let state = `${instance.instanceId}${Math.floor(
Math.random() * 1000000000
)}`;
// Clients get redirected here in order to create an OAuth authorize url and redirect them to AAD.
// There they will authenticate and give their consent to allow this app access to
// some resource they own.
server.get(`/${min.instance.botId}/auth`, function(req, res) {
let authorizationUrl = UrlJoin(
min.instance.authenticatorAuthorityHostUrl,
min.instance.authenticatorTenant,
"/oauth2/authorize"
);
authorizationUrl = `${authorizationUrl}?response_type=code&client_id=${
min.instance.authenticatorClientId
}&redirect_uri=${min.instance.botServerUrl}/${
min.instance.botId
}/token`;
res.redirect(authorizationUrl);
});
// After consent is granted AAD redirects here. The ADAL library
// is invoked via the AuthenticationContext and retrieves an
// access token that can be used to access the user owned resource.
server.get(`/${min.instance.botId}/token`, async (req, res) => {
let state = await min.adminService.getValue(
min.instance.instanceId,
"AntiCSRFAttackState"
);
if (req.query.state != state) {
let msg =
"WARNING: state field was not provided as anti-CSRF token";
logger.error(msg);
throw new Error(msg);
}
var authenticationContext = new AuthenticationContext(
UrlJoin(
min.instance.authenticatorAuthorityHostUrl,
min.instance.authenticatorTenant
)
);
let resource = "https://graph.microsoft.com";
authenticationContext.acquireTokenWithAuthorizationCode(
req.query.code,
UrlJoin(instance.botServerUrl, min.instance.botId, "/token"),
resource,
instance.authenticatorClientId,
instance.authenticatorClientSecret,
async (err, token) => {
if (err) {
let msg = `Error acquiring token: ${err}`;
logger.error(msg);
res.send(msg);
} else {
await this.adminService.setValue(
instance.instanceId,
"refreshToken",
token.refreshToken
);
await this.adminService.setValue(
instance.instanceId,
"accessToken",
token.accessToken
);
await this.adminService.setValue(
instance.instanceId,
"expiresOn",
token.expiresOn.toString()
);
await this.adminService.setValue(
instance.instanceId,
"AntiCSRFAttackState",
null
);
res.redirect(min.instance.botServerUrl);
}
}
);
});
// Setups handlers. // Setups handlers.
// send: function (context.activity, next) { // send: function (context.activity, next) {
@ -201,32 +293,33 @@ export class GBMinService {
// ) // )
// next() // next()
}) })
) );
} }
private async buildBotAdapter(instance: any) { private async buildBotAdapter(instance: any) {
let adapter = new BotFrameworkAdapter({ let adapter = new BotFrameworkAdapter({
appId: instance.marketplaceId, appId: instance.marketplaceId,
appPassword: instance.marketplacePassword appPassword: instance.marketplacePassword
}) });
const storage = new MemoryStorage() const storage = new MemoryStorage();
const conversationState = new ConversationState(storage) const conversationState = new ConversationState(storage);
const userState = new UserState(storage) const userState = new UserState(storage);
adapter.use(new BotStateSet(conversationState, userState)) adapter.use(new BotStateSet(conversationState, userState));
// The minimal bot is built here. // The minimal bot is built here.
let min = new GBMinInstance() let min = new GBMinInstance();
min.botId = instance.botId min.botId = instance.botId;
min.bot = adapter min.bot = adapter;
min.userState = userState min.userState = userState;
min.core = this.core min.core = this.core;
min.conversationalService = this.conversationalService min.conversationalService = this.conversationalService;
min.instance = await this.core.loadInstance(min.botId) min.adminService = this.adminService;
min.dialogs.add("textPrompt", new TextPrompt()) min.instance = await this.core.loadInstance(min.botId);
min.dialogs.add("textPrompt", new TextPrompt());
return { min, adapter, conversationState } return { min, adapter, conversationState };
} }
private invokeLoadBot(appPackages: any[], min: any, server: any) { private invokeLoadBot(appPackages: any[], min: any, server: any) {
@ -244,19 +337,18 @@ export class GBMinService {
GBCustomerSatisfactionPackage, GBCustomerSatisfactionPackage,
GBWhatsappPackage GBWhatsappPackage
].forEach(sysPackage => { ].forEach(sysPackage => {
logger.info(`Loading sys package: ${sysPackage.name}...`) let p = Object.create(sysPackage.prototype) as IGBPackage;
let p = Object.create(sysPackage.prototype) as IGBPackage p.loadBot(min);
p.loadBot(min) e.sysPackages.push(p);
e.sysPackages.push(p)
if (sysPackage.name === "GBWhatsappPackage") { if (sysPackage.name === "GBWhatsappPackage") {
let url = "/instances/:botId/whatsapp" let url = "/instances/:botId/whatsapp";
server.post(url, (req, res) => { server.post(url, (req, res) => {
p["channel"].received(req, res) p["channel"].received(req, res);
}) });
} }
}, this) }, this);
e.loadBot(min) e.loadBot(min);
}, this) }, this);
} }
/** /**
@ -272,12 +364,11 @@ export class GBMinService {
appPackages: any[] appPackages: any[]
) { ) {
return adapter.processActivity(req, res, async context => { return adapter.processActivity(req, res, async context => {
try { try {
const state = conversationState.get(context) const state = conversationState.get(context);
const dc = min.dialogs.createContext(context, state) const dc = min.dialogs.createContext(context, state);
dc.context.activity.locale = "en-US" dc.context.activity.locale = "en-US";
const user = min.userState.get(dc.context) const user = min.userState.get(dc.context);
if (!user.loaded) { if (!user.loaded) {
await min.conversationalService.sendEvent(dc, "loadInstance", { await min.conversationalService.sendEvent(dc, "loadInstance", {
@ -285,99 +376,95 @@ export class GBMinService {
botId: instance.botId, botId: instance.botId,
theme: instance.theme, theme: instance.theme,
secret: instance.webchatKey secret: instance.webchatKey
}) });
user.loaded = true user.loaded = true;
user.subjects = [] user.subjects = [];
} }
logger.info( logger.info(
`[User]: ${context.activity.type}, ChannelID: ${ `User>: ${context.activity.text} (${context.activity.type}, ${
context.activity.channelId context.activity.name
} Text: ${context.activity.text}.` }, ${context.activity.channelId}, {context.activity.value})`
) );
if ( if (
context.activity.type === "conversationUpdate" && context.activity.type === "conversationUpdate" &&
context.activity.membersAdded.length > 0 context.activity.membersAdded.length > 0
) { ) {
let member = context.activity.membersAdded[0];
let member = context.activity.membersAdded[0]
if (member.name === "GeneralBots") { if (member.name === "GeneralBots") {
logger.info(`Bot added to conversation, starting chat...`) logger.info(`Bot added to conversation, starting chat...`);
appPackages.forEach(e => { appPackages.forEach(e => {
e.onNewSession(min, dc) e.onNewSession(min, dc);
}) });
// Processes the root dialog. // Processes the root dialog.
await dc.begin("/") await dc.begin("/");
} else { } else {
logger.info(`Member added to conversation: ${member.name}`) logger.info(`Member added to conversation: ${member.name}`);
} }
// Processes messages. // Processes messages.
} else if (context.activity.type === "message") { } else if (context.activity.type === "message") {
// Checks for /admin request. // Checks for /admin request.
if (context.activity.text === "admin") { if (context.activity.text === "admin") {
await dc.begin("/admin") await dc.begin("/admin");
// Checks for /menu JSON signature. // Checks for /menu JSON signature.
} else if (context.activity.text.startsWith('{"title"')) {
} else if (context.activity.text.startsWith("{\"title\"")) { await dc.begin("/menu", {
await dc.begin("/menu", { data: JSON.parse(context.activity.text) }) data: JSON.parse(context.activity.text)
});
// Otherwise, continue to the active dialog in the stack. // Otherwise, continue to the active dialog in the stack.
} else { } else {
if (dc.activeDialog) { if (dc.activeDialog) {
await dc.continue() await dc.continue();
} else { } else {
await dc.begin("/answer", {query: context.activity.text}) await dc.begin("/answer", { query: context.activity.text });
} }
} }
// Processes events. // Processes events.
} else if (context.activity.type === "event") { } else if (context.activity.type === "event") {
// Empties dialog stack before going to the target. // Empties dialog stack before going to the target.
await dc.endAll() await dc.endAll();
if (context.activity.name === "whoAmI") { if (context.activity.name === "whoAmI") {
await dc.begin("/whoAmI") await dc.begin("/whoAmI");
} else if (context.activity.name === "showSubjects") { } else if (context.activity.name === "showSubjects") {
await dc.begin("/menu") await dc.begin("/menu");
} else if (context.activity.name === "giveFeedback") { } else if (context.activity.name === "giveFeedback") {
await dc.begin("/feedback", { await dc.begin("/feedback", {
fromMenu: true fromMenu: true
}) });
} else if (context.activity.name === "showFAQ") { } else if (context.activity.name === "showFAQ") {
await dc.begin("/faq") await dc.begin("/faq");
} else if (context.activity.name === "answerEvent") { } else if (context.activity.name === "answerEvent") {
await dc.begin("/answerEvent", { await dc.begin("/answerEvent", {
questionId: (context.activity as any).data, questionId: (context.activity as any).data,
fromFaq: true fromFaq: true
}) });
} else if (context.activity.name === "quality") { } else if (context.activity.name === "quality") {
await dc.begin("/quality", { score: (context.activity as any).data }) await dc.begin("/quality", {
score: (context.activity as any).data
});
} else if (context.activity.name === "updateToken") { } else if (context.activity.name === "updateToken") {
let token = (context.activity as any).data let token = (context.activity as any).data;
await dc.begin("/adminUpdateToken", { token: token }) await dc.begin("/adminUpdateToken", { token: token });
} else { } else {
await dc.continue() await dc.continue();
} }
} }
} catch (error) { } catch (error) {
let msg = `Error in main activity: ${error.message} ${error.stack? error.stack:""}` let msg = `Error in main activity: ${error.message} ${
logger.error(msg) error.stack ? error.stack : ""
}`;
logger.error(msg);
} }
}) });
} }
/** /**
@ -393,15 +480,15 @@ export class GBMinService {
headers: { headers: {
Authorization: `Bearer ${instance.webchatKey}` Authorization: `Bearer ${instance.webchatKey}`
} }
} };
try { try {
let json = await request(options) let json = await request(options);
return Promise.resolve(JSON.parse(json)) return Promise.resolve(JSON.parse(json));
} catch (error) { } catch (error) {
let msg = `Error calling Direct Line client, verify Bot endpoint on the cloud. Error is: ${error}.` let msg = `Error calling Direct Line client, verify Bot endpoint on the cloud. Error is: ${error}.`;
logger.error(msg) logger.error(msg);
return Promise.reject(msg) return Promise.reject(msg);
} }
} }
@ -420,14 +507,14 @@ export class GBMinService {
headers: { headers: {
"Ocp-Apim-Subscription-Key": instance.speechKey "Ocp-Apim-Subscription-Key": instance.speechKey
} }
} };
try { try {
return await request(options) return await request(options);
} catch (error) { } catch (error) {
let msg = `Error calling Speech to Text client. Error is: ${error}.` let msg = `Error calling Speech to Text client. Error is: ${error}.`;
logger.error(msg) logger.error(msg);
return Promise.reject(msg) return Promise.reject(msg);
} }
} }
} }

View file

@ -149,7 +149,7 @@ class GBUIApp extends React.Component {
let graphScopes = ["Directory.AccessAsUser.All"]; let graphScopes = ["Directory.AccessAsUser.All"];
let userAgentApplication = new UserAgentApplication( let userAgentApplication = new UserAgentApplication(
this.state.instanceClient.authenticatorClientID, this.state.instanceClient.authenticatorClientId,
authority, authority,
function(errorDesc, token, error, tokenType) { function(errorDesc, token, error, tokenType) {
if (error) { if (error) {

View file

@ -58,7 +58,7 @@ class GBLoginPlayer extends React.Component {
let graphScopes = ["Directory.AccessAsUser.All"]; let graphScopes = ["Directory.AccessAsUser.All"];
let userAgentApplication = new UserAgentApplication( let userAgentApplication = new UserAgentApplication(
this.state.login.authenticatorClientID, this.state.login.authenticatorClientId,
authority, authority,
function (errorDesc, token, error, tokenType) { function (errorDesc, token, error, tokenType) {
if (error) { if (error) {

View file

@ -119,14 +119,14 @@ class GBMarkdownPlayer extends Component {
} }
if (this.state.prevId) { if (this.state.prevId) {
prev = <a style={{ color: 'blue' }} prev = <a style={{ color: 'blue', cursor: 'pointer' }}
onPress={() => this.sendAnswer(this.state.prevId)}> onClick={() => this.sendAnswer(this.state.prevId)}>
Back Back
</a> </a>
} }
if (this.state.nextId) { if (this.state.nextId) {
next = <a style={{ color: 'blue' }} next = <a style={{ color: 'blue', cursor: 'pointer' }}
onPress={() => this.sendAnswer(this.state.nextId)}> onClick={() => this.sendAnswer(this.state.nextId)}>
Next Next
</a> </a>
} }

View file

@ -74,6 +74,8 @@ export class AskDialog extends IGBDialog {
dc, dc,
answer answer
); );
await dc.replace("/ask", { isReturning: true });
} }
}]) }])

View file

@ -442,7 +442,7 @@ export class KBService {
} }
async sendAnswer(conversationalService: IGBConversationalService, async sendAnswer(conversationalService: IGBConversationalService,
dc: any, answer: GuaribasAnswer): Promise<any> { dc: any, answer: GuaribasAnswer) {
if (answer.content.endsWith('.mp4')) { if (answer.content.endsWith('.mp4')) {
await conversationalService.sendEvent(dc, "play", { await conversationalService.sendEvent(dc, "play", {
@ -560,6 +560,7 @@ export class KBService {
async undeployKbFromStorage( async undeployKbFromStorage(
instance: IGBInstance, instance: IGBInstance,
deployer: GBDeployer,
packageId: number packageId: number
) { ) {
@ -576,8 +577,7 @@ export class KBService {
where: { instanceId: instance.instanceId, packageId: packageId } where: { instanceId: instance.instanceId, packageId: packageId }
}) })
return Promise.resolve() await deployer.rebuildIndex(instance)
} }
/** /**
@ -599,6 +599,8 @@ export class KBService {
instance.instanceId, instance.instanceId,
packageName) packageName)
await this.importKbPackage(localPath, p, instance) await this.importKbPackage(localPath, p, instance)
deployer.rebuildIndex(instance)
logger.info(`[GBDeployer] Finished import of ${localPath}`) logger.info(`[GBDeployer] Finished import of ${localPath}`)
} }
} }

View file

@ -48,6 +48,8 @@ export class GBSecurityPackage implements IGBPackage {
GuaribasUser, GuaribasUser,
GuaribasUserGroup GuaribasUserGroup
]) ])
core
} }
unloadPackage(core: IGBCoreService): void { unloadPackage(core: IGBCoreService): void {

View file

@ -1,6 +1,6 @@
{ {
"name": "botserver", "name": "botserver",
"version": "0.1.2", "version": "0.1.3",
"description": "General Bot Community Edition open-core server.", "description": "General Bot Community Edition open-core server.",
"main": "./src/app.ts", "main": "./src/app.ts",
"homepage": "http://www.generalbot.com", "homepage": "http://www.generalbot.com",
@ -30,6 +30,8 @@
"node": ">=8.9.4" "node": ">=8.9.4"
}, },
"dependencies": { "dependencies": {
"@microsoft/microsoft-graph-client": "^1.3.0",
"adal-node": "^0.1.28",
"async": "^2.6.1", "async": "^2.6.1",
"async-promises": "^0.2.1", "async-promises": "^0.2.1",
"body-parser": "^1.18.3", "body-parser": "^1.18.3",
@ -39,7 +41,7 @@
"botbuilder-choices": "^4.0.0-preview1.2", "botbuilder-choices": "^4.0.0-preview1.2",
"botbuilder-dialogs": "^4.0.0-preview1.2", "botbuilder-dialogs": "^4.0.0-preview1.2",
"botbuilder-prompts": "^4.0.0-preview1.2", "botbuilder-prompts": "^4.0.0-preview1.2",
"botlib": "^0.1.0", "botlib": "^0.1.1",
"chokidar": "^2.0.4", "chokidar": "^2.0.4",
"csv-parse": "^3.0.0", "csv-parse": "^3.0.0",
"dotenv-extended": "^2.3.0", "dotenv-extended": "^2.3.0",

View file

@ -31,100 +31,123 @@
| | | |
\*****************************************************************************/ \*****************************************************************************/
"use strict" "use strict";
const UrlJoin = require("url-join") const UrlJoin = require("url-join");
const logger = require("./logger") const logger = require("./logger");
const express = require("express") const express = require("express");
const bodyParser = require("body-parser") const bodyParser = require("body-parser");
const MicrosoftGraph = require("@microsoft/microsoft-graph-client");
import { Sequelize } from "sequelize-typescript" import { Sequelize } from "sequelize-typescript";
import { GBConfigService } from "../deploy/core.gbapp/services/GBConfigService" import { GBConfigService } from "../deploy/core.gbapp/services/GBConfigService";
import { GBConversationalService } from "../deploy/core.gbapp/services/GBConversationalService" import { GBConversationalService } from "../deploy/core.gbapp/services/GBConversationalService";
import { GBMinService } from "../deploy/core.gbapp/services/GBMinService" import { GBMinService } from "../deploy/core.gbapp/services/GBMinService";
import { GBDeployer } from "../deploy/core.gbapp/services/GBDeployer" import { GBDeployer } from "../deploy/core.gbapp/services/GBDeployer";
import { GBWhatsappPackage } from './../deploy/whatsapp.gblib/index' import { GBWhatsappPackage } from "./../deploy/whatsapp.gblib/index";
import { GBCoreService } from "../deploy/core.gbapp/services/GBCoreService" import { GBCoreService } from "../deploy/core.gbapp/services/GBCoreService";
import { GBImporter } from "../deploy/core.gbapp/services/GBImporter" import { GBImporter } from "../deploy/core.gbapp/services/GBImporter";
import { GBAnalyticsPackage } from "../deploy/analytics.gblib" import { GBAnalyticsPackage } from "../deploy/analytics.gblib";
import { GBCorePackage } from "../deploy/core.gbapp" import { GBCorePackage } from "../deploy/core.gbapp";
import { GBKBPackage } from '../deploy/kb.gbapp' import { GBKBPackage } from "../deploy/kb.gbapp";
import { GBSecurityPackage } from '../deploy/security.gblib' import { GBSecurityPackage } from "../deploy/security.gblib";
import { GBAdminPackage } from '../deploy/admin.gbapp/index' import { GBAdminPackage } from "../deploy/admin.gbapp/index";
import { GBCustomerSatisfactionPackage } from "../deploy/customer-satisfaction.gbapp" import { GBCustomerSatisfactionPackage } from "../deploy/customer-satisfaction.gbapp";
import { IGBPackage } from 'botlib' import { IGBPackage } from "botlib";
import { GBAdminService } from "../deploy/admin.gbapp/services/GBAdminService";
let appPackages = new Array<IGBPackage>() let appPackages = new Array<IGBPackage>();
/** /**
* General Bots open-core entry point. * General Bots open-core entry point.
*/ */
export class GBServer { export class GBServer {
/** Program entry-point. */ /** Program entry-point. */
static run() { static run() {
// Creates a basic HTTP server that will serve several URL, one for each // Creates a basic HTTP server that will serve several URL, one for each
// bot instance. This allows the same server to attend multiple Bot on // bot instance. This allows the same server to attend multiple Bot on
// the Marketplace until GB get serverless. // the Marketplace until GB get serverless.
let port = process.env.port || process.env.PORT || 4242 let port = process.env.port || process.env.PORT || 4242;
logger.info(`The Bot Server is in STARTING mode...`) logger.info(`The Bot Server is in STARTING mode...`);
let server = express() let server = express();
server.use(bodyParser.json()) // to support JSON-encoded bodies server.use(bodyParser.json()); // to support JSON-encoded bodies
server.use(bodyParser.urlencoded({ // to support URL-encoded bodies server.use(
extended: true bodyParser.urlencoded({
})) // to support URL-encoded bodies
extended: true
})
);
server.listen(port, () => { server.listen(port, () => {
(async () => { (async () => {
try { try {
logger.info(`Accepting connections on ${port}...`);
logger.info(`Accepting connections on ${port}...`)
// Reads basic configuration, initialize minimal services. // Reads basic configuration, initialize minimal services.
GBConfigService.init() GBConfigService.init();
let core = new GBCoreService() let core = new GBCoreService();
await core.initDatabase() await core.initDatabase();
// Boot a bot package if any. // Boot a bot package if any.
logger.info(`Starting instances...`) logger.info(`Starting instances...`);
let deployer = new GBDeployer(core, new GBImporter(core)) let deployer = new GBDeployer(core, new GBImporter(core));
// Build a minimal bot instance for each .gbot deployment. // Build a minimal bot instance for each .gbot deployment.
let conversationalService = new GBConversationalService(core) let conversationalService = new GBConversationalService(core);
let minService = new GBMinService(core, conversationalService, deployer); let adminService = new GBAdminService(core);
let password = GBConfigService.get("ADMIN_PASS");
if (!GBAdminService.StrongRegex.test(password)) {
throw new Error(
"STOP: Please, define a really strong password in ADMIN_PASS environment variable before running the server."
);
}
let minService = new GBMinService(
core,
conversationalService,
adminService,
deployer
);
// NOTE: the semicolon is necessary before this line. // NOTE: the semicolon is necessary before this line.
// Loads all system packages. // Loads all system packages.
[GBAdminPackage, GBAnalyticsPackage, GBCorePackage, GBSecurityPackage, [
GBKBPackage, GBCustomerSatisfactionPackage, GBWhatsappPackage].forEach(e => { GBAdminPackage,
logger.info(`Loading sys package: ${e.name}...`) GBAnalyticsPackage,
let p = Object.create(e.prototype) as IGBPackage GBCorePackage,
p.loadPackage(core, core.sequelize) GBSecurityPackage,
}) GBKBPackage,
GBCustomerSatisfactionPackage,
GBWhatsappPackage
].forEach(e => {
logger.info(`Loading sys package: ${e.name}...`);
let p = Object.create(e.prototype) as IGBPackage;
p.loadPackage(core, core.sequelize);
});
await deployer.deployPackages(core, server, appPackages) logger.info(`Deploying packages.`);
logger.info(`The Bot Server is in RUNNING mode...`) await deployer.deployPackages(core, server, appPackages);
logger.info(`Building minimal instances.`);
await minService.buildMin(server, appPackages);
let instance = await minService.buildMin(server, appPackages) logger.info(`All instances are now loaded and available.`);
logger.info(`Instance loaded: ${instance.botId}...`) logger.info(`The Bot Server is in RUNNING mode...`);
return core return core;
} catch (err) { } catch (err) {
logger.info(err) logger.info(`STOP: ${err} ${err.stack ? err.stack : ""}`);
} }
})();
})() });
})
} }
} }
// First line to run. // First line to run.
GBServer.run() GBServer.run();