new(whatsapp.gblib): Auto-create WhatsApp templates from articles in .docx.
This commit is contained in:
parent
1bb297f68b
commit
e8d0317f82
9 changed files with 383 additions and 209 deletions
|
@ -64,8 +64,6 @@ import { GBUtil } from '../../../src/util.js';
|
|||
import SwaggerClient from 'swagger-client';
|
||||
import { GBVMService } from './GBVMService.js';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Default check interval for user replay
|
||||
*/
|
||||
|
@ -324,7 +322,6 @@ export class DialogKeywords {
|
|||
|
||||
// https://weblog.west-wind.com/posts/2008/Mar/18/A-simple-formatDate-function-for-JavaScript
|
||||
public async format({ pid, value, format }) {
|
||||
|
||||
const { min, user } = await DialogKeywords.getProcessInfo(pid);
|
||||
const contentLocale = min.core.getParam(
|
||||
min.instance,
|
||||
|
@ -337,39 +334,31 @@ export class DialogKeywords {
|
|||
}
|
||||
var date: any = new Date(value); //don't change original date
|
||||
|
||||
if (!format)
|
||||
format = "MM/dd/yyyy";
|
||||
if (!format) format = 'MM/dd/yyyy';
|
||||
|
||||
var month = date.getMonth() + 1;
|
||||
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)
|
||||
format = format.replace("yyyy", year.toString());
|
||||
else if (format.indexOf("yy") > -1)
|
||||
format = format.replace("yy", year.toString().substr(2, 2));
|
||||
if (format.indexOf('yyyy') > -1) format = format.replace('yyyy', year.toString());
|
||||
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();
|
||||
if (format.indexOf("t") > -1) {
|
||||
if (hours > 11)
|
||||
format = format.replace("t", "pm")
|
||||
else
|
||||
format = format.replace("t", "am")
|
||||
if (format.indexOf('t') > -1) {
|
||||
if (hours > 11) format = format.replace('t', 'pm');
|
||||
else format = format.replace('t', 'am');
|
||||
}
|
||||
if (format.indexOf("HH") > -1)
|
||||
format = format.replace("HH", GBUtil.padL(hours.toString(), 2, "0"));
|
||||
if (format.indexOf("hh") > -1) {
|
||||
if (format.indexOf('HH') > -1) format = format.replace('HH', GBUtil.padL(hours.toString(), 2, '0'));
|
||||
if (format.indexOf('hh') > -1) {
|
||||
if (hours > 12) 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)
|
||||
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('mm') > -1) 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'));
|
||||
|
||||
return format;
|
||||
}
|
||||
|
@ -382,7 +371,6 @@ export class DialogKeywords {
|
|||
* https://stackoverflow.com/a/1214753/18511
|
||||
*/
|
||||
public async dateAdd({ pid, date, mode, units }) {
|
||||
|
||||
const { min, user } = await DialogKeywords.getProcessInfo(pid);
|
||||
const contentLocale = min.core.getParam(
|
||||
min.instance,
|
||||
|
@ -535,8 +523,8 @@ export class DialogKeywords {
|
|||
}
|
||||
|
||||
if (!body) {
|
||||
body = "";
|
||||
};
|
||||
body = '';
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:no-console
|
||||
|
||||
|
@ -550,7 +538,6 @@ export class DialogKeywords {
|
|||
body = result.value;
|
||||
}
|
||||
|
||||
|
||||
if (emailToken) {
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
sgMail.setApiKey(emailToken);
|
||||
|
@ -569,39 +556,35 @@ export class DialogKeywords {
|
|||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
let { client } = await GBDeployer.internalGetDriveClient(min);
|
||||
|
||||
const data = {
|
||||
"message": {
|
||||
"subject": subject,
|
||||
"body": {
|
||||
"contentType": "Text",
|
||||
"content": body
|
||||
message: {
|
||||
subject: subject,
|
||||
body: {
|
||||
contentType: 'Text',
|
||||
content: body
|
||||
},
|
||||
"toRecipients": [
|
||||
toRecipients: [
|
||||
{
|
||||
"emailAddress": {
|
||||
"address": to
|
||||
emailAddress: {
|
||||
address: to
|
||||
}
|
||||
}
|
||||
],
|
||||
"from": {
|
||||
"emailAddress": {
|
||||
"address": process.env.EMAIL_FROM
|
||||
from: {
|
||||
emailAddress: {
|
||||
address: process.env.EMAIL_FROM
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await client.api('/me/sendMail')
|
||||
.post(data);
|
||||
await client.api('/me/sendMail').post(data);
|
||||
|
||||
GBLogEx.info(min, `E-mail ${to} (${subject}) sent.`);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -630,14 +613,11 @@ export class DialogKeywords {
|
|||
let text;
|
||||
if (filename.endsWith('.docx')) {
|
||||
text = await min.kbService.getAnswerTextByMediaName(min.instance.instanceId, filename);
|
||||
}
|
||||
else{
|
||||
} else {
|
||||
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.
|
||||
|
||||
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 });
|
||||
|
||||
if (!people) {
|
||||
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}`);
|
||||
return people;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Defines the id generation policy.
|
||||
*
|
||||
|
@ -787,7 +764,6 @@ export class DialogKeywords {
|
|||
await DialogKeywords.setOption({ pid, name, value });
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns current if any continuation token for paginated HTTP requests.
|
||||
*
|
||||
|
@ -943,7 +919,6 @@ export class DialogKeywords {
|
|||
*
|
||||
*/
|
||||
public async hear({ pid, kind, args }) {
|
||||
|
||||
let { min, user, params } = await DialogKeywords.getProcessInfo(pid);
|
||||
|
||||
// Handles first arg as an array of args.
|
||||
|
@ -1122,7 +1097,6 @@ export class DialogKeywords {
|
|||
|
||||
result = answer;
|
||||
} else if (kind === 'date') {
|
||||
|
||||
const parseDate = str => {
|
||||
function pad(x) {
|
||||
return (('' + x).length == 2 ? '' : '0') + x;
|
||||
|
@ -1298,15 +1272,7 @@ export class DialogKeywords {
|
|||
let sec = new SecService();
|
||||
let user = await sec.getUserFromSystemId(fromOrDialogName);
|
||||
if (!user) {
|
||||
user = await sec.ensureUser(
|
||||
min,
|
||||
fromOrDialogName,
|
||||
fromOrDialogName,
|
||||
null,
|
||||
'whatsapp',
|
||||
'from',
|
||||
null
|
||||
);
|
||||
user = await sec.ensureUser(min, fromOrDialogName, fromOrDialogName, null, 'whatsapp', 'from', null);
|
||||
}
|
||||
await sec.updateUserHearOnDialog(user.userId, dialogName);
|
||||
}
|
||||
|
@ -1341,7 +1307,6 @@ export class DialogKeywords {
|
|||
|
||||
let count = API_RETRIES;
|
||||
while (count--) {
|
||||
|
||||
await GBUtil.sleep(DEFAULT_HEAR_POLL_INTERVAL);
|
||||
|
||||
try {
|
||||
|
@ -1364,33 +1329,23 @@ export class DialogKeywords {
|
|||
}
|
||||
} catch (err) {
|
||||
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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async start({ botId, botApiKey, userSystemId, text }) {
|
||||
|
||||
let min: GBMinInstance = GBServer.globals.minInstances.filter(p => p.instance.botId === botId)[0];
|
||||
let sec = new SecService();
|
||||
let user = await sec.getUserFromSystemId(userSystemId);
|
||||
if (!user) {
|
||||
user = await sec.ensureUser(
|
||||
min,
|
||||
userSystemId,
|
||||
userSystemId,
|
||||
null,
|
||||
'api',
|
||||
'API User',
|
||||
null
|
||||
);
|
||||
user = await sec.ensureUser(min, userSystemId, userSystemId, null, 'api', 'API User', null);
|
||||
}
|
||||
|
||||
|
||||
const pid = GBVMService.createProcessInfo(user, min, 'api', null);
|
||||
|
||||
const conversation = min['apiConversations'][pid];
|
||||
|
@ -1406,11 +1361,8 @@ export class DialogKeywords {
|
|||
conversation.conversationId = response.obj.conversationId;
|
||||
|
||||
return await GBVMService.callVM('start', min, null, pid);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static async getProcessInfo(pid: number) {
|
||||
const proc = GBServer.globals.processes[pid];
|
||||
const step = proc.step;
|
||||
|
@ -1439,15 +1391,13 @@ export class DialogKeywords {
|
|||
text = await min.conversationalService.translate(
|
||||
min,
|
||||
text,
|
||||
user.locale ? user.locale : min.core.getParam(min.instance, 'Locale',
|
||||
GBConfigService.get('LOCALE'))
|
||||
user.locale ? user.locale : min.core.getParam(min.instance, 'Locale', GBConfigService.get('LOCALE'))
|
||||
);
|
||||
GBLog.verbose(`Translated text(playMarkdown): ${text}.`);
|
||||
|
||||
if (step) {
|
||||
await min.conversationalService.sendText(min, step, text);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
await min.conversationalService['sendOnConversation'](min, user, text);
|
||||
}
|
||||
}
|
||||
|
@ -1484,7 +1434,6 @@ export class DialogKeywords {
|
|||
}
|
||||
|
||||
// GBFILE object.
|
||||
|
||||
else if (filename.url) {
|
||||
url = filename.url;
|
||||
nameOnly = Path.basename(filename.localName);
|
||||
|
@ -1493,9 +1442,7 @@ export class DialogKeywords {
|
|||
}
|
||||
|
||||
// Handles Markdown.
|
||||
|
||||
else if (filename.indexOf('.md') !== -1) {
|
||||
|
||||
GBLogEx.info(min, `BASIC: Sending the contents of ${filename} markdown to mobile ${mobile}.`);
|
||||
const md = await min.kbService.getAnswerTextByMediaName(min.instance.instanceId, filename);
|
||||
if (!md) {
|
||||
|
@ -1504,14 +1451,10 @@ export class DialogKeywords {
|
|||
await min.conversationalService['playMarkdown'](min, md, DialogKeywords.getChannel(), null, mobile);
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
// .gbdrive direct sending.
|
||||
|
||||
else {
|
||||
|
||||
|
||||
const ext = Path.extname(filename);
|
||||
const gbaiName = DialogKeywords.getGBAIPath(min.botId);
|
||||
|
||||
|
@ -1529,15 +1472,18 @@ export class DialogKeywords {
|
|||
const driveUrl = template['@microsoft.graph.downloadUrl'];
|
||||
const res = await fetch(driveUrl);
|
||||
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 });
|
||||
|
||||
url = urlJoin(GBServer.globals.publicAddress, min.botId, 'cache', Path.basename(localName1));
|
||||
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
|
||||
const ext = Path.extname(filename.localName);
|
||||
|
||||
// Prepare a cache to be referenced by Bot Framework.
|
||||
|
|
|
@ -128,6 +128,7 @@ export class KeywordsExpressions {
|
|||
|
||||
keywords[i++] = [/^\s*REM.*/gim, ''];
|
||||
|
||||
|
||||
keywords[i++] = [/^\s*CLOSE.*/gim, ''];
|
||||
|
||||
// Always autoclose keyword.
|
||||
|
|
|
@ -914,11 +914,28 @@ export class SystemKeywords {
|
|||
|
||||
body.values[0][filter ? index + 1 : index] = value;
|
||||
}
|
||||
await client
|
||||
|
||||
await retry(
|
||||
async bail => {
|
||||
const result = await client
|
||||
.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);
|
||||
|
||||
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);
|
||||
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}.`);
|
||||
}
|
||||
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);
|
||||
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());
|
||||
const data = new Uint8Array(buf);
|
||||
const pdf = await getDocument({ data }).promise;
|
||||
let pages = []
|
||||
let pages = [];
|
||||
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
|
@ -2688,19 +2705,20 @@ export class SystemKeywords {
|
|||
.map(item => item['str'])
|
||||
.join('')
|
||||
.replace(/\s/g, '');
|
||||
pages.push(text)
|
||||
|
||||
pages.push(text);
|
||||
}
|
||||
|
||||
return pages.join("");
|
||||
return pages.join('');
|
||||
}
|
||||
|
||||
public async setContext({ pid, text }) {
|
||||
const { min, user, params } = await DialogKeywords.getProcessInfo(pid);
|
||||
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);
|
||||
let memory;
|
||||
if (erase || !ChatServices.memoryMap[user.userSystemId]) {
|
||||
|
@ -2716,7 +2734,7 @@ export class SystemKeywords {
|
|||
memory = ChatServices.memoryMap[user.userSystemId];
|
||||
}
|
||||
|
||||
if (memory)
|
||||
if (memory && input)
|
||||
await memory.saveContext(
|
||||
{
|
||||
input: input
|
||||
|
@ -2725,9 +2743,5 @@ export class SystemKeywords {
|
|||
output: output
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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 =
|
||||
text.toLowerCase().endsWith('.jpg') ||
|
||||
text.toLowerCase().endsWith('.jpeg') ||
|
||||
|
@ -659,7 +664,7 @@ export class GBConversationalService {
|
|||
text = text.replace(/\n/g, '\\n');
|
||||
}
|
||||
|
||||
let template = isMedia ? image.replace(/\.[^/.]+$/, '') : 'broadcast1';
|
||||
template = isMedia ? image.replace(/\.[^/.]+$/, '') : template;
|
||||
|
||||
let data: any = {
|
||||
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);
|
||||
GBLogEx.info(min, `Sending answer file to mobile: ${mobile}. Header: ${urlImage}`);
|
||||
}
|
||||
|
|
|
@ -30,7 +30,6 @@
|
|||
|
||||
import Swagger from 'swagger-client';
|
||||
import { google } from 'googleapis';
|
||||
import { promisify } from 'util';
|
||||
import { PubSub } from '@google-cloud/pubsub';
|
||||
import Fs from 'fs';
|
||||
import { GBLog, GBMinInstance, GBService } from 'botlib';
|
||||
|
|
|
@ -255,7 +255,7 @@ export class ChatServices {
|
|||
public static memoryMap = {};
|
||||
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);
|
||||
|
||||
if (!answerMode || answerMode === 'nollm') {
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
* @fileoverview Knowledge base services and logic.
|
||||
*/
|
||||
|
||||
import html2md from 'html-to-md'
|
||||
import html2md from 'html-to-md';
|
||||
import Path from 'path';
|
||||
import Fs from 'fs';
|
||||
import urlJoin from 'url-join';
|
||||
|
@ -379,7 +379,7 @@ export class KBService implements IGBKBService {
|
|||
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[]> {
|
||||
|
@ -703,7 +703,7 @@ export class KBService implements IGBKBService {
|
|||
|
||||
// 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.
|
||||
|
||||
|
@ -713,7 +713,12 @@ export class KBService implements IGBKBService {
|
|||
/**
|
||||
* 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 data = { questions: [], answers: [] };
|
||||
|
||||
|
@ -735,14 +740,19 @@ export class KBService implements IGBKBService {
|
|||
});
|
||||
}
|
||||
} 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);
|
||||
let loader = new DocxLoader(localName);
|
||||
let doc = await loader.load();
|
||||
let content = doc[0].pageContent;
|
||||
|
||||
if (file.name.endsWith('zap.docx')){
|
||||
await min.whatsAppDirectLine.createOrUpdateTemplate(min, file.name, content);
|
||||
}
|
||||
|
||||
const answer = {
|
||||
instanceId: instance.instanceId,
|
||||
content: doc[0].pageContent,
|
||||
content: content,
|
||||
format: '.md',
|
||||
media: file.name,
|
||||
packageId: packageId,
|
||||
|
@ -973,8 +983,9 @@ export class KBService implements IGBKBService {
|
|||
}
|
||||
}
|
||||
|
||||
async getLogoByPage(page) {
|
||||
async getLogoByPage(min, page) {
|
||||
const checkPossibilities = async (page, possibilities) => {
|
||||
try {
|
||||
for (const possibility of possibilities) {
|
||||
const { tag, attributes } = possibility;
|
||||
|
||||
|
@ -990,6 +1001,9 @@ export class KBService implements IGBKBService {
|
|||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
await GBLogEx.info(min, error);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
@ -1045,7 +1059,7 @@ export class KBService implements IGBKBService {
|
|||
let browser = await puppeteer.launch({ headless: false });
|
||||
const page = await this.getFreshPage(browser, website);
|
||||
|
||||
let logo = await this.getLogoByPage(page);
|
||||
let logo = await this.getLogoByPage(min, page);
|
||||
if (logo) {
|
||||
path = DialogKeywords.getGBAIPath(min.botId);
|
||||
const logoPath = Path.join(process.env.PWD, 'work', path, 'cache');
|
||||
|
@ -1068,7 +1082,6 @@ export class KBService implements IGBKBService {
|
|||
} catch (error) {
|
||||
GBLogEx.debug(min, error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Extract dominant colors from the screenshot
|
||||
|
|
|
@ -46,6 +46,7 @@ import qrcode from 'qrcode-terminal';
|
|||
import express from 'express';
|
||||
import { GBSSR } from '../../core.gbapp/services/GBSSR.js';
|
||||
import pkg from 'whatsapp-web.js';
|
||||
import fetch, { Response } from 'node-fetch';
|
||||
import { DialogKeywords } from '../../basic.gblib/services/DialogKeywords.js';
|
||||
import { ChatServices } from '../../gpt.gblib/services/ChatServices.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 { GBLogEx } from '../../core.gbapp/services/GBLogEx.js';
|
||||
import { createBot } from 'whatsapp-cloud-api';
|
||||
import { promisify } from 'util';
|
||||
const stat = promisify(Fs.stat);
|
||||
|
||||
/**
|
||||
* Support for Whatsapp.
|
||||
|
@ -77,7 +80,10 @@ export class WhatsappDirectLine extends GBService {
|
|||
public whatsappServiceKey: string;
|
||||
public whatsappServiceNumber: string;
|
||||
public whatsappServiceUrl: string;
|
||||
public whatsappBusinessManagerId: string;
|
||||
public whatsappFBAppId: string;
|
||||
public botId: string;
|
||||
public botNumber: string;
|
||||
public min: GBMinInstance;
|
||||
private directLineSecret: string;
|
||||
private locale: string = 'pt-BR';
|
||||
|
@ -126,6 +132,26 @@ export class WhatsappDirectLine extends GBService {
|
|||
let options: any;
|
||||
|
||||
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':
|
||||
const accountSid = process.env.TWILIO_ACCOUNT_SID;
|
||||
const authToken = process.env.TWILIO_AUTH_TOKEN;
|
||||
|
@ -381,10 +407,9 @@ export class WhatsappDirectLine extends GBService {
|
|||
|
||||
// Ignore group messages without the mention to Bot.
|
||||
|
||||
let botNumber = this.min.core.getParam<string>(this.min.instance, 'Bot Number', null);
|
||||
if (botNumber && !answerText && !found) {
|
||||
botNumber = botNumber.replace('+', '');
|
||||
if (!message.body.startsWith('@' + botNumber)) {
|
||||
if (this.botNumber && !answerText && !found) {
|
||||
let n = this.botNumber.replace('+', '');
|
||||
if (!message.body.startsWith('@' + n)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -731,6 +756,119 @@ export class WhatsappDirectLine extends GBService {
|
|||
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) {
|
||||
try {
|
||||
const cmd = '/audio ';
|
||||
|
@ -747,26 +885,17 @@ export class WhatsappDirectLine extends GBService {
|
|||
|
||||
switch (this.provider) {
|
||||
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']) {
|
||||
const res = await driver.sendTemplate(to, msg['name'], 'pt_BR', msg['components']);
|
||||
await this.customClient.sendTemplate(to, msg['name'], 'pt_BR', msg['components']);
|
||||
} else {
|
||||
messages = msg.match(/(.|[\r\n]){1,4096}/g);
|
||||
|
||||
await CollectionUtil.asyncForEach(messages, async msg => {
|
||||
await driver.sendText(to, msg);
|
||||
await this.customClient.sendText(to, msg);
|
||||
|
||||
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)}`);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -117,11 +117,11 @@ export class GBServer {
|
|||
});
|
||||
|
||||
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) => {
|
||||
GBLogEx.error(0,`GBREJECTION: ${GBUtil.toYAML(JSON.stringify(err, Object.getOwnPropertyNames(err)))}`);
|
||||
GBLogEx.error(0,`GBREJECTION: ${GBUtil.toYAML(err)}`);
|
||||
});
|
||||
|
||||
// Creates working directory.
|
||||
|
|
Loading…
Add table
Reference in a new issue