new(whatsapp.gblib): Auto-create WhatsApp templates from articles in .docx.

This commit is contained in:
Rodrigo Rodriguez 2024-08-04 17:16:04 -03:00
parent 1bb297f68b
commit e8d0317f82
9 changed files with 383 additions and 209 deletions

View file

@ -64,8 +64,6 @@ import { GBUtil } from '../../../src/util.js';
import SwaggerClient from 'swagger-client'; import SwaggerClient from 'swagger-client';
import { GBVMService } from './GBVMService.js'; import { GBVMService } from './GBVMService.js';
/** /**
* Default check interval for user replay * Default check interval for user replay
*/ */
@ -212,28 +210,28 @@ export class DialogKeywords {
* *
* @example EXIT * @example EXIT
*/ */
public async exit({ }) { } public async exit({}) {}
/** /**
* Get active tasks. * Get active tasks.
* *
* @example list = ACTIVE TASKS * @example list = ACTIVE TASKS
*/ */
public async getActiveTasks({ pid }) { } public async getActiveTasks({ pid }) {}
/** /**
* Creates a new deal. * Creates a new deal.
* *
* @example CREATE DEAL dealname,contato,empresa,amount * @example CREATE DEAL dealname,contato,empresa,amount
*/ */
public async createDeal({ pid, dealName, contact, company, amount }) { } public async createDeal({ pid, dealName, contact, company, amount }) {}
/** /**
* Finds contacts in XRM. * Finds contacts in XRM.
* *
* @example list = FIND CONTACT "Sandra" * @example list = FIND CONTACT "Sandra"
*/ */
public async fndContact({ pid, name }) { } public async fndContact({ pid, name }) {}
public getContentLocaleWithCulture(contentLocale) { public getContentLocaleWithCulture(contentLocale) {
switch (contentLocale) { switch (contentLocale) {
@ -324,7 +322,6 @@ export class DialogKeywords {
// https://weblog.west-wind.com/posts/2008/Mar/18/A-simple-formatDate-function-for-JavaScript // https://weblog.west-wind.com/posts/2008/Mar/18/A-simple-formatDate-function-for-JavaScript
public async format({ pid, value, format }) { public async format({ pid, value, format }) {
const { min, user } = await DialogKeywords.getProcessInfo(pid); const { min, user } = await DialogKeywords.getProcessInfo(pid);
const contentLocale = min.core.getParam( const contentLocale = min.core.getParam(
min.instance, min.instance,
@ -337,39 +334,31 @@ export class DialogKeywords {
} }
var date: any = new Date(value); //don't change original date var date: any = new Date(value); //don't change original date
if (!format) if (!format) format = 'MM/dd/yyyy';
format = "MM/dd/yyyy";
var month = date.getMonth() + 1; var month = date.getMonth() + 1;
var year = date.getFullYear(); var year = date.getFullYear();
format = format.replace("MM", GBUtil.padL(month.toString(), 2, "0")); format = format.replace('MM', GBUtil.padL(month.toString(), 2, '0'));
if (format.indexOf("yyyy") > -1) if (format.indexOf('yyyy') > -1) format = format.replace('yyyy', year.toString());
format = format.replace("yyyy", year.toString()); else if (format.indexOf('yy') > -1) format = format.replace('yy', year.toString().substr(2, 2));
else if (format.indexOf("yy") > -1)
format = format.replace("yy", year.toString().substr(2, 2));
format = format.replace("dd", GBUtil.padL(date.getDate().toString(), 2, "0")); format = format.replace('dd', GBUtil.padL(date.getDate().toString(), 2, '0'));
var hours = date.getHours(); var hours = date.getHours();
if (format.indexOf("t") > -1) { if (format.indexOf('t') > -1) {
if (hours > 11) if (hours > 11) format = format.replace('t', 'pm');
format = format.replace("t", "pm") else format = format.replace('t', 'am');
else
format = format.replace("t", "am")
} }
if (format.indexOf("HH") > -1) if (format.indexOf('HH') > -1) format = format.replace('HH', GBUtil.padL(hours.toString(), 2, '0'));
format = format.replace("HH", GBUtil.padL(hours.toString(), 2, "0")); if (format.indexOf('hh') > -1) {
if (format.indexOf("hh") > -1) {
if (hours > 12) hours - 12; if (hours > 12) hours - 12;
if (hours == 0) hours = 12; if (hours == 0) hours = 12;
format = format.replace("hh", hours.toString().padL(2, "0")); format = format.replace('hh', hours.toString().padL(2, '0'));
} }
if (format.indexOf("mm") > -1) if (format.indexOf('mm') > -1) format = format.replace('mm', GBUtil.padL(date.getMinutes().toString(), 2, '0'));
format = format.replace("mm", GBUtil.padL(date.getMinutes().toString(), 2, "0")); if (format.indexOf('ss') > -1) format = format.replace('ss', GBUtil.padL(date.getSeconds().toString(), 2, '0'));
if (format.indexOf("ss") > -1)
format = format.replace("ss", GBUtil.padL(date.getSeconds().toString(), 2, "0"));
return format; return format;
} }
@ -382,7 +371,6 @@ export class DialogKeywords {
* https://stackoverflow.com/a/1214753/18511 * https://stackoverflow.com/a/1214753/18511
*/ */
public async dateAdd({ pid, date, mode, units }) { public async dateAdd({ pid, date, mode, units }) {
const { min, user } = await DialogKeywords.getProcessInfo(pid); const { min, user } = await DialogKeywords.getProcessInfo(pid);
const contentLocale = min.core.getParam( const contentLocale = min.core.getParam(
min.instance, min.instance,
@ -530,13 +518,13 @@ export class DialogKeywords {
public async sendEmail({ pid, to, subject, body }) { public async sendEmail({ pid, to, subject, body }) {
const { min, user } = await DialogKeywords.getProcessInfo(pid); const { min, user } = await DialogKeywords.getProcessInfo(pid);
if (!process.env.EMAIL_FROM){ if (!process.env.EMAIL_FROM) {
return; return;
} }
if (!body ) { if (!body) {
body = ""; body = '';
}; }
// tslint:disable-next-line:no-console // tslint:disable-next-line:no-console
@ -550,7 +538,6 @@ export class DialogKeywords {
body = result.value; body = result.value;
} }
if (emailToken) { if (emailToken) {
return new Promise<any>((resolve, reject) => { return new Promise<any>((resolve, reject) => {
sgMail.setApiKey(emailToken); sgMail.setApiKey(emailToken);
@ -569,39 +556,35 @@ export class DialogKeywords {
} }
}); });
}); });
} } else {
else {
let { client } = await GBDeployer.internalGetDriveClient(min); let { client } = await GBDeployer.internalGetDriveClient(min);
const data = { const data = {
"message": { message: {
"subject": subject, subject: subject,
"body": { body: {
"contentType": "Text", contentType: 'Text',
"content": body content: body
}, },
"toRecipients": [ toRecipients: [
{ {
"emailAddress": { emailAddress: {
"address": to address: to
} }
} }
], ],
"from": { from: {
"emailAddress": { emailAddress: {
"address": process.env.EMAIL_FROM address: process.env.EMAIL_FROM
} }
} }
} }
}; };
await client.api('/me/sendMail') await client.api('/me/sendMail').post(data);
.post(data);
GBLogEx.info(min, `E-mail ${to} (${subject}) sent.`); GBLogEx.info(min, `E-mail ${to} (${subject}) sent.`);
} }
} }
/** /**
@ -622,7 +605,7 @@ export class DialogKeywords {
* @example SEND TEMPLATE TO "+199988887777","image.jpg" * @example SEND TEMPLATE TO "+199988887777","image.jpg"
* *
*/ */
public async sendTemplateTo({ pid, mobile, filename}) { public async sendTemplateTo({ pid, mobile, filename }) {
const { min, user, proc } = await DialogKeywords.getProcessInfo(pid); const { min, user, proc } = await DialogKeywords.getProcessInfo(pid);
GBLogEx.info(min, `BASIC: SEND TEMPLATE TO '${mobile}',filename '${filename}'.`); GBLogEx.info(min, `BASIC: SEND TEMPLATE TO '${mobile}',filename '${filename}'.`);
const service = new GBConversationalService(min.core); const service = new GBConversationalService(min.core);
@ -630,14 +613,11 @@ export class DialogKeywords {
let text; let text;
if (filename.endsWith('.docx')) { if (filename.endsWith('.docx')) {
text = await min.kbService.getAnswerTextByMediaName(min.instance.instanceId, filename); text = await min.kbService.getAnswerTextByMediaName(min.instance.instanceId, filename);
} } else {
else{
text = filename; text = filename;
} }
return await service.fillAndBroadcastTemplate(min, mobile, text); return await service.fillAndBroadcastTemplate(min, filename, mobile, text);
} }
/** /**
@ -686,20 +666,17 @@ export class DialogKeywords {
// Checks access. // Checks access.
const filters = ["People.xlsx", `${role}=x`, `id=${user.userSystemId}`]; const filters = ['People.xlsx', `${role}=x`, `id=${user.userSystemId}`];
const people = await sys.find({ pid, handle: null, args: filters }); const people = await sys.find({ pid, handle: null, args: filters });
if (!people) { if (!people) {
throw new Error(`Invalid access. Check if People sheet has the role ${role} checked.`); throw new Error(`Invalid access. Check if People sheet has the role ${role} checked.`);
} } else {
else {
GBLogEx.info(min, `Allowed access for ${user.userSystemId} on ${role}`); GBLogEx.info(min, `Allowed access for ${user.userSystemId} on ${role}`);
return people; return people;
} }
} }
/** /**
* Defines the id generation policy. * Defines the id generation policy.
* *
@ -787,7 +764,6 @@ export class DialogKeywords {
await DialogKeywords.setOption({ pid, name, value }); await DialogKeywords.setOption({ pid, name, value });
} }
/** /**
* Returns current if any continuation token for paginated HTTP requests. * Returns current if any continuation token for paginated HTTP requests.
* *
@ -836,7 +812,7 @@ export class DialogKeywords {
* Defines page mode for paged GET calls. * Defines page mode for paged GET calls.
* *
* @example SET PAGE MODE "auto" * @example SET PAGE MODE "auto"
* data = GET url * data = GET url
* FOR EACH item in data * FOR EACH item in data
* ... * ...
* END FOR * END FOR
@ -916,7 +892,7 @@ export class DialogKeywords {
* @example MENU * @example MENU
* *
*/ */
public async showMenu({ }) { public async showMenu({}) {
// https://github.com/GeneralBots/BotServer/issues/237 // https://github.com/GeneralBots/BotServer/issues/237
// return await beginDialog('/menu'); // return await beginDialog('/menu');
} }
@ -943,7 +919,6 @@ export class DialogKeywords {
* *
*/ */
public async hear({ pid, kind, args }) { public async hear({ pid, kind, args }) {
let { min, user, params } = await DialogKeywords.getProcessInfo(pid); let { min, user, params } = await DialogKeywords.getProcessInfo(pid);
// Handles first arg as an array of args. // Handles first arg as an array of args.
@ -1122,7 +1097,6 @@ export class DialogKeywords {
result = answer; result = answer;
} else if (kind === 'date') { } else if (kind === 'date') {
const parseDate = str => { const parseDate = str => {
function pad(x) { function pad(x) {
return (('' + x).length == 2 ? '' : '0') + x; return (('' + x).length == 2 ? '' : '0') + x;
@ -1298,15 +1272,7 @@ export class DialogKeywords {
let sec = new SecService(); let sec = new SecService();
let user = await sec.getUserFromSystemId(fromOrDialogName); let user = await sec.getUserFromSystemId(fromOrDialogName);
if (!user) { if (!user) {
user = await sec.ensureUser( user = await sec.ensureUser(min, fromOrDialogName, fromOrDialogName, null, 'whatsapp', 'from', null);
min,
fromOrDialogName,
fromOrDialogName,
null,
'whatsapp',
'from',
null
);
} }
await sec.updateUserHearOnDialog(user.userId, dialogName); await sec.updateUserHearOnDialog(user.userId, dialogName);
} }
@ -1341,7 +1307,6 @@ export class DialogKeywords {
let count = API_RETRIES; let count = API_RETRIES;
while (count--) { while (count--) {
await GBUtil.sleep(DEFAULT_HEAR_POLL_INTERVAL); await GBUtil.sleep(DEFAULT_HEAR_POLL_INTERVAL);
try { try {
@ -1364,33 +1329,23 @@ export class DialogKeywords {
} }
} catch (err) { } catch (err) {
GBLog.error( GBLog.error(
`Error calling printMessages in messageBot API ${err.data === undefined ? err : err.data} ${err.errObj ? err.errObj.message : '' `Error calling printMessages in messageBot API ${err.data === undefined ? err : err.data} ${
err.errObj ? err.errObj.message : ''
}` }`
); );
return err; return err;
} }
}; }
} }
public async start({ botId, botApiKey, userSystemId, text }) { public async start({ botId, botApiKey, userSystemId, text }) {
let min: GBMinInstance = GBServer.globals.minInstances.filter(p => p.instance.botId === botId)[0]; let min: GBMinInstance = GBServer.globals.minInstances.filter(p => p.instance.botId === botId)[0];
let sec = new SecService(); let sec = new SecService();
let user = await sec.getUserFromSystemId(userSystemId); let user = await sec.getUserFromSystemId(userSystemId);
if (!user) { if (!user) {
user = await sec.ensureUser( user = await sec.ensureUser(min, userSystemId, userSystemId, null, 'api', 'API User', null);
min,
userSystemId,
userSystemId,
null,
'api',
'API User',
null
);
} }
const pid = GBVMService.createProcessInfo(user, min, 'api', null); const pid = GBVMService.createProcessInfo(user, min, 'api', null);
const conversation = min['apiConversations'][pid]; const conversation = min['apiConversations'][pid];
@ -1406,11 +1361,8 @@ export class DialogKeywords {
conversation.conversationId = response.obj.conversationId; conversation.conversationId = response.obj.conversationId;
return await GBVMService.callVM('start', min, null, pid); return await GBVMService.callVM('start', min, null, pid);
} }
public static async getProcessInfo(pid: number) { public static async getProcessInfo(pid: number) {
const proc = GBServer.globals.processes[pid]; const proc = GBServer.globals.processes[pid];
const step = proc.step; const step = proc.step;
@ -1439,15 +1391,13 @@ export class DialogKeywords {
text = await min.conversationalService.translate( text = await min.conversationalService.translate(
min, min,
text, text,
user.locale ? user.locale : min.core.getParam(min.instance, 'Locale', user.locale ? user.locale : min.core.getParam(min.instance, 'Locale', GBConfigService.get('LOCALE'))
GBConfigService.get('LOCALE'))
); );
GBLog.verbose(`Translated text(playMarkdown): ${text}.`); GBLog.verbose(`Translated text(playMarkdown): ${text}.`);
if (step) { if (step) {
await min.conversationalService.sendText(min, step, text); await min.conversationalService.sendText(min, step, text);
} } else {
else {
await min.conversationalService['sendOnConversation'](min, user, text); await min.conversationalService['sendOnConversation'](min, user, text);
} }
} }
@ -1484,7 +1434,6 @@ export class DialogKeywords {
} }
// GBFILE object. // GBFILE object.
else if (filename.url) { else if (filename.url) {
url = filename.url; url = filename.url;
nameOnly = Path.basename(filename.localName); nameOnly = Path.basename(filename.localName);
@ -1493,9 +1442,7 @@ export class DialogKeywords {
} }
// Handles Markdown. // Handles Markdown.
else if (filename.indexOf('.md') !== -1) { else if (filename.indexOf('.md') !== -1) {
GBLogEx.info(min, `BASIC: Sending the contents of ${filename} markdown to mobile ${mobile}.`); GBLogEx.info(min, `BASIC: Sending the contents of ${filename} markdown to mobile ${mobile}.`);
const md = await min.kbService.getAnswerTextByMediaName(min.instance.instanceId, filename); const md = await min.kbService.getAnswerTextByMediaName(min.instance.instanceId, filename);
if (!md) { if (!md) {
@ -1504,14 +1451,10 @@ export class DialogKeywords {
await min.conversationalService['playMarkdown'](min, md, DialogKeywords.getChannel(), null, mobile); await min.conversationalService['playMarkdown'](min, md, DialogKeywords.getChannel(), null, mobile);
return; return;
} }
// .gbdrive direct sending. // .gbdrive direct sending.
else { else {
const ext = Path.extname(filename); const ext = Path.extname(filename);
const gbaiName = DialogKeywords.getGBAIPath(min.botId); const gbaiName = DialogKeywords.getGBAIPath(min.botId);
@ -1529,15 +1472,18 @@ export class DialogKeywords {
const driveUrl = template['@microsoft.graph.downloadUrl']; const driveUrl = template['@microsoft.graph.downloadUrl'];
const res = await fetch(driveUrl); const res = await fetch(driveUrl);
let buf: any = Buffer.from(await res.arrayBuffer()); let buf: any = Buffer.from(await res.arrayBuffer());
let localName1 = Path.join('work', gbaiName, 'cache', `${fileOnly.replace(/\s/gi, '')}-${GBAdminService.getNumberIdentifier()}.${ext}`); let localName1 = Path.join(
'work',
gbaiName,
'cache',
`${fileOnly.replace(/\s/gi, '')}-${GBAdminService.getNumberIdentifier()}.${ext}`
);
Fs.writeFileSync(localName1, buf, { encoding: null }); Fs.writeFileSync(localName1, buf, { encoding: null });
url = urlJoin(GBServer.globals.publicAddress, min.botId, 'cache', Path.basename(localName1)); url = urlJoin(GBServer.globals.publicAddress, min.botId, 'cache', Path.basename(localName1));
} }
if (!url) { if (!url) {
const ext = Path.extname(filename.localName); const ext = Path.extname(filename.localName);
// Prepare a cache to be referenced by Bot Framework. // Prepare a cache to be referenced by Bot Framework.

View file

@ -128,6 +128,7 @@ export class KeywordsExpressions {
keywords[i++] = [/^\s*REM.*/gim, '']; keywords[i++] = [/^\s*REM.*/gim, ''];
keywords[i++] = [/^\s*CLOSE.*/gim, '']; keywords[i++] = [/^\s*CLOSE.*/gim, ''];
// Always autoclose keyword. // Always autoclose keyword.

View file

@ -914,11 +914,28 @@ export class SystemKeywords {
body.values[0][filter ? index + 1 : index] = value; body.values[0][filter ? index + 1 : index] = value;
} }
await client
.api( await retry(
`${baseUrl}/drive/items/${document.id}/workbook/worksheets('${sheets.value[0].name}')/range(address='${address}')` async bail => {
) const result = await client
.patch(body); .api(
`${baseUrl}/drive/items/${document.id}/workbook/worksheets('${sheets.value[0].name}')/range(address='${address}')`
)
.patch(body);
if (result.status != 200) {
GBLogEx.info(min, `Waiting 5 secs. before retrying HTTP ${result.status} GET: ${result.url}`);
await GBUtil.sleep(5 * 1000);
throw new Error(`BASIC: HTTP:${result.status} retry: ${result.statusText}.`);
}
},
{
retries: 5,
onRetry: err => {
GBLog.error(`Retrying HTTP GET due to: ${err.message}.`);
}
});
} }
/** /**
@ -1763,7 +1780,7 @@ export class SystemKeywords {
result = await fetch(url, options); result = await fetch(url, options);
if (result.status === 401) { if (result.status === 401) {
GBLogEx.info(min, `Waiting 5 secs. before retrynig HTTP 401 GET: ${url}`); GBLogEx.info(min, `Waiting 5 secs. before retrying HTTP 401 GET: ${url}`);
await GBUtil.sleep(5 * 1000); await GBUtil.sleep(5 * 1000);
throw new Error(`BASIC: HTTP:${result.status} retry: ${result.statusText}.`); throw new Error(`BASIC: HTTP:${result.status} retry: ${result.statusText}.`);
} }
@ -1773,7 +1790,7 @@ export class SystemKeywords {
throw new Error(`BASIC: HTTP:${result.status} retry: ${result.statusText}.`); throw new Error(`BASIC: HTTP:${result.status} retry: ${result.statusText}.`);
} }
if (result.status === 503) { if (result.status === 503) {
GBLogEx.info(min, `Waiting 1h before retrynig GET 503: ${url}`); GBLogEx.info(min, `Waiting 1h before retrying GET 503: ${url}`);
await GBUtil.sleep(60 * 60 * 1000); await GBUtil.sleep(60 * 60 * 1000);
throw new Error(`BASIC: HTTP:${result.status} retry: ${result.statusText}.`); throw new Error(`BASIC: HTTP:${result.status} retry: ${result.statusText}.`);
} }
@ -2679,7 +2696,7 @@ export class SystemKeywords {
let buf: any = Buffer.from(await res.arrayBuffer()); let buf: any = Buffer.from(await res.arrayBuffer());
const data = new Uint8Array(buf); const data = new Uint8Array(buf);
const pdf = await getDocument({ data }).promise; const pdf = await getDocument({ data }).promise;
let pages = [] let pages = [];
for (let i = 1; i <= pdf.numPages; i++) { for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i); const page = await pdf.getPage(i);
@ -2688,19 +2705,20 @@ export class SystemKeywords {
.map(item => item['str']) .map(item => item['str'])
.join('') .join('')
.replace(/\s/g, ''); .replace(/\s/g, '');
pages.push(text) pages.push(text);
} }
return pages.join(""); return pages.join('');
} }
public async setContext({pid, text}){ public async setContext({ pid, text }) {
const { min, user, params } = await DialogKeywords.getProcessInfo(pid); const { min, user, params } = await DialogKeywords.getProcessInfo(pid);
ChatServices.userSystemPrompt[user.userSystemId] = text; ChatServices.userSystemPrompt[user.userSystemId] = text;
await this.setMemoryContext({ pid, erase: true });
} }
public async setMemoryContext({pid, input, output, erase}){ public async setMemoryContext({ pid, input = null, output = null, erase }) {
const { min, user, params } = await DialogKeywords.getProcessInfo(pid); const { min, user, params } = await DialogKeywords.getProcessInfo(pid);
let memory; let memory;
if (erase || !ChatServices.memoryMap[user.userSystemId]) { if (erase || !ChatServices.memoryMap[user.userSystemId]) {
@ -2716,18 +2734,14 @@ export class SystemKeywords {
memory = ChatServices.memoryMap[user.userSystemId]; memory = ChatServices.memoryMap[user.userSystemId];
} }
if (memory) if (memory && input)
await memory.saveContext( await memory.saveContext(
{ {
input: input input: input
}, },
{ {
output: output output: output
} }
); );
} }
} }

View file

@ -641,7 +641,12 @@ export class GBConversationalService {
}); });
} }
public async fillAndBroadcastTemplate(min: GBMinInstance, mobile: string, text) {
public async fillAndBroadcastTemplate(min: GBMinInstance, template, mobile: string, text) {
template = template.replace(/\-/gi, '_')
template = template.replace(/\./gi, '_')
let isMedia = let isMedia =
text.toLowerCase().endsWith('.jpg') || text.toLowerCase().endsWith('.jpg') ||
text.toLowerCase().endsWith('.jpeg') || text.toLowerCase().endsWith('.jpeg') ||
@ -659,7 +664,7 @@ export class GBConversationalService {
text = text.replace(/\n/g, '\\n'); text = text.replace(/\n/g, '\\n');
} }
let template = isMedia ? image.replace(/\.[^/.]+$/, '') : 'broadcast1'; template = isMedia ? image.replace(/\.[^/.]+$/, '') : template;
let data: any = { let data: any = {
name: template, name: template,
@ -678,17 +683,6 @@ export class GBConversationalService {
] ]
}; };
if (!isMedia) {
data.components.push({
type: 'body',
parameters: [
{
type: 'text',
text: text
}
]
});
}
await this.sendToMobile(min, mobile, data, null); await this.sendToMobile(min, mobile, data, null);
GBLogEx.info(min, `Sending answer file to mobile: ${mobile}. Header: ${urlImage}`); GBLogEx.info(min, `Sending answer file to mobile: ${mobile}. Header: ${urlImage}`);
} }

View file

@ -30,7 +30,6 @@
import Swagger from 'swagger-client'; import Swagger from 'swagger-client';
import { google } from 'googleapis'; import { google } from 'googleapis';
import { promisify } from 'util';
import { PubSub } from '@google-cloud/pubsub'; import { PubSub } from '@google-cloud/pubsub';
import Fs from 'fs'; import Fs from 'fs';
import { GBLog, GBMinInstance, GBService } from 'botlib'; import { GBLog, GBMinInstance, GBService } from 'botlib';

View file

@ -255,7 +255,7 @@ export class ChatServices {
public static memoryMap = {}; public static memoryMap = {};
public static userSystemPrompt = {}; public static userSystemPrompt = {};
public static async answerByGPT(min: GBMinInstance, user, question: string, mode = null) { public static async answerByLLM(min: GBMinInstance, user, question: string, mode = null) {
const answerMode = min.core.getParam(min.instance, 'Answer Mode', null); const answerMode = min.core.getParam(min.instance, 'Answer Mode', null);
if (!answerMode || answerMode === 'nollm') { if (!answerMode || answerMode === 'nollm') {

View file

@ -32,7 +32,7 @@
* @fileoverview Knowledge base services and logic. * @fileoverview Knowledge base services and logic.
*/ */
import html2md from 'html-to-md' import html2md from 'html-to-md';
import Path from 'path'; import Path from 'path';
import Fs from 'fs'; import Fs from 'fs';
import urlJoin from 'url-join'; import urlJoin from 'url-join';
@ -379,7 +379,7 @@ export class KBService implements IGBKBService {
returnedScore: ${returnedScore} < required (searchScore): ${searchScore}` returnedScore: ${returnedScore} < required (searchScore): ${searchScore}`
); );
return await ChatServices.answerByGPT(min, user, query); return await ChatServices.answerByLLM(min, user, query);
} }
public async getSubjectItems(instanceId: number, parentId: number): Promise<GuaribasSubject[]> { public async getSubjectItems(instanceId: number, parentId: number): Promise<GuaribasSubject[]> {
@ -703,7 +703,7 @@ export class KBService implements IGBKBService {
// Import remaining .md files in articles directory. // Import remaining .md files in articles directory.
await this.importRemainingArticles(localPath, instance, packageStorage.packageId); await this.importRemainingArticles(min, localPath, instance, packageStorage.packageId);
// Import docs files in .docx directory. // Import docs files in .docx directory.
@ -713,7 +713,12 @@ export class KBService implements IGBKBService {
/** /**
* Import all .md files in articles folder that has not been referenced by tabular files. * Import all .md files in articles folder that has not been referenced by tabular files.
*/ */
public async importRemainingArticles(localPath: string, instance: IGBInstance, packageId: number): Promise<any> { public async importRemainingArticles(
min: GBMinInstance,
localPath: string,
instance: IGBInstance,
packageId: number
): Promise<any> {
const files = await walkPromise(urlJoin(localPath, 'articles')); const files = await walkPromise(urlJoin(localPath, 'articles'));
const data = { questions: [], answers: [] }; const data = { questions: [], answers: [] };
@ -735,14 +740,19 @@ export class KBService implements IGBKBService {
}); });
} }
} else if (file !== null && file.name.endsWith('.docx')) { } else if (file !== null && file.name.endsWith('.docx')) {
const path = DialogKeywords.getGBAIPath(instance.botId, `gbkb`); let path = DialogKeywords.getGBAIPath(instance.botId, `gbkb`);
const localName = Path.join('work', path, 'articles', file.name); const localName = Path.join('work', path, 'articles', file.name);
let loader = new DocxLoader(localName); let loader = new DocxLoader(localName);
let doc = await loader.load(); let doc = await loader.load();
let content = doc[0].pageContent;
const answer = { if (file.name.endsWith('zap.docx')){
await min.whatsAppDirectLine.createOrUpdateTemplate(min, file.name, content);
}
const answer = {
instanceId: instance.instanceId, instanceId: instance.instanceId,
content: doc[0].pageContent, content: content,
format: '.md', format: '.md',
media: file.name, media: file.name,
packageId: packageId, packageId: packageId,
@ -923,7 +933,7 @@ export class KBService implements IGBKBService {
// Check if urlToCheck contains any of the ignored URLs // Check if urlToCheck contains any of the ignored URLs
var isIgnored = false; var isIgnored = false;
if (websiteIgnoreUrls){ if (websiteIgnoreUrls) {
websiteIgnoreUrls.split(';').some(ignoredUrl => p.href.includes(ignoredUrl)); websiteIgnoreUrls.split(';').some(ignoredUrl => p.href.includes(ignoredUrl));
} }
@ -973,22 +983,26 @@ export class KBService implements IGBKBService {
} }
} }
async getLogoByPage(page) { async getLogoByPage(min, page) {
const checkPossibilities = async (page, possibilities) => { const checkPossibilities = async (page, possibilities) => {
for (const possibility of possibilities) { try {
const { tag, attributes } = possibility; for (const possibility of possibilities) {
const { tag, attributes } = possibility;
for (const attribute of attributes) { for (const attribute of attributes) {
const selector = `${tag}[${attribute}*="logo"]`; const selector = `${tag}[${attribute}*="logo"]`;
const elements = await page.$$(selector); const elements = await page.$$(selector);
for (const element of elements) { for (const element of elements) {
const src = await page.evaluate(el => el.getAttribute('src'), element); const src = await page.evaluate(el => el.getAttribute('src'), element);
if (src) { if (src) {
return src.split('?')[0]; return src.split('?')[0];
}
} }
} }
} }
} catch (error) {
await GBLogEx.info(min, error);
} }
return null; return null;
@ -1030,13 +1044,13 @@ export class KBService implements IGBKBService {
let files = []; let files = [];
let website = min.core.getParam<string>(min.instance, 'Website', null); let website = min.core.getParam<string>(min.instance, 'Website', null);
const maxDepth = min.core.getParam<number>(min.instance, 'Website Depth', 1); const maxDepth = min.core.getParam<number>(min.instance, 'Website Depth', 1);
const websiteIgnoreUrls = min.core.getParam<[]>(min.instance, 'Website Ignore URLs', null); const websiteIgnoreUrls = min.core.getParam<[]>(min.instance, 'Website Ignore URLs', null);
if (website) { if (website) {
// Removes last slash if any. // Removes last slash if any.
website.endsWith('/')?website.substring(0, website.length-1):website; website.endsWith('/') ? website.substring(0, website.length - 1) : website;
let path = DialogKeywords.getGBAIPath(min.botId, `gbot`); let path = DialogKeywords.getGBAIPath(min.botId, `gbot`);
const directoryPath = Path.join(process.env.PWD, 'work', path, 'Website'); const directoryPath = Path.join(process.env.PWD, 'work', path, 'Website');
@ -1045,30 +1059,29 @@ export class KBService implements IGBKBService {
let browser = await puppeteer.launch({ headless: false }); let browser = await puppeteer.launch({ headless: false });
const page = await this.getFreshPage(browser, website); const page = await this.getFreshPage(browser, website);
let logo = await this.getLogoByPage(page); let logo = await this.getLogoByPage(min, page);
if (logo) { if (logo) {
path = DialogKeywords.getGBAIPath(min.botId); path = DialogKeywords.getGBAIPath(min.botId);
const logoPath = Path.join(process.env.PWD, 'work', path, 'cache'); const logoPath = Path.join(process.env.PWD, 'work', path, 'cache');
const baseUrl = page.url().split('/').slice(0, 3).join('/'); const baseUrl = page.url().split('/').slice(0, 3).join('/');
logo = logo.startsWith('https') ? logo : urlJoin(baseUrl, logo); logo = logo.startsWith('https') ? logo : urlJoin(baseUrl, logo);
try {
const logoBinary = await page.goto(logo);
const buffer = await logoBinary.buffer();
const logoFilename = Path.basename(logo);
sharp(buffer)
.resize({
width: 48,
height: 48,
fit: 'inside', // Resize the image to fit within the specified dimensions
withoutEnlargement: true // Don't enlarge the image if its dimensions are already smaller
})
.toFile(Path.join(logoPath, logoFilename));
await min.core['setConfig'](min, 'Logo', logoFilename);
} catch (error) {
GBLogEx.debug(min, error);
}
try {
const logoBinary = await page.goto(logo);
const buffer = await logoBinary.buffer();
const logoFilename = Path.basename(logo);
sharp(buffer)
.resize({
width: 48,
height: 48,
fit: 'inside', // Resize the image to fit within the specified dimensions
withoutEnlargement: true // Don't enlarge the image if its dimensions are already smaller
})
.toFile(Path.join(logoPath, logoFilename));
await min.core['setConfig'](min, 'Logo', logoFilename);
} catch (error) {
GBLogEx.debug(min, error);
}
} }
// Extract dominant colors from the screenshot // Extract dominant colors from the screenshot

View file

@ -46,6 +46,7 @@ import qrcode from 'qrcode-terminal';
import express from 'express'; import express from 'express';
import { GBSSR } from '../../core.gbapp/services/GBSSR.js'; import { GBSSR } from '../../core.gbapp/services/GBSSR.js';
import pkg from 'whatsapp-web.js'; import pkg from 'whatsapp-web.js';
import fetch, { Response } from 'node-fetch';
import { DialogKeywords } from '../../basic.gblib/services/DialogKeywords.js'; import { DialogKeywords } from '../../basic.gblib/services/DialogKeywords.js';
import { ChatServices } from '../../gpt.gblib/services/ChatServices.js'; import { ChatServices } from '../../gpt.gblib/services/ChatServices.js';
import { GBAdminService } from '../../admin.gbapp/services/GBAdminService.js'; import { GBAdminService } from '../../admin.gbapp/services/GBAdminService.js';
@ -55,6 +56,8 @@ import twilio from 'twilio';
import { GBVMService } from '../../basic.gblib/services/GBVMService.js'; import { GBVMService } from '../../basic.gblib/services/GBVMService.js';
import { GBLogEx } from '../../core.gbapp/services/GBLogEx.js'; import { GBLogEx } from '../../core.gbapp/services/GBLogEx.js';
import { createBot } from 'whatsapp-cloud-api'; import { createBot } from 'whatsapp-cloud-api';
import { promisify } from 'util';
const stat = promisify(Fs.stat);
/** /**
* Support for Whatsapp. * Support for Whatsapp.
@ -77,7 +80,10 @@ export class WhatsappDirectLine extends GBService {
public whatsappServiceKey: string; public whatsappServiceKey: string;
public whatsappServiceNumber: string; public whatsappServiceNumber: string;
public whatsappServiceUrl: string; public whatsappServiceUrl: string;
public whatsappBusinessManagerId: string;
public whatsappFBAppId: string;
public botId: string; public botId: string;
public botNumber: string;
public min: GBMinInstance; public min: GBMinInstance;
private directLineSecret: string; private directLineSecret: string;
private locale: string = 'pt-BR'; private locale: string = 'pt-BR';
@ -126,6 +132,26 @@ export class WhatsappDirectLine extends GBService {
let options: any; let options: any;
switch (this.provider) { switch (this.provider) {
case 'meta':
this.botNumber = this.min.core.getParam<string>(this.min.instance, 'Bot Number', null);
let whatsappServiceNumber, whatsappServiceKey, url;
if (this.botNumber && this.min.instance.whatsappServiceNumber) {
whatsappServiceNumber = this.min.instance.whatsappServiceNumber;
whatsappServiceKey = this.min.instance.whatsappServiceKey;
url = this.min.instance.whatsappServiceUrl;
} else {
whatsappServiceNumber = GBServer.globals.minBoot.instance.whatsappServiceNumber;
whatsappServiceKey = GBServer.globals.minBoot.instance.whatsappServiceKey;
url = GBServer.globals.minBoot.instance.whatsappServiceUrl;
}
if (url) {
const parts = url.split(';');
this.whatsappBusinessManagerId = parts[0];
this.whatsappFBAppId = parts[1];
}
this.customClient = createBot(whatsappServiceNumber, whatsappServiceKey);
break;
case 'official': case 'official':
const accountSid = process.env.TWILIO_ACCOUNT_SID; const accountSid = process.env.TWILIO_ACCOUNT_SID;
const authToken = process.env.TWILIO_AUTH_TOKEN; const authToken = process.env.TWILIO_AUTH_TOKEN;
@ -381,10 +407,9 @@ export class WhatsappDirectLine extends GBService {
// Ignore group messages without the mention to Bot. // Ignore group messages without the mention to Bot.
let botNumber = this.min.core.getParam<string>(this.min.instance, 'Bot Number', null); if (this.botNumber && !answerText && !found) {
if (botNumber && !answerText && !found) { let n = this.botNumber.replace('+', '');
botNumber = botNumber.replace('+', ''); if (!message.body.startsWith('@' + n)) {
if (!message.body.startsWith('@' + botNumber)) {
return; return;
} }
} }
@ -731,6 +756,119 @@ export class WhatsappDirectLine extends GBService {
await this.sendFileToDevice(to, url, 'Audio', msg, chatId); await this.sendFileToDevice(to, url, 'Audio', msg, chatId);
} }
// Function to create or update a template using WhatsApp Business API
public async createOrUpdateTemplate(min: GBMinInstance, template, text) {
template = template.replace(/\-/gi, '_')
template = template.replace(/\./gi, '_')
let image = /(.*)\n/gim.exec(text)[0].trim();
let path = DialogKeywords.getGBAIPath(min.botId, `gbkb`);
path = Path.join(process.env.PWD, 'work', path, 'images', image);
text = text.substring(image.length + 1).trim();
text = text.replace(/\n/g, '\\n');
const handleImage = await min.whatsAppDirectLine.uploadLargeFile(min, path);
let data: any = {
name: template,
components: [
{
type: 'HEADER',
format: 'IMAGE',
example: { header_handle: [handleImage] }
},
{
type: 'BODY',
text: text
}
]
};
const name = data.name;
// Define the API base URL and endpoints
const baseUrl = 'https://graph.facebook.com/v20.0'; // API version 20.0
const businessAccountId = this.whatsappBusinessManagerId;
const accessToken = this.whatsappServiceKey;
// Endpoint for listing templates
const listTemplatesEndpoint = `${baseUrl}/${businessAccountId}/message_templates?access_token=${accessToken}`;
// Step 1: Check if the template exists
const listResponse = await fetch(listTemplatesEndpoint, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`
}
});
if (!listResponse.ok) {
throw new Error('Failed to list templates');
}
const templates = await listResponse.json();
const templateExists = templates.data.find(template => template.name === name);
if (templateExists) {
// Step 2: Update the template
const updateTemplateEndpoint = `${baseUrl}/${templateExists.id}`;
// Removes the first HEADER element.
data.components.shift();
const updateResponse = await fetch(updateTemplateEndpoint, {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
components: data.components
})
});
if (!updateResponse.ok) {
throw new Error(`Failed to update template: ${name} ${await updateResponse.text()}`);
}
GBLogEx.info(min, `Template updated: ${name}`);
} else {
// Step 3: Create the template
const createTemplateEndpoint = `${baseUrl}/${businessAccountId}/message_templates`;
const createResponse = await fetch(createTemplateEndpoint, {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: data['name'],
language: 'pt_BR',
category: 'MARKETING',
components: data.components
})
});
if (!createResponse.ok) {
const body = await createResponse.text();
throw new Error(`Failed to create template: ${name} ${body}`);
}
GBLogEx.info(min, `Template created: ${name}`);
}
await GBUtil.sleep(20 * 1000);
}
public async sendToDevice(to: any, msg: string, conversationId) { public async sendToDevice(to: any, msg: string, conversationId) {
try { try {
const cmd = '/audio '; const cmd = '/audio ';
@ -747,26 +885,17 @@ export class WhatsappDirectLine extends GBService {
switch (this.provider) { switch (this.provider) {
case 'meta': case 'meta':
let whatsappServiceNumber, whatsappServiceKey;
if (botNumber && this.min.instance.whatsappServiceNumber) {
whatsappServiceNumber = this.min.instance.whatsappServiceNumber;
whatsappServiceKey = this.min.instance.whatsappServiceKey;
} else {
whatsappServiceNumber = GBServer.globals.minBoot.instance.whatsappServiceNumber;
whatsappServiceKey = GBServer.globals.minBoot.instance.whatsappServiceKey;
}
const driver = createBot(whatsappServiceNumber, whatsappServiceKey);
if (msg['name']) { if (msg['name']) {
const res = await driver.sendTemplate(to, msg['name'], 'pt_BR', msg['components']); await this.customClient.sendTemplate(to, msg['name'], 'pt_BR', msg['components']);
} else { } else {
messages = msg.match(/(.|[\r\n]){1,4096}/g); messages = msg.match(/(.|[\r\n]){1,4096}/g);
await CollectionUtil.asyncForEach(messages, async msg => { await CollectionUtil.asyncForEach(messages, async msg => {
await driver.sendText(to, msg); await this.customClient.sendText(to, msg);
await GBUtil.sleep(3000); if (messages.length > 1) {
await GBUtil.sleep(3000);
}
}); });
} }
@ -1092,4 +1221,82 @@ export class WhatsappDirectLine extends GBService {
GBLog.error(`Error on Whatsapp callback: ${GBUtil.toYAML(error)}`); GBLog.error(`Error on Whatsapp callback: ${GBUtil.toYAML(error)}`);
} }
} }
public async uploadLargeFile(min, filePath) {
const CHUNK_SIZE = 4 * 1024 * 1024; // 4MB chunks
let uploadSessionId;
const fileSize = (await stat(filePath)).size;
const fileName = filePath.split('/').pop();
const fileType = mime.lookup(filePath);
const appId = this.whatsappFBAppId;
const userAccessToken = this.whatsappServiceKey;
let h;
try {
if (!fileType) {
throw new Error('Unsupported file type');
}
// Step 1: Start an upload session
const startResponse = await fetch(
`https://graph.facebook.com/v20.0/${appId}/uploads?file_name=${fileName}&file_length=${fileSize}&file_type=${fileType}&access_token=${userAccessToken}`,
{
method: 'POST'
}
);
const startData = await startResponse.json();
if (!startResponse.ok) {
throw new Error(startData.error.message);
}
uploadSessionId = startData.id.split(':')[1];
// Step 2: Upload the file in chunks
let startOffset = 0;
while (startOffset < fileSize) {
const endOffset = Math.min(startOffset + CHUNK_SIZE, fileSize);
const fileStream = Fs.createReadStream(filePath, { start: startOffset, end: endOffset - 1 });
const chunkSize = endOffset - startOffset;
const uploadResponse = await fetch(`https://graph.facebook.com/v20.0/upload:${uploadSessionId}`, {
method: 'POST',
headers: {
Authorization: `OAuth ${userAccessToken}`,
file_offset: startOffset.toString(),
'Content-Length': chunkSize.toString()
},
body: fileStream
});
const uploadData = await uploadResponse.json();
if (!h) {
h = uploadData.h;
}
if (!uploadResponse.ok) {
throw new Error(uploadData.error.message);
}
startOffset = endOffset;
}
// Step 3: Get the file handle
const finalizeResponse = await fetch(`https://graph.facebook.com/v20.0/upload:${uploadSessionId}`, {
method: 'GET',
headers: {
Authorization: `OAuth ${userAccessToken}`
}
});
const finalizeData = await finalizeResponse.json();
if (!finalizeResponse.ok) {
throw new Error(finalizeData.error.message);
}
console.log('Upload completed successfully with file handle:', finalizeData.h);
return h;
} catch (error) {
console.error('Error during file upload:', error);
}
}
} }

View file

@ -117,11 +117,11 @@ export class GBServer {
}); });
process.on('uncaughtException', (err, p) => { process.on('uncaughtException', (err, p) => {
GBLogEx.error(0, `GBEXCEPTION: ${GBUtil.toYAML(JSON.stringify(err, Object.getOwnPropertyNames(err)))}`); GBLogEx.error(0, `GBEXCEPTION: ${GBUtil.toYAML(err)}`);
}); });
process.on('unhandledRejection', (err, p) => { process.on('unhandledRejection', (err, p) => {
GBLogEx.error(0,`GBREJECTION: ${GBUtil.toYAML(JSON.stringify(err, Object.getOwnPropertyNames(err)))}`); GBLogEx.error(0,`GBREJECTION: ${GBUtil.toYAML(err)}`);
}); });
// Creates working directory. // Creates working directory.