mirror of
https://github.com/cheveguerra/whaticket-community.git
synced 2026-04-17 19:37:02 +00:00
Feat. Api send
This commit is contained in:
@@ -37,6 +37,7 @@
|
|||||||
"sequelize-cli": "^5.5.1",
|
"sequelize-cli": "^5.5.1",
|
||||||
"sequelize-typescript": "^1.1.0",
|
"sequelize-typescript": "^1.1.0",
|
||||||
"socket.io": "^3.0.5",
|
"socket.io": "^3.0.5",
|
||||||
|
"uuid": "^8.3.2",
|
||||||
"whatsapp-web.js": "^1.15.3",
|
"whatsapp-web.js": "^1.15.3",
|
||||||
"yup": "^0.32.8"
|
"yup": "^0.32.8"
|
||||||
},
|
},
|
||||||
@@ -53,6 +54,7 @@
|
|||||||
"@types/multer": "^1.4.4",
|
"@types/multer": "^1.4.4",
|
||||||
"@types/node": "^14.11.8",
|
"@types/node": "^14.11.8",
|
||||||
"@types/supertest": "^2.0.10",
|
"@types/supertest": "^2.0.10",
|
||||||
|
"@types/uuid": "^8.3.3",
|
||||||
"@types/validator": "^13.1.0",
|
"@types/validator": "^13.1.0",
|
||||||
"@types/yup": "^0.29.8",
|
"@types/yup": "^0.29.8",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.4.0",
|
"@typescript-eslint/eslint-plugin": "^4.4.0",
|
||||||
|
|||||||
92
backend/src/controllers/ApiController.ts
Normal file
92
backend/src/controllers/ApiController.ts
Normal file
@@ -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<Response> => {
|
||||||
|
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();
|
||||||
|
};
|
||||||
@@ -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", {});
|
||||||
|
}
|
||||||
|
};
|
||||||
39
backend/src/middleware/isAuthApi.ts
Normal file
39
backend/src/middleware/isAuthApi.ts
Normal file
@@ -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<void> => {
|
||||||
|
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;
|
||||||
14
backend/src/routes/apiRoutes.ts
Normal file
14
backend/src/routes/apiRoutes.ts
Normal file
@@ -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;
|
||||||
@@ -10,6 +10,7 @@ import messageRoutes from "./messageRoutes";
|
|||||||
import whatsappSessionRoutes from "./whatsappSessionRoutes";
|
import whatsappSessionRoutes from "./whatsappSessionRoutes";
|
||||||
import queueRoutes from "./queueRoutes";
|
import queueRoutes from "./queueRoutes";
|
||||||
import quickAnswerRoutes from "./quickAnswerRoutes";
|
import quickAnswerRoutes from "./quickAnswerRoutes";
|
||||||
|
import apiRoutes from "./apiRoutes";
|
||||||
|
|
||||||
const routes = Router();
|
const routes = Router();
|
||||||
|
|
||||||
@@ -23,5 +24,6 @@ routes.use(messageRoutes);
|
|||||||
routes.use(whatsappSessionRoutes);
|
routes.use(whatsappSessionRoutes);
|
||||||
routes.use(queueRoutes);
|
routes.use(queueRoutes);
|
||||||
routes.use(quickAnswerRoutes);
|
routes.use(quickAnswerRoutes);
|
||||||
|
routes.use("/api/messages", apiRoutes);
|
||||||
|
|
||||||
export default routes;
|
export default routes;
|
||||||
|
|||||||
@@ -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<Response | undefined> => {
|
||||||
|
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;
|
||||||
@@ -7,24 +7,28 @@ import Ticket from "../../models/Ticket";
|
|||||||
interface Request {
|
interface Request {
|
||||||
media: Express.Multer.File;
|
media: Express.Multer.File;
|
||||||
ticket: Ticket;
|
ticket: Ticket;
|
||||||
|
body?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SendWhatsAppMedia = async ({
|
const SendWhatsAppMedia = async ({
|
||||||
media,
|
media,
|
||||||
ticket
|
ticket,
|
||||||
|
body
|
||||||
}: Request): Promise<WbotMessage> => {
|
}: Request): Promise<WbotMessage> => {
|
||||||
try {
|
try {
|
||||||
const wbot = await GetTicketWbot(ticket);
|
const wbot = await GetTicketWbot(ticket);
|
||||||
|
|
||||||
const newMedia = MessageMedia.fromFilePath(media.path);
|
const newMedia = MessageMedia.fromFilePath(media.path);
|
||||||
|
|
||||||
const sentMessage = await wbot.sendMessage(
|
const sentMessage = await wbot.sendMessage(
|
||||||
`${ticket.contact.number}@${ticket.isGroup ? "g" : "c"}.us`,
|
`${ticket.contact.number}@${ticket.isGroup ? "g" : "c"}.us`,
|
||||||
newMedia,
|
newMedia,
|
||||||
{ sendAudioAsVoice: true }
|
{
|
||||||
|
caption: body,
|
||||||
|
sendAudioAsVoice: true
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
await ticket.update({ lastMessage: media.filename });
|
await ticket.update({ lastMessage: body || media.filename });
|
||||||
|
|
||||||
fs.unlinkSync(media.path);
|
fs.unlinkSync(media.path);
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import Paper from "@material-ui/core/Paper";
|
|||||||
import Typography from "@material-ui/core/Typography";
|
import Typography from "@material-ui/core/Typography";
|
||||||
import Container from "@material-ui/core/Container";
|
import Container from "@material-ui/core/Container";
|
||||||
import Select from "@material-ui/core/Select";
|
import Select from "@material-ui/core/Select";
|
||||||
|
import TextField from "@material-ui/core/TextField";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
import api from "../../services/api";
|
import api from "../../services/api";
|
||||||
@@ -16,13 +17,15 @@ const useStyles = makeStyles(theme => ({
|
|||||||
root: {
|
root: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
padding: theme.spacing(4),
|
padding: theme.spacing(8, 8, 3),
|
||||||
},
|
},
|
||||||
|
|
||||||
paper: {
|
paper: {
|
||||||
padding: theme.spacing(2),
|
padding: theme.spacing(2),
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
marginBottom: 12,
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
settingOption: {
|
settingOption: {
|
||||||
@@ -31,6 +34,7 @@ const useStyles = makeStyles(theme => ({
|
|||||||
margin: {
|
margin: {
|
||||||
margin: theme.spacing(1),
|
margin: theme.spacing(1),
|
||||||
},
|
},
|
||||||
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const Settings = () => {
|
const Settings = () => {
|
||||||
@@ -117,7 +121,21 @@ const Settings = () => {
|
|||||||
{i18n.t("settings.settings.userCreation.options.disabled")}
|
{i18n.t("settings.settings.userCreation.options.disabled")}
|
||||||
</option>
|
</option>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
|
<Paper className={classes.paper}>
|
||||||
|
<TextField
|
||||||
|
id="api-token-setting"
|
||||||
|
readonly
|
||||||
|
label="Token Api"
|
||||||
|
margin="dense"
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
value={settings && settings.length > 0 && getSettingValue("userApiToken")}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
</Container>
|
</Container>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user