@@ -0,0 +1,2 @@ | |||
node_modules/ | |||
.env |
@@ -0,0 +1,120 @@ | |||
const { proto } = require("@whiskeysockets/baileys/WAProto"); | |||
const { | |||
Curve, | |||
signedKeyPair, | |||
} = require("@whiskeysockets/baileys/lib/Utils/crypto"); | |||
const { | |||
generateRegistrationId, | |||
} = require("@whiskeysockets/baileys/lib/Utils/generics"); | |||
const { randomBytes } = require("crypto"); | |||
const initAuthCreds = () => { | |||
const identityKey = Curve.generateKeyPair(); | |||
return { | |||
noiseKey: Curve.generateKeyPair(), | |||
signedIdentityKey: identityKey, | |||
signedPreKey: signedKeyPair(identityKey, 1), | |||
registrationId: generateRegistrationId(), | |||
advSecretKey: randomBytes(32).toString("base64"), | |||
processedHistoryMessages: [], | |||
nextPreKeyId: 1, | |||
firstUnuploadedPreKeyId: 1, | |||
accountSettings: { | |||
unarchiveChats: false, | |||
}, | |||
}; | |||
}; | |||
const BufferJSON = { | |||
replacer: (k, value) => { | |||
if ( | |||
Buffer.isBuffer(value) || | |||
value instanceof Uint8Array || | |||
value?.type === "Buffer" | |||
) { | |||
return { | |||
type: "Buffer", | |||
data: Buffer.from(value?.data || value).toString("base64"), | |||
}; | |||
} | |||
return value; | |||
}, | |||
reviver: (_, value) => { | |||
if ( | |||
typeof value === "object" && | |||
!!value && | |||
(value.buffer === true || value.type === "Buffer") | |||
) { | |||
const val = value.data || value.value; | |||
return typeof val === "string" | |||
? Buffer.from(val, "base64") | |||
: Buffer.from(val || []); | |||
} | |||
return value; | |||
}, | |||
}; | |||
module.exports = useMongoDBAuthState = async (collection) => { | |||
const writeData = (data, id) => { | |||
const informationToStore = JSON.parse( | |||
JSON.stringify(data, BufferJSON.replacer) | |||
); | |||
const update = { | |||
$set: { | |||
...informationToStore, | |||
}, | |||
}; | |||
return collection.updateOne({ _id: id }, update, { upsert: true }); | |||
}; | |||
const readData = async (id) => { | |||
try { | |||
const data = JSON.stringify(await collection.findOne({ _id: id })); | |||
return JSON.parse(data, BufferJSON.reviver); | |||
} catch (error) { | |||
return null; | |||
} | |||
}; | |||
const removeData = async (id) => { | |||
try { | |||
await collection.deleteOne({ _id: id }); | |||
} catch (_a) {} | |||
}; | |||
const creds = (await readData("creds")) || (0, initAuthCreds)(); | |||
return { | |||
state: { | |||
creds, | |||
keys: { | |||
get: async (type, ids) => { | |||
const data = {}; | |||
await Promise.all( | |||
ids.map(async (id) => { | |||
let value = await readData(`${type}-${id}`); | |||
if (type === "app-state-sync-key") { | |||
value = proto.Message.AppStateSyncKeyData.fromObject(data); | |||
} | |||
data[id] = value; | |||
}) | |||
); | |||
return data; | |||
}, | |||
set: async (data) => { | |||
const tasks = []; | |||
for (const category of Object.keys(data)) { | |||
for (const id of Object.keys(data[category])) { | |||
const value = data[category][id]; | |||
const key = `${category}-${id}`; | |||
tasks.push(value ? writeData(value, key) : removeData(key)); | |||
} | |||
} | |||
await Promise.all(tasks); | |||
}, | |||
}, | |||
}, | |||
saveCreds: () => { | |||
return writeData(creds, "creds"); | |||
}, | |||
}; | |||
}; |
@@ -0,0 +1,21 @@ | |||
module.exports = { | |||
root: true, | |||
env: { browser: true, es2020: true }, | |||
extends: [ | |||
"eslint:recommended", | |||
"plugin:react/recommended", | |||
"plugin:react/jsx-runtime", | |||
"plugin:react-hooks/recommended", | |||
], | |||
ignorePatterns: ["dist", ".eslintrc.cjs"], | |||
parserOptions: { ecmaVersion: "latest", sourceType: "module" }, | |||
settings: { react: { version: "18.2" } }, | |||
plugins: ["react-refresh"], | |||
rules: { | |||
"react-refresh/only-export-components": [ | |||
"warn", | |||
{ allowConstantExport: true }, | |||
], | |||
"no-unused-vars": "off", | |||
}, | |||
}; |
@@ -0,0 +1,24 @@ | |||
# Logs | |||
logs | |||
*.log | |||
npm-debug.log* | |||
yarn-debug.log* | |||
yarn-error.log* | |||
pnpm-debug.log* | |||
lerna-debug.log* | |||
node_modules | |||
dist | |||
dist-ssr | |||
*.local | |||
# Editor directories and files | |||
.vscode/* | |||
!.vscode/extensions.json | |||
.idea | |||
.DS_Store | |||
*.suo | |||
*.ntvs* | |||
*.njsproj | |||
*.sln | |||
*.sw? |
@@ -0,0 +1,8 @@ | |||
# React + Vite | |||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. | |||
Currently, two official plugins are available: | |||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh | |||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh |
@@ -0,0 +1,13 @@ | |||
<!doctype html> | |||
<html lang="en"> | |||
<head> | |||
<meta charset="UTF-8" /> | |||
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> | |||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |||
<title>Vite + React</title> | |||
</head> | |||
<body> | |||
<div id="root"></div> | |||
<script type="module" src="/src/main.jsx"></script> | |||
</body> | |||
</html> |
@@ -0,0 +1,38 @@ | |||
{ | |||
"name": "client", | |||
"private": true, | |||
"version": "0.0.0", | |||
"type": "module", | |||
"scripts": { | |||
"dev": "vite", | |||
"build": "vite build", | |||
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", | |||
"preview": "vite preview" | |||
}, | |||
"dependencies": { | |||
"@reduxjs/toolkit": "^1.9.7", | |||
"@tanstack/react-query": "^5.0.0-beta.35", | |||
"axios": "^1.6.2", | |||
"react": "^18.2.0", | |||
"react-dom": "^18.2.0", | |||
"react-icons": "^4.12.0", | |||
"react-loader-spinner": "^5.4.5", | |||
"react-redux": "^8.1.3", | |||
"react-router-dom": "^6.17.0", | |||
"socket.io-client": "^4.7.2", | |||
"whatsapp-api": "file:.." | |||
}, | |||
"devDependencies": { | |||
"@types/react": "^18.2.37", | |||
"@types/react-dom": "^18.2.15", | |||
"@vitejs/plugin-react-swc": "^3.5.0", | |||
"autoprefixer": "^10.4.16", | |||
"eslint": "^8.53.0", | |||
"eslint-plugin-react": "^7.33.2", | |||
"eslint-plugin-react-hooks": "^4.6.0", | |||
"eslint-plugin-react-refresh": "^0.4.4", | |||
"postcss": "^8.4.32", | |||
"tailwindcss": "^3.3.6", | |||
"vite": "^5.0.0" | |||
} | |||
} |
@@ -0,0 +1,6 @@ | |||
export default { | |||
plugins: { | |||
tailwindcss: {}, | |||
autoprefixer: {}, | |||
}, | |||
} |
@@ -0,0 +1,47 @@ | |||
import React from "react"; | |||
import { createBrowserRouter, RouterProvider } from "react-router-dom"; | |||
import { QueryClientProvider } from "@tanstack/react-query"; | |||
import { queryClient } from "./utils/http"; | |||
import SignIn from "./pages/Signin"; | |||
import Signup from "./pages/Signup"; | |||
import PrivateRoute from "./components/PrivateRoute"; | |||
import QRScan from "./pages/QRScan"; | |||
import Messages from "./pages/Messages"; | |||
import ReloadCheck from "./components/ReloadCheck"; | |||
const router = createBrowserRouter([ | |||
{ | |||
element: <ReloadCheck />, | |||
children: [ | |||
{ | |||
path: "/", | |||
element: <SignIn />, | |||
}, | |||
{ | |||
path: "/signup", | |||
element: <Signup />, | |||
}, | |||
{ | |||
element: <PrivateRoute />, | |||
children: [ | |||
{ | |||
path: "/authcheck", | |||
element: <QRScan />, | |||
}, | |||
{ | |||
path: "/messages", | |||
element: <Messages />, | |||
}, | |||
], | |||
}, | |||
], | |||
}, | |||
]); | |||
export default function App() { | |||
return ( | |||
<QueryClientProvider client={queryClient}> | |||
<RouterProvider router={router} /> | |||
</QueryClientProvider> | |||
); | |||
} |
@@ -0,0 +1,28 @@ | |||
/* eslint-disable react/prop-types */ | |||
import React from "react"; | |||
const Message = (props) => { | |||
const { message } = props; | |||
const messageDate = new Date(message.timestamp); | |||
const formattedDate = messageDate.toLocaleDateString("en-US", { | |||
day: "numeric", | |||
month: "short", | |||
year: "2-digit", | |||
}); | |||
return ( | |||
<div className='flex flex-col gap-1 bg-white p-4 rounded-lg shadow-lg'> | |||
<div className='flex flex-col gap-1 text-lg font-semibold text-zinc-800'> | |||
<h1>{message.username}</h1> | |||
<h1>{message.phoneNumber}</h1> | |||
</div> | |||
<div> | |||
<h1>{message.conversation}</h1> | |||
<h1>{formattedDate}</h1> | |||
</div> | |||
</div> | |||
); | |||
}; | |||
export default Message; |
@@ -0,0 +1,10 @@ | |||
import { useSelector } from "react-redux"; | |||
import { Navigate, Outlet } from "react-router-dom"; | |||
const PrivateRoute = () => { | |||
const { currentUser } = useSelector((state) => state.user); | |||
console.log(currentUser); | |||
return currentUser ? <Outlet /> : <Navigate to='/signup' />; | |||
}; | |||
export default PrivateRoute; |
@@ -0,0 +1,26 @@ | |||
import axios from "axios"; | |||
import { useDispatch, useSelector } from "react-redux"; | |||
import { Outlet } from "react-router-dom"; | |||
import { signInSuccess } from "../redux/user/userSlice"; | |||
import { useEffect, useState } from "react"; | |||
const ReloadCheck = () => { | |||
const dispatch = useDispatch(); | |||
const [getMeFetched, setGetMeFetched] = useState(false); | |||
useEffect(() => { | |||
axios | |||
.get("/api/getMe") | |||
.then((response) => { | |||
dispatch(signInSuccess(response.data)); | |||
setGetMeFetched(true); | |||
}) | |||
.catch((err) => { | |||
setGetMeFetched(true); | |||
}); | |||
}, []); | |||
return getMeFetched && <Outlet />; | |||
}; | |||
export default ReloadCheck; |
@@ -0,0 +1,16 @@ | |||
/* eslint-disable react/prop-types */ | |||
import React from "react"; | |||
const Tag = (props) => { | |||
const { tag, tagDeleteSubmitHandler } = props; | |||
return ( | |||
<div | |||
onClick={() => tagDeleteSubmitHandler(tag)} | |||
className='flex text-xs font-bold bg-green-500 rounded-2xl p-2 text-white cursor-pointer transition shadow-md hover:opacity-95 hover:shadow-lg' | |||
> | |||
{tag} | |||
</div> | |||
); | |||
}; | |||
export default Tag; |
@@ -0,0 +1,9 @@ | |||
import { io } from "socket.io-client"; | |||
const socket = io("http://localhost:3000", { | |||
reconnection: true, // Enable reconnection | |||
reconnectionAttempts: 5, // Maximum number of reconnection attempts | |||
reconnectionDelay: 1000, | |||
}); | |||
export default socket; |
@@ -0,0 +1,7 @@ | |||
@tailwind base; | |||
@tailwind components; | |||
@tailwind utilities; | |||
body { | |||
background-color: rgb(241, 245, 241); | |||
} |
@@ -0,0 +1,12 @@ | |||
import React from "react"; | |||
import { store } from "./redux/store.js"; | |||
import { Provider } from "react-redux"; | |||
import ReactDOM from "react-dom/client"; | |||
import App from "./App.jsx"; | |||
import "./index.css"; | |||
ReactDOM.createRoot(document.getElementById("root")).render( | |||
<Provider store={store}> | |||
<App /> | |||
</Provider> | |||
); |
@@ -0,0 +1,133 @@ | |||
/* eslint-disable react-hooks/exhaustive-deps */ | |||
import { useMutation, useQuery } from "@tanstack/react-query"; | |||
import React, { useEffect, useRef, useState } from "react"; | |||
import { allMessages, delTag, newTag, queryClient } from "../utils/http"; | |||
import socket from "../connection/socket"; | |||
import Message from "../components/Message"; | |||
import { useNavigate } from "react-router-dom"; | |||
import { CiSquarePlus } from "react-icons/ci"; | |||
import { useDispatch, useSelector } from "react-redux"; | |||
import Tag from "../components/Tag"; | |||
import { signInSuccess } from "../redux/user/userSlice"; | |||
const Messages = () => { | |||
// const { data, isLoading } = useQuery({ | |||
// queryKey: ["messages"], | |||
// queryFn: getMessages, | |||
// }); | |||
const dispatch = useDispatch(); | |||
const { currentUser } = useSelector((state) => state.user); | |||
const [data, setData] = useState([]); | |||
const navigate = useNavigate(); | |||
const tagRef = useRef(); | |||
useEffect(() => { | |||
if (localStorage.getItem("firstLoadDone") === null) { | |||
// If it's the first load, set the flag in local storage to true and reload the page | |||
localStorage.setItem("firstLoadDone", 1); | |||
} else { | |||
socket.emit("whatsapp connect", currentUser._id); | |||
} | |||
socket.on("new message", (message) => { | |||
setData((messages) => [message, ...messages]); | |||
}); | |||
socket.on("user disconnected", () => { | |||
navigate("/authcheck", { replace: true }); | |||
}); | |||
return () => { | |||
// Clean up socket event listeners when the component unmounts | |||
socket.off("user disconnected"); | |||
socket.off("new message"); | |||
}; | |||
}, []); | |||
const { data: oldMessages } = useQuery({ | |||
queryKey: ["Messages"], | |||
queryFn: allMessages, | |||
}); | |||
const { mutate: addTag } = useMutation({ | |||
mutationFn: newTag, | |||
onSuccess: (data) => { | |||
dispatch(signInSuccess(data)); | |||
tagRef.current.value = ""; | |||
queryClient.invalidateQueries(["Messages"]); | |||
}, | |||
}); | |||
const { mutate: removeTag } = useMutation({ | |||
mutationFn: delTag, | |||
onSuccess: (data) => { | |||
dispatch(signInSuccess(data)); | |||
queryClient.invalidateQueries(["Messages"]); | |||
}, | |||
}); | |||
const onAddSubmitHandler = (event) => { | |||
event.preventDefault(); | |||
addTag({ formData: { tag: tagRef.current.value } }); | |||
}; | |||
const onRemoveSubmitHandler = (tag) => { | |||
removeTag({ formData: { tag } }); | |||
}; | |||
return ( | |||
<div className='max-w-4xl mx-auto p-6'> | |||
<div className='mb-5 flex gap-2'> | |||
<form | |||
className='relative flex-[1_1_auto] h-fit' | |||
onSubmit={onAddSubmitHandler} | |||
> | |||
<input | |||
type='text' | |||
className='p-4 border rounded-lg w-full' | |||
ref={tagRef} | |||
placeholder='Add new tag...' | |||
/> | |||
<button | |||
style={{ top: "50%", transform: "translateY(-50%)" }} | |||
className='absolute right-5 text-black hover:opacity-80' | |||
> | |||
<CiSquarePlus style={{ width: "1.75em", height: "1.75em" }} /> | |||
</button> | |||
</form> | |||
<div className='self-stretch flex flex-wrap gap-2 flex-[8_8_auto] p-3 bg-white rounded-lg'> | |||
{currentUser.tags.map((tag, i) => ( | |||
<Tag | |||
key={i} | |||
tag={tag} | |||
tagDeleteSubmitHandler={onRemoveSubmitHandler} | |||
/> | |||
))} | |||
</div> | |||
</div> | |||
<p className='text-center text-3xl font-bold mb-8'>Messages</p> | |||
<div className='flex flex-col gap-5 mb-8'> | |||
<div className='flex flex-col gap-5'> | |||
{data.map((message, i) => ( | |||
<Message | |||
key={i} | |||
message={message} | |||
/> | |||
))} | |||
</div> | |||
<div className='flex flex-col gap-5'> | |||
{oldMessages && | |||
oldMessages.map((message, i) => ( | |||
<Message | |||
key={i} | |||
message={message} | |||
/> | |||
))} | |||
</div> | |||
</div> | |||
</div> | |||
); | |||
}; | |||
export default Messages; |
@@ -0,0 +1,51 @@ | |||
/* eslint-disable react-hooks/exhaustive-deps */ | |||
import React, { useEffect, useState } from "react"; | |||
import { useNavigate } from "react-router-dom"; | |||
import { useDispatch, useSelector } from "react-redux"; | |||
import socket from "../connection/socket"; | |||
const QRScan = () => { | |||
const [qr, setQR] = useState(null); | |||
const navigate = useNavigate(); | |||
const [presentState, setPresentState] = useState("status-check"); | |||
const { currentUser } = useSelector((state) => state.user); | |||
useEffect(() => { | |||
socket.emit("whatsapp connect", currentUser._id); | |||
socket.on("qrCode", (qrCodeDataURL) => { | |||
setQR(qrCodeDataURL); | |||
setPresentState("scan-qr"); | |||
}); | |||
socket.on("user connected", () => { | |||
navigate("/messages", { replace: true }); | |||
setPresentState("authenticated"); | |||
}); | |||
// eslint-disable-next-line react-hooks/exhaustive-deps | |||
return () => { | |||
// Clean up socket event listeners when the component unmounts | |||
socket.off("user connected"); | |||
}; | |||
}, []); | |||
return ( | |||
<> | |||
{qr ? ( | |||
<div className='flex flex-col items-center justify-center gap-1 mt-16'> | |||
<img src={qr} /> | |||
<p className='text-center font-medium'> | |||
scan this QR code to continue | |||
</p> | |||
</div> | |||
) : ( | |||
<h1 className='text-center mt-16'>Checking User status...</h1> | |||
)} | |||
<div className='flex flex-col text-lg items-center font-bold justify-center gap-1 mt-4'> | |||
<p>{currentUser.username}</p> | |||
<p>{currentUser.email}</p> | |||
</div> | |||
</> | |||
); | |||
}; | |||
export default QRScan; |
@@ -0,0 +1,68 @@ | |||
import React, { useState } from "react"; | |||
import { Link, useNavigate } from "react-router-dom"; | |||
import { signInHelper } from "../utils/http"; | |||
import { useMutation } from "@tanstack/react-query"; | |||
import { signInSuccess } from "../redux/user/userSlice"; | |||
import { useDispatch } from "react-redux"; | |||
const SignIn = () => { | |||
const [user, setUser] = useState({ email: "", password: "" }); | |||
const dispatch = useDispatch(); | |||
const navigate = useNavigate(); | |||
const { mutate, isPending, isError, error } = useMutation({ | |||
mutationFn: signInHelper, | |||
onSuccess: (data) => { | |||
dispatch(signInSuccess(data)); | |||
navigate("/authcheck", { replace: true }); | |||
}, | |||
}); | |||
const formSubmitHandler = (e) => { | |||
e.preventDefault(); | |||
mutate({ formData: user }); | |||
}; | |||
const formInputChangeHandler = (event) => { | |||
setUser((prevUser) => ({ | |||
...prevUser, | |||
[event.target.id]: event.target.value, | |||
})); | |||
}; | |||
return ( | |||
<div className='max-w-lg mx-auto mt-32'> | |||
<h1 className='text-center text-3xl font-bold'>Sign in</h1> | |||
<form | |||
onSubmit={formSubmitHandler} | |||
className='flex flex-col gap-3 mt-8' | |||
> | |||
<input | |||
type='text' | |||
className='p-3 rounded-lg border' | |||
onChange={formInputChangeHandler} | |||
id='email' | |||
placeholder='Email' | |||
/> | |||
<input | |||
type='password' | |||
id='password' | |||
onChange={formInputChangeHandler} | |||
className='p-3 rounded-lg border' | |||
placeholder='Password' | |||
/> | |||
<button className='p-3 rounded-[32px] text-white uppercase font-bold disabled:opacity-80 transition duration-300 shadow hover:shadow-lg hover:opacity-95 bg-teal-600 hover:bg-teal-700'> | |||
Submit | |||
</button> | |||
</form> | |||
<div className='flex gap-2 mt-2'> | |||
<p>{"Don't have an account?"}</p> | |||
<Link to={"/signup"}> | |||
<span className='text-blue-700'>Sign up</span> | |||
</Link> | |||
</div> | |||
</div> | |||
); | |||
}; | |||
export default SignIn; |
@@ -0,0 +1,79 @@ | |||
import { useMutation } from "@tanstack/react-query"; | |||
import React, { useState } from "react"; | |||
import { Link, useNavigate } from "react-router-dom"; | |||
import { signUpHelper } from "../utils/http"; | |||
import { useDispatch } from "react-redux"; | |||
import { signInSuccess } from "../redux/user/userSlice"; | |||
const Signup = () => { | |||
const [user, setUser] = useState({ username: "", password: "", email: "" }); | |||
const dispatch = useDispatch(); | |||
const navigate = useNavigate(); | |||
const { mutate, isPending, isError, error } = useMutation({ | |||
mutationFn: signUpHelper, | |||
onSuccess: (data) => { | |||
dispatch(signInSuccess(data)); | |||
navigate("/authcheck", { replace: true }); | |||
}, | |||
}); | |||
const formSubmitHandler = (e) => { | |||
e.preventDefault(); | |||
mutate({ formData: user }); | |||
}; | |||
const formInputChangeHandler = (event) => { | |||
setUser((prevUser) => ({ | |||
...prevUser, | |||
[event.target.id]: event.target.value, | |||
})); | |||
}; | |||
return ( | |||
<div className='max-w-lg mx-auto mt-32'> | |||
<h1 className='text-center text-3xl font-bold'>Sign Up</h1> | |||
<form | |||
onSubmit={formSubmitHandler} | |||
className='flex flex-col gap-3 mt-8' | |||
> | |||
<input | |||
id='username' | |||
onChange={formInputChangeHandler} | |||
type='text' | |||
className='p-3 rounded-lg border' | |||
placeholder='Username' | |||
/> | |||
<input | |||
onChange={formInputChangeHandler} | |||
id='email' | |||
type='text' | |||
className='p-3 rounded-lg border' | |||
placeholder='Email' | |||
/> | |||
<input | |||
onChange={formInputChangeHandler} | |||
id='password' | |||
type='password' | |||
className='p-3 rounded-lg border' | |||
placeholder='Password' | |||
/> | |||
<button | |||
disabled={isPending} | |||
className='p-3 rounded-[32px] text-white uppercase font-bold transition duration-300 shadow hover:shadow-lg hover:opacity-95 bg-teal-600 hover:bg-teal-700' | |||
> | |||
{isPending ? "Submitting..." : `Submit`} | |||
</button> | |||
</form> | |||
<div className='flex gap-2 mt-2'> | |||
<p>Have an account?</p> | |||
<Link to={"/"}> | |||
<span className='text-blue-700'>Sign in</span> | |||
</Link> | |||
</div> | |||
</div> | |||
); | |||
}; | |||
export default Signup; |
@@ -0,0 +1,19 @@ | |||
import { createSlice } from "@reduxjs/toolkit"; | |||
const initialSocketState = { | |||
socketValue: null, | |||
}; | |||
const socketSlice = createSlice({ | |||
name: "socket", | |||
initialState: initialSocketState, | |||
reducers: { | |||
setNewSocket: (state, action) => { | |||
state.socketValue = action.payload; | |||
}, | |||
}, | |||
}); | |||
export const { setNewSocket } = socketSlice.actions; | |||
export default socketSlice.reducer; |
@@ -0,0 +1,14 @@ | |||
import { configureStore } from "@reduxjs/toolkit"; | |||
import userReducer from "./user/userSlice.js"; | |||
import socketReducer from "./socket/socketSlice.js"; | |||
export const store = configureStore({ | |||
reducer: { | |||
user: userReducer, | |||
socket: socketReducer, | |||
}, | |||
middleware: (getDefaultMiddleware) => | |||
getDefaultMiddleware({ | |||
serializableCheck: true, | |||
}), | |||
}); |
@@ -0,0 +1,22 @@ | |||
import { createSlice } from "@reduxjs/toolkit"; | |||
const initialUserState = { | |||
currentUser: null, | |||
}; | |||
const userSlice = createSlice({ | |||
name: "user", | |||
initialState: initialUserState, | |||
reducers: { | |||
signInSuccess: (state, action) => { | |||
state.currentUser = action.payload; | |||
}, | |||
signOutSuccess: (state) => { | |||
state.currentUser = null; | |||
}, | |||
}, | |||
}); | |||
export const { signInSuccess, signOutSuccess } = userSlice.actions; | |||
export default userSlice.reducer; |
@@ -0,0 +1,44 @@ | |||
import { QueryClient } from "@tanstack/react-query"; | |||
import axios from "axios"; | |||
export const queryClient = new QueryClient(); | |||
export const signUpHelper = async ({ formData }) => { | |||
const response = await axios.post("/api/signup", formData); | |||
return response.data; | |||
}; | |||
export const signInHelper = async ({ formData }) => { | |||
const response = await axios.post("/api/signin", formData); | |||
return response.data; | |||
}; | |||
export const createWhatsappServer = async () => { | |||
const response = await axios.get("/api/createSession"); | |||
return response.statusText; | |||
}; | |||
export const getMessages = async () => { | |||
const response = await axios.get("/api"); | |||
return response.data; | |||
}; | |||
export const allMessages = async () => { | |||
try { | |||
const response = await axios.get("/api/refreshMessages"); | |||
return response.data; | |||
} catch (error) { | |||
console.error("Error fetching messages:", error); | |||
throw error; | |||
} | |||
}; | |||
export const newTag = async ({ formData }) => { | |||
const response = await axios.post("/api/tag/add", formData); | |||
return response.data; | |||
}; | |||
export const delTag = async ({ formData }) => { | |||
const response = await axios.post("/api/tag/del", formData); | |||
return response.data; | |||
}; |
@@ -0,0 +1,8 @@ | |||
/** @type {import('tailwindcss').Config} */ | |||
export default { | |||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], | |||
theme: { | |||
extend: {}, | |||
}, | |||
plugins: [], | |||
}; |
@@ -0,0 +1,15 @@ | |||
import { defineConfig } from "vite"; | |||
import react from "@vitejs/plugin-react-swc"; | |||
// https://vitejs.dev/config/ | |||
export default defineConfig({ | |||
server: { | |||
proxy: { | |||
"/api": { | |||
target: "http://localhost:3000", | |||
secure: false, | |||
}, | |||
}, | |||
}, | |||
plugins: [react()], | |||
}); |
@@ -0,0 +1,8 @@ | |||
class MyError extends Error { | |||
constructor(message, statusCode) { | |||
super(message); | |||
this.status = statusCode; | |||
} | |||
} | |||
module.exports = MyError; |
@@ -0,0 +1,14 @@ | |||
const mongoose = require("mongoose"); | |||
const mongoConnection = () => { | |||
mongoose | |||
.connect(process.env.mongo_url) | |||
.then(() => { | |||
console.log("Mongo DB connected"); | |||
}) | |||
.catch((err) => { | |||
console.log("Error connecting"); | |||
}); | |||
}; | |||
module.exports = mongoConnection; |
@@ -0,0 +1,88 @@ | |||
const JWT = require("jsonwebtoken"); | |||
const User = require("../models/user"); | |||
exports.signAccessToken = (userId) => { | |||
return new Promise((resolve, reject) => { | |||
const payload = {}; | |||
const access_key = process.env.ACCESS_TOKEN_SECRET; | |||
const options = { | |||
audience: userId, | |||
issuer: "NeonFlake", | |||
expiresIn: "10d", | |||
}; | |||
JWT.sign(payload, access_key, options, (err, data) => { | |||
if (err) reject({ status: 500, message: err.message }); | |||
resolve(data); | |||
}); | |||
}); | |||
}; | |||
exports.verifyAccessToken = (req, res, next) => { | |||
if (!req?.cookies.jwt) | |||
return res | |||
.status(401) | |||
.json({ status: 401, message: "Access Token is required" }); | |||
const token = req.cookies.jwt; | |||
JWT.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, payload) => { | |||
if (err) { | |||
if (err.name === "JsonWebTokenError") | |||
throw next({ status: 403, message: "Authentication failed" }); | |||
else throw next({ status: 401, message: err.message }); | |||
} | |||
req.userId = payload.aud; | |||
User.findById(req.userId) | |||
.then((data) => { | |||
if (data) { | |||
req.data = data; | |||
next(); | |||
} else { | |||
throw next({ status: 401, message: "User not found" }); | |||
} | |||
}) | |||
.catch((err) => { | |||
next({ status: 500, message: err.message }); | |||
}); | |||
}); | |||
}; | |||
exports.signRefreshToken = (userId) => { | |||
return new Promise((resolve, reject) => { | |||
const payload = {}; | |||
const refresh_key = process.env.REFRESH_TOKEN_SECRET; | |||
const options = { | |||
audience: userId, | |||
issuer: "Apoorv Pandey", | |||
expiresIn: "1d", | |||
}; | |||
JWT.sign(payload, refresh_key, options, (err, result) => { | |||
if (err) reject({ status: 500, message: err.message }); | |||
resolve(result); | |||
}); | |||
}); | |||
}; | |||
exports.verifyRefreshToken = (req, res, next) => { | |||
if (!req.cookies.jwt) | |||
return next({ | |||
status: 401, | |||
message: "Error no refresh token provided", | |||
}); | |||
const token = req.cookies.jwt; | |||
JWT.verify(token, process.env.REFRESH_TOKEN_SECRET, (err, payload) => { | |||
if (err && err.name !== "TokenExpiredError") | |||
return next({ status: 401, message: err.message }); | |||
else if (err && err.name === "TokenExpiredError") { | |||
const output = JWT.verify(token, process.env.REFRESH_TOKEN_SECRET, { | |||
ignoreExpiration: true, | |||
}); | |||
req.userInfo = { token: output.aud, isExpired: true }; | |||
} else { | |||
req.userInfo = { token: payload.aud, isExpired: false }; | |||
} | |||
next(); | |||
}); | |||
}; |
@@ -0,0 +1,328 @@ | |||
require("dotenv").config(); | |||
const { DisconnectReason } = require("@whiskeysockets/baileys"); | |||
const makeWASocket = require("@whiskeysockets/baileys").default; | |||
const express = require("express"); | |||
const app = express(); | |||
const { Server } = require("socket.io"); | |||
const http = require("http"); | |||
const cors = require("cors"); | |||
const qrCode = require("qrcode"); | |||
const mongoConnection = require("./db/dbConnect"); | |||
const cookieParser = require("cookie-parser"); | |||
const bcrypt = require("bcryptjs"); | |||
const User = require("./models/user"); | |||
const path = require("path"); | |||
const { signAccessToken, verifyAccessToken } = require("./helper/jwt_helper"); | |||
const { | |||
createSignUpValidation, | |||
createSignInValidation, | |||
} = require("./validation/user.validity"); | |||
const MyError = require("./config/error"); | |||
const useMongoDBAuthState = require("./auth/mongoAuthState"); | |||
const { MongoClient } = require("mongodb"); | |||
mongoConnection(); | |||
app.use(cookieParser()); | |||
app.use(express.urlencoded({ extended: false })); | |||
app.use(express.json()); | |||
app.use(express.static(path.join(__dirname, "client", "dist"))); | |||
const server = http.createServer(app); | |||
const io = new Server(server, { | |||
cors: "http://localhost:3000", | |||
}); | |||
const mongoClient = new MongoClient(process.env.mongodb_url, { | |||
useNewUrlParser: true, | |||
useUnifiedTopology: true, | |||
}); | |||
mongoClient.connect().then(() => console.log("mongoClient connected")); | |||
const sock = {}; | |||
// Function to set a global variable for a specific ID | |||
function setSock(id, value) { | |||
sock[id] = value; | |||
} | |||
let groupMessageEventListener; | |||
let connectionUpdateListener; | |||
async function generateQRCode(data) { | |||
return new Promise((resolve, reject) => { | |||
qrCode.toDataURL(data, (err, url) => { | |||
if (err) { | |||
reject(err); | |||
} else { | |||
resolve(url); | |||
} | |||
}); | |||
}); | |||
} | |||
async function connectionLogic(id, socket, isError) { | |||
// const { state, saveCreds } = await useMultiFileAuthState("auth_info_baileys"); | |||
const user = await User.findById(id); | |||
console.log(user); | |||
const collection = mongoClient | |||
.db("whatsapp_api") | |||
.collection(`auth_info_${id}`); | |||
const chatsCollection = mongoClient | |||
.db("whatsapp_chats") | |||
.collection(`all_chats_${id}`); | |||
const { state, saveCreds } = await useMongoDBAuthState(collection); | |||
try { | |||
if (isError || user.isLogged === false) { | |||
let sock = makeWASocket({ | |||
printQRInTerminal: true, | |||
auth: state, | |||
}); | |||
setSock(id, sock); | |||
} else { | |||
sock[id].ev.off("messages.upsert", groupMessageEventListener); | |||
sock[id].ev.off("connection.update", connectionUpdateListener); | |||
} | |||
} catch (error) { | |||
console.error(error); | |||
} | |||
connectionUpdateListener = async (update) => { | |||
const { connection, lastDisconnect } = update; | |||
if (!connection && update?.qr) { | |||
const qrCodeDataURL = await generateQRCode(update.qr); | |||
console.log("QR created"); | |||
socket.emit("qrCode", qrCodeDataURL); | |||
} | |||
if (connection === "close") { | |||
const shouldReconnect = | |||
lastDisconnect?.error?.output?.statusCode !== | |||
DisconnectReason.loggedOut; | |||
console.log( | |||
"connection closed due to ", | |||
lastDisconnect.error, | |||
", reconnecting ", | |||
shouldReconnect | |||
); | |||
if ( | |||
lastDisconnect.error?.output?.statusCode === DisconnectReason.loggedOut | |||
) { | |||
console.log("User logged out Rereun the connection"); | |||
// Handle user logout, perform cleanup, or redirect as needed | |||
await mongoClient.db("whatsapp_api").dropCollection(`auth_info_${id}`); | |||
user.isLogged = false; | |||
await user.save(); | |||
socket.emit("user disconnected"); | |||
} | |||
// if (lastDisconnect.error?.output?.statusCode === 440) { | |||
// await mongoClient.db("whatsapp_api").dropCollection(`auth_info_${id}`); | |||
// connectionLogic(id, socket); | |||
// } | |||
if (shouldReconnect) { | |||
connectionLogic(id, socket, true); | |||
} | |||
} else if (connection === "open") { | |||
console.log("opened connection"); | |||
user.isLogged = true; | |||
await user.save(); | |||
socket.emit("user connected"); | |||
} | |||
}; | |||
groupMessageEventListener = async (messageInfoUpsert) => { | |||
if ( | |||
messageInfoUpsert.messages[0].key.remoteJid.split("@")[1] === "g.us" && | |||
messageInfoUpsert.messages[0].message?.conversation | |||
) { | |||
const user = await User.findById(id); | |||
const tags = user.tags; | |||
const newMessage = messageInfoUpsert.messages[0].message?.conversation; | |||
const isRequiredMessage = | |||
tags.length === 0 | |||
? true | |||
: tags.reduce((accum, curr) => { | |||
if (accum) return accum; | |||
else { | |||
const regex = new RegExp(curr, "i"); | |||
console.log(accum, curr, newMessage); | |||
console.log(regex.test(newMessage)); | |||
return regex.test(newMessage); | |||
} | |||
}, false); | |||
console.log(isRequiredMessage); | |||
const newGrpMessage = { | |||
conversation: messageInfoUpsert.messages[0].message?.conversation, | |||
username: messageInfoUpsert.messages[0].pushName, | |||
phoneNumber: messageInfoUpsert.messages[0].key.participant | |||
.split("@")[0] | |||
.slice(2), | |||
timestamp: new Date( | |||
messageInfoUpsert.messages[0].messageTimestamp * 1000 | |||
).toISOString(), | |||
}; | |||
// console.log(messageInfoUpsert.messages[0]); | |||
if (isRequiredMessage) { | |||
socket.emit("new message", newGrpMessage); | |||
chatsCollection.insertOne(newGrpMessage); | |||
console.log(newGrpMessage); | |||
} | |||
} | |||
}; | |||
sock[id].ev.on("connection.update", connectionUpdateListener); | |||
sock[id].ev.on("messages.upsert", groupMessageEventListener); | |||
sock[id].ev.on("creds.update", saveCreds); | |||
if (user.isLogged === true) { | |||
socket.emit("user connected"); | |||
} | |||
} | |||
app.get("/", (req, res) => { | |||
res.status(200).json("Hey this is done"); | |||
}); | |||
app.post("/api/signup", async (req, res, next) => { | |||
try { | |||
await createSignUpValidation.validateAsync(req.body); | |||
const { username, password, email } = req.body; | |||
const hashedPassword = await bcrypt.hash(password, 10); | |||
const newUser = new User({ username, password: hashedPassword, email }); | |||
await newUser.save(); | |||
const token = await signAccessToken(newUser.id); | |||
const maxAgeInSeconds = 10 * 24 * 60 * 60; | |||
res.cookie("jwt", token, { | |||
httpOnly: true, | |||
maxAge: maxAgeInSeconds, | |||
}); | |||
res.status(201).json(newUser); | |||
} catch (error) { | |||
next(error); | |||
} | |||
}); | |||
app.get("/api/getMe", verifyAccessToken, async (req, res, next) => { | |||
try { | |||
const data = req.data; | |||
res.status(200).json(data); | |||
} catch (error) { | |||
next(error); | |||
} | |||
}); | |||
app.post("/api/signin", async (req, res, next) => { | |||
try { | |||
await createSignInValidation.validateAsync(req.body); | |||
const { email, password } = req.body; | |||
console.log(email, password); | |||
const validUser = await User.checkUser(email, password); | |||
if (!validUser) throw new MyError("Invalid email or password"); | |||
const token = await signAccessToken(validUser.id); | |||
const maxAgeInSeconds = 10 * 24 * 60 * 60; | |||
res.cookie("jwt", token, { | |||
httpOnly: true, | |||
maxAge: maxAgeInSeconds, | |||
}); | |||
res.status(200).json(validUser); | |||
} catch (error) { | |||
next(error); | |||
} | |||
}); | |||
app.get("/api/refreshMessages", verifyAccessToken, async (req, res, next) => { | |||
try { | |||
const data = req.data; | |||
const chatsCollection = mongoClient | |||
.db("whatsapp_chats") | |||
.collection(`all_chats_${data.id}`); | |||
console.log(sock); | |||
const messages = await chatsCollection | |||
.find({}) | |||
.sort({ timestamp: -1 }) | |||
.limit(20) | |||
.toArray(); | |||
res.status(200).json(messages); | |||
} catch (error) { | |||
next(error); | |||
} | |||
}); | |||
app.post("/api/tag/add", verifyAccessToken, async (req, res, next) => { | |||
try { | |||
const data = req.data; | |||
const { tag } = req.body; | |||
const userTag = await User.findByIdAndUpdate( | |||
data.id, | |||
{ $addToSet: { tags: tag } }, | |||
{ new: true } | |||
); | |||
if (userTag) { | |||
return res.status(201).json(userTag); | |||
} else { | |||
return res.status(404).json("User not found"); | |||
} | |||
} catch (error) { | |||
next(error); | |||
} | |||
}); | |||
app.post("/api/tag/del", verifyAccessToken, async (req, res, next) => { | |||
try { | |||
const data = req.data; | |||
const { tag: deleteTag } = req.body; | |||
let user = await User.findById(data.id); | |||
let tags = user.tags; | |||
tags = tags.filter((tag) => tag !== deleteTag); | |||
user.tags = tags; | |||
await user.save(); | |||
res.status(200).json(user); | |||
} catch (error) { | |||
next(error); | |||
} | |||
}); | |||
// app.get("/createServer", verifyAccessToken, (req, res) => { | |||
// const data = req.data; | |||
// connectionLogic(data.id); | |||
// res.status(200).json("Server created"); | |||
// }); | |||
process.on("exit", async () => { | |||
User.updateMany({}, { isLogged: false }); | |||
}); | |||
io.on("connection", (socket) => { | |||
console.log("a user connected"); | |||
socket.on("whatsapp connect", (id) => { | |||
connectionLogic(id, socket, false); | |||
}); | |||
}); | |||
app.get("*", (req, res) => { | |||
res.sendFile(path.join(__dirname, "client", "dist", "index.html")); | |||
}); | |||
app.use((err, req, res, next) => { | |||
res.status(err.status || 500); | |||
res.json({ | |||
status: err.status || 500, | |||
message: err.message || "Internal Server error", | |||
}); | |||
}); | |||
server.listen(3000, () => { | |||
console.log("listening on *:3000"); | |||
}); |
@@ -0,0 +1,44 @@ | |||
const { Schema, model } = require("mongoose"); | |||
const bcrypt = require("bcryptjs"); | |||
const userSchema = new Schema({ | |||
username: { | |||
type: String, | |||
reuired: true, | |||
unique: true, | |||
}, | |||
email: { | |||
type: String, | |||
required: true, | |||
unique: true, | |||
}, | |||
password: { | |||
type: String, | |||
required: true, | |||
}, | |||
phoneNumber: { | |||
type: String, | |||
}, | |||
isLogged: { | |||
type: Boolean, | |||
default: false, | |||
}, | |||
tags: { | |||
type: [String], | |||
default: [], | |||
}, | |||
}); | |||
userSchema.statics.checkUser = async function (email, password) { | |||
const foundUser = await this.findOne({ email: email }); | |||
let isValid = false; | |||
if (foundUser) { | |||
isValid = await bcrypt.compare(password, foundUser.password); | |||
} | |||
return isValid ? foundUser : false; | |||
}; | |||
const User = model("User", userSchema); | |||
module.exports = User; |
@@ -0,0 +1,30 @@ | |||
{ | |||
"name": "baileys-whatsapp", | |||
"version": "1.0.0", | |||
"description": "", | |||
"main": "index.js", | |||
"scripts": { | |||
"dev": "nodemon index.js", | |||
"start": "node index.js", | |||
"build": "npm install && npm install --prefix client && npm run build --prefix client" | |||
}, | |||
"keywords": [], | |||
"author": "", | |||
"license": "ISC", | |||
"dependencies": { | |||
"@whiskeysockets/baileys": "github:WhiskeySockets/Baileys", | |||
"bcryptjs": "^2.4.3", | |||
"cookie-parser": "^1.4.6", | |||
"cors": "^2.8.5", | |||
"dotenv": "^16.3.1", | |||
"express": "^4.18.2", | |||
"joi": "^17.11.0", | |||
"jsonwebtoken": "^9.0.2", | |||
"mongodb": "^6.3.0", | |||
"mongoose": "^8.0.3", | |||
"nodemon": "^3.0.2", | |||
"qrcode": "^1.5.3", | |||
"qrcode-terminal": "^0.12.0", | |||
"socket.io": "^4.7.2" | |||
} | |||
} |
@@ -0,0 +1,12 @@ | |||
const Joi = require("joi"); | |||
exports.createSignUpValidation = Joi.object({ | |||
username: Joi.string().required().min(2), | |||
email: Joi.string().email().required(), | |||
password: Joi.string().min(6).required(), | |||
}); | |||
exports.createSignInValidation = Joi.object({ | |||
email: Joi.string().email().required(), | |||
password: Joi.string().min(6).required(), | |||
}); |