feat: add options to handle whatsapp session on frontend

This commit is contained in:
canove
2020-10-27 16:08:50 -03:00
parent 73cb08a557
commit e573656276
11 changed files with 408 additions and 84 deletions

View File

@@ -2,8 +2,6 @@ import { Request, Response } from "express";
import { getIO } from "../libs/socket";
import { removeWbot } from "../libs/wbot";
import { StartWhatsAppSession } from "../services/WbotServices/StartWhatsAppSession";
import wbotMessageListener from "../services/WbotServices/wbotMessageListener";
import wbotMonitor from "../services/WbotServices/wbotMonitor";
import CreateWhatsAppService from "../services/WhatsappService/CreateWhatsAppService";
import DeleteWhatsAppService from "../services/WhatsappService/DeleteWhatsAppService";

View File

@@ -1,10 +1,7 @@
import { Request, Response } from "express";
// import Whatsapp from "../models/Whatsapp";
// import { getIO } from "../libs/socket";
import { getWbot } from "../libs/wbot";
import ShowWhatsAppService from "../services/WhatsappService/ShowWhatsAppService";
import { StartWhatsAppSession } from "../services/WbotServices/StartWhatsAppSession";
// import wbotMonitor from "../services/wbotMonitor";
const store = async (req: Request, res: Response): Promise<Response> => {
const { whatsappId } = req.params;
@@ -15,6 +12,17 @@ const store = async (req: Request, res: Response): Promise<Response> => {
return res.status(200).json({ message: "Starting session." });
};
const update = async (req: Request, res: Response): Promise<Response> => {
const { whatsappId } = req.params;
const whatsapp = await ShowWhatsAppService(whatsappId);
await whatsapp.update({ session: "" });
StartWhatsAppSession(whatsapp);
return res.status(200).json({ message: "Starting session." });
};
const remove = async (req: Request, res: Response): Promise<Response> => {
const { whatsappId } = req.params;
const whatsapp = await ShowWhatsAppService(whatsappId);
@@ -26,4 +34,4 @@ const remove = async (req: Request, res: Response): Promise<Response> => {
return res.status(200).json({ message: "Session disconnected." });
};
export default { store, remove };
export default { store, remove, update };

View File

@@ -3,7 +3,6 @@ import { Client } from "whatsapp-web.js";
import { getIO } from "./socket";
import Whatsapp from "../models/Whatsapp";
import AppError from "../errors/AppError";
import { StartWhatsAppSession } from "../services/WbotServices/StartWhatsAppSession";
interface Session extends Client {
id?: number;
@@ -22,10 +21,10 @@ export const initWbot = async (whatsapp: Whatsapp): Promise<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 currentSessionIndex = sessions.findIndex(s => s.id === whatsapp.id);
if (currentSessionIndex !== -1) {
sessions[currentSessionIndex].destroy();
sessions.splice(currentSessionIndex, 1);
}
const wbot: Session = new Client({
@@ -37,7 +36,13 @@ export const initWbot = async (whatsapp: Whatsapp): Promise<Session> => {
wbot.on("qr", async qr => {
console.log("Session:", sessionName);
qrCode.generate(qr, { small: true });
await whatsapp.update({ qrcode: qr, status: "qrcode" });
await whatsapp.update({ qrcode: qr, status: "qrcode", retries: 0 });
const sessionIndex = sessions.findIndex(s => s.id === whatsapp.id);
if (sessionIndex === -1) {
wbot.id = whatsapp.id;
sessions.push(wbot);
}
io.emit("whatsappSession", {
action: "update",
@@ -48,20 +53,14 @@ export const initWbot = async (whatsapp: Whatsapp): Promise<Session> => {
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
session: JSON.stringify(session)
});
});
wbot.on("auth_failure", async msg => {
console.error("Session:", sessionName, "AUTHENTICATION FAILURE", msg);
if (whatsapp.retries > 2) {
if (whatsapp.retries > 1) {
await whatsapp.update({ session: "", retries: 0 });
}
@@ -76,7 +75,6 @@ export const initWbot = async (whatsapp: Whatsapp): Promise<Session> => {
session: whatsapp
});
StartWhatsAppSession(whatsapp);
reject(new Error("Error starting whatsapp session."));
});
@@ -95,8 +93,13 @@ export const initWbot = async (whatsapp: Whatsapp): Promise<Session> => {
});
wbot.sendPresenceAvailable();
wbot.id = whatsapp.id;
sessions.push(wbot);
const sessionIndex = sessions.findIndex(s => s.id === whatsapp.id);
if (sessionIndex === -1) {
wbot.id = whatsapp.id;
sessions.push(wbot);
}
resolve(wbot);
});
} catch (err) {

View File

@@ -11,6 +11,12 @@ whatsappSessionRoutes.post(
WhatsAppSessionController.store
);
whatsappSessionRoutes.put(
"/whatsappsession/:whatsappId",
isAuth,
WhatsAppSessionController.update
);
whatsappSessionRoutes.delete(
"/whatsappsession/:whatsappId",
isAuth,

View File

@@ -1,11 +1,20 @@
import { initWbot } from "../../libs/wbot";
import Whatsapp from "../../models/Whatsapp";
import wbotMessageListener from "./wbotMessageListener";
import { getIO } from "../../libs/socket";
import wbotMonitor from "./wbotMonitor";
export const StartWhatsAppSession = async (
whatsapp: Whatsapp
): Promise<void> => {
await whatsapp.update({ status: "OPENING" });
const io = getIO();
io.emit("whatsappSession", {
action: "update",
session: whatsapp
});
try {
const wbot = await initWbot(whatsapp);
wbotMessageListener(wbot);

View File

@@ -54,7 +54,7 @@ const wbotMonitor = async (
wbot.on("disconnected", async reason => {
console.log("Disconnected session:", sessionName, reason);
try {
await whatsapp.update({ status: "DISCONNECTED", session: "" });
await whatsapp.update({ status: "OPENING", session: "" });
} catch (err) {
Sentry.captureException(err);
console.log(err);

View File

@@ -16,7 +16,7 @@ interface Response {
const CreateWhatsAppService = async ({
name,
status = "INITIALIZING",
status = "OPENING",
isDefault = false
}: Request): Promise<Response> => {
const schema = Yup.object().shape({

View File

@@ -14,8 +14,19 @@ import {
Table,
TableHead,
Paper,
Tooltip,
Typography,
} from "@material-ui/core";
import { Edit, DeleteOutline, CheckCircle } from "@material-ui/icons";
import {
Edit,
CheckCircle,
SignalCellularConnectedNoInternet2Bar,
SignalCellularConnectedNoInternet0Bar,
Schedule,
SignalCellular4Bar,
CropFree,
DeleteOutline,
} from "@material-ui/icons";
import MainContainer from "../../components/MainContainer";
import MainHeader from "../../components/MainHeader";
@@ -26,6 +37,7 @@ import TableRowSkeleton from "../../components/TableRowSkeleton";
import api from "../../services/api";
import WhatsAppModal from "../../components/WhatsAppModal";
import ConfirmationModal from "../../components/ConfirmationModal";
import ButtonWithSpinner from "../../components/ButtonWithSpinner";
import QrcodeModal from "../../components/QrcodeModal";
import { i18n } from "../../translate/i18n";
@@ -56,6 +68,7 @@ const reducer = (state, action) => {
state[whatsAppIndex].status = whatsApp.status;
state[whatsAppIndex].updatedAt = whatsApp.updatedAt;
state[whatsAppIndex].qrcode = whatsApp.qrcode;
state[whatsAppIndex].retries = whatsApp.retries;
return [...state];
} else {
return [...state];
@@ -84,8 +97,47 @@ const useStyles = makeStyles(theme => ({
overflowY: "scroll",
...theme.scrollbarStyles,
},
customTableCell: {
display: "flex",
alignItems: "center",
justifyContent: "center",
},
tooltip: {
backgroundColor: "#f5f5f9",
color: "rgba(0, 0, 0, 0.87)",
fontSize: theme.typography.pxToRem(14),
border: "1px solid #dadde9",
maxWidth: 450,
},
tooltipPopper: {
textAlign: "center",
},
}));
const CustomToolTip = ({ title, content, children }) => {
const classes = useStyles();
return (
<Tooltip
arrow
classes={{
tooltip: classes.tooltip,
popper: classes.tooltipPopper,
}}
title={
<React.Fragment>
<Typography gutterBottom color="inherit">
{title}
</Typography>
{content && <Typography>{content}</Typography>}
</React.Fragment>
}
>
{children}
</Tooltip>
);
};
const Connections = () => {
const classes = useStyles();
@@ -94,15 +146,28 @@ const Connections = () => {
const [whatsAppModalOpen, setWhatsAppModalOpen] = useState(false);
const [qrModalOpen, setQrModalOpen] = useState(false);
const [selectedWhatsApp, setSelectedWhatsApp] = useState(null);
const [loading, setLoading] = useState(true);
const [confirmModalOpen, setConfirmModalOpen] = useState(false);
const [deletingWhatsApp, setDeletingWhatsApp] = useState(null);
const confirmationModalInitialState = {
action: "",
title: "",
message: "",
whatsAppId: "",
open: false,
};
const [confirmModalInfo, setConfirmModalInfo] = useState(
confirmationModalInitialState
);
useEffect(() => {
setLoading(true);
const fetchSession = async () => {
try {
const { data } = await api.get("/whatsapp/");
dispatch({ type: "LOAD_WHATSAPPS", payload: data });
setLoading(false);
} catch (err) {
setLoading(false);
const errorMsg = err.response?.data?.error;
if (errorMsg) {
if (i18n.exists(`backendErrors.${errorMsg}`)) {
@@ -144,19 +209,27 @@ const Connections = () => {
};
}, []);
// const handleDisconnectSession = async whatsAppId => {
// try {
// await api.put(`/whatsapp/whatsApp/${whatsAppId}`, {
// whatsApp: "",
// status: "pending",
// });
// } catch (err) {
// console.log(err);
// if (err.response && err.response.data && err.response.data.error) {
// toast.error(err.response.data.error);
// }
// }
// };
const handleStartWhatsAppSession = async whatsAppId => {
try {
await api.post(`/whatsappsession/${whatsAppId}`);
} catch (err) {
console.log(err);
if (err.response && err.response.data && err.response.data.error) {
toast.error(err.response.data.error);
}
}
};
const handleRequestNewQrCode = async whatsAppId => {
try {
await api.put(`/whatsappsession/${whatsAppId}`);
} catch (err) {
console.log(err);
if (err.response && err.response.data && err.response.data.error) {
toast.error(err.response.data.error);
}
}
};
const handleOpenWhatsAppModal = () => {
setSelectedWhatsApp(null);
@@ -183,51 +256,187 @@ const Connections = () => {
setWhatsAppModalOpen(true);
};
const handleDeleteWhatsApp = async whatsAppId => {
try {
await api.delete(`/whatsapp/${whatsAppId}`);
toast.success(i18n.t("connections.toasts.deleted"));
} catch (err) {
const errorMsg = err.response?.data?.error;
if (errorMsg) {
if (i18n.exists(`backendErrors.${errorMsg}`)) {
toast.error(i18n.t(`backendErrors.${errorMsg}`));
const renderActionButtons = whatsApp => {
return (
<>
{whatsApp.status === "qrcode" && (
<Button
size="small"
variant="contained"
color="primary"
onClick={() => handleOpenQrModal(whatsApp)}
>
{i18n.t("connections.buttons.qrcode")}
</Button>
)}
{whatsApp.status === "DISCONNECTED" && (
<>
<Button
size="small"
variant="outlined"
color="primary"
onClick={() => handleStartWhatsAppSession(whatsApp.id)}
>
{i18n.t("connections.buttons.tryAgain")}
</Button>{" "}
<Button
size="small"
variant="outlined"
color="secondary"
onClick={() => handleRequestNewQrCode(whatsApp.id)}
>
{i18n.t("connections.buttons.newQr")}
</Button>
</>
)}
{(whatsApp.status === "CONNECTED" ||
whatsApp.status === "PAIRING" ||
whatsApp.status === "TIMEOUT") && (
<Button
size="small"
variant="outlined"
color="secondary"
onClick={() => {
handleOpenConfirmationModal("disconnect", whatsApp.id);
}}
>
{i18n.t("connections.buttons.disconnect")}
</Button>
)}
{whatsApp.status === "OPENING" && (
<ButtonWithSpinner
size="small"
variant="outlined"
loading={true}
color="default"
>
{i18n.t("connections.buttons.connecting")}
</ButtonWithSpinner>
)}
</>
);
};
const renderStatusToolTips = whatsApp => {
return (
<div className={classes.customTableCell}>
{whatsApp.status === "DISCONNECTED" && (
<CustomToolTip
title={i18n.t("connections.toolTips.disconnected.title")}
content={i18n.t("connections.toolTips.disconnected.content")}
>
<SignalCellularConnectedNoInternet0Bar color="secondary" />
</CustomToolTip>
)}
{whatsApp.status === "OPENING" && (
<CustomToolTip title={i18n.t("connections.toolTips.opening.title")}>
<Schedule color="disabled" />
</CustomToolTip>
)}
{whatsApp.status === "qrcode" && (
<CustomToolTip
title={i18n.t("connections.toolTips.qrcode.title")}
content={i18n.t("connections.toolTips.qrcode.content")}
>
<CropFree />
</CustomToolTip>
)}
{whatsApp.status === "CONNECTED" && (
<CustomToolTip title={i18n.t("connections.toolTips.connected.title")}>
<SignalCellular4Bar style={{ color: green[500] }} />
</CustomToolTip>
)}
{(whatsApp.status === "TIMEOUT" || whatsApp.status === "PAIRING") && (
<CustomToolTip
title={i18n.t("connections.toolTips.timeout.title")}
content={i18n.t("connections.toolTips.timeout.content")}
>
<SignalCellularConnectedNoInternet2Bar color="secondary" />
</CustomToolTip>
)}
</div>
);
};
const handleOpenConfirmationModal = (action, whatsAppId) => {
if (action === "disconnect") {
setConfirmModalInfo({
action: action,
title: i18n.t("connections.confirmationModal.disconnectTitle"),
message: i18n.t("connections.confirmationModal.disconnectMessage"),
whatsAppId: whatsAppId,
});
}
if (action === "delete") {
setConfirmModalInfo({
action: action,
title: i18n.t("connections.confirmationModal.deleteTitle"),
message: i18n.t("connections.confirmationModal.deleteMessage"),
whatsAppId: whatsAppId,
});
}
setConfirmModalOpen(true);
};
const handleSubmitConfirmationModal = async () => {
if (confirmModalInfo.action === "disconnect") {
try {
await api.delete(`/whatsappsession/${confirmModalInfo.whatsAppId}`);
} catch (err) {
const errorMsg = err.response?.data?.error;
if (errorMsg) {
if (i18n.exists(`backendErrors.${errorMsg}`)) {
toast.error(i18n.t(`backendErrors.${errorMsg}`));
} else {
toast.error(err.response.data.error);
}
} else {
toast.error(err.response.data.error);
toast.error("Unknown error");
}
} else {
toast.error("Unknown error");
}
}
setDeletingWhatsApp(null);
if (confirmModalInfo.action === "delete") {
try {
await api.delete(`/whatsapp/${confirmModalInfo.whatsAppId}`);
toast.success(i18n.t("connections.toasts.deleted"));
} catch (err) {
const errorMsg = err.response?.data?.error;
if (errorMsg) {
if (i18n.exists(`backendErrors.${errorMsg}`)) {
toast.error(i18n.t(`backendErrors.${errorMsg}`));
} else {
toast.error(err.response.data.error);
}
} else {
toast.error("Unknown error");
}
}
}
setConfirmModalInfo(confirmationModalInitialState);
};
return (
<MainContainer>
<ConfirmationModal
title={
deletingWhatsApp &&
`${i18n.t("connections.confirmationModal.deleteTitle")} ${
deletingWhatsApp.name
}?`
}
title={confirmModalInfo.title}
open={confirmModalOpen}
setOpen={setConfirmModalOpen}
onConfirm={() => handleDeleteWhatsApp(deletingWhatsApp.id)}
onConfirm={handleSubmitConfirmationModal}
>
{i18n.t("connections.confirmationModal.deleteMessage")}
{confirmModalInfo.message}
</ConfirmationModal>
<QrcodeModal
open={qrModalOpen}
onClose={handleCloseQrModal}
whatsAppId={
selectedWhatsApp && !whatsAppModalOpen && selectedWhatsApp.id
}
whatsAppId={!whatsAppModalOpen && selectedWhatsApp?.id}
/>
<WhatsAppModal
open={whatsAppModalOpen}
onClose={handleCloseWhatsAppModal}
whatsAppId={selectedWhatsApp && !qrModalOpen && selectedWhatsApp.id}
whatsAppId={!qrModalOpen && selectedWhatsApp?.id}
/>
<MainHeader>
<Title>{i18n.t("connections.title")}</Title>
@@ -251,6 +460,9 @@ const Connections = () => {
<TableCell align="center">
{i18n.t("connections.table.status")}
</TableCell>
<TableCell align="center">
{i18n.t("connections.table.session")}
</TableCell>
<TableCell align="center">
{i18n.t("connections.table.lastUpdate")}
</TableCell>
@@ -263,35 +475,28 @@ const Connections = () => {
</TableRow>
</TableHead>
<TableBody>
{false ? (
{loading ? (
<TableRowSkeleton />
) : (
<>
{whatsApps &&
whatsApps.length > 0 &&
whatsApps.map((whatsApp, index) => (
{whatsApps?.length > 0 &&
whatsApps.map(whatsApp => (
<TableRow key={whatsApp.id}>
<TableCell align="center">{whatsApp.name}</TableCell>
<TableCell align="center">
{whatsApp.status === "qrcode" ? (
<Button
size="small"
variant="contained"
color="primary"
onClick={() => handleOpenQrModal(whatsApp)}
>
QR CODE
</Button>
) : (
whatsApp.status
)}
{renderStatusToolTips(whatsApp)}
</TableCell>
<TableCell align="center">
{renderActionButtons(whatsApp)}
</TableCell>
<TableCell align="center">
{format(parseISO(whatsApp.updatedAt), "dd/MM/yy HH:mm")}
</TableCell>
<TableCell align="center">
{whatsApp.isDefault && (
<CheckCircle style={{ color: green[500] }} />
<div className={classes.customTableCell}>
<CheckCircle style={{ color: green[500] }} />
</div>
)}
</TableCell>
<TableCell align="center">
@@ -305,8 +510,7 @@ const Connections = () => {
<IconButton
size="small"
onClick={e => {
setConfirmModalOpen(true);
setDeletingWhatsApp(whatsApp);
handleOpenConfirmationModal("delete", whatsApp.id);
}}
>
<DeleteOutline />

View File

@@ -48,9 +48,40 @@ const messages = {
confirmationModal: {
deleteTitle: "Delete",
deleteMessage: "Are you sure? It cannot be reverted.",
disconnectTitle: "Disconnect",
disconnectMessage:
"You will need to read QR Code again to start receiving messages.",
},
buttons: {
add: "Add WhatsApp",
disconnect: "Disconnect",
tryAgain: "Try Again",
qrcode: "QR CODE",
newQr: "New QR CODE",
connecting: "Connectiing",
},
toolTips: {
disconnected: {
title: "Failed to start WhatsApp session",
content:
"Make sure your cell phone is connected to the internet and try again, or request a new QR Code",
},
opening: {
title: "Starting session...",
},
qrcode: {
title: "Waiting for QR Code read",
content:
"Click on 'QR CODE' button and read the QR Code with your cell phone to start session",
},
connected: {
title: "Connection established",
},
timeout: {
title: "Connection with cell phone has been lost",
content:
"Make sure your cell phone is connected to the internet and WhatsApp is open, or click on 'Disconnect' button to get a new QRcode",
},
},
table: {
name: "Name",
@@ -58,6 +89,7 @@ const messages = {
lastUpdate: "Last Update",
default: "Default",
actions: "Actions",
session: "Session",
},
},
whatsappModal: {

View File

@@ -50,9 +50,40 @@ const messages = {
confirmationModal: {
deleteTitle: "Borrar",
deleteMessage: "¿Estás seguro? Este proceso no puede ser revertido.",
disconnectTitle: "Desconectar",
disconnectMessage:
"Você precisará ler o QR code novamente para começar a receber mensagens.",
},
buttons: {
add: "Agrega WhatsApp",
disconnect: "Desconectar",
tryAgain: "Inténtalo de nuevo",
qrcode: "QR CODE",
newQr: "Nuevo QR CODE",
connecting: "Conectando",
},
toolTips: {
disconnected: {
title: "No se pudo iniciar la sesión de WhatsApp",
content:
"Asegúrese de que su teléfono celular esté conectado a Internet y vuelva a intentarlo o solicite un nuevo código QR",
},
opening: {
title: "Iniciando sesión ...",
},
qrcode: {
title: "Esperando la lectura del código QR",
content:
"Haga clic en el botón 'CÓDIGO QR' y lea el Código QR con su teléfono celular para iniciar la sesión",
},
connected: {
title: "Conexión establecida",
},
timeout: {
title: "Se perdió la conexión con el teléfono celular",
content:
"Asegúrese de que su teléfono celular esté conectado a Internet y que WhatsApp esté abierto, o haga clic en el botón 'Desconectar' para obtener un nuevo código QR",
},
},
table: {
name: "Nombre",
@@ -60,6 +91,7 @@ const messages = {
lastUpdate: "Última Actualización",
default: "Por Defecto",
actions: "Acciones",
session: "Sesión",
},
},
whatsappModal: {

View File

@@ -48,9 +48,40 @@ const messages = {
confirmationModal: {
deleteTitle: "Deletar",
deleteMessage: "Você tem certeza? Essa ação não pode ser revertida.",
disconnectTitle: "Desconectar",
disconnectMessage:
"Você precisará ler o código QR novamente para começar a receber mensagens.",
},
buttons: {
add: "Adicionar WhatsApp",
disconnect: "desconectar",
tryAgain: "Tentar novamente",
qrcode: "QR CODE",
newQr: "Novo QR CODE",
connecting: "Conectando",
},
toolTips: {
disconnected: {
title: "Falha ao iniciar sessão do WhatsApp",
content:
"Certifique-se de que seu celular esteja conectado à internet e tente novamente, ou solicite um novo QR Code",
},
opening: {
title: "Iniciando a sessão...",
},
qrcode: {
title: "Esperando leitura do QR Code",
content:
"Clique no botão 'QR CODE' e leia o QR Code com o seu celular para iniciar a sessão",
},
connected: {
title: "Conexão estabelecida!",
},
timeout: {
title: "A conexão com o celular foi perdida",
content:
"Certifique-se de que seu celular esteja conectado à internet e o WhatsApp esteja aberto, ou clique no botão 'Desconectar' para obter um novo QR Code",
},
},
table: {
name: "Nome",
@@ -58,6 +89,7 @@ const messages = {
lastUpdate: "Última atualização",
default: "Padrão",
actions: "Ações",
session: "Sessão",
},
},
whatsappModal: {