@@ -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(), | |||||
}); |