From bce9566b6e63897c36e4d5571ee38ccb6871dfed Mon Sep 17 00:00:00 2001 From: Wender Teixeira Date: Thu, 30 Dec 2021 19:18:30 -0300 Subject: [PATCH] Feat. Api send --- backend/package.json | 2 + backend/src/controllers/ApiController.ts | 92 +++++++++++++++++++ ...20200904070006-create-apiToken-settings.ts | 23 +++++ backend/src/middleware/isAuthApi.ts | 39 ++++++++ backend/src/routes/apiRoutes.ts | 14 +++ backend/src/routes/index.ts | 2 + .../ListSettingByValueService.ts | 22 +++++ .../WbotServices/SendWhatsAppMedia.ts | 12 ++- frontend/src/pages/Settings/index.js | 20 +++- 9 files changed, 221 insertions(+), 5 deletions(-) create mode 100644 backend/src/controllers/ApiController.ts create mode 100644 backend/src/database/seeds/20200904070006-create-apiToken-settings.ts create mode 100644 backend/src/middleware/isAuthApi.ts create mode 100644 backend/src/routes/apiRoutes.ts create mode 100644 backend/src/services/SettingServices/ListSettingByValueService.ts diff --git a/backend/package.json b/backend/package.json index ba4ac10..a2beb90 100644 --- a/backend/package.json +++ b/backend/package.json @@ -37,6 +37,7 @@ "sequelize-cli": "^5.5.1", "sequelize-typescript": "^1.1.0", "socket.io": "^3.0.5", + "uuid": "^8.3.2", "whatsapp-web.js": "^1.15.3", "yup": "^0.32.8" }, @@ -53,6 +54,7 @@ "@types/multer": "^1.4.4", "@types/node": "^14.11.8", "@types/supertest": "^2.0.10", + "@types/uuid": "^8.3.3", "@types/validator": "^13.1.0", "@types/yup": "^0.29.8", "@typescript-eslint/eslint-plugin": "^4.4.0", diff --git a/backend/src/controllers/ApiController.ts b/backend/src/controllers/ApiController.ts new file mode 100644 index 0000000..33fb7df --- /dev/null +++ b/backend/src/controllers/ApiController.ts @@ -0,0 +1,92 @@ +import { Request, Response } from "express"; +import * as Yup from "yup"; +import AppError from "../errors/AppError"; +import GetDefaultWhatsApp from "../helpers/GetDefaultWhatsApp"; +import SetTicketMessagesAsRead from "../helpers/SetTicketMessagesAsRead"; +import Message from "../models/Message"; +import CreateOrUpdateContactService from "../services/ContactServices/CreateOrUpdateContactService"; +import FindOrCreateTicketService from "../services/TicketServices/FindOrCreateTicketService"; +import ShowTicketService from "../services/TicketServices/ShowTicketService"; +import CheckIsValidContact from "../services/WbotServices/CheckIsValidContact"; +import CheckContactNumber from "../services/WbotServices/CheckNumber"; +import GetProfilePicUrl from "../services/WbotServices/GetProfilePicUrl"; +import SendWhatsAppMedia from "../services/WbotServices/SendWhatsAppMedia"; +import SendWhatsAppMessage from "../services/WbotServices/SendWhatsAppMessage"; + +type MessageData = { + body: string; + fromMe: boolean; + read: boolean; + quotedMsg?: Message; +}; + +interface ContactData { + number: string; +} + +const createContact = async (newContact: string) => { + await CheckIsValidContact(newContact); + + const validNumber: any = await CheckContactNumber(newContact); + + const profilePicUrl = await GetProfilePicUrl(validNumber); + + const number = validNumber; + + const contactData = { + name: `${number}`, + number, + profilePicUrl, + isGroup: false + }; + + const contact = await CreateOrUpdateContactService(contactData); + + const defaultWhatsapp = await GetDefaultWhatsApp(); + + const createTicket = await FindOrCreateTicketService( + contact, + defaultWhatsapp.id, + 1 + ); + + const ticket = await ShowTicketService(createTicket.id); + + SetTicketMessagesAsRead(ticket); + + return ticket; +}; + +export const index = async (req: Request, res: Response): Promise => { + const newContact: ContactData = req.body; + const { body, quotedMsg }: MessageData = req.body; + const medias = req.files as Express.Multer.File[]; + + newContact.number = newContact.number.replace("-", "").replace(" ", ""); + + const schema = Yup.object().shape({ + number: Yup.string() + .required() + .matches(/^\d+$/, "Invalid number format. Only numbers is allowed.") + }); + + try { + await schema.validate(newContact); + } catch (err: any) { + throw new AppError(err.message); + } + + const contactAndTicket = await createContact(newContact.number); + + if (medias) { + await Promise.all( + medias.map(async (media: Express.Multer.File) => { + await SendWhatsAppMedia({ body, media, ticket: contactAndTicket }); + }) + ); + } else { + await SendWhatsAppMessage({ body, ticket: contactAndTicket, quotedMsg }); + } + + return res.send(); +}; diff --git a/backend/src/database/seeds/20200904070006-create-apiToken-settings.ts b/backend/src/database/seeds/20200904070006-create-apiToken-settings.ts new file mode 100644 index 0000000..f5e2b36 --- /dev/null +++ b/backend/src/database/seeds/20200904070006-create-apiToken-settings.ts @@ -0,0 +1,23 @@ +import { QueryInterface } from "sequelize"; +import { v4 as uuidv4 } from "uuid"; + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.bulkInsert( + "Settings", + [ + { + key: "userApiToken", + value: uuidv4(), + createdAt: new Date(), + updatedAt: new Date() + } + ], + {} + ); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.bulkDelete("Settings", {}); + } +}; diff --git a/backend/src/middleware/isAuthApi.ts b/backend/src/middleware/isAuthApi.ts new file mode 100644 index 0000000..cc36460 --- /dev/null +++ b/backend/src/middleware/isAuthApi.ts @@ -0,0 +1,39 @@ +import { Request, Response, NextFunction } from "express"; + +import AppError from "../errors/AppError"; +import ListSettingByValueService from "../services/SettingServices/ListSettingByValueService"; + +const isAuthApi = async ( + req: Request, + res: Response, + next: NextFunction +): Promise => { + const authHeader = req.headers.authorization; + + if (!authHeader) { + throw new AppError("ERR_SESSION_EXPIRED", 401); + } + + const [, token] = authHeader.split(" "); + + try { + const getToken = await ListSettingByValueService(token); + if (!getToken) { + throw new AppError("ERR_SESSION_EXPIRED", 401); + } + + if (getToken.value !== token) { + throw new AppError("ERR_SESSION_EXPIRED", 401); + } + } catch (err) { + console.log(err); + throw new AppError( + "Invalid token. We'll try to assign a new one on next request", + 403 + ); + } + + return next(); +}; + +export default isAuthApi; diff --git a/backend/src/routes/apiRoutes.ts b/backend/src/routes/apiRoutes.ts new file mode 100644 index 0000000..e990052 --- /dev/null +++ b/backend/src/routes/apiRoutes.ts @@ -0,0 +1,14 @@ +import express from "express"; +import multer from "multer"; +import uploadConfig from "../config/upload"; + +import * as ApiController from "../controllers/ApiController"; +import isAuthApi from "../middleware/isAuthApi"; + +const upload = multer(uploadConfig); + +const ApiRoutes = express.Router(); + +ApiRoutes.post("/send", isAuthApi, upload.array("medias"), ApiController.index); + +export default ApiRoutes; diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index 36d14bd..e741c38 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -10,6 +10,7 @@ import messageRoutes from "./messageRoutes"; import whatsappSessionRoutes from "./whatsappSessionRoutes"; import queueRoutes from "./queueRoutes"; import quickAnswerRoutes from "./quickAnswerRoutes"; +import apiRoutes from "./apiRoutes"; const routes = Router(); @@ -23,5 +24,6 @@ routes.use(messageRoutes); routes.use(whatsappSessionRoutes); routes.use(queueRoutes); routes.use(quickAnswerRoutes); +routes.use("/api/messages", apiRoutes); export default routes; diff --git a/backend/src/services/SettingServices/ListSettingByValueService.ts b/backend/src/services/SettingServices/ListSettingByValueService.ts new file mode 100644 index 0000000..d914a9b --- /dev/null +++ b/backend/src/services/SettingServices/ListSettingByValueService.ts @@ -0,0 +1,22 @@ +import AppError from "../../errors/AppError"; +import Setting from "../../models/Setting"; + +interface Response { + key: string; + value: string; +} +const ListSettingByKeyService = async ( + value: string +): Promise => { + const settings = await Setting.findOne({ + where: { value } + }); + + if (!settings) { + throw new AppError("ERR_NO_API_TOKEN_FOUND", 404); + } + + return { key: settings.key, value: settings.value }; +}; + +export default ListSettingByKeyService; diff --git a/backend/src/services/WbotServices/SendWhatsAppMedia.ts b/backend/src/services/WbotServices/SendWhatsAppMedia.ts index 455d5c3..80778c2 100644 --- a/backend/src/services/WbotServices/SendWhatsAppMedia.ts +++ b/backend/src/services/WbotServices/SendWhatsAppMedia.ts @@ -7,24 +7,28 @@ import Ticket from "../../models/Ticket"; interface Request { media: Express.Multer.File; ticket: Ticket; + body?: string; } const SendWhatsAppMedia = async ({ media, - ticket + ticket, + body }: Request): Promise => { try { const wbot = await GetTicketWbot(ticket); const newMedia = MessageMedia.fromFilePath(media.path); - const sentMessage = await wbot.sendMessage( `${ticket.contact.number}@${ticket.isGroup ? "g" : "c"}.us`, newMedia, - { sendAudioAsVoice: true } + { + caption: body, + sendAudioAsVoice: true + } ); - await ticket.update({ lastMessage: media.filename }); + await ticket.update({ lastMessage: body || media.filename }); fs.unlinkSync(media.path); diff --git a/frontend/src/pages/Settings/index.js b/frontend/src/pages/Settings/index.js index 2fc4097..855055d 100644 --- a/frontend/src/pages/Settings/index.js +++ b/frontend/src/pages/Settings/index.js @@ -6,6 +6,7 @@ import Paper from "@material-ui/core/Paper"; import Typography from "@material-ui/core/Typography"; import Container from "@material-ui/core/Container"; import Select from "@material-ui/core/Select"; +import TextField from "@material-ui/core/TextField"; import { toast } from "react-toastify"; import api from "../../services/api"; @@ -16,13 +17,15 @@ const useStyles = makeStyles(theme => ({ root: { display: "flex", alignItems: "center", - padding: theme.spacing(4), + padding: theme.spacing(8, 8, 3), }, paper: { padding: theme.spacing(2), display: "flex", alignItems: "center", + marginBottom: 12, + }, settingOption: { @@ -31,6 +34,7 @@ const useStyles = makeStyles(theme => ({ margin: { margin: theme.spacing(1), }, + })); const Settings = () => { @@ -117,7 +121,21 @@ const Settings = () => { {i18n.t("settings.settings.userCreation.options.disabled")} + + + + 0 && getSettingValue("userApiToken")} + /> + + );