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 { 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.

View file

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

View file

@ -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
}
);
}
}

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 =
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}`);
}

View file

@ -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';

View file

@ -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') {

View file

@ -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

View file

@ -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);
}
}
}

View file

@ -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.