/*****************************************************************************\ | ( )_ _ | | _ _ _ __ _ _ __ ___ ___ _ _ | ,_)(_) ___ ___ _ | | ( '_`\ ( '__)/'_` ) /'_ `\/' _ ` _ `\ /'_` )| | | |/',__)/' v `\ /'_`\ | | | (_) )| | ( (_| |( (_) || ( ) ( ) |( (_| || |_ | |\__, \| (˅) |( (_) ) | | | ,__/'(_) `\__,_)`\__ |(_) (_) (_)`\__,_)`\__)(_)(____/(_) (_)`\___/' | | | | ( )_) | | | (_) \___/' | | | | General Bots Copyright (c) Pragmatismo.io. All rights reserved. | | Licensed under the AGPL-3.0. | | | | According to our dual licensing model, this program can be used either | | under the terms of the GNU Affero General Public License, version 3, | | or under a proprietary license. | | | | The texts of the GNU Affero General Public License with an additional | | permission and of our proprietary license can be found at and | | in the LICENSE file you have received along with this program. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY, without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | "General Bots" is a registered trademark of Pragmatismo.io. | | The licensing of the program under the AGPLv3 does not imply a | | trademark license. Therefore any rights, title and interest in | | our trademarks remain entirely with us. | | | \*****************************************************************************/ 'use strict'; import { TurnContext, BotAdapter } from 'botbuilder'; import { WaterfallStepContext, WaterfallDialog } from 'botbuilder-dialogs'; import { GBLog, GBMinInstance } from 'botlib'; import * as request from 'request-promise-native'; import urlJoin = require('url-join'); import { GBAdminService } from '../../admin.gbapp/services/GBAdminService'; import { AzureDeployerService } from '../../azuredeployer.gbapp/services/AzureDeployerService'; import { GBDeployer } from './GBDeployer'; import { CollectionUtil } from 'pragmatismo-io-framework'; import { Messages } from '../strings'; import { GBServer } from '../../../src/app'; import { SecService } from '../../security.gbapp/services/SecService'; const request = require('request-promise-native'); const MicrosoftGraph = require('@microsoft/microsoft-graph-client'); /** * @fileoverview General Bots server core. */ /** * BASIC system class for extra manipulation of bot behaviour. */ class SysClass { public min: GBMinInstance; private readonly deployer: GBDeployer; constructor(min: GBMinInstance, deployer: GBDeployer) { this.min = min; this.deployer = deployer; } public async getFileContents(url) { const options = { url: url, method: 'GET', encoding: 'binary' }; const res = await request(options); return Buffer.from(res, 'binary').toString(); } public async getRandomId() { return GBAdminService.getRndReadableIdentifier().substr(5); } public async getStock(symbol) { var options = { uri: `http://live-nse.herokuapp.com/?symbol=${symbol}` }; let data = await request.get(options); return data; } public async gotoDialog(from: string, dialogName: string) { let sec = new SecService(); let user = await sec.getUserFromSystemId(from); if (!user) { user = await sec.ensureUser(this.min.instance.instanceId, from, from, null, 'whatsapp', 'from'); } await sec.updateUserHearOnDialog(user.userId, dialogName); } public async wait(seconds: number) { // tslint:disable-next-line no-string-based-set-timeout GBLog.info(`BASIC: Talking to a specific user (TALK TO).`); const timeout = async (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); await timeout(seconds * 1000); } public async talkTo(mobile: any, message: string) { GBLog.info(`BASIC: Talking '${message}' to a specific user (${mobile}) (TALK TO). `); await this.min.conversationalService.sendMarkdownToMobile(this.min, null, mobile, message); } public async sendSmsTo(mobile, message) { await this.min.conversationalService.sendSms(this.min, mobile, message); } public async set(file: string, address: string, value: any): Promise { GBLog.info(`BASIC: Defining '${address}' in '${file}' to '${value}' (SET). `); let token = await this.min.adminService.acquireElevatedToken(this.min.instance.instanceId); let siteId = process.env.STORAGE_SITE_ID; let libraryId = process.env.STORAGE_LIBRARY; let client = MicrosoftGraph.Client.init({ authProvider: done => { done(null, token); } }); const botId = this.min.instance.botId; const path = `/${botId}.gbai/${botId}.gbdata`; address = address.indexOf(':') !== -1 ? address : address + ":" + address; let res = await client .api(`https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${libraryId}/drive/root:${path}:/children`) .get(); let document = res.value.filter(m => { return m.name.toLowerCase() === file.toLowerCase(); }); if (!document || document.length === 0) { throw `File '${file}' specified on save GBasic command SET not found. Check the file extension (.xlsx) and the associated .gbdata/.gbdialog.`; } let body = { values: [[]] }; body.values[0][0] = value; let sheets = await client .api( `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${libraryId}/drive/items/${document[0].id}/workbook/worksheets` ) .get(); await client .api( `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${libraryId}/drive/items/${document[0].id}/workbook/worksheets('${sheets.value[0].name}')/range(address='${address}')` ) .patch(body); } public async save(file: string, ...args): Promise { GBLog.info(`BASIC: Saving '${file}' (SAVE). Args: ${args.join(',')}.`); let token = await this.min.adminService.acquireElevatedToken(this.min.instance.instanceId); let siteId = process.env.STORAGE_SITE_ID; let libraryId = process.env.STORAGE_LIBRARY; let client = MicrosoftGraph.Client.init({ authProvider: done => { done(null, token); } }); const botId = this.min.instance.botId; const path = `/${botId}.gbai/${botId}.gbdata`; let res = await client .api(`https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${libraryId}/drive/root:${path}:/children`) .get(); let document = res.value.filter(m => { return m.name.toLowerCase() === file.toLowerCase(); }); await client .api( `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${libraryId}/drive/items/${document[0].id}/workbook/worksheets('Sheet1')/range(address='A2:Z2')/insert` ) .post({}); if (!document || document.length === 0) { throw `File '${file}' specified on save GBasic command SAVE not found. Check the .gbdata or the .gbdialog associated.`; } if (args.length > 128) { throw `File '${file}' has a SAVE call with more than 128 arguments. Check the .gbdialog associated.`; } let body = { values: [[]] }; for (let index = 0; index < 128; index++) { body.values[0][index] = args[index]; } let res2 = await client .api( `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${libraryId}/drive/items/${document[0].id}/workbook/worksheets('Sheet1')/range(address='A2:DX2')` ) .patch(body); } public async get(file: string, address: string): Promise { let token = await this.min.adminService.acquireElevatedToken(this.min.instance.instanceId); let client = MicrosoftGraph.Client.init({ authProvider: done => { done(null, token); } }); let siteId = process.env.STORAGE_SITE_ID; let libraryId = process.env.STORAGE_LIBRARY; const botId = this.min.instance.botId; const path = `/${botId}.gbai/${botId}.gbdata`; let res = await client .api(`https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${libraryId}/drive/root:${path}:/children`) .get(); // Performs validation. let document = res.value.filter(m => { return m.name.toLowerCase() === file.toLowerCase(); }); if (!document || document.length === 0) { throw `File '${file}' specified on save GBasic command GET not found. Check the .gbdata or the .gbdialog associated.`; } // Creates workbook session that will be discarded. let results = await client .api( `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${libraryId}/drive/items/${document[0].id}/workbook/worksheets('Sheet1')/range(address='${address}')` ) .get(); let val = results.text[0][0]; GBLog.info(`BASIC: Getting '${file}' (GET). Value= ${val}.`); return val; } public async find(file: string, ...args): Promise { let token = await this.min.adminService.acquireElevatedToken(this.min.instance.instanceId); let client = MicrosoftGraph.Client.init({ authProvider: done => { done(null, token); } }); let siteId = process.env.STORAGE_SITE_ID; let libraryId = process.env.STORAGE_LIBRARY; const botId = this.min.instance.botId; const path = `/${botId}.gbai/${botId}.gbdata`; let res = await client .api(`https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${libraryId}/drive/root:${path}:/children`) .get(); // Performs validation. let document = res.value.filter(m => { return m.name.toLowerCase() === file.toLowerCase(); }); if (!document || document.length === 0) { throw `File '${file}' specified on save GBasic command FIND not found. Check the .gbdata or the .gbdialog associated.`; } if (args.length > 1) { throw `File '${file}' has a FIND call with more than 1 arguments. Check the .gbdialog associated.`; } // Creates workbook session that will be discarded. const filter = args[0].split('='); const columnName = filter[0]; const value = filter[1]; let results = await client .api( `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${libraryId}/drive/items/${document[0].id}/workbook/worksheets('Sheet1')/range(address='A1:Z100')` ) .get(); let columnIndex = 0; const header = results.text[0]; for (; columnIndex < header.length; columnIndex++) { if (header[columnIndex].toLowerCase() === columnName.toLowerCase()) { break; } } // As BASIC uses arrays starting with 1 (one) as index, // a ghost element is added at 0 (zero) position. let array = []; array.push({ 'this is a base 1': 'array' }); let foundIndex = 0; for (; foundIndex < results.text.length; foundIndex++) { // Filter results action. if (results.text[foundIndex][columnIndex].toLowerCase() === value.toLowerCase()) { let output = {}; const row = results.text[foundIndex]; for (let colIndex = 0; colIndex < row.length; colIndex++) { output[header[colIndex]] = row[colIndex]; } output['line'] = foundIndex + 1; array.push(output); } } if (array.length === 1) { GBLog.info(`BASIC: FIND the data set is empty.`); return null; } else if (array.length === 2) { GBLog.info(`BASIC: FIND single result: ${array[0]}.`); return array[1]; } else { GBLog.info(`BASIC: FIND multiple result count: ${array.length}.`); return array; } } /** * * folder = CREATE FOLDER "notes\01" * * * @param name Folder name containing tree separated by slash. * * */ public async createFolder(name: string) { let token = await this.min.adminService.acquireElevatedToken(this.min.instance.instanceId); let siteId = process.env.STORAGE_SITE_ID; let libraryId = process.env.STORAGE_LIBRARY; const botId = this.min.instance.botId; const path = urlJoin(`/${botId}.gbai/${botId}.gbdata`, name); return new Promise((resolve, reject) => { let client = MicrosoftGraph.Client.init({ authProvider: done => { done(null, token); } }); const body = { "name": name, "folder": {}, "@microsoft.graph.conflictBehavior": "rename" } client.api(`https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${libraryId}/drive/root:/${path}:/children`) .post(body, (err, res) => { if (err) { reject(err) } else { resolve(res); } }); }); } /** * * folder = CREATE FOLDER "notes\10" * SHARE FOLDER folder, "nome@domain.com", "E-mail message" * */ public async shareFolder(folderReference, email: string, message: string) { let token = await this.min.adminService.acquireElevatedToken(this.min.instance.instanceId); const driveId = folderReference.parentReference.driveId; const itemId = folderReference.id; return new Promise((resolve, reject) => { let client = MicrosoftGraph.Client.init({ authProvider: done => { done(null, token); } }); const body = { "recipients": [ { "email": email } ], "message": message, "requireSignIn": true, "sendInvitation": true, "roles": ["write"] }; client.api(`https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/invite`) .post(body, (err, res) => { if (err) { reject(err) } else { resolve(res); } }); }); } public async copyFile(src, dst) { // let token = await this.min.adminService.acquireElevatedToken(this.min.instance.instanceId); // let siteId = process.env.STORAGE_SITE_ID; // let libraryId = process.env.STORAGE_LIBRARY; // const botId = this.min.instance.botId; // const path = urlJoin(`/${botId}.gbai/${botId}.gbdata`, name); // let client = MicrosoftGraph.Client.init({ // authProvider: done => { // done(null, token); // } // }); // const body = // { // "parentReference": { driveId: gbaiDest.parentReference.driveId, id: gbaiDest.id }, // "name": `${botName}.${kind}` // } // const packageName = `${templateName.split('.')[0]}.${kind}`; // try { // const src = await client.api( // `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${libraryId}/drive/root:/${source}`) // .get(); // return await client.api( // `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${libraryId}/drive/items/${src.id}/copy`) // .post(body); // } catch (error) { // if (error.code === "itemNotFound") { // } else if (error.code === "nameAlreadyExists") { // let src = await client.api( // `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${libraryId}/drive/root:/${templateName}/${packageName}:/children`) // .get(); // const dstName = `${botName}.gbai/${botName}.${kind}`; // let dst = await client.api(`https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${libraryId}/drive/root:/${dstName}`) // .get(); // await CollectionUtil.asyncForEach(src.value, async item => { // const body = // { // "parentReference": { driveId: dst.parentReference.driveId, id: dst.id } // } // await client.api( // `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${templateId}/drive/items/${item.id}/copy`) // .post(body); // }); // } // else { // GBLog.error(error); // throw error; // } // } } public generatePassword() { return GBAdminService.getRndPassword(); } public async createABotFarmUsing( botId: string, username: string, password: string, location: string, nlpAuthoringKey: string, appId: string, appPassword: string, subscriptionId: string ) { const service = new AzureDeployerService(this.deployer); await service.deployToCloud( botId, username, password, location, nlpAuthoringKey, appId, appPassword, subscriptionId ); } /** * Generic function to call any REST API. */ public async sendEmail(to, subject, body) { // tslint:disable-next-line:no-console GBLog.info(`[E-mail]: to:${to}, subject: ${subject}, body: ${body}.`); } /** * Generic function to call any REST API. */ public async httpGet(url: string) { const options = { uri: url }; let result = await request.get(options); GBLog.info(`[GET]: ${url} : ${result}`); return JSON.parse(result); } /** * Generic function to call any REST API by POST. */ public async httpPost(url: string, data) { const options = { uri: url, json: true, body: data }; let result = await request.post(options); GBLog.info(`[POST]: ${url} (${data}): ${result}`); return JSON.parse(result); } public async numberOnly(text: string) { return text.replace(/\D/gi, ''); } } /** * Base services of conversation to be called by BASIC. */ export class DialogClass { public min: GBMinInstance; public context: TurnContext; public step: WaterfallStepContext; public internalSys: SysClass; constructor(min: GBMinInstance, deployer: GBDeployer) { this.min = min; this.internalSys = new SysClass(min, deployer); } public static setup(bot: BotAdapter, min: GBMinInstance) { min.dialogs.add( new WaterfallDialog('/gbasic-email', [ async step => { const locale = step.context.activity.locale; if ((step.options as any).ask) { await min.conversationalService.sendText(min, step, Messages[locale].whats_email); } return await step.prompt('textPrompt', {}); }, async step => { const locale = step.context.activity.locale; const extractEntity = text => { return text.match(/([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)/gi); }; const value = extractEntity(step.result); if (value === null) { await min.conversationalService.sendText(min, step, Messages[locale].validation_enter_valid_email); return await step.replaceDialog('/gbasic-email', { ask: true }); } else { return await step.endDialog(value[0]); } } ]) ); } public sys(): SysClass { return this.internalSys; } public async getToday(step) { var d = new Date(), month = '' + (d.getMonth() + 1), day = '' + d.getDate(), year = d.getFullYear(); if (month.length < 2) month = '0' + month; if (day.length < 2) day = '0' + day; const locale = step.context.activity.locale; switch (locale) { case 'pt-BR': return [day, month, year].join('/'); case 'en-US': return [month, day, year].join('/'); default: return [year, month, day].join('/'); } } public async isAffirmative(step, text) { return text.toLowerCase().match(Messages['pt-BR'].affirmative_sentences); // TODO: Dynamitize. } public async exit(step) { await step.endDialog(); } public async getNow(step) { const nowUTC = new Date(); const now = new Date((typeof nowUTC === "string" ? new Date(nowUTC) : nowUTC).toLocaleString("en-US", { timeZone: process.env.DEFAULT_TIMEZONE })); return now.getHours() + ':' + now.getMinutes(); } public async sendFileTo(step, mobile, filename, caption) { return await this.internalSendFile(null, mobile, filename, caption); } public async sendFile(step, filename, caption) { return await this.internalSendFile(step, null, filename, caption); } private async internalSendFile(step, mobile, filename, caption) { if (filename.indexOf('.md') > -1) { GBLog.info(`BASIC: Sending the contents of ${filename} markdown to mobile.`); let md = await this.min.kbService.getAnswerTextByMediaName(this.min.instance.instanceId, filename); await this.min.conversationalService.sendMarkdownToMobile(this.min, step, mobile, md); } else { GBLog.info(`BASIC: Sending the file ${filename} to mobile.`); let url = urlJoin( GBServer.globals.publicAddress, 'kb', `${this.min.botId}.gbai`, `${this.min.botId}.gbkb`, 'assets', filename ); await this.min.conversationalService.sendFile(this.min, step, mobile, url, caption); } } public async setLanguage(step, language) { const locale = step.context.activity.locale; const user = await this.min.userProfile.get(step.context, {}); let sec = new SecService(); user.systemUser = await sec.updateUserLocale(user.systemUser.userId, language); await this.min.userProfile.set(step.context, user); } public async getFrom(step) { return step.context.activity.from.id; } public async getUserName(step) { return step.context.activity.from.name; } public async getUserMobile(step) { if (isNaN(step.context.activity.from.id)) { return 'No mobile available.'; } else { return step.context.activity.from.id; } } public async showMenu(step) { return await step.beginDialog('/menu'); } public async transfer(step) { return await step.beginDialog('/t'); } public async hear(step, promise, previousResolve, kind, ...args) { function random(low, high) { return Math.random() * (high - low) + low; } const idPromise = random(0, 120000000); this.min.cbMap[idPromise] = {}; this.min.cbMap[idPromise].promise = promise; const opts = { id: idPromise, previousResolve: previousResolve, kind: kind, args }; if (previousResolve !== undefined) { previousResolve(opts); } else { await step.beginDialog('/hear', opts); } } public async talk(step, text: string) { return await this.min.conversationalService.sendText(this.min, step, text); } }