* 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:
parent
7375f179b2
commit
3832f27451
19 changed files with 678 additions and 461 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
21
deploy/admin.gbapp/strings.ts
Normal file
21
deploy/admin.gbapp/strings.ts
Normal 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}.`
|
||||||
|
}
|
||||||
|
};
|
|
@ -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;
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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)
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,6 +74,8 @@ export class AskDialog extends IGBDialog {
|
||||||
dc,
|
dc,
|
||||||
answer
|
answer
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await dc.replace("/ask", { isReturning: true });
|
||||||
}
|
}
|
||||||
}])
|
}])
|
||||||
|
|
||||||
|
|
|
@ -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}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,8 @@ export class GBSecurityPackage implements IGBPackage {
|
||||||
GuaribasUser,
|
GuaribasUser,
|
||||||
GuaribasUserGroup
|
GuaribasUserGroup
|
||||||
])
|
])
|
||||||
|
|
||||||
|
core
|
||||||
}
|
}
|
||||||
|
|
||||||
unloadPackage(core: IGBCoreService): void {
|
unloadPackage(core: IGBCoreService): void {
|
||||||
|
|
|
@ -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",
|
||||||
|
|
135
src/app.ts
135
src/app.ts
|
@ -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();
|
||||||
|
|
Loading…
Add table
Reference in a new issue