Passo a Passo
IZING.IO + FACEBOOK + INSTAGRAM
# Download GIT, Node, Docker Desktop e Ngrok (dev local)
- criar banco de dados
docker run --name postgresql -e TZ="America/Sao_Paulo" --restart=always -e POSTGRES_USER=izing -e POSTGRES_PASSWORD=password -p 5432:5432 -v /data:/var/lib/postgresql/dataizing -d postgres
docker run -e TZ="America/Sao_Paulo" --name redis-izing -p 6379:6379 -d --restart=always redis:latest redis-server --appendonly yes --requirepass "password"
# Clonar repositório
- git clone https://github.com/ldurans/izing.open.io.git
- config env
# Instalar back
- cd backend
- npm i -f
- npx sequelize db:migrate
- npx sequelize db:seed:all
- npm run dev:server
# Instalar front
- cd frontend
- npm i -f
- export NODE_OPTIONS=--openssl-legacy-provider && npm quasar build -P -m -pwa
- export NODE_OPTIONS=--openssl-legacy-provider && npx quasar dev
###### Construir integração
# Ngrok
- ngrok http 3100
- https://7401-2804-3d34-5005-ec01-00-1.ngrok-free.app
# Back
- backend/package.json
# executar npm i -f (backend)
- backend/src/database/seeds/20200904070004-create-default-settings.ts
- backend/src/models/Contact.ts
- backend/src/models/Whatsapp.ts
- backend/src/helpers/ConvertMp3ToMp4.ts
- backend/src/helpers/DownloadFiles.ts
- backend/src/helpers/SetChannelWebhook.ts
- backend/src/helpers/ShowHubToken.ts
- backend/src/services/WbotNotificame/CreateChannelsService.ts
- backend/src/services/WbotNotificame/CreateMessageService.ts
- backend/src/services/WbotNotificame/CreateOrUpdateTicketService.ts
- backend/src/services/WbotNotificame/FindOrCreateContactService.ts
- backend/src/services/WbotNotificame/HubMessageListener.ts
- backend/src/services/WbotNotificame/ListChannels.ts
- backend/src/services/WbotNotificame/SendMediaMessageService.ts
- backend/src/services/WbotNotificame/SendTextMessageService.ts
- backend/src/services/WbotNotificame/UpdateMessageAck.ts
- backend/src/services/WbotServices/StartAllWhatsAppsSessions.ts
- backend/src/controllers/ChannelHubController.ts
- backend/src/controllers/ContactController.ts
- backend/src/controllers/MessageHubController.ts
- backend/src/controllers/WebhookHubController.ts
- backend/src/routes/hubChannelRoutes.ts
- backend/src/routes/hubMessageRoutes.ts
- backend/src/routes/hubWebhookRoutes.ts
- backend/src/routes/index.ts
# Front:
- frontend/public/hub-logo.png
- frontend/public/hub_facebook-logo.png
- frontend/public/hub_instagram-logo.png
- frontend/src/pages/atendimento/InputMensagem.vue
- frontend/src/pages/configuracoes/Index.vue
- frontend/src/pages/contatos/ContatoModal.vue
- frontend/src/pages/sessaoWhatsapp/Index.vue
- frontend/src/pages/sessaoWhatsapp/ModalWhatsapp.vue
- frontend/src/service/hub.js
BACKEND
package.json
{"name":"backend","version":"2.0.0","description":"izing.open.io - sistema de atendimentos multicanal","main":"index.js","scripts":{"build":"tsc","watch":"tsc -w","start":"nodemon --inspect dist\/server.js","dev:server":"ts-node-dev --inspect=9229 --respawn --transpile-only --ignore node_modules src\/server.ts","pretest":"NODE_ENV=test sequelize db:migrate && NODE_ENV=test sequelize db:seed:all","test":"NODE_ENV=test jest","posttest":"NODE_ENV=test sequelize db:migrate:undo:all","db:migrate":"rm -Rf dist && npm run build && npx sequelize db:migrate","db:seed":"npx sequelize db:seed:all"},"engines":{"node":">=18"},"author":"Durans (lumardyelson@gmail.com)","private":true,"license":"AGPL-3.0-or-later","dependencies":{"@androz2091\/insta.js":"^1.6.1","@ffmpeg-installer\/ffmpeg":"^1.1.0","@sentry\/node":"5.27.0","@types\/pino":"^6.3.5","amqplib":"^0.10.3","asterisk-manager":"^0.2.0","axios":"^0.21.1","bcryptjs":"^2.4.3","body-parser":"^1.19.0","bull":"3.22.8","bull-board":"^1.4.1","content-disposition":"^0.5.4","cookie-parser":"^1.4.5","cors":"^2.8.5","date-fns":"^2.16.1","date-fns-tz":"^1.1.3","dotenv":"^8.2.0","express":"^4.17.1","express-async-errors":"^3.1.1","file-type":"^19.5.0","fluent-ffmpeg":"^2.1.2","helmet":"^4.4.1","http-graceful-shutdown":"^2.4.0","instagram_mqtt":"^1.2.2","instagram-private-api":"^1.45.3","ioredis":"^5.2.5","is-base64":"^1.1.0","jsonwebtoken":"^8.5.1","lodash":"^4.17.21","messaging-api-messenger":"^1.1.0","mime-types":"^2.1.29","multer":"^1.4.2","mustache":"^4.2.0","newrelic":"^8.16.0","notificamehubsdk":"^0.0.19","pg":"^8.4.1","pino":"^6.10.0","pino-http":"^5.6.0","pino-pretty":"^9.1.1","qrcode-terminal":"^0.12.0","redis":"^3.1.2","reflect-metadata":"^0.1.13","sequelize":"^5.22.5","sequelize-typescript":"^1.1.0","sharp":"^0.32.0","slugify":"^1.6.1","socket.io":"^3.1.2","socket.io-redis":"^6.1.1","telegraf":"^4.0.3","uuid":"^8.3.2","whatsapp-web.js":"github:ldurans\/whatsapp-web.js#webpack-exodus","winston":"^3.3.3","xlsx":"^0.18.5","yup":"^0.29.3"},"devDependencies":{"@types\/amqplib":"^0.10.1","@types\/bcryptjs":"^2.4.2","@types\/bluebird":"^3.5.32","@types\/body-parser":"^1.19.0","@types\/bull":"^3.15.0","@types\/content-disposition":"^0.5.4","@types\/cookie-parser":"^1.4.2","@types\/cors":"^2.8.7","@types\/express":"^4.17.8","@types\/factory-girl":"^5.0.2","@types\/faker":"^5.1.3","@types\/fluent-ffmpeg":"^2.1.20","@types\/helmet":"^4.0.0","@types\/is-base64":"^1.1.0","@types\/jest":"^26.0.15","@types\/jsonwebtoken":"^8.5.0","@types\/lodash":"^4.14.176","@types\/mime-types":"^2.1.0","@types\/multer":"^1.4.4","@types\/mustache":"^4.2.2","@types\/node":"^14.11.8","@types\/pino-http":"^5.4.2","@types\/redis":"^2.8.28","@types\/sequelize":"^4.28.14","@types\/sharp":"^0.31.1","@types\/socket.io":"^2.1.13","@types\/socket.io-redis":"^1.0.27","@types\/supertest":"^2.0.10","@types\/uuid":"^8.3.0","@types\/validator":"^13.1.0","@types\/yup":"^0.29.8","@typescript-eslint\/eslint-plugin":"^4.4.0","@typescript-eslint\/parser":"^4.4.0","eslint":"^7.10.0","eslint-config-airbnb-base":"^14.2.0","eslint-config-prettier":"^6.12.0","eslint-import-resolver-typescript":"^2.3.0","eslint-plugin-import":"^2.22.1","eslint-plugin-prettier":"^3.1.4","factory-girl":"^5.0.4","faker":"^5.1.0","javascript-obfuscator":"^4.0.2","jest":"^26.6.0","nodemon":"^2.0.4","prettier":"^2.1.2","sequelize-cli":"^6.4.1","supertest":"^5.0.0","ts-jest":"^26.4.1","ts-node-dev":"^1.0.0-pre.63","typegram":"^3.5.1","typescript":"^4.8.4"}}
DATABASE
backend\src\database\seeds\20200904070004-create-default-settings.ts
import { QueryInterface } from "sequelize";
module.exports = {
up: (queryInterface: QueryInterface) => {
return queryInterface.sequelize.query(
`
INSERT INTO public."Settings" ("key", value, "createdAt", "updatedAt", "tenantId", id) VALUES('userCreation', 'disabled', '2020-12-12 16:08:45.354', '2020-12-12 16:08:45.354', 1, 1);
INSERT INTO public."Settings" ("key", value, "createdAt", "updatedAt", "tenantId", id) VALUES('NotViewTicketsQueueUndefined', 'disabled', '2020-12-12 16:08:45.354', '2020-12-12 16:08:45.354', 1, 2);
INSERT INTO public."Settings" ("key", value, "createdAt", "updatedAt", "tenantId", id) VALUES('NotViewTicketsChatBot', 'disabled', '2020-12-12 16:08:45.354', '2020-12-12 16:08:45.354', 1, 3);
INSERT INTO public."Settings" ("key", value, "createdAt", "updatedAt", "tenantId", id) VALUES('DirectTicketsToWallets', 'disabled', '2020-12-12 16:08:45.354', '2020-12-12 16:08:45.354', 1, 4);
INSERT INTO public."Settings" ("key", value, "createdAt", "updatedAt", "tenantId", id) VALUES('NotViewAssignedTickets', 'disabled', '2020-12-12 16:08:45.354', '2020-12-12 16:08:45.354', 1, 6);
INSERT INTO public."Settings" ("key", value, "createdAt", "updatedAt", "tenantId", id) VALUES('botTicketActive', '3', '2020-12-12 16:08:45.354', '2022-07-01 21:10:02.076', 1, 5);
INSERT INTO public."Settings" ("key", value, "createdAt", "updatedAt", "tenantId", id) VALUES('ignoreGroupMsg', 'enabled', '2022-12-16 16:08:45.354' , '2022-12-16 21:10:02.076', 1, 7);
INSERT INTO public."Settings" ("key", value, "createdAt", "updatedAt", "tenantId", id) VALUES('rejectCalls', 'disabled', '2020-12-12 16:08:45.354', '2020-12-12 16:08:45.354', 1, 9);
INSERT INTO public."Settings" ("key", value, "createdAt", "updatedAt", "tenantId", id) VALUES('callRejectMessage', 'As chamadas de voz e vídeo estão desabilitas para esse WhatsApp, favor enviar uma mensagem de texto.', '2020-12-12 16:08:45.354', '2020-12-12 16:08:45.354', 1, 10);
INSERT INTO public."Settings" ("key", value, "createdAt", "updatedAt", "tenantId", id) VALUES('hubToken', 'disabled', '2020-12-12 16:08:45.354', '2022-07-01 21:10:02.076', 1, 11);
`
);
},
down: (queryInterface: QueryInterface) => {
return queryInterface.bulkDelete("Tenants", {});
}
};
MODELS
backend\src\models\Contact.ts
import {
Table,
Column,
CreatedAt,
UpdatedAt,
Model,
PrimaryKey,
AutoIncrement,
AllowNull,
Default,
HasMany,
BeforeCreate,
ForeignKey,
BelongsTo,
BelongsToMany
} from "sequelize-typescript";
import Campaign from "./Campaign";
import CampaignContacts from "./CampaignContacts";
import ContactCustomField from "./ContactCustomField";
import ContactWallet from "./ContactWallet";
// import Message from "./Message";
import Tags from "./Tag";
import Tenant from "./Tenant";
import Ticket from "./Ticket";
import ContactTag from "./ContactTag";
import User from "./User";
@Table
class Contact extends Model {
@PrimaryKey
@AutoIncrement
@Column
id: number;
@Column
name: string;
@AllowNull(true)
@Column
number: string;
@AllowNull(true)
@Default(null)
@Column
email: string;
@Column
profilePicUrl: string;
@AllowNull(true)
@Default(null)
@Column
pushname: string;
@AllowNull(true)
@Default(null)
@Column
telegramId: string;
@AllowNull(true)
@Default(null)
@Column
messengerId: string;
@AllowNull(true)
@Default(null)
@Column
instagramPK: number;
@Default(false)
@Column
isUser: boolean;
@Default(false)
@Column
isWAContact: boolean;
@Default(false)
@Column
isGroup: boolean;
@CreatedAt
createdAt: Date;
@UpdatedAt
updatedAt: Date;
@HasMany(() => Ticket)
tickets: Ticket[];
@HasMany(() => ContactCustomField)
extraInfo: ContactCustomField[];
@BelongsToMany(() => Tags, () => ContactTag, "contactId", "tagId")
tags: Tags[];
@BelongsToMany(() => User, () => ContactWallet, "contactId", "walletId")
wallets: ContactWallet[];
@HasMany(() => ContactWallet)
contactWallets: ContactWallet[];
@HasMany(() => CampaignContacts)
campaignContacts: CampaignContacts[];
@BelongsToMany(
() => Campaign,
() => CampaignContacts,
"contactId",
"campaignId"
)
campaign: Campaign[];
@ForeignKey(() => Tenant)
@Column
tenantId: number;
@BelongsTo(() => Tenant)
tenant: Tenant;
}
export default Contact;
backend\src\models\Whatsapp.ts
import { sign } from "jsonwebtoken";
import {
Table,
Column,
CreatedAt,
UpdatedAt,
Model,
DataType,
PrimaryKey,
AutoIncrement,
Default,
AllowNull,
HasMany,
Unique,
ForeignKey,
BelongsTo,
AfterUpdate,
BeforeCreate,
BeforeUpdate
// DefaultScope
} from "sequelize-typescript";
import webHooks from "../config/webHooks.dev.json";
import authConfig from "../config/auth";
import Queue from "../libs/Queue";
import ApiConfig from "./ApiConfig";
import Tenant from "./Tenant";
import Ticket from "./Ticket";
import ChatFlow from "./ChatFlow";
// @DefaultScope(() => ({
// where: { isDeleted: false }
// }))
@Table
class Whatsapp extends Model {
@PrimaryKey
@AutoIncrement
@Column
id: number;
@AllowNull
@Unique
@Column(DataType.TEXT)
name: string;
@Column(DataType.TEXT)
session: string;
@Column(DataType.TEXT)
qrcode: string;
@Column
status: string;
@Column
battery: string;
@Column
plugged: boolean;
@Default(true)
@Column
isActive: boolean;
@Default(false)
@Column
isDeleted: boolean;
@Column
retries: number;
@Default(false)
@AllowNull
@Column
isDefault: boolean;
@Default(null)
@AllowNull
@Column
tokenTelegram: string;
@Default(null)
@AllowNull
@Column
instagramUser: string;
@Default(null)
@AllowNull
@Column
instagramKey: string;
@Default(null)
@AllowNull
@Column
fbPageId: string;
@Default(null)
@AllowNull
@Column(DataType.JSONB)
// eslint-disable-next-line @typescript-eslint/ban-types
fbObject: object;
@Default("whatsapp")
@Column(DataType.ENUM("whatsapp", "telegram", "instagram", "messenger"))
type: string;
@CreatedAt
createdAt: Date;
@UpdatedAt
updatedAt: Date;
@Column
number: string;
@Column(DataType.JSONB)
// eslint-disable-next-line @typescript-eslint/ban-types
phone: object;
@HasMany(() => Ticket)
tickets: Ticket[];
@ForeignKey(() => Tenant)
@Column
tenantId: number;
@BelongsTo(() => Tenant)
tenant: Tenant;
@ForeignKey(() => ChatFlow)
@Column
chatFlowId: number;
@BelongsTo(() => ChatFlow)
chatFlow: ChatFlow;
@Default(null)
@AllowNull
@Column(DataType.ENUM("360", "gupshup"))
wabaBSP: string;
@Default(null)
@AllowNull
@Column(DataType.TEXT)
tokenAPI: string;
@Default(null)
@AllowNull
@Column(DataType.TEXT)
tokenHook: string;
@Default(null)
@AllowNull
@Column(DataType.TEXT)
farewellMessage: string;
@Column(DataType.VIRTUAL)
get UrlWabaWebHook(): string | null {
const key = this.getDataValue("tokenHook");
const wabaBSP = this.getDataValue("wabaBSP");
let BACKEND_URL;
BACKEND_URL = process.env.BACKEND_URL;
if (process.env.NODE_ENV === "dev") {
BACKEND_URL = webHooks.urlWabahooks;
}
return `${BACKEND_URL}/wabahooks/${wabaBSP}/${key}`;
}
@Column(DataType.VIRTUAL)
get UrlMessengerWebHook(): string | null {
const key = this.getDataValue("tokenHook");
let BACKEND_URL;
BACKEND_URL = process.env.BACKEND_URL;
if (process.env.NODE_ENV === "dev") {
BACKEND_URL = webHooks.urlWabahooks;
}
return `${BACKEND_URL}/fb-messenger-hooks/${key}`;
}
@AfterUpdate
// eslint-disable-next-line @typescript-eslint/no-explicit-any
static async HookStatus(instance: Whatsapp & any): Promise {
const { status, name, qrcode, number, tenantId, id: sessionId } = instance;
const payload: any = {
name,
number,
status,
qrcode,
timestamp: Date.now(),
type: "hookSessionStatus"
};
const apiConfig: any = await ApiConfig.findAll({
where: { tenantId, sessionId }
});
if (!apiConfig) return;
await Promise.all(
apiConfig.map((api: ApiConfig) => {
if (api.urlServiceStatus) {
if (api.authToken) {
payload.authToken = api.authToken;
}
return Queue.add("WebHooksAPI", {
url: api.urlServiceStatus,
type: payload.type,
payload
});
}
})
);
}
@BeforeUpdate
@BeforeCreate
static async CreateTokenWebHook(instance: Whatsapp): Promise {
const { secret } = authConfig;
if (
!instance?.tokenHook &&
(instance.type === "waba" || instance.type === "messenger")
) {
const tokenHook = sign(
{
tenantId: instance.tenantId,
whatsappId: instance.id
// wabaBSP: instance.wabaBSP
},
secret,
{
expiresIn: "10000d"
}
);
instance.tokenHook = tokenHook;
}
}
}
export default Whatsapp;
HELPERS
backend\src\helpers\ConvertMp3ToMp4.ts
import ffmpeg from "fluent-ffmpeg";
import { path as ffmpegPath } from "@ffmpeg-installer/ffmpeg";
import fs from "fs";
import { logger } from "../utils/logger";
// CONVERTER MP3 PARA MP4
const convertMp3ToMp4 = (input: string, outputMP4: string): Promise => {
return new Promise((resolve, reject) => {
ffmpeg.setFfmpegPath(ffmpegPath);
logger.info(`Converting ${input} to ${outputMP4}`);
if (!fs.existsSync(input)) {
const errorMsg = `Input file does not exist: ${input}`;
logger.error(errorMsg);
return reject(new Error(errorMsg));
}
ffmpeg(input)
.inputFormat("mp3")
.output(outputMP4)
.outputFormat("mp4")
.on("start", (commandLine) => {
logger.info(`Spawned Ffmpeg with command: ${commandLine}`);
})
.on("error", (error: Error) => {
logger.info(`Encoding Error: ${error.message}`);
reject(error);
})
.on("progress", (progress) => {
logger.info(`Processing: ${progress.percent}% done`);
})
.on("end", () => {
logger.info("Video Transcoding succeeded !");
resolve();
})
.run();
});
};
export { convertMp3ToMp4 };
backend\src\helpers\DownloadFiles.ts
import axios from "axios";
import { extname, join } from "path";
import { writeFile } from "fs/promises";
import mime from "mime-types";
import { logger } from "../utils/logger";
export const downloadFiles = async (url: string) => {
try {
const { data } = await axios.get(url, {
responseType: "arraybuffer"
});
let type
if (url.includes('com/ig_messaging_cdn')) {
const { fileTypeFromBuffer } = await (eval(
'import("file-type")'
) as Promise);
const fileTypeResult = await fileTypeFromBuffer(data);
if (!fileTypeResult) {
throw new Error('Não foi possível determinar o tipo do arquivo.');
}
type = fileTypeResult.ext
} else {
type = url.split("?")[0].split(".").pop();
}
logger.info("type " + type);
const filename = `${new Date().getTime()}.${type}`;
const filePath = `${__dirname}/../../public/${filename}`;
await writeFile(
join(__dirname, "..", "..", "public", filename),
data,
"base64"
);
const mimeType = mime.lookup(filePath);
const extension = extname(filePath);
const originalname = url.split("/").pop();
const media = {
mimeType,
extension,
filename,
data,
originalname
};
return media;
} catch (error) {
logger.warn("e1 " + error)
throw error;
}
};
backend\src\helpers\SetChannelWebhook.ts
import Whatsapp from "../models/Whatsapp";
import { IChannel } from "../controllers/ChannelHubController";
import { showHubToken } from "./ShowHubToken";
import { Client, MessageSubscription } from "notificamehubsdk";
import { logger } from "../utils/logger";
export const setChannelWebhook = async (
whatsapp: IChannel,
whatsappId: string
) => {
const notificameHubToken = await showHubToken(whatsapp.tenantId.toString());
const client = new Client(notificameHubToken);
const url = `${process.env.BACKEND_URL}/hub-webhook/${whatsapp.number}`;
// const url = `https://7401-2804-3d34-5005-ec01-00-1.ngrok-free.app/hub-webhook/${whatsapp.number}`;
const subscription = new MessageSubscription(
{
url
},
{
channel: whatsapp.number
}
);
client
.createSubscription(subscription)
.then((response: any) => {
logger.info("Webhook subscribed " + response);
})
.catch((error: any) => {
logger.warn("Webhook subscribed e " + error);
});
await Whatsapp.update(
{
status: "CONNECTED"
},
{
where: {
id: whatsappId
}
}
);
};
backend\src\helpers\ShowHubToken.ts
import Setting from "../models/Setting";
export const showHubToken = async (tenantId: string): Promise => {
const hubToken = await Setting.findOne({
where: { key: "hubToken", tenantId }
});
console.log()
if (!hubToken?.value || typeof hubToken?.value !== 'string') {
throw new Error("Notificame Hub token not found");
}
return hubToken.value;
};
SERVICES
backend\src\services\WbotNotificame\CreateChannelsService.ts
import Tenant from "../../models/Tenant";
import AppError from "../../errors/AppError";
import Whatsapp from "../../models/Whatsapp";
import { IChannel } from "../../controllers/ChannelHubController";
import { getIO } from "../../libs/socket";
interface Request {
tenantId: number;
channels: IChannel[];
}
interface Response {
whatsapps: Whatsapp[];
}
const CreateChannelsService = async ({
tenantId,
channels = []
}: Request): Promise => {
const tenant = await Tenant.findOne({
where: {
id: tenantId
}
});
if (tenant !== null) {
let whatsappCount = await Whatsapp.count({
where: {
tenantId
}
});
whatsappCount += channels.length;
}
channels = channels.map(channel => {
return {
...channel,
status: "CONNECTED",
tenantId
};
});
const whatsapps = await Whatsapp.bulkCreate(channels, {
ignoreDuplicates: true
});
return { whatsapps };
};
export default CreateChannelsService;
backend\src\services\WbotNotificame\CreateMessageService.ts
import Message from "../../models/Message";
import { logger } from "../../utils/logger";
import socketEmit from "../../helpers/socketEmit";
import { v4 as uuidv4 } from 'uuid';
import Ticket from "../../models/Ticket";
interface MessageData {
id: string;
contactId: number;
body: string;
ticketId: number;
fromMe: boolean;
tenantId: number;
fileName?: string;
mediaType?: string;
originalName?: string;
scheduleDate?: string | Date;
ack?: any;
}
const CreateMessageService = async (
messageData: MessageData
): Promise => {
logger.info("creating message ");
// logger.info("messageData " + JSON.stringify(messageData));
const message = await Message.findOne({
where: {
messageId: messageData.id,
tenantId: messageData.tenantId
}
});
if(message){
logger.info("creating message exists");
return
}
const {
id,
contactId,
body,
ticketId,
fromMe,
tenantId,
fileName,
mediaType,
originalName,
scheduleDate,
ack
} = messageData;
if ((!body || body === "") && (!fileName || fileName === "")) {
return;
}
const idHub = uuidv4();
const data: any = {
contactId,
body,
ticketId,
fromMe,
tenantId,
messageId: id,
ack: 2,
status: "sended",
idFront: idHub,
read: false,
scheduleDate
};
if (fileName) {
data.mediaUrl = fileName;
data.mediaType = mediaType === "photo" ? "image" : mediaType;
data.body = data.mediaUrl;
}
// logger.info("creating message data" + JSON.stringify(data));
try {
const newMessage = await Message.create(data);
const messageCreated = await Message.findByPk(newMessage.id, {
include: [
{
model: Ticket,
as: "ticket",
where: { tenantId },
include: ["contact"]
},
{
model: Message,
as: "quotedMsg",
include: ["contact"]
}
]
});
if (!messageCreated) {
throw new Error("ERR_CREATING_MESSAGE_SYSTEM 2");
}
socketEmit({
tenantId,
type: "chat:create",
payload: messageCreated
});
return newMessage;
} catch (error) {
logger.warn("e8" + error);
}
};
export default CreateMessageService;
backend\src\services\WbotNotificame\CreateOrUpdateTicketService.ts
import { Op } from "sequelize";
import Ticket from "../../models/Ticket";
import Whatsapp from "../../models/Whatsapp";
import { IContent } from "./HubMessageListener";
import { getIO } from "../../libs/socket";
import Tenant from "../../models/Tenant";
import Contact from "../../models/Contact";
import User from "../../models/User";
import { logger } from "../../utils/logger";
import socketEmit from "../../helpers/socketEmit";
interface TicketData {
contactId: number;
channel: string;
contents: IContent[];
whatsapp: Whatsapp;
}
const CreateOrUpdateTicketService = async (
ticketData: TicketData
): Promise => {
logger.info("creating ticket ");
const { contactId, channel, contents, whatsapp } = ticketData;
const io = getIO();
const SettingTenant = await Tenant.findOne({
where: { id: whatsapp.tenantId }
});
let statusCondition = ["open", "pending"];
let orderClause: [string, string][] = [];
const ticketExists = await Ticket.findOne({
where: {
status: {
[Op.or]: statusCondition
// [Op.or]: ["open", "pending"]
// // manter historico groupTickets
// [Op.or]: ["open", "pending", "closed"]
},
tenantId: whatsapp.tenantId,
whatsappId: whatsapp.id,
contactId: contactId
},
order: orderClause.length > 0 ? orderClause : undefined,
include: [
{
model: Contact,
as: "contact",
include: [
"extraInfo",
"tags",
{
association: "wallets",
attributes: ["id", "name"]
}
]
},
{
model: User,
as: "user",
attributes: ["id", "name", "isOnline"]
},
{
association: "whatsapp",
attributes: ["id", "name", "tokenAPI", "chatFlowId", "status", "bmToken", "wabaVersion"]
}
]
});
if (ticketExists) {
logger.info("ticket exists ");
let newStatus = ticketExists.status;
let newQueueId = ticketExists.queueId;
if (ticketExists.status === "closed") {
newStatus = "pending";
}
await ticketExists.update({
answered: false,
lastMessage: contents[0].text,
status: newStatus,
queueId: newQueueId
});
logger.info("ticket queue updated " + newQueueId);
await ticketExists.reload({
include: [
{
association: "contact"
},
{
association: "user"
},
{
association: "queue"
},
{
association: "whatsapp"
}
]
});
socketEmit({
tenantId: whatsapp.tenantId,
type: "ticket:update",
payload: ticketExists
});
return ticketExists;
}
const newTicket = await Ticket.create({
status: "pending",
channel: "hub_" + channel,
lastMessage: contents[0].text,
contactId,
whatsappId: whatsapp.id,
tenantId: whatsapp.tenantId,
});
await newTicket.reload({
include: [
{
association: "contact"
},
{
association: "user"
},
{
association: "queue"
},
{
association: "whatsapp"
}
]
});
socketEmit({
tenantId: whatsapp.tenantId,
type: "ticket:update",
payload: newTicket
});
return newTicket;
};
export default CreateOrUpdateTicketService;
backend\src\services\WbotNotificame\FindOrCreateContactService.ts
import Contact from "../../models/Contact";
import Whatsapp from "../../models/Whatsapp";
interface HubContact {
name: string;
firstName: string;
lastName: string;
picture: string;
from: string;
whatsapp: Whatsapp;
channel: string;
}
const FindOrCreateContactService = async (
contact: HubContact
): Promise => {
const { name, picture, firstName, lastName, from, whatsapp, channel } = contact;
let numberFb
let numberIg
let contactExists
if(channel === 'facebook'){
numberFb = from
contactExists = await Contact.findOne({
where: {
messengerId: from,
tenantId: whatsapp.tenantId,
}
});
}
if(channel === 'instagram'){
numberIg = from
contactExists = await Contact.findOne({
where: {
instagramPK: from,
tenantId: whatsapp.tenantId,
}
});
}
if (contactExists) {
await contactExists.update({ name: name || firstName || 'Name Unavailable' , firstName, lastName, profilePicUrl: picture })
return contactExists;
}
const newContact = await Contact.create({
name: name || firstName || '',
firstName,
lastName,
profilePicUrl: picture,
number: null,
tenantId: whatsapp.tenantId,
messengerId: numberFb || null,
instagramPK: numberIg || null
});
return newContact;
};
export default FindOrCreateContactService;
backend\src\services\WbotNotificame\HubMessageListener.ts
import Whatsapp from "../../models/Whatsapp";
import socketEmit from "../../helpers/socketEmit";
import { downloadFiles } from "../../helpers/DownloadFiles";
import { logger } from "../../utils/logger";
import CreateMessageService from "./CreateMessageService";
import FindOrCreateContactService from "./FindOrCreateContactService";
import { UpdateMessageAck } from "./UpdateMessageAck";
import FindOrCreateTicketService from "../TicketServices/FindOrCreateTicketService";
export interface HubInMessage {
type: "MESSAGE";
id: string;
timestamp: string;
subscriptionId: string;
channel: "telegram" | "whatsapp" | "facebook" | "instagram" | "sms" | "email";
direction: "IN";
message: {
id: string;
from: string;
to: string;
direction: "IN";
channel:
| "telegram"
| "whatsapp"
| "facebook"
| "instagram"
| "sms"
| "email";
visitor: {
name: string;
firstName: string;
lastName: string;
picture: string;
};
contents: IContent[];
timestamp: string;
};
}
export interface IContent {
type: "text" | "image" | "audio" | "video" | "file" | "location";
text?: string;
url?: string;
fileUrl?: string;
latitude?: number;
longitude?: number;
filename?: string;
fileSize?: number;
fileMimeType?: string;
}
export interface HubConfirmationSentMessage {
type: "MESSAGE_STATUS";
timestamp: string;
subscriptionId: string;
channel: "telegram" | "whatsapp" | "facebook" | "instagram" | "sms" | "email";
messageId: string;
contentIndex: number;
messageStatus: {
timestamp: string;
code: "SENT" | "REJECTED";
description: string;
};
}
const verifySentMessageStatus = (message: HubConfirmationSentMessage) => {
const {
messageStatus: { code }
} = message;
const isMessageSent = code === "SENT";
if (isMessageSent) {
return true;
}
return false;
};
const HubMessageListener = async (
message: any | HubInMessage | HubConfirmationSentMessage,
whatsapp: Whatsapp,
medias: Express.Multer.File[]
) => {
logger.info("HubMessageListener " + JSON.stringify(message));
if(message.direction === 'IN'){
message.fromMe = false
}
// const ignoreEvent =
// message?.message.visitor?.name === "" || !message?.message.visitor?.name;
const ignoreEvent = message.direction === 'OUT'
if (ignoreEvent) {
return;
}
const isMessageFromMe = message.type === "MESSAGE_STATUS";
logger.info("HubMessageListener MESSAGE_STATUS " + isMessageFromMe)
if (isMessageFromMe) {
const isMessageSent = verifySentMessageStatus(
message as HubConfirmationSentMessage
);
if (isMessageSent) {
logger.info("HubMessageListener: message sent ");
UpdateMessageAck(message.messageId);
} else {
logger.info("HubMessageListener: message not sent " + message.messageStatus.code + " - " + message.messageStatus.description);
}
return;
}
const {
message: { id, from, channel, contents, visitor }
} = message as HubInMessage;
try {
const contact = await FindOrCreateContactService({
...visitor,
from,
whatsapp,
channel
});
const unreadMessages = 1
message.body = contents[0].text
const ticket = await FindOrCreateTicketService({
contact,
whatsappId: whatsapp.id!,
unreadMessages,
tenantId: whatsapp.tenantId,
groupContact: undefined,
msg: message,
channel: 'hub_' + channel
});
if (contents[0]?.type === "text") {
await CreateMessageService({
id,
contactId: contact.id,
body: contents[0].text || '',
ticketId: ticket.id,
fromMe: false,
tenantId: whatsapp.tenantId
});
await ticket.update({lastMessage: contents[0].text})
socketEmit({
tenantId: whatsapp.tenantId,
type: "ticket:update",
payload: ticket
});
} else if (contents[0]?.fileUrl) {
const media = await downloadFiles(contents[0].fileUrl);
if (typeof media.mimeType === "string") {
await CreateMessageService({
id,
contactId: contact.id,
body: contents[0].text || '',
ticketId: ticket.id,
fromMe: false,
tenantId: whatsapp.tenantId,
fileName: `${media.filename}`,
mediaType: media.mimeType.split("/")[0],
originalName: media.originalname
});
await ticket.update({lastMessage: media.filename || ''})
socketEmit({
tenantId: whatsapp.tenantId,
type: "ticket:update",
payload: ticket
});
} else {
// Lidar com o caso em que mimeType é false, se necessário
logger.warn("Unable to determine the media type")
}
}
if (ticket?.isFarewellMessage) {
return;
}
} catch (error: any) {
logger.warn("e4 " + error)
}
};
export default HubMessageListener;
backend\src\services\WbotNotificame\ListChannels.ts
import { showHubToken } from "../../helpers/ShowHubToken";
import { logger } from "../../utils/logger";
import { Client } from "notificamehubsdk";
require("dotenv").config();
const ListChannels = async (tenantId: string): Promise => {
try {
const notificameHubToken = await showHubToken(tenantId);
if (!notificameHubToken) {
throw new Error("NOTIFICAMEHUB_TOKEN_NOT_FOUND");
}
const client = new Client(notificameHubToken);
const response = await client.listChannels();
logger.info("" + JSON.stringify(response));
return response;
} catch (error: any) {
logger.warn(" Error in ListChannels: ", error);
if (error instanceof Error) {
throw new Error(error.message);
} else {
throw new Error("An unknown error occurred: " + JSON.stringify(error));
}
}
};
export default ListChannels;
backend\src\services\WbotNotificame\SendMediaMessageService.ts
require("dotenv").config();
import { Client, FileContent } from "notificamehubsdk";
import Contact from "../../models/Contact";
import CreateMessageService from "./CreateMessageService";
import { showHubToken } from "../../helpers/ShowHubToken";
import { logger } from "../../utils/logger";
import Whatsapp from "../../models/Whatsapp";
import { convertMp3ToMp4 } from "../../helpers/ConvertMp3ToMp4";
import Ticket from "../../models/Ticket";
import socketEmit from "../../helpers/socketEmit";
import { v4 as uuidV4 } from "uuid";
export const SendMediaMessageService = async (
media: Express.Multer.File,
message: string,
ticketId: number,
contact: Contact,
whatsapp: any
) => {
let channel
let mediaUrl
const ticket = await Ticket.findOne({
where: { id: ticketId }
});
if(!whatsapp.tenantId || !whatsapp.type || !whatsapp.number){
channel = await Whatsapp.findOne({
where: { id: whatsapp.id }
});
whatsapp = channel
// whatsapp.type = channel?.type
}
const notificameHubToken = await showHubToken(
whatsapp.tenantId.toString()
);
logger.info("Chamou hub send media");
const client = new Client(notificameHubToken);
logger.info("ticket?.channel " + ticket?.channel);
const channelClient = client.setChannel(ticket?.channel.split('hub_')[1]);
try{
message = message.replace(/\n/g, " ");
} catch(e){
logger.warn("Replacing newlines: " + e.message);
}
logger.info("media " + JSON.stringify(media));
// const backendUrl = process.env.BACKEND_URL;
const backendUrl = 'https://7401-2804-3d34-5005-ec01-00-1.ngrok-free.app';
const filename = encodeURIComponent(media.filename);
mediaUrl = `${backendUrl}/public/${filename}`;
if (media.mimetype.includes("image")) {
if (ticket?.channel.split('hub_')[1] === "telegram") {
media.mimetype = "photo";
} else {
media.mimetype = "image";
}
} else if (
(ticket?.channel.split('hub_')[1] === "telegram" || ticket?.channel.split('hub_')[1] === "facebook") &&
media.mimetype.includes("audio")
) {
media.mimetype = "audio";
} else if (
(ticket?.channel.split('hub_')[1] === "telegram" || ticket?.channel.split('hub_')[1] === "facebook") &&
media.mimetype.includes("video")
) {
media.mimetype = "video";
} else if (ticket?.channel.split('hub_')[1] === "telegram" || ticket?.channel.split('hub_')[1] === "facebook") {
media.mimetype = "file";
}
try {
if (media.originalname.includes('.mp3') && ticket?.channel.split('hub_')[1] === "instagram") {
const inputPath = media.path;
const outputMP4Path = `${media.destination}/${media.filename.split('.')[0]}.mp4`;
try {
await convertMp3ToMp4(inputPath, outputMP4Path);
media.filename = outputMP4Path.split('/').pop() ?? 'default.mp4';
mediaUrl = `${backendUrl}/public/${media.filename}`;
media.originalname = media.filename
media.mimetype = 'audio'
} catch(e){
}
}
if (media.originalname.includes('.mp4') && ticket?.channel.split('hub_')[1] === "instagram") {
media.mimetype = 'video'
}
const content = new FileContent(
mediaUrl,
media.mimetype,
media.filename,
media.filename
);
let contactNumber
if(ticket?.channel === 'hub_facebook'){
contactNumber = contact.messengerId
}
if(ticket?.channel === 'hub_instagram'){
contactNumber = contact.instagramPK
}
logger.info("whatsapp.number " + whatsapp.number + " contactNumber " + contactNumber + " content " + content + " message " + message);
let response = await channelClient.sendMessage(
whatsapp.number,
contactNumber,
content
);
logger.info("Hub response: " + JSON.stringify(response));
let data: any;
try {
const jsonStart = response.indexOf("{");
const jsonResponse = response.substring(jsonStart);
data = JSON.parse(jsonResponse);
} catch (error) {
data = response;
}
const newMessage = await CreateMessageService({
id: data?.id || uuidV4(),
contactId: contact.id,
body: `${media.filename}`,
ticketId,
fromMe: true,
tenantId: whatsapp.tenantId,
fileName: `${media.filename}`,
mediaType: media.mimetype.split("/")[0] || media.mimetype,
originalName: media.originalname
});
if(ticket){
await ticket.update({lastMessage: media.filename || '', aswered: true})
socketEmit({
tenantId: whatsapp.tenantId,
type: "ticket:update",
payload: ticket
});
}
return newMessage;
} catch (error) {
logger.warn("e6 " + JSON.stringify(error));
}
};
backend\src\services\WbotNotificame\SendTextMessageService.ts
require("dotenv").config();
import { Client, TextContent } from "notificamehubsdk";
import Contact from "../../models/Contact";
import CreateMessageService from "./CreateMessageService";
import { showHubToken } from "../../helpers/ShowHubToken";
import { logger } from "../../utils/logger";
import Whatsapp from "../../models/Whatsapp";
import Ticket from "../../models/Ticket";
import socketEmit from "../../helpers/socketEmit";
import { pupa } from "../../utils/pupa";
import User from "../../models/User";
export const SendTextMessageService = async (
message: string,
ticketId: number,
contact: Contact,
whatsapp: any
) => {
let channel
const ticket = await Ticket.findOne({
where: { id: ticketId },
include: [
{
model: Contact
},
{
model: User
}
]
});
let body = pupa(message || "", {
protocol: ticket?.protocol || '',
name: ticket?.contact?.name || '',
});
if(!whatsapp.tenantId || !whatsapp.type || !whatsapp.number){
channel = await Whatsapp.findOne({
where: { number: whatsapp.number }
});
whatsapp = channel
}
const notificameHubToken = await showHubToken(
whatsapp.tenantId.toString()
);
const client = new Client(notificameHubToken);
logger.info("ticket?.channel " + ticket?.channel);
const channelClient = client.setChannel(ticket?.channel.split('hub_')[1]);
const content = new TextContent(body);
let contactNumber
if(ticket?.channel === 'hub_facebook'){
contactNumber = contact.messengerId
}
if(ticket?.channel === 'hub_instagram'){
contactNumber = contact.instagramPK
}
try {
logger.info("whatsapp.number " + whatsapp.number + " contactNumber " + contactNumber + " content " + content + " message " + body);
let response = await channelClient.sendMessage(
whatsapp.number,
contactNumber,
content
);
logger.info("" + JSON.stringify(response));
let data: any;
try {
const jsonStart = response.indexOf("{");
const jsonResponse = response.substring(jsonStart);
data = JSON.parse(jsonResponse);
} catch (error) {
data = response;
}
const newMessage = await CreateMessageService({
id: data.id,
contactId: contact.id,
body: body,
ticketId,
fromMe: true,
tenantId: whatsapp.tenantId
});
if(ticket){
await ticket.update({lastMessage: body || '', aswered: true})
socketEmit({
tenantId: whatsapp.tenantId,
type: "ticket:update",
payload: ticket
});
}
return newMessage;
} catch (error) {
logger.warn("e7 " + JSON.stringify(error));
}
};
backend\src\services\WbotNotificame\UpdateMessageAck.ts
import Message from "../../models/Message";
export const UpdateMessageAck = async (messageId: string): Promise => {
const message = await Message.findOne({
where: {
id: messageId
}
});
if (!message) {
return;
}
await message.update({
ack: 4
});
};
backend\src\services\WbotServices\StartAllWhatsAppsSessions.ts
import { Op } from "sequelize";
// import { initInstaBot } from "../../libs/InstaBot";
import Whatsapp from "../../models/Whatsapp";
import { StartInstaBotSession } from "../InstagramBotServices/StartInstaBotSession";
import { StartMessengerBot } from "../MessengerChannelServices/StartMessengerBot";
import { StartTbotSession } from "../TbotServices/StartTbotSession";
import { StartWaba360 } from "../WABA360/StartWaba360";
import { StartWhatsAppSession } from "./StartWhatsAppSession";
import { setChannelWebhook } from "../../helpers/SetChannelWebhook";
// import { StartTbotSession } from "../TbotServices/StartTbotSession";
export const StartAllWhatsAppsSessions = async (): Promise => {
const whatsapps = await Whatsapp.findAll({
where: {
[Op.or]: [
{
[Op.and]: {
type: {
[Op.in]: ["instagram", "telegram", "waba", "messenger", "hub_facebook", "hub_instagram"]
},
status: {
[Op.notIn]: ["DISCONNECTED"]
}
}
},
{
[Op.and]: {
type: "whatsapp"
},
status: {
[Op.notIn]: ["DISCONNECTED", "qrcode"]
// "DISCONNECTED"
}
}
],
isActive: true
}
});
const whatsappSessions = whatsapps.filter(w => w.type === "whatsapp");
const telegramSessions = whatsapps.filter(
w => w.type === "telegram" && !!w.tokenTelegram
);
const instagramSessions = whatsapps.filter(w => w.type === "instagram");
const waba360Sessions = whatsapps.filter(w => w.type === "waba");
const messengerSessions = whatsapps.filter(w => w.type === "messenger");
const hubSessions = whatsapps.filter(w => w.type.includes("hub"));
if (whatsappSessions.length > 0) {
whatsappSessions.forEach(whatsapp => {
StartWhatsAppSession(whatsapp);
});
}
if (telegramSessions.length > 0) {
telegramSessions.forEach(whatsapp => {
StartTbotSession(whatsapp);
});
}
if (waba360Sessions.length > 0) {
waba360Sessions.forEach(channel => {
if (channel.tokenAPI && channel.wabaBSP === "360") {
StartWaba360(channel);
}
});
}
if (instagramSessions.length > 0) {
instagramSessions.forEach(channel => {
if (channel.instagramKey) {
StartInstaBotSession(channel);
}
});
}
if (messengerSessions.length > 0) {
messengerSessions.forEach(channel => {
if (channel.tokenAPI) {
StartMessengerBot(channel);
}
});
}
if (hubSessions.length > 0) {
hubSessions.forEach(channel => {
setChannelWebhook(channel, channel.id.toString());
});
}
};
CONTROLLERS
backend\src\controllers\ChannelHubController.ts
import { Request, Response } from "express";
import CreateChannelsService from "../services/WbotNotificame/CreateChannelsService";
import { getIO } from "../libs/socket";
import ListChannels from "../services/WbotNotificame/ListChannels";
import { setChannelWebhook } from "../helpers/SetChannelWebhook";
export interface IChannel {
name: string;
status?: string;
isDefault?: boolean;
tenantId: string | number;
type: string;
profilePic?: string;
phone?: any;
number?: any;
}
export const store = async (req: Request, res: Response): Promise => {
const { channels = [] } = req.body;
const tenantId = Number(req.user.tenantId);
const { whatsapps } = await CreateChannelsService({
tenantId,
channels
});
whatsapps.forEach(whatsapp => {
setTimeout(() => {
const whatsappChannel: IChannel = {
name: whatsapp.name,
status: whatsapp.status,
isDefault: whatsapp.isDefault,
tenantId: whatsapp.tenantId,
type: whatsapp.type,
phone: whatsapp.phone
};
setChannelWebhook(whatsappChannel, whatsapp.id.toString());
}, 2000);
});
console.log('3')
const io = getIO();
whatsapps.forEach(whatsapp => {
io.emit(`${tenantId}:whatsapp`, {
action: "update",
whatsapp
});
});
return res.status(200).json(whatsapps);
};
export const index = async (req: Request, res: Response): Promise => {
const { tenantId } = req.user;
try {
const channels = await ListChannels(tenantId.toString());
return res.status(200).json(channels);
} catch (error) {
return res.status(500).json({ error: error.message });
}
};
backend\src\controllers\ContactController.ts
import * as Yup from "yup";
import { Request, Response } from "express";
import { head } from "lodash";
import XLSX from "xlsx";
import path from "path";
import { v4 as uuidV4 } from "uuid";
import fs from "fs";
import ListContactsService from "../services/ContactServices/ListContactsService";
import CreateContactService from "../services/ContactServices/CreateContactService";
import ShowContactService from "../services/ContactServices/ShowContactService";
import UpdateContactService from "../services/ContactServices/UpdateContactService";
import DeleteContactService from "../services/ContactServices/DeleteContactService";
import UpdateContactTagsService from "../services/ContactServices/UpdateContactTagsService";
import CheckIsValidContact from "../services/WbotServices/CheckIsValidContact";
import GetProfilePicUrl from "../services/WbotServices/GetProfilePicUrl";
import AppError from "../errors/AppError";
import UpdateContactWalletsService from "../services/ContactServices/UpdateContactWalletsService";
import SyncContactsWhatsappInstanceService from "../services/WbotServices/SyncContactsWhatsappInstanceService";
import Whatsapp from "../models/Whatsapp";
import { ImportFileContactsService } from "../services/WbotServices/ImportFileContactsService";
import Contact from "../models/Contact";
type IndexQuery = {
searchParam: string;
pageNumber: string;
};
interface ExtraInfo {
name: string;
value: string;
}
interface ContactData {
name: string;
number: string;
email?: string;
extraInfo?: ExtraInfo[];
wallets?: null | number[] | string[];
}
export const index = async (req: Request, res: Response): Promise => {
const { tenantId, id: userId, profile } = req.user;
const { searchParam, pageNumber } = req.query as IndexQuery;
const { contacts, count, hasMore } = await ListContactsService({
searchParam,
pageNumber,
tenantId,
profile,
userId
});
return res.json({ contacts, count, hasMore });
};
export const store = async (req: Request, res: Response): Promise => {
const { tenantId } = req.user;
const newContact: ContactData = req.body;
newContact.number = newContact.number.replace("-", "").replace(" ", "");
const schema = Yup.object().shape({
name: Yup.string().required(),
// number: Yup.string()
// .required()
// .matches(/^\d+$/, "Invalid number format. Only numbers is allowed.")
});
try {
await schema.validate(newContact);
} catch (err) {
throw new AppError(err.message);
}
const waNumber = await CheckIsValidContact(newContact.number, tenantId);
const profilePicUrl = await GetProfilePicUrl(newContact.number, tenantId);
const contact = await CreateContactService({
...newContact,
number: waNumber.user,
profilePicUrl,
tenantId
});
return res.status(200).json(contact);
};
export const show = async (req: Request, res: Response): Promise => {
const { contactId } = req.params;
const { tenantId } = req.user;
const contact = await ShowContactService({ id: contactId, tenantId });
return res.status(200).json(contact);
};
export const update = async (
req: Request,
res: Response
): Promise => {
const contactData: ContactData = req.body;
const { tenantId } = req.user;
const schema = Yup.object().shape({
name: Yup.string(),
// number: Yup.string().matches(
// /^\d+$/,
// "Invalid number format. Only numbers is allowed."
// )
});
try {
await schema.validate(contactData);
} catch (err) {
throw new AppError(err.message);
}
let waNumber
let contact
const { contactId } = req.params;
console.log(contactData.number)
if(contactData.number !== 'null' && contactData.number !== ''){
waNumber = await CheckIsValidContact(contactData.number, tenantId);
contactData.number = waNumber.user;
contact = await UpdateContactService({
contactData,
contactId,
tenantId
});
} else {
contact = await UpdateContactService({
contactData,
contactId,
tenantId
});
}
return res.status(200).json(contact);
};
export const remove = async (
req: Request,
res: Response
): Promise => {
const { contactId } = req.params;
const { tenantId } = req.user;
await DeleteContactService({ id: contactId, tenantId });
return res.status(200).json({ message: "Contact deleted" });
};
export const updateContactTags = async (
req: Request,
res: Response
): Promise => {
const { tags } = req.body;
const { contactId } = req.params;
const { tenantId } = req.user;
const contact = await UpdateContactTagsService({
tags,
contactId,
tenantId
});
return res.status(200).json(contact);
};
export const updateContactWallet = async (
req: Request,
res: Response
): Promise => {
const { wallets } = req.body;
const { contactId } = req.params;
const { tenantId } = req.user;
const contact = await UpdateContactWalletsService({
wallets,
contactId,
tenantId
});
return res.status(200).json(contact);
};
export const syncContacts = async (
req: Request,
res: Response
): Promise => {
const { tenantId } = req.user;
const sessoes = await Whatsapp.findAll({
where: {
tenantId,
status: "CONNECTED",
type: "whatsapp"
}
});
if (!sessoes.length) {
throw new AppError(
"Não existem sessões ativas para sincronização dos contatos."
);
}
await Promise.all(
sessoes.map(async s => {
if (s.id) {
if (s.id) {
await SyncContactsWhatsappInstanceService(s.id, +tenantId);
}
}
})
);
return res
.status(200)
.json({ message: "Contatos estão sendo sincronizados." });
};
export const upload = async (req: Request, res: Response) => {
const files = req.files as Express.Multer.File[];
const file: Express.Multer.File = head(files) as Express.Multer.File;
const { tenantId } = req.user;
let { tags, wallets } = req.body;
if (tags) {
tags = tags.split(",");
}
if (wallets) {
wallets = wallets.split();
}
const response = await ImportFileContactsService(
+tenantId,
file,
tags,
wallets
);
// const io = getIO();
// io.emit(`company-${companyId}-contact`, {
// action: "reload",
// records: response
// });
return res.status(200).json(response);
};
export const exportContacts = async (req: Request, res: Response) => {
const { tenantId } = req.user;
const contacts = await Contact.findAll({
where: { tenantId },
attributes: ["id", "name", "number", "email"],
order: [["name", "ASC"]],
raw: true
});
// Cria um novo workbook e worksheet
const workbook = XLSX.utils.book_new();
const worksheet = XLSX.utils.json_to_sheet(contacts);
// Adiciona o worksheet ao workbook
XLSX.utils.book_append_sheet(workbook, worksheet, "Contatos");
// Gera o arquivo Excel no formato .xlsx
const excelBuffer = XLSX.write(workbook, {
bookType: "xlsx",
type: "buffer"
});
// Define o nome do arquivo
const fileName = `${uuidV4()}_contatos.xlsx`;
const filePath = path.join(__dirname, "..", "..", "public", "downloads");
const file = path.join(filePath, fileName);
// Cria os diretórios de downloads se eles não existirem
if (!fs.existsSync(filePath)) {
fs.mkdirSync(filePath, { recursive: true });
}
// Salva o arquivo no diretório de downloads
fs.writeFile(file, excelBuffer, err => {
if (err) {
console.error("Erro ao salvar arquivo:", err);
return res.status(500).send("Erro ao exportar contatos");
}
const { BACKEND_URL } = process.env;
const downloadLink = `${BACKEND_URL}:${process.env.PROXY_PORT}/public/downloads/${fileName}`;
res.send({ downloadLink });
});
};
backend\src\controllers\MessageHubController.ts
import { Request, Response } from "express";
import Contact from "../models/Contact";
import Ticket from "../models/Ticket";
import { SendTextMessageService } from "../services/WbotNotificame/SendTextMessageService";
import Whatsapp from "../models/Whatsapp";
import { SendMediaMessageService } from "../services/WbotNotificame/SendMediaMessageService";
import { logger } from "../utils/logger";
export const send = async (req: Request, res: Response): Promise => {
const { body: message } = req.body;
const { ticketId } = req.params;
const medias = req.files as Express.Multer.File[];
logger.info("Sending hub message controller");
const ticket = await Ticket.findByPk(ticketId, {
include: [
{
model: Contact,
as: "contact",
include: [
"extraInfo",
"tags",
{
association: "wallets",
attributes: ["id", "name"]
}
]
},
{
model: Whatsapp,
as: "whatsapp",
attributes: ["number", "type", "tenantId"]
}
]
});
if (!ticket) {
return res.status(404).json({ message: "Ticket not found" });
}
try {
if (medias) {
await Promise.all(
medias.map(async (media: Express.Multer.File) => {
await SendMediaMessageService(
media,
message,
ticket.id,
ticket.contact,
ticket.whatsapp
);
})
);
} else {
await SendTextMessageService(
message,
ticket.id,
ticket.contact,
ticket.whatsapp
);
}
return res.status(200).json({ message: "Message sent" });
} catch (error) {
logger.warn("e " + error)
return res.status(400).json({ message: error });
}
};
backend\src\controllers\WebhookHubController.ts
import { Request, Response } from "express";
import Whatsapp from "../models/Whatsapp";
import HubMessageListener from "../services/WbotNotificame/HubMessageListener";
import { logger } from "../utils/logger";
export const listen = async (
req: Request,
res: Response
): Promise => {
logger.info("Webhook received");
const medias = req.files as Express.Multer.File[];
const { number } = req.params;
const whatsapp = await Whatsapp.findOne({
where: { number: number }
});
if (!whatsapp) {
return res.status(404).json({ message: "Whatsapp channel not found" });
}
try {
await HubMessageListener(req.body, whatsapp, medias);
return res.status(200).json({ message: "Webhook received" });
} catch (error) {
return res.status(400).json({ message: error });
}
};
ROUTES
backend\src\routes\hubChannelRoutes.ts
import express from "express";
import * as ChannelController from "../controllers/ChannelHubController";
import isAuth from "../middleware/isAuth";
const hubChannelRoutes = express.Router();
hubChannelRoutes.post("/hub-channel/", isAuth, ChannelController.store);
hubChannelRoutes.get("/hub-channel/", isAuth, ChannelController.index);
export default hubChannelRoutes;
backend\src\routes\hubMessageRoutes.ts
import express from "express";
import uploadConfig from "../config/upload";
import * as MessageController from "../controllers/MessageHubController";
import isAuth from "../middleware/isAuth";
import multer from "multer";
const hubMessageRoutes = express.Router();
const upload = multer(uploadConfig);
hubMessageRoutes.post(
"/hub-message/:ticketId",
isAuth,
upload.array("medias"),
MessageController.send
);
export default hubMessageRoutes;
backend\src\routes\hubWebhookRoutes.ts
import express from "express";
import uploadConfig from "../config/upload";
import * as WebhookController from "../controllers/WebhookHubController";
import multer from "multer";
const hubWebhookRoutes = express.Router();
const upload = multer(uploadConfig);
hubWebhookRoutes.post(
"/hub-webhook/:number",
upload.array("medias"),
WebhookController.listen
);
export default hubWebhookRoutes;
backend\src\routes\index.ts
import { Router } from "express";
import userRoutes from "./userRoutes";
import authRoutes from "./authRoutes";
import settingRoutes from "./settingRoutes";
import contactRoutes from "./contactRoutes";
import ticketRoutes from "./ticketRoutes";
import whatsappRoutes from "./whatsappRoutes";
import messageRoutes from "./messageRoutes";
import whatsappSessionRoutes from "./whatsappSessionRoutes";
import autoReplyRoutes from "./autoReplyRoutes";
import fastReplyRoutes from "./fastReplyRoutes";
import queueRoutes from "./queueRoutes";
import statisticsRoutes from "./statisticsRoutes";
import tagRoutes from "./tagRoutes";
import campaignRoutes from "./campaignRoutes";
import campaignContactsRoutes from "./campaignContactsRoutes";
import apiConfigRoutes from "./apiConfigRoutes";
import apiExternalRoutes from "./apiExternalRoutes";
import chatFlowRoutes from "./chatFlowRoutes";
import tenantRoutes from "./tenantRoutes";
import WebHooksRoutes from "./WebHooksRoutes";
import adminRoutes from "./adminRoutes";
import facebookRoutes from "./facebookRoutes";
import hubChannelRoutes from "./hubChannelRoutes";
import hubMessageRoutes from "./hubMessageRoutes";
import hubWebhookRoutes from "./hubWebhookRoutes";
const routes = Router();
routes.use(userRoutes);
routes.use("/auth", authRoutes);
routes.use(settingRoutes);
routes.use(contactRoutes);
routes.use(ticketRoutes);
routes.use(whatsappRoutes);
routes.use(messageRoutes);
routes.use(messageRoutes);
routes.use(whatsappSessionRoutes);
routes.use(autoReplyRoutes);
routes.use(queueRoutes);
routes.use(fastReplyRoutes);
routes.use(statisticsRoutes);
routes.use(tagRoutes);
routes.use(campaignRoutes);
routes.use(campaignContactsRoutes);
routes.use(apiConfigRoutes);
routes.use(apiExternalRoutes);
routes.use(chatFlowRoutes);
routes.use(tenantRoutes);
routes.use(WebHooksRoutes);
routes.use(adminRoutes);
routes.use(facebookRoutes);
routes.use(hubChannelRoutes)
routes.use(hubMessageRoutes)
routes.use(hubWebhookRoutes)
export default routes;
FRONTEND
IMAGENS
PAGES
frontend\src\pages\atendimento\Index.vue
{{ username }}
Perfil
Sair
$router.push({ name: 'home-dashboard' })"
>
Retornar ao menu
Filtros Avançados
Abertos
Pendentes
Resolvidos
Filtro Avançado
Contatos
Carregando...
{{ $q.dark.isActive ? 'Desativar' : 'Ativar' }} Modo Escuro (Dark Mode)
Dados Contato
{{ ticketFocado.contact.name || '' }}
{{ ticketFocado.contact.number || '' }}
{{ ticketFocado.contact.email || '' }}
Etiquetas
{{ opt.tag }}
Ops... Sem etiquetas criadas!
Cadastre novas etiquetas na administração de sistemas.
Carteira
{{ opt.name }}
Ops... Sem carteiras disponíveis!!
Mensagens Agendadas
Agendado para: {{ $formatarData(message.scheduleDate, 'dd/MM/yyyy HH:mm') }}
Msg: {{ message.mediaName || message.body }}
Outras Informações
{{ info.value }}
Logs Ticket: {{ ticketFocado.id }}
{{ log.user && log.user.name || 'Bot' }}:
{{ messagesLog[log.type] && messagesLog[log.type].message }}
frontend\src\pages\atendimento\InputMensagem.vue
Ops... Nada por aqui!
Cadastre suas mensagens na administração de sistema.
{{ resposta.key }}
{{ resposta.message }}
{{ resposta.message }}
Enviar arquivo
Emoji
Enviar link para videoconferencia
{{ sign ? 'Desativar' : 'Ativar' }} Assinatura
textChat.trim().length ? enviarMensagem() : ''"
v-show="!cMostrarEnvioArquivo"
class="col-grow q-mx-xs text-grey-10 inputEnvioMensagem"
bg-color="grey-2"
color="grey-7"
placeholder="Digita sua mensagem"
input-style="max-height: 30vh"
autogrow
rounded
dense
outlined
v-model="textChat"
:value="textChat"
@paste="handleInputPaste"
>
Emoji
Enviar arquivo
Mensagens Rápidas
Enviar Mensagem
Enviar Áudio
{{ urlMediaPreview.title }}
* Confirmar envio: Enter
** Cancelar: ESC
frontend\src\pages\configuracoes\Index.vue
Configurações
Módulo: Atendimento
Não visualizar Tickets já atribuidos à outros usuários
Somente o usuário responsável pelo ticket e/ou os administradores visualizarão a atendimento.
Não visualizar Tickets no ChatBot
Somente administradores poderão visualizar tickets que estivem interagindo com o ChatBot.
Forçar atendimento via Carteira
Caso o contato tenha carteira vínculada, o sistema irá direcionar o atendimento somente para os donos da carteira de clientes.
Fluxo ativo para o Bot de atendimento
Fluxo a ser utilizado pelo Bot para os novos atendimentos
Ignorar Mensagens de Grupo
Habilitando esta opção o sistema não abrirá ticket para grupos
Recusar chamadas no Whatsapp
Quando ativo, as ligações de aúdio e vídeo serão recusadas, automaticamente.
Crie uma conta em hub.notificame.com.br e gere o seu token
{{ montarUrlIntegração() }}
HUB Notificame (Beta)
frontend\src\pages\contatos\ContatoModal.vue
{{ contactId ? 'Editar Contato' : 'Adicionar Contato' }}
Dados Contato
Informações adicionais
frontend\src\pages\sessaoWhatsapp\Index.vue
Canais
Nome: {{ item.name }}
{{ item.type }}
Página:
{{ item.fbObject && item.fbObject.name || 'Nenhuma página configurada.' }}
Conectando
Deletar conexáo
handleRequestNewQrCode(v, 'btn-qrCode')"
/>
frontend\src\pages\sessaoWhatsapp\ModalWhatsapp.vue
{{ whatsapp.id ? 'Editar' :
'Adicionar'
}}
Canal
Variáveis
{{ variavel.label }}
SERVICES
frontend\src\service\hub.js
import request from 'src/service/request'
export function AdicionarHub (data) {
return request({
url: '/hub-channel/',
method: 'post',
data
})
}
export function ListarHub () {
return request({
url: '/hub-channel/',
method: 'get'
})
}
export function EnviarMensagemHub (ticketId, data) {
return request({
url: `/hub-message/${ticketId}`,
method: 'post',
data
})
}