Merge pull request #24 from canove/typescript

Typescript adoption on backend code.
This commit is contained in:
Cassio Santos
2020-09-24 15:41:20 -03:00
committed by GitHub
153 changed files with 9668 additions and 2573 deletions

View File

@@ -64,25 +64,71 @@ Install puppeteer dependencies:
sudo apt-get install -y libgbm-dev wget unzip fontconfig locales gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils
```
- Clone this repo
- On backend folder:
- Copy .env.example to .env and fill it with database details.
- Install dependecies: `npm install` (Only in the first time)
- Create database tables: `npx sequelize db:migrate` (Only in the first time)
- Fill database with initial values: `npx sequelize db:seed:all`
- Start backend: `npm start`
- In another terminal, on frontend folder:
- Copy .env.example to .env and fill it with backend URL (normally localhost:port)
- Install dependecies: `npm install` (Only in the first time)
- Start frontend: `npm start`
Clone this repo
```bash
git clone https://github.com/canove/whaticket/ whaticket
```
Go to backend folder and create .env file:
```bash
cp .env.example .env
nano .env
```
Fill `.env` file with environment variables:
```bash
NODE_ENV=DEVELOPMENT #it helps on debugging
BACKEND_URL=http://localhost
PROXY_PORT=8080
PORT=8080
DB_HOST= #DB host IP, usually localhost
DB_USER=
DB_PASS=
DB_NAME=
```
Install backend dependencies, build app, run migrations and seeds:
```bash
npm install
npm build
npx sequelize db:migrate
npx sequelize db:seed:all
```
Start backend:
```bash
npm start
```
Open a second terminal, go to frontend folder and create .env file:
```bash
nano .env
REACT_APP_BACKEND_URL = http://localhost:8080/ # Your previous configured backend app URL.
```
Start frontend app:
```bash
npm start
```
- Go to http://your_server_ip:3000/signup
- Create an user and login with it.
- On the sidebard, go to _Connections_ and create your first WhatsApp connection.
- On the sidebard, go to _Connections_ page and create your first WhatsApp connection.
- Wait for QR CODE button to appear, click it and read qr code.
- Done. Every message received by your synced WhatsApp number will appear in Tickets List.
## Basic production deployment (Ubuntu 18.04 VPS)
All instructions below assumes you are NOT running as root, since it will give error in puppeteer. See
You'll need two subdomains forwarding to yours VPS ip to follow these instructions. We'll use `myapp.mydomain.com` to frontend and `api.mydomain.com` to backend in the following example. We'll also use an dedicated user with sudo privileges no deploy it (not root).
Update all system packages:
@@ -152,11 +198,12 @@ Install puppeteer dependencies:
sudo apt-get install -y libgbm-dev wget unzip fontconfig locales gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils
```
Install backend dependencies, run migrations and seeds:
Install backend dependencies, build app, run migrations and seeds:
```bash
cd whaticket/backend
npm install
npm build
npx sequelize db:migrate
npx sequelize db:seed:all
```

9
backend/.editorconfig Normal file
View File

@@ -0,0 +1,9 @@
root = true
[*]
end_of_line = lf
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

3
backend/.eslintignore Normal file
View File

@@ -0,0 +1,3 @@
/*.js
node_modules
dist

47
backend/.eslintrc.json Normal file
View File

@@ -0,0 +1,47 @@
{
"env": {
"es2021": true,
"node": true
},
"extends": [
"airbnb-base",
"plugin:@typescript-eslint/recommended",
"prettier/@typescript-eslint",
"plugin:prettier/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": ["@typescript-eslint", "prettier"],
"rules": {
"@typescript-eslint/no-unused-vars": [
"error",
{ "argsIgnorePattern": "_" }
],
"import/prefer-default-export": "off",
"no-console": "off",
"no-param-reassign": "off",
"prettier/prettier": "error",
"import/extensions": [
"error",
"ignorePackages",
{
"ts": "never"
}
],
"quotes": [
1,
"double",
{
"avoidEscape": true
}
]
},
"settings": {
"import/resolver": {
"typescript": {}
}
}
}

5
backend/.gitignore vendored
View File

@@ -1,9 +1,10 @@
node_modules
public/*
!public/.gitkeep
dist
!public/.gitkeep
.env
package-lock.json
yarn.lock
/src/config/sentry.js
/src/config/sentry.js

View File

@@ -1,8 +1,8 @@
const { resolve } = require("path");
module.exports = {
config: resolve(__dirname, "src", "config", "database.js"),
"modules-path": resolve(__dirname, "src", "models"),
"migrations-path": resolve(__dirname, "src", "database", "migrations"),
"seeders-path": resolve(__dirname, "src", "database", "seeds"),
"config": resolve(__dirname, "dist", "config", "database.js"),
"modules-path": resolve(__dirname, "dist", "models"),
"migrations-path": resolve(__dirname, "dist", "database", "migrations"),
"seeders-path": resolve(__dirname, "dist", "database", "seeds")
};

View File

@@ -1,39 +1,58 @@
{
"name": "backend",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "nodemon src/app.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"nodemonConfig": {
"ignore": [
"controllers/session.json"
]
},
"author": "",
"license": "ISC",
"dependencies": {
"@sentry/node": "5.22.3",
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"date-fns": "^2.16.1",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-async-errors": "^3.1.1",
"jsonwebtoken": "^8.5.1",
"multer": "^1.4.2",
"mysql2": "^2.1.0",
"qrcode-terminal": "^0.12.0",
"sequelize": "^6.3.5",
"socket.io": "^2.3.0",
"whatsapp-web.js": "^1.8.2",
"youch": "^2.0.10",
"yup": "^0.29.3"
},
"devDependencies": {
"nodemon": "^2.0.4",
"sequelize-cli": "^6.2.0"
}
"name": "backend",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "tsc",
"watch": "tsc -w",
"start": "nodemon dist/server.js",
"dev:server": "ts-node-dev --respawn --transpile-only --ignore node_modules src/server.ts"
},
"author": "",
"license": "MIT",
"dependencies": {
"@sentry/node": "5.22.3",
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"date-fns": "^2.16.1",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-async-errors": "^3.1.1",
"jsonwebtoken": "^8.5.1",
"multer": "^1.4.2",
"mysql2": "^2.1.0",
"qrcode-terminal": "^0.12.0",
"reflect-metadata": "^0.1.13",
"sequelize": "5",
"sequelize-cli": "5",
"sequelize-typescript": "^1.1.0",
"socket.io": "^2.3.0",
"whatsapp-web.js": "^1.8.2",
"yup": "^0.29.3"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.2",
"@types/bluebird": "^3.5.32",
"@types/cors": "^2.8.7",
"@types/express": "^4.17.8",
"@types/jsonwebtoken": "^8.5.0",
"@types/multer": "^1.4.4",
"@types/node": "^14.10.1",
"@types/socket.io": "^2.1.11",
"@types/validator": "^13.1.0",
"@types/yup": "^0.29.7",
"@typescript-eslint/eslint-plugin": "^4.1.0",
"@typescript-eslint/parser": "^4.1.0",
"eslint": "^7.9.0",
"eslint-config-airbnb-base": "^14.2.0",
"eslint-config-prettier": "^6.11.0",
"eslint-import-resolver-typescript": "^2.3.0",
"eslint-plugin-import": "^2.21.2",
"eslint-plugin-prettier": "^3.1.4",
"nodemon": "^2.0.4",
"prettier": "^2.1.1",
"ts-node-dev": "^1.0.0-pre.62",
"typescript": "^4.0.2"
}
}

View File

@@ -0,0 +1,5 @@
module.exports = {
singleQuote: false,
trailingComma: "none",
arrowParens: "avoid",
};

5
backend/src/@types/express.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare namespace Express {
export interface Request {
user: { id: string; profile: string };
}
}

View File

@@ -0,0 +1 @@
declare module "qrcode-terminal";

View File

@@ -1,85 +0,0 @@
require("express-async-errors");
require("./database");
const express = require("express");
const path = require("path");
const Youch = require("youch");
const cors = require("cors");
const multer = require("multer");
const Sentry = require("@sentry/node");
const { initWbot } = require("./libs/wbot");
const wbotMessageListener = require("./services/wbotMessageListener");
const wbotMonitor = require("./services/wbotMonitor");
const Whatsapp = require("./models/Whatsapp");
const Router = require("./router");
const app = express();
const server = app.listen(process.env.PORT, () => {
console.log(`Server started on port: ${process.env.PORT}`);
});
Sentry.init({ dsn: process.env.SENTRY_DSN });
const fileStorage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, path.resolve(__dirname, "..", "public"));
},
filename: (req, file, cb) => {
cb(null, new Date().getTime() + path.extname(file.originalname));
},
});
app.use(Sentry.Handlers.requestHandler());
app.use(cors());
app.use(express.json());
app.use(multer({ storage: fileStorage }).single("media"));
app.use("/public", express.static(path.join(__dirname, "..", "public")));
app.use(Router);
const io = require("./libs/socket").init(server);
io.on("connection", socket => {
console.log("Client Connected");
socket.on("joinChatBox", ticketId => {
console.log("A client joined a ticket channel");
socket.join(ticketId);
});
socket.on("joinNotification", () => {
console.log("A client joined notification channel");
socket.join("notification");
});
socket.on("disconnect", () => {
console.log("Client disconnected");
});
});
const startWhatsAppSessions = async () => {
const whatsapps = await Whatsapp.findAll();
if (whatsapps.length > 0) {
whatsapps.forEach(whatsapp => {
initWbot(whatsapp)
.then(() => {
wbotMessageListener(whatsapp);
wbotMonitor(whatsapp);
})
.catch(err => console.log(err));
});
}
};
startWhatsAppSessions();
app.use(Sentry.Handlers.errorHandler());
app.use(async (err, req, res, next) => {
if (process.env.NODE_ENV === "DEVELOPMENT") {
const errors = await new Youch(err, req).toJSON();
console.log(err);
return res.status(500).json(errors);
}
return res.status(500).json({ error: "Internal server error" });
});

View File

@@ -1,4 +0,0 @@
module.exports = {
secret: "mysecret",
expiresIn: "7d",
};

View File

@@ -0,0 +1,4 @@
export default {
secret: "mysecret",
expiresIn: "7d"
};

View File

@@ -1,16 +0,0 @@
require("dotenv/config");
module.exports = {
define: {
charset: "utf8mb4",
collate: "utf8mb4_bin",
},
dialect: "mysql",
timezone: "-03:00",
host: process.env.DB_HOST,
database: process.env.DB_NAME,
username: process.env.DB_USER,
password: process.env.DB_PASS,
logging: false,
seederStorage: "sequelize",
};

View File

@@ -0,0 +1,15 @@
require("dotenv/config");
module.exports = {
define: {
charset: "utf8mb4",
collate: "utf8mb4_bin"
},
dialect: "mysql",
timezone: "-03:00",
host: process.env.DB_HOST,
database: process.env.DB_NAME,
username: process.env.DB_USER,
password: process.env.DB_PASS,
logging: false
};

View File

@@ -0,0 +1,16 @@
import path from "path";
import multer from "multer";
const publicFolder = path.resolve(__dirname, "..", "..", "public");
export default {
directory: publicFolder,
storage: multer.diskStorage({
destination: publicFolder,
filename(req, file, cb) {
const fileName = new Date().getTime() + path.extname(file.originalname);
return cb(null, fileName);
}
})
};

View File

@@ -1,170 +0,0 @@
const Sequelize = require("sequelize");
const { Op } = require("sequelize");
const Contact = require("../models/Contact");
const Whatsapp = require("../models/Whatsapp");
const ContactCustomField = require("../models/ContactCustomField");
const { getIO } = require("../libs/socket");
const { getWbot } = require("../libs/wbot");
exports.index = async (req, res) => {
const { searchParam = "", pageNumber = 1 } = req.query;
const whereCondition = {
[Op.or]: [
{
name: Sequelize.where(
Sequelize.fn("LOWER", Sequelize.col("name")),
"LIKE",
"%" + searchParam.toLowerCase() + "%"
),
},
{ number: { [Op.like]: `%${searchParam}%` } },
],
};
let limit = 20;
let offset = limit * (pageNumber - 1);
const { count, rows: contacts } = await Contact.findAndCountAll({
where: whereCondition,
limit,
offset,
order: [["createdAt", "DESC"]],
});
const hasMore = count > offset + contacts.length;
return res.json({ contacts, count, hasMore });
};
exports.store = async (req, res) => {
const defaultWhatsapp = await Whatsapp.findOne({
where: { default: true },
});
if (!defaultWhatsapp) {
return res
.status(404)
.json({ error: "No default WhatsApp found. Check Connection page." });
}
const wbot = getWbot(defaultWhatsapp);
const io = getIO();
const newContact = req.body;
try {
const isValidNumber = await wbot.isRegisteredUser(
`${newContact.number}@c.us`
);
if (!isValidNumber) {
return res
.status(400)
.json({ error: "The suplied number is not a valid Whatsapp number" });
}
} catch (err) {
console.log(err);
return res.status(500).json({
error: "Could not check whatsapp contact. Check connection page.",
});
}
const profilePicUrl = await wbot.getProfilePicUrl(
`${newContact.number}@c.us`
);
const contact = await Contact.create(
{ ...newContact, profilePicUrl },
{
include: "extraInfo",
}
);
io.emit("contact", {
action: "create",
contact: contact,
});
return res.status(200).json(contact);
};
exports.show = async (req, res) => {
const { contactId } = req.params;
const contact = await Contact.findByPk(contactId, {
include: "extraInfo",
attributes: ["id", "name", "number", "email"],
});
if (!contact) {
return res.status(404).json({ error: "No contact found with this id." });
}
return res.status(200).json(contact);
};
exports.update = async (req, res) => {
const io = getIO();
const updatedContact = req.body;
const { contactId } = req.params;
const contact = await Contact.findByPk(contactId, {
include: "extraInfo",
});
if (!contact) {
return res.status(404).json({ error: "No contact found with this ID" });
}
if (updatedContact.extraInfo) {
await Promise.all(
updatedContact.extraInfo.map(async info => {
await ContactCustomField.upsert({ ...info, contactId: contact.id });
})
);
await Promise.all(
contact.extraInfo.map(async oldInfo => {
let stillExists = updatedContact.extraInfo.findIndex(
info => info.id === oldInfo.id
);
if (stillExists === -1) {
await ContactCustomField.destroy({ where: { id: oldInfo.id } });
}
})
);
}
await contact.update(updatedContact);
io.emit("contact", {
action: "update",
contact: contact,
});
return res.status(200).json(contact);
};
exports.delete = async (req, res) => {
const io = getIO();
const { contactId } = req.params;
const contact = await Contact.findByPk(contactId);
if (!contact) {
return res.status(404).json({ error: "No contact found with this ID" });
}
await contact.destroy();
io.emit("contact", {
action: "delete",
contactId: contactId,
});
return res.status(200).json({ message: "Contact deleted" });
};

View File

@@ -0,0 +1,103 @@
import { Request, Response } from "express";
import { getIO } from "../libs/socket";
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 CheckIsValidContact from "../services/WbotServices/CheckIsValidContact";
import GetProfilePicUrl from "../services/WbotServices/GetProfilePicUrl";
type IndexQuery = {
searchParam: string;
pageNumber: string;
};
export const index = async (req: Request, res: Response): Promise<Response> => {
const { searchParam, pageNumber } = req.query as IndexQuery;
const { contacts, count, hasMore } = await ListContactsService({
searchParam,
pageNumber
});
return res.json({ contacts, count, hasMore });
};
interface ExtraInfo {
name: string;
value: string;
}
interface ContactData {
name: string;
number: string;
email?: string;
extraInfo?: ExtraInfo[];
}
export const store = async (req: Request, res: Response): Promise<Response> => {
const newContact: ContactData = req.body;
await CheckIsValidContact(newContact.number);
const profilePicUrl = await GetProfilePicUrl(newContact.number);
const contact = await CreateContactService({
...newContact,
profilePicUrl
});
const io = getIO();
io.emit("contact", {
action: "create",
contact
});
return res.status(200).json(contact);
};
export const show = async (req: Request, res: Response): Promise<Response> => {
const { contactId } = req.params;
const contact = await ShowContactService(contactId);
return res.status(200).json(contact);
};
export const update = async (
req: Request,
res: Response
): Promise<Response> => {
const contactData: ContactData = req.body;
const { contactId } = req.params;
const contact = await UpdateContactService({ contactData, contactId });
const io = getIO();
io.emit("contact", {
action: "update",
contact
});
return res.status(200).json(contact);
};
export const remove = async (
req: Request,
res: Response
): Promise<Response> => {
const { contactId } = req.params;
await DeleteContactService(contactId);
const io = getIO();
io.emit("contact", {
action: "delete",
contactId
});
return res.status(200).json({ message: "Contact deleted" });
};

View File

@@ -1,38 +0,0 @@
const Contact = require("../models/Contact");
const Whatsapp = require("../models/Whatsapp");
const { getIO } = require("../libs/socket");
const { getWbot, initWbot } = require("../libs/wbot");
exports.store = async (req, res, next) => {
const defaultWhatsapp = await Whatsapp.findOne({
where: { default: true },
});
if (!defaultWhatsapp) {
return res
.status(404)
.json({ error: "No default WhatsApp found. Check Connection page." });
}
const io = getIO();
const wbot = getWbot(defaultWhatsapp.id);
let phoneContacts;
try {
phoneContacts = await wbot.getContacts();
} catch (err) {
console.log(err);
return res.status(500).json({
error: "Could not check whatsapp contact. Check connection page.",
});
}
await Promise.all(
phoneContacts.map(async ({ number, name }) => {
await Contact.create({ number, name });
})
);
return res.status(200).json({ message: "contacts imported" });
};

View File

@@ -0,0 +1,8 @@
import { Request, Response } from "express";
import ImportContactsService from "../services/WbotServices/ImportContactsService";
export const store = async (req: Request, res: Response): Promise<Response> => {
await ImportContactsService();
return res.status(200).json({ message: "contacts imported" });
};

View File

@@ -1,203 +0,0 @@
const Message = require("../models/Message");
const Contact = require("../models/Contact");
const User = require("../models/User");
const Whatsapp = require("../models/Whatsapp");
const Ticket = require("../models/Ticket");
const { getIO } = require("../libs/socket");
const { getWbot } = require("../libs/wbot");
const Sequelize = require("sequelize");
const { MessageMedia } = require("whatsapp-web.js");
const setMessagesAsRead = async ticket => {
const io = getIO();
const wbot = getWbot(ticket.whatsappId);
await Message.update(
{ read: true },
{
where: {
ticketId: ticket.id,
read: false,
},
}
);
try {
await wbot.sendSeen(`${ticket.contact.number}@c.us`);
} catch (err) {
console.log(
"Could not mark messages as read. Maybe whatsapp session disconnected?"
);
}
io.to("notification").emit("ticket", {
action: "updateUnread",
ticketId: ticket.id,
});
};
exports.index = async (req, res, next) => {
const { ticketId } = req.params;
const { searchParam = "", pageNumber = 1 } = req.query;
const whereCondition = {
body: Sequelize.where(
Sequelize.fn("LOWER", Sequelize.col("body")),
"LIKE",
"%" + searchParam.toLowerCase() + "%"
),
};
const limit = 20;
const offset = limit * (pageNumber - 1);
const ticket = await Ticket.findByPk(ticketId, {
include: [
{
model: Contact,
as: "contact",
include: "extraInfo",
attributes: ["id", "name", "number", "profilePicUrl"],
},
{
model: User,
as: "user",
},
],
});
if (!ticket) {
return res.status(404).json({ error: "No ticket found with this ID" });
}
await setMessagesAsRead(ticket);
const ticketMessages = await ticket.getMessages({
where: whereCondition,
limit,
offset,
order: [["createdAt", "DESC"]],
});
const count = await ticket.countMessages();
const serializedMessages = ticketMessages.map(message => {
return {
...message.dataValues,
mediaUrl: `${
message.mediaUrl
? `${process.env.BACKEND_URL}:${process.env.PROXY_PORT}/public/${message.mediaUrl}`
: ""
}`,
};
});
const hasMore = count > offset + ticketMessages.length;
return res.json({
messages: serializedMessages.reverse(),
ticket,
count,
hasMore,
});
};
exports.store = async (req, res, next) => {
const io = getIO();
const { ticketId } = req.params;
const message = req.body;
const media = req.file;
let sentMessage;
const ticket = await Ticket.findByPk(ticketId, {
include: [
{
model: Contact,
as: "contact",
attributes: ["number", "name", "profilePicUrl"],
},
],
});
if (!ticket) {
return res.status(404).json({ error: "No ticket found with this ID" });
}
if (!ticket.whatsappId) {
const defaultWhatsapp = await Whatsapp.findOne({
where: { default: true },
});
if (!defaultWhatsapp) {
return res
.status(404)
.json({ error: "No default WhatsApp found. Check Connection page." });
}
await ticket.setWhatsapp(defaultWhatsapp);
}
const wbot = getWbot(ticket.whatsappId);
try {
if (media) {
const newMedia = MessageMedia.fromFilePath(req.file.path);
message.mediaUrl = req.file.filename;
if (newMedia.mimetype) {
message.mediaType = newMedia.mimetype.split("/")[0];
} else {
message.mediaType = "other";
}
sentMessage = await wbot.sendMessage(
`${ticket.contact.number}@c.us`,
newMedia
);
await ticket.update({ lastMessage: message.mediaUrl });
} else {
sentMessage = await wbot.sendMessage(
`${ticket.contact.number}@c.us`,
message.body
);
await ticket.update({ lastMessage: message.body });
}
} catch (err) {
console.log(
"Could not create whatsapp message. Is session details valid? "
);
}
if (sentMessage) {
message.id = sentMessage.id.id;
const newMessage = await ticket.createMessage(message);
const serialziedMessage = {
...newMessage.dataValues,
mediaUrl: `${
message.mediaUrl
? `${process.env.BACKEND_URL}:${process.env.PROXY_PORT}/public/${message.mediaUrl}`
: ""
}`,
};
io.to(ticketId).to("notification").emit("appMessage", {
action: "create",
message: serialziedMessage,
ticket: ticket,
contact: ticket.contact,
});
await setMessagesAsRead(ticket);
return res.status(200).json({ newMessage, ticket });
}
return res
.status(500)
.json({ error: "Cannot sent whatsapp message. Check connection page." });
};

View File

@@ -0,0 +1,79 @@
import { Request, Response } from "express";
import { Message as WbotMessage } from "whatsapp-web.js";
import SetTicketMessagesAsRead from "../helpers/SetTicketMessagesAsRead";
import { getIO } from "../libs/socket";
import CreateMessageService from "../services/MessageServices/CreateMessageService";
import ListMessagesService from "../services/MessageServices/ListMessagesService";
import ShowTicketService from "../services/TicketServices/ShowTicketService";
import SendWhatsAppMedia from "../services/WbotServices/SendWhatsAppMedia";
import SendWhatsAppMessage from "../services/WbotServices/SendWhatsAppMessage";
type IndexQuery = {
searchParam: string;
pageNumber: string;
};
type MessageData = {
body: string;
fromMe: boolean;
read: boolean;
};
export const index = async (req: Request, res: Response): Promise<Response> => {
const { ticketId } = req.params;
const { searchParam, pageNumber } = req.query as IndexQuery;
const { count, messages, ticket, hasMore } = await ListMessagesService({
searchParam,
pageNumber,
ticketId
});
await SetTicketMessagesAsRead(ticket);
return res.json({ count, messages, ticket, hasMore });
};
export const store = async (req: Request, res: Response): Promise<Response> => {
const { ticketId } = req.params;
const { body, fromMe, read }: MessageData = req.body;
const media = req.file;
const ticket = await ShowTicketService(ticketId);
let sentMessage: WbotMessage;
if (media) {
sentMessage = await SendWhatsAppMedia({ media, ticket });
} else {
sentMessage = await SendWhatsAppMessage({ body, ticket });
}
const newMessage = {
id: sentMessage.id.id,
body,
fromMe,
read,
mediaType: sentMessage.type,
mediaUrl: media?.filename
};
const message = await CreateMessageService({
messageData: newMessage,
ticketId: ticket.id
});
const io = getIO();
io.to(ticketId).to("notification").emit("appMessage", {
action: "create",
message,
ticket,
contact: ticket.contact
});
await SetTicketMessagesAsRead(ticket);
return res.status(200).json(message);
};

View File

@@ -1,32 +0,0 @@
const jwt = require("jsonwebtoken");
const authConfig = require("../config/auth");
const User = require("../models/User");
exports.store = async (req, res, next) => {
const { email, password } = req.body;
const user = await User.findOne({ where: { email: email } });
if (!user) {
return res.status(404).json({ error: "No user found with this email" });
}
if (!(await user.checkPassword(password))) {
return res.status(401).json({ error: "Password does not match" });
}
const token = jwt.sign(
{ email: user.email, userId: user.id },
authConfig.secret,
{
expiresIn: authConfig.expiresIn,
}
);
return res.status(200).json({
token: token,
username: user.name,
profile: user.profile,
userId: user.id,
});
};

View File

@@ -0,0 +1,16 @@
import { Request, Response } from "express";
import AuthUserService from "../services/UserServices/AuthUserSerice";
export const store = async (req: Request, res: Response): Promise<Response> => {
const { email, password } = req.body;
const { token, user } = await AuthUserService({ email, password });
return res.status(200).json({
token,
username: user.name,
profile: user.profile,
userId: user.id
});
};

View File

@@ -1,39 +0,0 @@
const Setting = require("../models/Setting");
const { getIO } = require("../libs/socket");
exports.index = async (req, res) => {
if (req.user.profile !== "admin") {
return res
.status(403)
.json({ error: "Only administrators can access this route." });
}
const settings = await Setting.findAll();
return res.status(200).json(settings);
};
exports.update = async (req, res) => {
if (req.user.profile !== "admin") {
return res
.status(403)
.json({ error: "Only administrators can access this route." });
}
const io = getIO();
const { settingKey } = req.params;
const setting = await Setting.findByPk(settingKey);
if (!setting) {
return res.status(404).json({ error: "No setting found with this ID" });
}
await setting.update(req.body);
io.emit("settings", {
action: "update",
setting,
});
return res.status(200).json(setting);
};

View File

@@ -0,0 +1,41 @@
import { Request, Response } from "express";
import { getIO } from "../libs/socket";
import AppError from "../errors/AppError";
import UpdateSettingService from "../services/SettingServices/UpdateSettingService";
import ListSettingsService from "../services/SettingServices/ListSettingsService";
export const index = async (req: Request, res: Response): Promise<Response> => {
if (req.user.profile !== "admin") {
throw new AppError("Only administrators can access resource.", 403);
}
const settings = await ListSettingsService();
return res.status(200).json(settings);
};
export const update = async (
req: Request,
res: Response
): Promise<Response> => {
if (req.user.profile !== "admin") {
throw new AppError("Only administrators can access this route.", 403);
}
const { settingKey: key } = req.params;
const { value } = req.body;
const setting = await UpdateSettingService({
key,
value
});
const io = getIO();
io.emit("settings", {
action: "update",
setting
});
return res.status(200).json(setting);
};

View File

@@ -1,185 +0,0 @@
const Sequelize = require("sequelize");
const { startOfDay, endOfDay, parseISO } = require("date-fns");
const Ticket = require("../models/Ticket");
const Contact = require("../models/Contact");
const Message = require("../models/Message");
const Whatsapp = require("../models/Whatsapp");
const { getIO } = require("../libs/socket");
exports.index = async (req, res) => {
const {
pageNumber = 1,
status = "",
date = "",
searchParam = "",
showAll,
} = req.query;
const userId = req.user.id;
const limit = 20;
const offset = limit * (pageNumber - 1);
let includeCondition = [
{
model: Contact,
as: "contact",
attributes: ["name", "number", "profilePicUrl"],
},
];
let whereCondition = { userId: userId };
if (showAll === "true") {
whereCondition = {};
}
if (status) {
whereCondition = {
...whereCondition,
status: status,
};
}
if (searchParam) {
includeCondition = [
...includeCondition,
{
model: Message,
as: "messages",
attributes: ["id", "body"],
where: {
body: Sequelize.where(
Sequelize.fn("LOWER", Sequelize.col("body")),
"LIKE",
"%" + searchParam.toLowerCase() + "%"
),
},
required: false,
duplicating: false,
},
];
whereCondition = {
[Sequelize.Op.or]: [
{
"$contact.name$": Sequelize.where(
Sequelize.fn("LOWER", Sequelize.col("name")),
"LIKE",
"%" + searchParam.toLowerCase() + "%"
),
},
{ "$contact.number$": { [Sequelize.Op.like]: `%${searchParam}%` } },
{
"$message.body$": Sequelize.where(
Sequelize.fn("LOWER", Sequelize.col("body")),
"LIKE",
"%" + searchParam.toLowerCase() + "%"
),
},
],
};
}
if (date) {
whereCondition = {
...whereCondition,
createdAt: {
[Sequelize.Op.between]: [
startOfDay(parseISO(date)),
endOfDay(parseISO(date)),
],
},
};
}
const { count, rows: tickets } = await Ticket.findAndCountAll({
where: whereCondition,
distinct: true,
include: includeCondition,
limit,
offset,
order: [["updatedAt", "DESC"]],
});
const hasMore = count > offset + tickets.length;
return res.status(200).json({ count, tickets, hasMore });
};
exports.store = async (req, res) => {
const io = getIO();
const defaultWhatsapp = await Whatsapp.findOne({
where: { default: true },
});
if (!defaultWhatsapp) {
return res
.status(404)
.json({ error: "No default WhatsApp found. Check Connection page." });
}
const ticket = await defaultWhatsapp.createTicket(req.body);
const contact = await ticket.getContact();
const serializaedTicket = { ...ticket.dataValues, contact: contact };
io.to("notification").emit("ticket", {
action: "create",
ticket: serializaedTicket,
});
return res.status(200).json(ticket);
};
exports.update = async (req, res) => {
const io = getIO();
const { ticketId } = req.params;
const ticket = await Ticket.findByPk(ticketId, {
include: [
{
model: Contact,
as: "contact",
attributes: ["name", "number", "profilePicUrl"],
},
],
});
if (!ticket) {
return res.status(404).json({ error: "No ticket found with this ID" });
}
await ticket.update(req.body);
io.to("notification").emit("ticket", {
action: "updateStatus",
ticket: ticket,
});
return res.status(200).json(ticket);
};
exports.delete = async (req, res) => {
const io = getIO();
const { ticketId } = req.params;
const ticket = await Ticket.findByPk(ticketId);
if (!ticket) {
return res.status(400).json({ error: "No ticket found with this ID" });
}
await ticket.destroy();
io.to("notification").emit("ticket", {
action: "delete",
ticketId: ticket.id,
});
return res.status(200).json({ message: "ticket deleted" });
};

View File

@@ -0,0 +1,92 @@
import { Request, Response } from "express";
import { getIO } from "../libs/socket";
import CreateTicketService from "../services/TicketServices/CreateTicketService";
import DeleteTicketService from "../services/TicketServices/DeleteTicketService";
import ListTicketsService from "../services/TicketServices/ListTicketsService";
import UpdateTicketService from "../services/TicketServices/UpdateTicketService";
type IndexQuery = {
searchParam: string;
pageNumber: string;
status: string;
date: string;
showAll: string;
};
interface TicketData {
contactId: number;
status: string;
}
export const index = async (req: Request, res: Response): Promise<Response> => {
const {
pageNumber,
status,
date,
searchParam,
showAll
} = req.query as IndexQuery;
const userId = req.user.id;
const { tickets, count, hasMore } = await ListTicketsService({
searchParam,
pageNumber,
status,
date,
showAll,
userId
});
return res.status(200).json({ tickets, count, hasMore });
};
export const store = async (req: Request, res: Response): Promise<Response> => {
const { contactId, status }: TicketData = req.body;
const ticket = await CreateTicketService({ contactId, status });
const io = getIO();
io.to("notification").emit("ticket", {
action: "create",
ticket
});
return res.status(200).json(ticket);
};
export const update = async (
req: Request,
res: Response
): Promise<Response> => {
const { ticketId } = req.params;
const ticketData: TicketData = req.body;
const ticket = await UpdateTicketService({ ticketData, ticketId });
const io = getIO();
io.to("notification").emit("ticket", {
action: "updateStatus",
ticket
});
return res.status(200).json(ticket);
};
export const remove = async (
req: Request,
res: Response
): Promise<Response> => {
const { ticketId } = req.params;
await DeleteTicketService(ticketId);
const io = getIO();
io.to("notification").emit("ticket", {
action: "delete",
ticketId: +ticketId
});
return res.status(200).json({ message: "ticket deleted" });
};

View File

@@ -1,181 +0,0 @@
const Sequelize = require("sequelize");
const Yup = require("yup");
const { Op } = require("sequelize");
const User = require("../models/User");
const Setting = require("../models/Setting");
const { getIO } = require("../libs/socket");
exports.index = async (req, res) => {
if (req.user.profile !== "admin") {
return res
.status(403)
.json({ error: "Only administrators can access this route." });
}
const { searchParam = "", pageNumber = 1 } = req.query;
const whereCondition = {
[Op.or]: [
{
name: Sequelize.where(
Sequelize.fn("LOWER", Sequelize.col("name")),
"LIKE",
"%" + searchParam.toLowerCase() + "%"
),
},
{ email: { [Op.like]: `%${searchParam.toLowerCase()}%` } },
],
};
let limit = 20;
let offset = limit * (pageNumber - 1);
const { count, rows: users } = await User.findAndCountAll({
attributes: ["name", "id", "email", "profile"],
where: whereCondition,
limit,
offset,
order: [["createdAt", "DESC"]],
});
const hasMore = count > offset + users.length;
return res.status(200).json({ users, count, hasMore });
};
exports.store = async (req, res, next) => {
console.log(req.url);
const schema = Yup.object().shape({
name: Yup.string().required().min(2),
email: Yup.string()
.email()
.required()
.test(
"Check-email",
"An user with this email already exists",
async value => {
const userFound = await User.findOne({ where: { email: value } });
return !Boolean(userFound);
}
),
password: Yup.string().required().min(5),
});
if (req.url === "/signup") {
const { value: userCreation } = await Setting.findByPk("userCreation");
if (userCreation === "disabled") {
return res
.status(403)
.json({ error: "User creation is disabled by administrator." });
}
} else if (req.user.profile !== "admin") {
return res
.status(403)
.json({ error: "Only administrators can create users." });
}
try {
await schema.validate(req.body);
} catch (err) {
return res.status(400).json({ error: err.message });
}
const io = getIO();
const { name, id, email, profile } = await User.create(req.body);
io.emit("user", {
action: "create",
user: { name, id, email, profile },
});
return res.status(201).json({ message: "User created!", userId: id });
};
exports.show = async (req, res) => {
const { userId } = req.params;
const user = await User.findByPk(userId, {
attributes: ["id", "name", "email", "profile"],
});
if (!user) {
res.status(400).json({ error: "No user found with this id." });
}
return res.status(200).json(user);
};
exports.update = async (req, res) => {
const schema = Yup.object().shape({
name: Yup.string().min(2),
email: Yup.string().email(),
password: Yup.string(),
});
if (req.user.profile !== "admin") {
return res
.status(403)
.json({ error: "Only administrators can edit users." });
}
await schema.validate(req.body);
const io = getIO();
const { userId } = req.params;
const user = await User.findByPk(userId, {
attributes: ["name", "id", "email", "profile"],
});
if (!user) {
res.status(404).json({ error: "No user found with this id." });
}
if (user.profile === "admin" && req.body.profile === "user") {
const adminUsers = await User.count({ where: { profile: "admin" } });
if (adminUsers <= 1) {
return res
.status(403)
.json({ error: "There must be at leat one admin user." });
}
}
await user.update(req.body);
io.emit("user", {
action: "update",
user: user,
});
return res.status(200).json(user);
};
exports.delete = async (req, res) => {
const io = getIO();
const { userId } = req.params;
const user = await User.findByPk(userId);
if (!user) {
res.status(400).json({ error: "No user found with this id." });
}
if (req.user.profile !== "admin") {
return res
.status(403)
.json({ error: "Only administrators can edit users." });
}
await user.destroy();
io.emit("user", {
action: "delete",
userId: userId,
});
return res.status(200).json({ message: "User deleted" });
};

View File

@@ -0,0 +1,109 @@
import { Request, Response } from "express";
import { getIO } from "../libs/socket";
import CheckSettingsHelper from "../helpers/CheckSettings";
import AppError from "../errors/AppError";
import CreateUserService from "../services/UserServices/CreateUserService";
import ListUsersService from "../services/UserServices/ListUsersService";
import UpdateUserService from "../services/UserServices/UpdateUserService";
import ShowUserService from "../services/UserServices/ShowUserService";
import DeleteUserService from "../services/UserServices/DeleteUserService";
type IndexQuery = {
searchParam: string;
pageNumber: string;
};
export const index = async (req: Request, res: Response): Promise<Response> => {
if (req.user.profile !== "admin") {
throw new AppError("Only administrators can access this route.", 403); // should be handled better.
}
const { searchParam, pageNumber } = req.query as IndexQuery;
const { users, count, hasMore } = await ListUsersService({
searchParam,
pageNumber
});
return res.json({ users, count, hasMore });
};
export const store = async (req: Request, res: Response): Promise<Response> => {
const { email, password, name, profile } = req.body;
if (
req.url === "/signup" &&
(await CheckSettingsHelper("userCreation")) === "disabled"
) {
throw new AppError("User creation is disabled by administrator.", 403);
} else if (req.url !== "/signup" && req.user.profile !== "admin") {
throw new AppError("Only administrators can create users.", 403);
}
const user = await CreateUserService({
email,
password,
name,
profile
});
const io = getIO();
io.emit("user", {
action: "create",
user
});
return res.status(200).json(user);
};
export const show = async (req: Request, res: Response): Promise<Response> => {
const { userId } = req.params;
const user = await ShowUserService(userId);
return res.status(200).json(user);
};
export const update = async (
req: Request,
res: Response
): Promise<Response> => {
if (req.user.profile !== "admin") {
throw new AppError("Only administrators can edit users.", 403);
}
const { userId } = req.params;
const userData = req.body;
const user = await UpdateUserService({ userData, userId });
const io = getIO();
io.emit("user", {
action: "update",
user
});
return res.status(200).json(user);
};
export const remove = async (
req: Request,
res: Response
): Promise<Response> => {
const { userId } = req.params;
if (req.user.profile !== "admin") {
throw new AppError("Only administrators can delete users.", 403);
}
await DeleteUserService(userId);
const io = getIO();
io.emit("user", {
action: "delete",
userId
});
return res.status(200).json({ message: "User deleted" });
};

View File

@@ -1,141 +0,0 @@
const Yup = require("yup");
const Whatsapp = require("../models/Whatsapp");
const { getIO } = require("../libs/socket");
const { getWbot, initWbot, removeWbot } = require("../libs/wbot");
const wbotMessageListener = require("../services/wbotMessageListener");
const wbotMonitor = require("../services/wbotMonitor");
exports.index = async (req, res) => {
const whatsapps = await Whatsapp.findAll();
return res.status(200).json(whatsapps);
};
exports.store = async (req, res) => {
const schema = Yup.object().shape({
name: Yup.string().required().min(2),
default: Yup.boolean()
.required()
.test(
"Check-default",
"Only one default whatsapp is permited",
async value => {
if (value === true) {
const whatsappFound = await Whatsapp.findOne({
where: { default: true },
});
return !Boolean(whatsappFound);
} else return true;
}
),
});
try {
await schema.validate(req.body);
} catch (err) {
return res.status(400).json({ error: err.message });
}
const io = getIO();
const whatsapp = await Whatsapp.create(req.body);
if (!whatsapp) {
return res.status(400).json({ error: "Cannot create whatsapp session." });
}
initWbot(whatsapp)
.then(() => {
wbotMessageListener(whatsapp);
wbotMonitor(whatsapp);
})
.catch(err => console.log(err));
io.emit("whatsapp", {
action: "update",
whatsapp: whatsapp,
});
return res.status(200).json(whatsapp);
};
exports.show = async (req, res) => {
const { whatsappId } = req.params;
const whatsapp = await Whatsapp.findByPk(whatsappId);
if (!whatsapp) {
return res.status(200).json({ message: "Session not found" });
}
return res.status(200).json(whatsapp);
};
exports.update = async (req, res) => {
const { whatsappId } = req.params;
const schema = Yup.object().shape({
name: Yup.string().required().min(2),
default: Yup.boolean()
.required()
.test(
"Check-default",
"Only one default whatsapp is permited",
async value => {
if (value === true) {
const whatsappFound = await Whatsapp.findOne({
where: { default: true },
});
if (whatsappFound) {
return !(whatsappFound.id !== +whatsappId);
} else {
return true;
}
} else return true;
}
),
});
try {
await schema.validate(req.body);
} catch (err) {
return res.status(400).json({ error: err.message });
}
const io = getIO();
const whatsapp = await Whatsapp.findByPk(whatsappId);
if (!whatsapp) {
return res.status(404).json({ message: "Whatsapp not found" });
}
await whatsapp.update(req.body);
io.emit("whatsapp", {
action: "update",
whatsapp: whatsapp,
});
return res.status(200).json({ message: "Whatsapp updated" });
};
exports.delete = async (req, res) => {
const io = getIO();
const { whatsappId } = req.params;
const whatsapp = await Whatsapp.findByPk(whatsappId);
if (!whatsapp) {
return res.status(404).json({ message: "Whatsapp not found" });
}
await whatsapp.destroy();
removeWbot(whatsapp.id);
io.emit("whatsapp", {
action: "delete",
whatsappId: whatsapp.id,
});
return res.status(200).json({ message: "Whatsapp deleted." });
};

View File

@@ -0,0 +1,94 @@
import { Request, Response } from "express";
import { getIO } from "../libs/socket";
import { initWbot, removeWbot } from "../libs/wbot";
import wbotMessageListener from "../services/WbotServices/wbotMessageListener";
import wbotMonitor from "../services/WbotServices/wbotMonitor";
import CreateWhatsAppService from "../services/WhatsappService/CreateWhatsAppService";
import DeleteWhatsAppService from "../services/WhatsappService/DeleteWhatsAppService";
import ListWhatsAppsService from "../services/WhatsappService/ListWhatsAppsService";
import ShowWhatsAppService from "../services/WhatsappService/ShowWhatsAppService";
import UpdateWhatsAppService from "../services/WhatsappService/UpdateWhatsAppService";
// import Yup from "yup";
// import Whatsapp from "../models/Whatsapp";
// import { getIO } from "../libs/socket";
// import { getWbot, initWbot, removeWbot } from "../libs/wbot";
// import wbotMessageListener from "../services/wbotMessageListener";
// import wbotMonitor from "../services/wbotMonitor";
interface WhatsappData {
name: string;
status?: string;
isDefault?: boolean;
}
export const index = async (req: Request, res: Response): Promise<Response> => {
const whatsapps = await ListWhatsAppsService();
return res.status(200).json(whatsapps);
};
export const store = async (req: Request, res: Response): Promise<Response> => {
const { name, status, isDefault }: WhatsappData = req.body;
const whatsapp = await CreateWhatsAppService({ name, status, isDefault });
initWbot(whatsapp)
.then(() => {
wbotMessageListener(whatsapp);
wbotMonitor(whatsapp);
})
.catch(err => console.log(err));
const io = getIO();
io.emit("whatsapp", {
action: "update",
whatsapp
});
return res.status(200).json(whatsapp);
};
export const show = async (req: Request, res: Response): Promise<Response> => {
const { whatsappId } = req.params;
const whatsapp = await ShowWhatsAppService(whatsappId);
return res.status(200).json(whatsapp);
};
export const update = async (
req: Request,
res: Response
): Promise<Response> => {
const { whatsappId } = req.params;
const whatsappData = req.body;
const whatsapp = await UpdateWhatsAppService({ whatsappData, whatsappId });
const io = getIO();
io.emit("whatsapp", {
action: "update",
whatsapp
});
return res.status(200).json(whatsapp);
};
export const remove = async (
req: Request,
res: Response
): Promise<Response> => {
const { whatsappId } = req.params;
await DeleteWhatsAppService(whatsappId);
removeWbot(+whatsappId);
const io = getIO();
io.emit("whatsapp", {
action: "delete",
whatsappId: +whatsappId
});
return res.status(200).json({ message: "Whatsapp deleted." });
};

View File

@@ -1,32 +1,32 @@
// const Whatsapp = require("../models/Whatsapp");
// const { getIO } = require("../libs/socket");
// const { getWbot, initWbot, removeWbot } = require("../libs/wbot");
// const wbotMessageListener = require("../services/wbotMessageListener");
// const wbotMonitor = require("../services/wbotMonitor");
// exports.show = async (req, res) => {
// const { whatsappId } = req.params;
// const dbSession = await Whatsapp.findByPk(whatsappId);
// if (!dbSession) {
// return res.status(200).json({ message: "Session not found" });
// }
// return res.status(200).json(dbSession);
// };
// exports.delete = async (req, res) => {
// const { whatsappId } = req.params;
// const dbSession = await Whatsapp.findByPk(whatsappId);
// if (!dbSession) {
// return res.status(404).json({ message: "Session not found" });
// }
// const wbot = getWbot(dbSession.id);
// wbot.logout();
// return res.status(200).json({ message: "Session disconnected." });
// };
// const Whatsapp = require("../models/Whatsapp");
// const { getIO } = require("../libs/socket");
// const { getWbot, initWbot, removeWbot } = require("../libs/wbot");
// const wbotMessageListener = require("../services/wbotMessageListener");
// const wbotMonitor = require("../services/wbotMonitor");
// exports.show = async (req, res) => {
// const { whatsappId } = req.params;
// const dbSession = await Whatsapp.findByPk(whatsappId);
// if (!dbSession) {
// return res.status(200).json({ message: "Session not found" });
// }
// return res.status(200).json(dbSession);
// };
// exports.delete = async (req, res) => {
// const { whatsappId } = req.params;
// const dbSession = await Whatsapp.findByPk(whatsappId);
// if (!dbSession) {
// return res.status(404).json({ message: "Session not found" });
// }
// const wbot = getWbot(dbSession.id);
// wbot.logout();
// return res.status(200).json({ message: "Session disconnected." });
// };

View File

@@ -1,36 +0,0 @@
const Sequelize = require("sequelize");
const dbConfig = require("../config/database");
const User = require("../models/User");
const Contact = require("../models/Contact");
const Ticket = require("../models/Ticket");
const Message = require("../models/Message");
const Whatsapp = require("../models/Whatsapp");
const ContactCustomField = require("../models/ContactCustomField");
const Setting = require("../models/Setting");
const models = [
User,
Contact,
Ticket,
Message,
Whatsapp,
ContactCustomField,
Setting,
];
class Database {
constructor() {
this.init();
}
init() {
this.sequelize = new Sequelize(dbConfig);
models
.map(model => model.init(this.sequelize))
.map(model => model.associate && model.associate(this.sequelize.models));
}
}
module.exports = new Database();

View File

@@ -0,0 +1,28 @@
import { Sequelize } from "sequelize-typescript";
import User from "../models/User";
import Setting from "../models/Setting";
import Contact from "../models/Contact";
import Ticket from "../models/Ticket";
import Whatsapp from "../models/Whatsapp";
import ContactCustomField from "../models/ContactCustomField";
import Message from "../models/Message";
// eslint-disable-next-line
const dbConfig = require("../config/database");
// import dbConfig from "../config/database";
const sequelize = new Sequelize(dbConfig);
const models = [
User,
Contact,
Ticket,
Message,
Whatsapp,
ContactCustomField,
Setting
];
sequelize.addModels(models);
export default sequelize;

View File

@@ -1,39 +0,0 @@
"use strict";
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable("Users", {
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true,
allowNull: false,
},
name: {
type: Sequelize.STRING,
allowNull: false,
},
email: {
type: Sequelize.STRING,
allowNull: false,
unique: true,
},
passwordHash: {
type: Sequelize.STRING,
allowNull: false,
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false,
},
});
},
down: queryInterface => {
return queryInterface.dropTable("Users");
},
};

View File

@@ -0,0 +1,39 @@
import { QueryInterface, DataTypes } from "sequelize";
module.exports = {
up: (queryInterface: QueryInterface) => {
return queryInterface.createTable("Users", {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
allowNull: false
},
name: {
type: DataTypes.STRING,
allowNull: false
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
passwordHash: {
type: DataTypes.STRING,
allowNull: false
},
createdAt: {
type: DataTypes.DATE,
allowNull: false
},
updatedAt: {
type: DataTypes.DATE,
allowNull: false
}
});
},
down: (queryInterface: QueryInterface) => {
return queryInterface.dropTable("Users");
}
};

View File

@@ -1,38 +0,0 @@
"use strict";
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable("Contacts", {
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true,
allowNull: false,
},
name: {
type: Sequelize.STRING,
allowNull: false,
},
number: {
type: Sequelize.STRING,
allowNull: false,
unique: true,
},
profilePicUrl: {
type: Sequelize.STRING,
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false,
},
});
},
down: queryInterface => {
return queryInterface.dropTable("Contacts");
},
};

View File

@@ -0,0 +1,38 @@
import { QueryInterface, DataTypes } from "sequelize";
module.exports = {
up: (queryInterface: QueryInterface) => {
return queryInterface.createTable("Contacts", {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
allowNull: false
},
name: {
type: DataTypes.STRING,
allowNull: false
},
number: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
profilePicUrl: {
type: DataTypes.STRING
},
createdAt: {
type: DataTypes.DATE,
allowNull: false
},
updatedAt: {
type: DataTypes.DATE,
allowNull: false
}
});
},
down: (queryInterface: QueryInterface) => {
return queryInterface.dropTable("Contacts");
}
};

View File

@@ -1,46 +0,0 @@
"use strict";
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable("Tickets", {
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true,
allowNull: false,
},
status: {
type: Sequelize.STRING,
defaultValue: "pending",
allowNull: false,
},
lastMessage: {
type: Sequelize.STRING,
},
contactId: {
type: Sequelize.INTEGER,
references: { model: "Contacts", key: "id" },
onUpdate: "CASCADE",
onDelete: "CASCADE",
},
userId: {
type: Sequelize.INTEGER,
references: { model: "Users", key: "id" },
onUpdate: "CASCADE",
onDelete: "SET NULL",
},
createdAt: {
type: Sequelize.DATE(6),
allowNull: false,
},
updatedAt: {
type: Sequelize.DATE(6),
allowNull: false,
},
});
},
down: queryInterface => {
return queryInterface.dropTable("Tickets");
},
};

View File

@@ -0,0 +1,46 @@
import { QueryInterface, DataTypes } from "sequelize";
module.exports = {
up: (queryInterface: QueryInterface) => {
return queryInterface.createTable("Tickets", {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
allowNull: false
},
status: {
type: DataTypes.STRING,
defaultValue: "pending",
allowNull: false
},
lastMessage: {
type: DataTypes.STRING
},
contactId: {
type: DataTypes.INTEGER,
references: { model: "Contacts", key: "id" },
onUpdate: "CASCADE",
onDelete: "CASCADE"
},
userId: {
type: DataTypes.INTEGER,
references: { model: "Users", key: "id" },
onUpdate: "CASCADE",
onDelete: "SET NULL"
},
createdAt: {
type: DataTypes.DATE(6),
allowNull: false
},
updatedAt: {
type: DataTypes.DATE(6),
allowNull: false
}
});
},
down: (queryInterface: QueryInterface) => {
return queryInterface.dropTable("Tickets");
}
};

View File

@@ -1,58 +0,0 @@
"use strict";
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable("Messages", {
id: {
type: Sequelize.STRING,
primaryKey: true,
allowNull: false,
},
body: {
type: Sequelize.TEXT,
allowNull: false,
},
ack: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0,
},
read: {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false,
},
mediaType: {
type: Sequelize.STRING,
},
mediaUrl: {
type: Sequelize.STRING,
},
userId: {
type: Sequelize.INTEGER,
references: { model: "Users", key: "id" },
onUpdate: "CASCADE",
onDelete: "SET NULL",
},
ticketId: {
type: Sequelize.INTEGER,
references: { model: "Tickets", key: "id" },
onUpdate: "CASCADE",
onDelete: "CASCADE",
allowNull: false,
},
createdAt: {
type: Sequelize.DATE(6),
allowNull: false,
},
updatedAt: {
type: Sequelize.DATE(6),
allowNull: false,
},
});
},
down: queryInterface => {
return queryInterface.dropTable("Messages");
},
};

View File

@@ -0,0 +1,58 @@
import { QueryInterface, DataTypes } from "sequelize";
module.exports = {
up: (queryInterface: QueryInterface) => {
return queryInterface.createTable("Messages", {
id: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false
},
body: {
type: DataTypes.TEXT,
allowNull: false
},
ack: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0
},
read: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
},
mediaType: {
type: DataTypes.STRING
},
mediaUrl: {
type: DataTypes.STRING
},
userId: {
type: DataTypes.INTEGER,
references: { model: "Users", key: "id" },
onUpdate: "CASCADE",
onDelete: "SET NULL"
},
ticketId: {
type: DataTypes.INTEGER,
references: { model: "Tickets", key: "id" },
onUpdate: "CASCADE",
onDelete: "CASCADE",
allowNull: false
},
createdAt: {
type: DataTypes.DATE(6),
allowNull: false
},
updatedAt: {
type: DataTypes.DATE(6),
allowNull: false
}
});
},
down: (queryInterface: QueryInterface) => {
return queryInterface.dropTable("Messages");
}
};

View File

@@ -1,41 +0,0 @@
"use strict";
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable("Whatsapps", {
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true,
allowNull: false,
},
session: {
type: Sequelize.TEXT,
},
qrcode: {
type: Sequelize.TEXT,
},
status: {
type: Sequelize.STRING,
},
battery: {
type: Sequelize.STRING,
},
plugged: {
type: Sequelize.BOOLEAN,
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false,
},
});
},
down: queryInterface => {
return queryInterface.dropTable("Whatsapps");
},
};

View File

@@ -0,0 +1,41 @@
import { QueryInterface, DataTypes } from "sequelize";
module.exports = {
up: (queryInterface: QueryInterface) => {
return queryInterface.createTable("Whatsapps", {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
allowNull: false
},
session: {
type: DataTypes.TEXT
},
qrcode: {
type: DataTypes.TEXT
},
status: {
type: DataTypes.STRING
},
battery: {
type: DataTypes.STRING
},
plugged: {
type: DataTypes.BOOLEAN
},
createdAt: {
type: DataTypes.DATE,
allowNull: false
},
updatedAt: {
type: DataTypes.DATE,
allowNull: false
}
});
},
down: (queryInterface: QueryInterface) => {
return queryInterface.dropTable("Whatsapps");
}
};

View File

@@ -1,41 +0,0 @@
"use strict";
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable("ContactCustomFields", {
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true,
allowNull: false,
},
name: {
type: Sequelize.STRING,
allowNull: false,
},
value: {
type: Sequelize.STRING,
allowNull: false,
},
contactId: {
type: Sequelize.INTEGER,
references: { model: "Contacts", key: "id" },
onUpdate: "CASCADE",
onDelete: "CASCADE",
allowNull: false,
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false,
},
});
},
down: queryInterface => {
return queryInterface.dropTable("ContactCustomFields");
},
};

View File

@@ -0,0 +1,41 @@
import { QueryInterface, DataTypes } from "sequelize";
module.exports = {
up: (queryInterface: QueryInterface) => {
return queryInterface.createTable("ContactCustomFields", {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
allowNull: false
},
name: {
type: DataTypes.STRING,
allowNull: false
},
value: {
type: DataTypes.STRING,
allowNull: false
},
contactId: {
type: DataTypes.INTEGER,
references: { model: "Contacts", key: "id" },
onUpdate: "CASCADE",
onDelete: "CASCADE",
allowNull: false
},
createdAt: {
type: DataTypes.DATE,
allowNull: false
},
updatedAt: {
type: DataTypes.DATE,
allowNull: false
}
});
},
down: (queryInterface: QueryInterface) => {
return queryInterface.dropTable("ContactCustomFields");
}
};

View File

@@ -1,15 +0,0 @@
"use strict";
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.addColumn("Contacts", "email", {
type: Sequelize.STRING,
allowNull: false,
defaultValue: "",
});
},
down: queryInterface => {
return queryInterface.removeColumn("Contacts", "email");
},
};

View File

@@ -0,0 +1,15 @@
import { QueryInterface, DataTypes } from "sequelize";
module.exports = {
up: (queryInterface: QueryInterface) => {
return queryInterface.addColumn("Contacts", "email", {
type: DataTypes.STRING,
allowNull: false,
defaultValue: ""
});
},
down: (queryInterface: QueryInterface) => {
return queryInterface.removeColumn("Contacts", "email");
}
};

View File

@@ -1,16 +0,0 @@
"use strict";
module.exports = {
up: queryInterface => {
return queryInterface.removeColumn("Messages", "userId");
},
down: (queryInterface, Sequelize) => {
return queryInterface.addColumn("Messages", "userId", {
type: Sequelize.INTEGER,
references: { model: "Users", key: "id" },
onUpdate: "CASCADE",
onDelete: "SET NULL",
});
},
};

View File

@@ -0,0 +1,16 @@
import { QueryInterface, DataTypes } from "sequelize";
module.exports = {
up: (queryInterface: QueryInterface) => {
return queryInterface.removeColumn("Messages", "userId");
},
down: (queryInterface: QueryInterface) => {
return queryInterface.addColumn("Messages", "userId", {
type: DataTypes.INTEGER,
references: { model: "Users", key: "id" },
onUpdate: "CASCADE",
onDelete: "SET NULL"
});
}
};

View File

@@ -1,15 +0,0 @@
"use strict";
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.addColumn("Messages", "fromMe", {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false,
});
},
down: queryInterface => {
return queryInterface.removeColumn("Messages", "fromMe");
},
};

View File

@@ -0,0 +1,15 @@
import { QueryInterface, DataTypes } from "sequelize";
module.exports = {
up: (queryInterface: QueryInterface) => {
return queryInterface.addColumn("Messages", "fromMe", {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
});
},
down: (queryInterface: QueryInterface) => {
return queryInterface.removeColumn("Messages", "fromMe");
}
};

View File

@@ -1,15 +0,0 @@
"use strict";
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.changeColumn("Tickets", "lastMessage", {
type: Sequelize.TEXT,
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.changeColumn("Tickets", "lastMessage", {
type: Sequelize.STRING,
});
},
};

View File

@@ -0,0 +1,15 @@
import { QueryInterface, DataTypes } from "sequelize";
module.exports = {
up: (queryInterface: QueryInterface) => {
return queryInterface.changeColumn("Tickets", "lastMessage", {
type: DataTypes.TEXT
});
},
down: (queryInterface: QueryInterface) => {
return queryInterface.changeColumn("Tickets", "lastMessage", {
type: DataTypes.STRING
});
}
};

View File

@@ -1,15 +0,0 @@
"use strict";
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.addColumn("Users", "profile", {
type: Sequelize.STRING,
allowNull: false,
defaultValue: "admin",
});
},
down: queryInterface => {
return queryInterface.removeColumn("Users", "profile");
},
};

View File

@@ -0,0 +1,15 @@
import { QueryInterface, DataTypes } from "sequelize";
module.exports = {
up: (queryInterface: QueryInterface) => {
return queryInterface.addColumn("Users", "profile", {
type: DataTypes.STRING,
allowNull: false,
defaultValue: "admin"
});
},
down: (queryInterface: QueryInterface) => {
return queryInterface.removeColumn("Users", "profile");
}
};

View File

@@ -1,29 +0,0 @@
"use strict";
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable("Settings", {
key: {
type: Sequelize.STRING,
primaryKey: true,
allowNull: false,
},
value: {
type: Sequelize.TEXT,
allowNull: false,
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false,
},
});
},
down: queryInterface => {
return queryInterface.dropTable("Settings");
},
};

View File

@@ -0,0 +1,29 @@
import { QueryInterface, DataTypes } from "sequelize";
module.exports = {
up: (queryInterface: QueryInterface) => {
return queryInterface.createTable("Settings", {
key: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false
},
value: {
type: DataTypes.TEXT,
allowNull: false
},
createdAt: {
type: DataTypes.DATE,
allowNull: false
},
updatedAt: {
type: DataTypes.DATE,
allowNull: false
}
});
},
down: (queryInterface: QueryInterface) => {
return queryInterface.dropTable("Settings");
}
};

View File

@@ -1,15 +0,0 @@
"use strict";
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.addColumn("Whatsapps", "name", {
type: Sequelize.STRING,
allowNull: false,
unique: true,
});
},
down: queryInterface => {
return queryInterface.removeColumn("Whatsapps", "name");
},
};

View File

@@ -0,0 +1,15 @@
import { QueryInterface, DataTypes } from "sequelize";
module.exports = {
up: (queryInterface: QueryInterface) => {
return queryInterface.addColumn("Whatsapps", "name", {
type: DataTypes.STRING,
allowNull: false,
unique: true
});
},
down: (queryInterface: QueryInterface) => {
return queryInterface.removeColumn("Whatsapps", "name");
}
};

View File

@@ -1,15 +0,0 @@
"use strict";
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.addColumn("Whatsapps", "default", {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false,
});
},
down: queryInterface => {
return queryInterface.removeColumn("Whatsapps", "default");
},
};

View File

@@ -0,0 +1,15 @@
import { QueryInterface, DataTypes } from "sequelize";
module.exports = {
up: (queryInterface: QueryInterface) => {
return queryInterface.addColumn("Whatsapps", "default", {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
});
},
down: (queryInterface: QueryInterface) => {
return queryInterface.removeColumn("Whatsapps", "default");
}
};

View File

@@ -1,16 +0,0 @@
"use strict";
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.addColumn("Tickets", "whatsappId", {
type: Sequelize.INTEGER,
references: { model: "Whatsapps", key: "id" },
onUpdate: "CASCADE",
onDelete: "SET NULL",
});
},
down: queryInterface => {
return queryInterface.removeColumn("Tickets", "whatsappId");
},
};

View File

@@ -0,0 +1,16 @@
import { QueryInterface, DataTypes } from "sequelize";
module.exports = {
up: (queryInterface: QueryInterface) => {
return queryInterface.addColumn("Tickets", "whatsappId", {
type: DataTypes.INTEGER,
references: { model: "Whatsapps", key: "id" },
onUpdate: "CASCADE",
onDelete: "SET NULL"
});
},
down: (queryInterface: QueryInterface) => {
return queryInterface.removeColumn("Tickets", "whatsappId");
}
};

View File

@@ -0,0 +1,11 @@
import { QueryInterface } from "sequelize";
module.exports = {
up: (queryInterface: QueryInterface) => {
return queryInterface.renameColumn("Whatsapps", "default", "isDefault");
},
down: (queryInterface: QueryInterface) => {
return queryInterface.renameColumn("Whatsapps", "isDefault", "default");
}
};

View File

@@ -1,22 +0,0 @@
"use strict";
module.exports = {
up: queryInterface => {
return queryInterface.bulkInsert(
"Settings",
[
{
key: "userCreation",
value: "enabled",
createdAt: new Date(),
updatedAt: new Date(),
},
],
{}
);
},
down: queryInterface => {
return queryInterface.bulkDelete("Settings", null, {});
},
};

View File

@@ -0,0 +1,22 @@
import { QueryInterface } from "sequelize";
module.exports = {
up: (queryInterface: QueryInterface) => {
return queryInterface.bulkInsert(
"Settings",
[
{
key: "userCreation",
value: "enabled",
createdAt: new Date(),
updatedAt: new Date()
}
],
{}
);
},
down: (queryInterface: QueryInterface) => {
return queryInterface.bulkDelete("Settings", {});
}
};

View File

@@ -0,0 +1,12 @@
class AppError {
public readonly message: string;
public readonly statusCode: number;
constructor(message: string, statusCode = 400) {
this.message = message;
this.statusCode = statusCode;
}
}
export default AppError;

View File

@@ -0,0 +1,16 @@
import Setting from "../models/Setting";
import AppError from "../errors/AppError";
const CheckSettings = async (key: string): Promise<string> => {
const setting = await Setting.findOne({
where: { key }
});
if (!setting) {
throw new AppError("No setting found with this id.", 404);
}
return setting.value;
};
export default CheckSettings;

View File

@@ -0,0 +1,16 @@
import AppError from "../errors/AppError";
import Whatsapp from "../models/Whatsapp";
const GetDefaultWhatsApp = async (): Promise<Whatsapp> => {
const defaultWhatsapp = await Whatsapp.findOne({
where: { isDefault: true }
});
if (!defaultWhatsapp) {
throw new AppError("No default WhatsApp found. Check Connection page.");
}
return defaultWhatsapp;
};
export default GetDefaultWhatsApp;

View File

@@ -0,0 +1,22 @@
import { Client as Session } from "whatsapp-web.js";
import { getWbot } from "../libs/wbot";
import AppError from "../errors/AppError";
import GetDefaultWhatsApp from "./GetDefaultWhatsApp";
import Ticket from "../models/Ticket";
const GetTicketWbot = async (ticket: Ticket): Promise<Session> => {
if (!ticket.whatsappId) {
const defaultWhatsapp = await GetDefaultWhatsApp();
if (!defaultWhatsapp) {
throw new AppError("No default WhatsApp found. Check Connection page.");
}
await ticket.$set("whatsapp", defaultWhatsapp);
}
const wbot = getWbot(ticket.whatsappId);
return wbot;
};
export default GetTicketWbot;

View File

@@ -0,0 +1,33 @@
import { getIO } from "../libs/socket";
import Message from "../models/Message";
import Ticket from "../models/Ticket";
import GetTicketWbot from "./GetTicketWbot";
const SetTicketMessagesAsRead = async (ticket: Ticket): Promise<void> => {
await Message.update(
{ read: true },
{
where: {
ticketId: ticket.id,
read: false
}
}
);
try {
const wbot = await GetTicketWbot(ticket);
await wbot.sendSeen(`${ticket.contact.number}@c.us`);
} catch (err) {
console.log(
"Could not mark messages as read. Maybe whatsapp session disconnected?"
);
}
const io = getIO();
io.to("notification").emit("ticket", {
action: "updateUnread",
ticketId: ticket.id
});
};
export default SetTicketMessagesAsRead;

View File

@@ -1,14 +0,0 @@
let io;
module.exports = {
init: httpServer => {
io = require("socket.io")(httpServer);
return io;
},
getIO: () => {
if (!io) {
throw new Error("Socket IO not initialized");
}
return io;
},
};

View File

@@ -0,0 +1,32 @@
import socketIo, { Server as SocketIO } from "socket.io";
import { Server } from "http";
import AppError from "../errors/AppError";
let io: SocketIO;
export const initIO = (httpServer: Server): SocketIO => {
io = socketIo(httpServer);
io.on("connection", socket => {
console.log("Client Connected");
socket.on("joinChatBox", ticketId => {
console.log("A client joined a ticket channel");
socket.join(ticketId);
});
socket.on("joinNotification", () => {
console.log("A client joined notification channel");
socket.join("notification");
});
socket.on("disconnect", () => {
console.log("Client disconnected");
});
});
return io;
};
export const getIO = (): SocketIO => {
if (!io) {
throw new AppError("Socket IO not initialized");
}
return io;
};

View File

@@ -1,110 +0,0 @@
const qrCode = require("qrcode-terminal");
const { Client } = require("whatsapp-web.js");
const Whatsapp = require("../models/Whatsapp");
const { getIO } = require("../libs/socket");
let sessions = [];
module.exports = {
initWbot: async whatsapp => {
try {
const io = getIO();
const sessionName = whatsapp.name;
let sessionCfg;
if (whatsapp && whatsapp.session) {
sessionCfg = JSON.parse(whatsapp.session);
}
const sessionIndex = sessions.findIndex(s => s.id === whatsapp.id);
if (sessionIndex !== -1) {
sessions[sessionIndex].destroy();
sessions.splice(sessionIndex, 1);
}
const wbot = new Client({
session: sessionCfg,
restartOnAuthFail: true,
});
wbot.initialize();
wbot.on("qr", async qr => {
console.log("Session:", sessionName);
qrCode.generate(qr, { small: true });
await whatsapp.update({ qrcode: qr, status: "qrcode" });
io.emit("whatsappSession", {
action: "update",
session: whatsapp,
});
});
wbot.on("authenticated", async session => {
console.log("Session:", sessionName, "AUTHENTICATED");
await whatsapp.update({
session: JSON.stringify(session),
status: "authenticated",
});
io.emit("whatsappSession", {
action: "update",
session: whatsapp,
});
});
wbot.on("auth_failure", async msg => {
console.error("Session:", sessionName, "AUTHENTICATION FAILURE", msg);
await whatsapp.update({ session: "" });
});
wbot.on("ready", async () => {
console.log("Session:", sessionName, "READY");
await whatsapp.update({
status: "CONNECTED",
qrcode: "",
});
io.emit("whatsappSession", {
action: "update",
session: whatsapp,
});
wbot.sendPresenceAvailable();
});
wbot.id = whatsapp.id;
sessions.push(wbot);
} catch (err) {
console.log(err);
}
return null;
},
getWbot: whatsappId => {
const sessionIndex = sessions.findIndex(s => s.id === whatsappId);
if (sessionIndex === -1) {
console.log("This Wbot session is not initialized");
return null;
}
return sessions[sessionIndex];
},
removeWbot: whatsappId => {
try {
const sessionIndex = sessions.findIndex(s => s.id === whatsappId);
if (sessionIndex !== -1) {
sessions[sessionIndex].destroy();
sessions.splice(sessionIndex, 1);
}
} catch (err) {
console.log(err);
}
},
};

112
backend/src/libs/wbot.ts Normal file
View File

@@ -0,0 +1,112 @@
import qrCode from "qrcode-terminal";
import { Client } from "whatsapp-web.js";
import { getIO } from "./socket";
import Whatsapp from "../models/Whatsapp";
import AppError from "../errors/AppError";
interface Session extends Client {
id?: number;
}
const sessions: Session[] = [];
export const initWbot = async (whatsapp: Whatsapp): Promise<void> => {
try {
const io = getIO();
const sessionName = whatsapp.name;
let sessionCfg;
if (whatsapp && whatsapp.session) {
sessionCfg = JSON.parse(whatsapp.session);
}
const sessionIndex = sessions.findIndex(s => s.id === whatsapp.id);
if (sessionIndex !== -1) {
sessions[sessionIndex].destroy();
sessions.splice(sessionIndex, 1);
}
const wbot: Session = new Client({
session: sessionCfg,
restartOnAuthFail: true
});
wbot.initialize();
wbot.on("qr", async qr => {
console.log("Session:", sessionName);
qrCode.generate(qr, { small: true });
await whatsapp.update({ qrcode: qr, status: "qrcode" });
io.emit("whatsappSession", {
action: "update",
session: whatsapp
});
});
wbot.on("authenticated", async session => {
console.log("Session:", sessionName, "AUTHENTICATED");
await whatsapp.update({
session: JSON.stringify(session),
status: "authenticated"
});
io.emit("whatsappSession", {
action: "update",
session: whatsapp
});
});
wbot.on("auth_failure", async msg => {
console.error("Session:", sessionName, "AUTHENTICATION FAILURE", msg);
await whatsapp.update({ session: "" });
});
wbot.on("ready", async () => {
console.log("Session:", sessionName, "READY");
await whatsapp.update({
status: "CONNECTED",
qrcode: ""
});
io.emit("whatsappSession", {
action: "update",
session: whatsapp
});
wbot.sendPresenceAvailable();
});
wbot.id = whatsapp.id;
sessions.push(wbot);
} catch (err) {
console.log(err);
}
};
export const getWbot = (whatsappId: number): Session => {
const sessionIndex = sessions.findIndex(s => s.id === whatsappId);
if (sessionIndex === -1) {
throw new AppError(
"This WhatsApp session is not initialized. Check connections page."
);
}
return sessions[sessionIndex];
};
export const removeWbot = (whatsappId: number): void => {
try {
const sessionIndex = sessions.findIndex(s => s.id === whatsappId);
if (sessionIndex !== -1) {
sessions[sessionIndex].destroy();
sessions.splice(sessionIndex, 1);
}
} catch (err) {
console.log(err);
}
};

View File

@@ -1,36 +0,0 @@
const jwt = require("jsonwebtoken");
const util = require("util");
const User = require("../models/User");
const authConfig = require("../config/auth");
module.exports = async (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({ error: "Token not provided" });
}
const [, token] = authHeader.split(" ");
try {
const decoded = await util.promisify(jwt.verify)(token, authConfig.secret);
const user = await User.findByPk(decoded.userId, {
attributes: ["id", "name", "profile", "email"],
});
if (!user) {
return res
.status(401)
.json({ error: "The token corresponding user does not exists." });
}
req.user = user;
return next();
} catch (err) {
console.log(err);
return res.status(401).json({ error: "Invalid Token" });
}
};

View File

@@ -0,0 +1,39 @@
import { verify } from "jsonwebtoken";
import { Request, Response, NextFunction } from "express";
import AppError from "../errors/AppError";
import authConfig from "../config/auth";
interface TokenPayload {
id: string;
username: string;
profile: string;
iat: number;
exp: number;
}
const isAuth = (req: Request, res: Response, next: NextFunction): void => {
const authHeader = req.headers.authorization;
if (!authHeader) {
throw new AppError("Token not provided.", 403);
}
const [, token] = authHeader.split(" ");
try {
const decoded = verify(token, authConfig.secret);
const { id, profile } = decoded as TokenPayload;
req.user = {
id,
profile
};
} catch (err) {
throw new AppError("Invalid token.", 403);
}
return next();
};
export default isAuth;

View File

@@ -1,29 +0,0 @@
const Sequelize = require("sequelize");
class Contact extends Sequelize.Model {
static init(sequelize) {
super.init(
{
name: { type: Sequelize.STRING },
number: { type: Sequelize.STRING, allowNull: false, unique: true },
email: { type: Sequelize.STRING, allowNull: false, defaultValue: "" },
profilePicUrl: { type: Sequelize.STRING },
},
{
sequelize,
}
);
return this;
}
static associate(models) {
this.hasMany(models.Ticket, { foreignKey: "contactId", as: "contact" });
this.hasMany(models.ContactCustomField, {
foreignKey: "contactId",
as: "extraInfo",
});
}
}
module.exports = Contact;

View File

@@ -0,0 +1,53 @@
import {
Table,
Column,
CreatedAt,
UpdatedAt,
Model,
PrimaryKey,
AutoIncrement,
AllowNull,
Unique,
Default,
HasMany
} from "sequelize-typescript";
import ContactCustomField from "./ContactCustomField";
import Ticket from "./Ticket";
@Table
class Contact extends Model<Contact> {
@PrimaryKey
@AutoIncrement
@Column
id: number;
@Column
name: string;
@AllowNull(false)
@Unique
@Column
number: string;
@AllowNull(false)
@Default("")
@Column
email: string;
@Column
profilePicUrl: string;
@CreatedAt
createdAt: Date;
@UpdatedAt
updatedAt: Date;
@HasMany(() => Ticket)
tickets: Ticket[];
@HasMany(() => ContactCustomField)
extraInfo: ContactCustomField[];
}
export default Contact;

View File

@@ -1,26 +0,0 @@
const Sequelize = require("sequelize");
class ContactCustomField extends Sequelize.Model {
static init(sequelize) {
super.init(
{
name: { type: Sequelize.STRING },
value: { type: Sequelize.STRING },
},
{
sequelize,
}
);
return this;
}
static associate(models) {
this.belongsTo(models.Contact, {
foreignKey: "contactId",
as: "extraInfo",
});
}
}
module.exports = ContactCustomField;

View File

@@ -0,0 +1,41 @@
import {
Table,
Column,
CreatedAt,
UpdatedAt,
Model,
PrimaryKey,
AutoIncrement,
ForeignKey,
BelongsTo
} from "sequelize-typescript";
import Contact from "./Contact";
@Table
class ContactCustomField extends Model<ContactCustomField> {
@PrimaryKey
@AutoIncrement
@Column
id: number;
@Column
name: string;
@Column
value: string;
@ForeignKey(() => Contact)
@Column
contactId: number;
@BelongsTo(() => Contact)
contact: Contact;
@CreatedAt
createdAt: Date;
@UpdatedAt
updatedAt: Date;
}
export default ContactCustomField;

View File

@@ -1,35 +0,0 @@
const Sequelize = require("sequelize");
class Message extends Sequelize.Model {
static init(sequelize) {
super.init(
{
ack: { type: Sequelize.INTEGER, defaultValue: 0 },
read: { type: Sequelize.BOOLEAN, defaultValue: false },
fromMe: { type: Sequelize.BOOLEAN, defaultValue: false },
body: { type: Sequelize.TEXT },
mediaUrl: { type: Sequelize.STRING },
mediaType: { type: Sequelize.STRING },
createdAt: {
type: Sequelize.DATE(6),
allowNull: false,
},
updatedAt: {
type: Sequelize.DATE(6),
allowNull: false,
},
},
{
sequelize,
}
);
return this;
}
static associate(models) {
this.belongsTo(models.Ticket, { foreignKey: "ticketId", as: "messages" });
}
}
module.exports = Message;

View File

@@ -0,0 +1,66 @@
import {
Table,
Column,
CreatedAt,
UpdatedAt,
Model,
DataType,
PrimaryKey,
AutoIncrement,
Default,
BelongsTo,
ForeignKey
} from "sequelize-typescript";
import Ticket from "./Ticket";
@Table
class Message extends Model<Message> {
@PrimaryKey
@Column
id: string;
@Default(0)
@Column
ack: number;
@Default(false)
@Column
read: boolean;
@Default(false)
@Column
fromMe: boolean;
@Column(DataType.TEXT)
body: string;
@Column(DataType.STRING)
get mediaUrl(): string | null {
if (this.getDataValue("mediaUrl")) {
return `${process.env.BACKEND_URL}:${
process.env.PROXY_PORT
}/public/${this.getDataValue("mediaUrl")}`;
}
return null;
}
@Column
mediaType: string;
@CreatedAt
@Column(DataType.DATE(6))
createdAt: Date;
@UpdatedAt
@Column(DataType.DATE(6))
updatedAt: Date;
@ForeignKey(() => Ticket)
@Column
ticketId: number;
@BelongsTo(() => Ticket)
ticket: Ticket;
}
export default Message;

View File

@@ -1,24 +0,0 @@
const Sequelize = require("sequelize");
class Setting extends Sequelize.Model {
static init(sequelize) {
super.init(
{
key: {
type: Sequelize.STRING,
primaryKey: true,
allowNull: false,
unique: true,
},
value: { type: Sequelize.TEXT, allowNull: false },
},
{
sequelize,
}
);
return this;
}
}
module.exports = Setting;

View File

@@ -0,0 +1,26 @@
import {
Table,
Column,
CreatedAt,
UpdatedAt,
Model,
PrimaryKey
} from "sequelize-typescript";
@Table
class Setting extends Model<Setting> {
@PrimaryKey
@Column
key: string;
@Column
value: string;
@CreatedAt
createdAt: Date;
@UpdatedAt
updatedAt: Date;
}
export default Setting;

View File

@@ -1,50 +0,0 @@
const Sequelize = require("sequelize");
const Message = require("./Message");
class Ticket extends Sequelize.Model {
static init(sequelize) {
super.init(
{
status: { type: Sequelize.STRING, defaultValue: "pending" },
userId: { type: Sequelize.INTEGER, defaultValue: null },
unreadMessages: { type: Sequelize.VIRTUAL },
lastMessage: { type: Sequelize.STRING },
},
{
sequelize,
}
);
this.addHook("afterFind", async result => {
if (result && result.length > 0) {
await Promise.all(
result.map(async ticket => {
ticket.unreadMessages = await Message.count({
where: { ticketId: ticket.id, read: false },
});
})
);
}
});
this.addHook("beforeUpdate", async ticket => {
ticket.unreadMessages = await Message.count({
where: { ticketId: ticket.id, read: false },
});
});
return this;
}
static associate(models) {
this.belongsTo(models.Contact, { foreignKey: "contactId", as: "contact" });
this.belongsTo(models.User, { foreignKey: "userId", as: "user" });
this.belongsTo(models.Whatsapp, {
foreignKey: "whatsappId",
as: "whatsapp",
});
this.hasMany(models.Message, { foreignKey: "ticketId", as: "messages" });
}
}
module.exports = Ticket;

View File

@@ -0,0 +1,89 @@
import {
Table,
Column,
CreatedAt,
UpdatedAt,
Model,
DataType,
PrimaryKey,
ForeignKey,
BelongsTo,
HasMany,
AutoIncrement,
AfterFind,
BeforeUpdate
} from "sequelize-typescript";
import Contact from "./Contact";
import Message from "./Message";
import User from "./User";
import Whatsapp from "./Whatsapp";
@Table
class Ticket extends Model<Ticket> {
@PrimaryKey
@AutoIncrement
@Column
id: number;
@Column({ defaultValue: "pending" })
status: string;
@Column(DataType.VIRTUAL)
unreadMessages: number;
@Column
lastMessage: string;
@CreatedAt
createdAt: Date;
@UpdatedAt
updatedAt: Date;
@ForeignKey(() => User)
@Column
userId: number;
@BelongsTo(() => User)
user: User;
@ForeignKey(() => Contact)
@Column
contactId: number;
@BelongsTo(() => Contact)
contact: Contact;
@ForeignKey(() => Whatsapp)
@Column
whatsappId: number;
@BelongsTo(() => Whatsapp)
whatsapp: Whatsapp;
@HasMany(() => Message)
messages: Message[];
@AfterFind
static async countTicketsUnreadMessages(tickets: Ticket[]): Promise<void> {
if (tickets && tickets.length > 0) {
await Promise.all(
tickets.map(async ticket => {
ticket.unreadMessages = await Message.count({
where: { ticketId: ticket.id, read: false }
});
})
);
}
}
@BeforeUpdate
static async countTicketUnreadMessags(ticket: Ticket): Promise<void> {
ticket.unreadMessages = await Message.count({
where: { ticketId: ticket.id, read: false }
});
}
}
export default Ticket;

View File

@@ -1,32 +0,0 @@
const Sequelize = require("sequelize");
const bcrypt = require("bcryptjs");
class User extends Sequelize.Model {
static init(sequelize) {
super.init(
{
name: { type: Sequelize.STRING },
password: { type: Sequelize.VIRTUAL },
profile: { type: Sequelize.STRING, defaultValue: "admin" },
passwordHash: { type: Sequelize.STRING },
email: { type: Sequelize.STRING },
},
{
sequelize,
}
);
this.addHook("beforeSave", async user => {
if (user.password) {
user.passwordHash = await bcrypt.hash(user.password, 8);
}
});
return this;
}
checkPassword(password) {
return bcrypt.compare(password, this.passwordHash);
}
}
module.exports = User;

View File

@@ -0,0 +1,63 @@
import {
Table,
Column,
CreatedAt,
UpdatedAt,
Model,
DataType,
BeforeCreate,
BeforeUpdate,
PrimaryKey,
AutoIncrement,
Default,
HasMany
} from "sequelize-typescript";
import { hash, compare } from "bcryptjs";
import Ticket from "./Ticket";
@Table
class User extends Model<User> {
@PrimaryKey
@AutoIncrement
@Column
id: number;
@Column
name: string;
@Column
email: string;
@Column(DataType.VIRTUAL)
password: string;
@Column
passwordHash: string;
@Default("admin")
@Column
profile: string;
@CreatedAt
createdAt: Date;
@UpdatedAt
updatedAt: Date;
@HasMany(() => Ticket)
tickets: Ticket[];
@BeforeUpdate
@BeforeCreate
static hashPassword = async (instance: User): Promise<void> => {
if (instance.password) {
instance.passwordHash = await hash(instance.password, 8);
}
};
public checkPassword = async (password: string): Promise<boolean> => {
return compare(password, this.getDataValue("passwordHash"));
};
}
export default User;

View File

@@ -1,32 +0,0 @@
const Sequelize = require("sequelize");
class Whatsapp extends Sequelize.Model {
static init(sequelize) {
super.init(
{
session: { type: Sequelize.TEXT },
qrcode: { type: Sequelize.TEXT },
name: { type: Sequelize.STRING, unique: true, allowNull: false },
status: { type: Sequelize.STRING },
battery: { type: Sequelize.STRING },
plugged: { type: Sequelize.BOOLEAN },
default: {
type: Sequelize.BOOLEAN,
defaultValue: false,
allowNull: false,
},
},
{
sequelize,
}
);
return this;
}
static associate(models) {
this.hasMany(models.Ticket, { foreignKey: "whatsappId", as: "tickets" });
}
}
module.exports = Whatsapp;

View File

@@ -0,0 +1,59 @@
import {
Table,
Column,
CreatedAt,
UpdatedAt,
Model,
DataType,
PrimaryKey,
AutoIncrement,
Default,
AllowNull,
HasMany,
Unique
} from "sequelize-typescript";
import Ticket from "./Ticket";
@Table
class Whatsapp extends Model<Whatsapp> {
@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(false)
@AllowNull
@Column
isDefault: boolean;
@CreatedAt
createdAt: Date;
@UpdatedAt
updatedAt: Date;
@HasMany(() => Ticket)
tickets: Ticket[];
}
export default Whatsapp;

View File

@@ -1,21 +0,0 @@
const express = require("express");
const AuthRoutes = require("./routes/auth");
const TicketsRoutes = require("./routes/tickets");
const MessagesRoutes = require("./routes/messages");
const ContactsRoutes = require("./routes/contacts");
const WhatsRoutes = require("./routes/whatsapp");
const UsersRoutes = require("./routes/users");
const SettingsRoutes = require("./routes/settings");
const routes = express.Router();
routes.use("/auth", AuthRoutes);
routes.use(TicketsRoutes);
routes.use(MessagesRoutes);
routes.use(ContactsRoutes);
routes.use(WhatsRoutes);
routes.use(UsersRoutes);
routes.use(SettingsRoutes);
module.exports = routes;

View File

@@ -1,16 +0,0 @@
const express = require("express");
const SessionController = require("../../controllers/SessionController");
const UserController = require("../../controllers/UserController");
const isAuth = require("../../middleware/is-auth");
const routes = express.Router();
routes.post("/signup", UserController.store);
routes.post("/login", SessionController.store);
routes.get("/check", isAuth, (req, res) => {
res.status(200).json({ authenticated: true });
});
module.exports = routes;

View File

@@ -1,21 +0,0 @@
const express = require("express");
const isAuth = require("../../middleware/is-auth");
const ContactController = require("../../controllers/ContactController");
const ImportPhoneContactsController = require("../../controllers/ImportPhoneContactsController");
const routes = express.Router();
routes.post("/contacts/import", isAuth, ImportPhoneContactsController.store);
routes.get("/contacts", isAuth, ContactController.index);
routes.get("/contacts/:contactId", isAuth, ContactController.show);
routes.post("/contacts", isAuth, ContactController.store);
routes.put("/contacts/:contactId", isAuth, ContactController.update);
routes.delete("/contacts/:contactId", isAuth, ContactController.delete);
module.exports = routes;

View File

@@ -1,12 +0,0 @@
const express = require("express");
const isAuth = require("../../middleware/is-auth");
const MessageController = require("../../controllers/MessageController");
const routes = express.Router();
routes.get("/messages/:ticketId", isAuth, MessageController.index);
routes.post("/messages/:ticketId", isAuth, MessageController.store);
module.exports = routes;

View File

@@ -1,14 +0,0 @@
const express = require("express");
const isAuth = require("../../middleware/is-auth");
const SettingController = require("../../controllers/SettingController");
const routes = express.Router();
routes.get("/settings", isAuth, SettingController.index);
// routes.get("/settings/:settingKey", isAuth, SettingsController.show);
routes.put("/settings/:settingKey", isAuth, SettingController.update);
module.exports = routes;

View File

@@ -1,16 +0,0 @@
const express = require("express");
const isAuth = require("../../middleware/is-auth");
const TicketController = require("../../controllers/TicketController");
const routes = express.Router();
routes.get("/tickets", isAuth, TicketController.index);
routes.post("/tickets", isAuth, TicketController.store);
routes.put("/tickets/:ticketId", isAuth, TicketController.update);
routes.delete("/tickets/:ticketId", isAuth, TicketController.delete);
module.exports = routes;

View File

@@ -1,18 +0,0 @@
const express = require("express");
const isAuth = require("../../middleware/is-auth");
const UserController = require("../../controllers/UserController");
const routes = express.Router();
routes.get("/users", isAuth, UserController.index);
routes.post("/users", isAuth, UserController.store);
routes.put("/users/:userId", isAuth, UserController.update);
routes.get("/users/:userId", isAuth, UserController.show);
routes.delete("/users/:userId", isAuth, UserController.delete);
module.exports = routes;

Some files were not shown because too many files have changed in this diff Show More