understanding commited on
Commit
ba5c923
·
verified ·
1 Parent(s): 5610588

Upload 41 files

Browse files
.env ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ PORT=8080
2
+ API_KEY=YOUR_API_KEY
.gitignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ /node_modules
2
+
3
+
4
+ package-lock.json
5
+ .vercel
6
+
7
+
8
+
9
+ /auth_info_baileys
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Zain ul din
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
dist/baileys/db.js ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.connectDB = connectDB;
13
+ const mongodb_1 = require("mongodb");
14
+ const dbName = "whatsapp";
15
+ const collectionName = "authState";
16
+ function connectDB() {
17
+ return __awaiter(this, void 0, void 0, function* () {
18
+ const client = new mongodb_1.MongoClient(process.env.MONGO_URL || "");
19
+ yield client.connect();
20
+ const db = client.db(dbName);
21
+ const collection = db.collection(collectionName);
22
+ return { client, collection };
23
+ });
24
+ }
dist/baileys/index.js ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.connectToWhatsApp = connectToWhatsApp;
46
+ const baileys_1 = __importStar(require("@whiskeysockets/baileys"));
47
+ const fs = __importStar(require("fs"));
48
+ const db_1 = require("./db");
49
+ const mongo_baileys_1 = require("mongo-baileys");
50
+ function connectToWhatsApp(onStart) {
51
+ return __awaiter(this, void 0, void 0, function* () {
52
+ const { state, saveCreds } = process.env.MONGO_URL
53
+ ? yield (0, mongo_baileys_1.useMongoDBAuthState)((yield (0, db_1.connectDB)()).collection)
54
+ : yield (0, baileys_1.useMultiFileAuthState)("auth_info_baileys");
55
+ const sock = (0, baileys_1.default)({
56
+ // can provide additional config here
57
+ printQRInTerminal: true,
58
+ mobile: false,
59
+ keepAliveIntervalMs: 10000,
60
+ syncFullHistory: false,
61
+ markOnlineOnConnect: true,
62
+ defaultQueryTimeoutMs: undefined,
63
+ auth: state
64
+ });
65
+ sock.ev.on("creds.update", saveCreds);
66
+ const setupAuth = new Promise((resolve, rej) => __awaiter(this, void 0, void 0, function* () {
67
+ sock.ev.on("connection.update", (update) => {
68
+ var _a, _b, _c, _d;
69
+ const { connection, lastDisconnect, qr } = update;
70
+ global.waQrCode = qr || null;
71
+ try {
72
+ if (connection === "close" && lastDisconnect) {
73
+ const statusCode = (_b = (_a = lastDisconnect.error) === null || _a === void 0 ? void 0 : _a.output) === null || _b === void 0 ? void 0 : _b.statusCode;
74
+ const shouldReconnect = ((_d = (_c = lastDisconnect.error) === null || _c === void 0 ? void 0 : _c.output) === null || _d === void 0 ? void 0 : _d.statusCode) !==
75
+ baileys_1.DisconnectReason.loggedOut;
76
+ console.log("connection closed due to ", lastDisconnect.error, ", status code: ", statusCode, ", reconnecting ", shouldReconnect);
77
+ // reconnect if not logged out
78
+ if (shouldReconnect) {
79
+ connectToWhatsApp();
80
+ }
81
+ else {
82
+ // clear credentials
83
+ if (lastDisconnect.error) {
84
+ if (fs.existsSync("./auth_info_baileys")) {
85
+ fs.rmSync("./auth_info_baileys", {
86
+ force: true,
87
+ recursive: true
88
+ });
89
+ }
90
+ }
91
+ }
92
+ }
93
+ else if (connection === "open") {
94
+ console.log("\n ✔ opened connection \n");
95
+ resolve(null);
96
+ }
97
+ }
98
+ catch (err) {
99
+ console.log(err);
100
+ }
101
+ });
102
+ }));
103
+ const FIVE_MIN_IN_MS = 1000 * 60 * 5;
104
+ yield Promise.race([
105
+ setupAuth,
106
+ new Promise((_, rej) => setTimeout(() => rej("Timeout while setting up connection to whatsapp"), FIVE_MIN_IN_MS))
107
+ ]);
108
+ global.waSock = sock;
109
+ });
110
+ }
dist/controllers/home.js ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getQrCodeController = exports.homeController = void 0;
4
+ const homeController = (req, res) => {
5
+ res.render("index");
6
+ };
7
+ exports.homeController = homeController;
8
+ const getQrCodeController = (req, res) => {
9
+ const apiKey = req.body.api_key;
10
+ if (apiKey != process.env.API_KEY) {
11
+ res.render("error", { message: "Invalid Credentials" });
12
+ return;
13
+ }
14
+ if (global.waQrCode) {
15
+ res.render("qr", { qr: global.waQrCode });
16
+ }
17
+ else {
18
+ res.render("qr", { qr: "" });
19
+ }
20
+ };
21
+ exports.getQrCodeController = getQrCodeController;
dist/controllers/index.js ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getQrCodeController = exports.homeController = exports.pingController = void 0;
7
+ const ping_1 = __importDefault(require("./ping"));
8
+ exports.pingController = ping_1.default;
9
+ const home_1 = require("./home");
10
+ Object.defineProperty(exports, "getQrCodeController", { enumerable: true, get: function () { return home_1.getQrCodeController; } });
11
+ Object.defineProperty(exports, "homeController", { enumerable: true, get: function () { return home_1.homeController; } });
dist/controllers/ping.js ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ const delay_1 = require("../lib/delay");
13
+ const pingController = (req, res) => __awaiter(void 0, void 0, void 0, function* () {
14
+ if (global.waSock == null) {
15
+ res.status(500).send({ error: "WA is not connected" });
16
+ return;
17
+ }
18
+ const { message, numbers, image } = req.body;
19
+ for (let number of numbers) {
20
+ const id = `${number}@s.whatsapp.net`;
21
+ if (image) {
22
+ const imgToBase64 = Buffer.from(image, "base64");
23
+ yield global.waSock.sendMessage(id, {
24
+ image: imgToBase64,
25
+ caption: message
26
+ });
27
+ }
28
+ else {
29
+ yield global.waSock.sendMessage(id, { text: message });
30
+ }
31
+ yield (0, delay_1.delay)(100 * Math.random());
32
+ }
33
+ res.status(200).send({ message: "success" });
34
+ });
35
+ exports.default = pingController;
dist/index.js ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ const express_1 = __importDefault(require("express"));
16
+ const routes_1 = require("./routes");
17
+ const body_parser_1 = __importDefault(require("body-parser"));
18
+ const dotenv_1 = __importDefault(require("dotenv"));
19
+ const cors_1 = __importDefault(require("cors"));
20
+ const baileys_1 = require("./baileys");
21
+ const app = (0, express_1.default)();
22
+ const PORT = process.env.PORT || 8080;
23
+ dotenv_1.default.config();
24
+ app.set("views", "./views");
25
+ app.set("view engine", "ejs");
26
+ app.use((0, cors_1.default)({ origin: "*" }));
27
+ app.use(express_1.default.static("public"));
28
+ app.use(body_parser_1.default.json({ limit: "20mb" }));
29
+ app.use(express_1.default.urlencoded({ extended: true }));
30
+ app.use(routes_1.homeRoute);
31
+ app.use(routes_1.pingRoute);
32
+ app.use((_, res) => {
33
+ res.status(404).json({ message: "Resource not found" });
34
+ });
35
+ (() => __awaiter(void 0, void 0, void 0, function* () {
36
+ yield (0, baileys_1.connectToWhatsApp)();
37
+ }))();
38
+ app.listen(PORT, () => {
39
+ console.log(`server is listening on localhost:${PORT}`);
40
+ });
dist/lib/delay.js ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.delay = void 0;
4
+ const delay = (ms) => new Promise((r) => setTimeout(r, ms));
5
+ exports.delay = delay;
dist/middlewares/api-key-authentication.js ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const authentication = (req, res, next) => {
4
+ const authHeader = req.headers["authorization"];
5
+ if (!authHeader) {
6
+ return res.status(401).json({ message: "Missing authorization header" });
7
+ }
8
+ const apiKey = authHeader.split("Bearer ").at(-1);
9
+ if (!apiKey) {
10
+ return res.status(401).json({ message: "Invalid authorization format" });
11
+ }
12
+ if (process.env.API_KEY && apiKey === process.env.API_KEY) {
13
+ next();
14
+ }
15
+ else {
16
+ res.status(401).json({ message: "Unauthorized" });
17
+ }
18
+ };
19
+ exports.default = authentication;
dist/middlewares/ping-message-validator.js ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const zod_1 = require("zod");
4
+ const pingMessageSchema = zod_1.z.object({
5
+ message: zod_1.z
6
+ .string()
7
+ .min(1, "Message is required and must be a non-empty string"),
8
+ numbers: zod_1.z
9
+ .array(zod_1.z
10
+ .string()
11
+ .min(12, "Each number must be at least 12 characters long")
12
+ .regex(/^\d{12}$/, "Invalid phone number format. Correct example: 123456789012"))
13
+ .max(5, "You can provide a maximum of 5 phone numbers"),
14
+ image: zod_1.z.string().optional()
15
+ });
16
+ const validatePingMessage = (req, res, next) => {
17
+ try {
18
+ pingMessageSchema.parse(req.body);
19
+ next();
20
+ }
21
+ catch (error) {
22
+ if (error instanceof zod_1.z.ZodError) {
23
+ res.status(400).json({ errors: error.errors });
24
+ }
25
+ else {
26
+ res.status(500).json({ message: "Internal Server Error" });
27
+ }
28
+ }
29
+ };
30
+ exports.default = validatePingMessage;
dist/routes/home.js ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const express_1 = __importDefault(require("express"));
7
+ const controllers_1 = require("../controllers");
8
+ const router = express_1.default.Router();
9
+ router.get("/", controllers_1.homeController);
10
+ router.post("/", controllers_1.getQrCodeController);
11
+ exports.default = router;
dist/routes/index.js ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.homeRoute = exports.pingRoute = void 0;
7
+ const ping_1 = __importDefault(require("./ping"));
8
+ exports.pingRoute = ping_1.default;
9
+ const home_1 = __importDefault(require("./home"));
10
+ exports.homeRoute = home_1.default;
dist/routes/ping.js ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const express_1 = __importDefault(require("express"));
7
+ const controllers_1 = require("../controllers");
8
+ const ping_message_validator_1 = __importDefault(require("../middlewares/ping-message-validator"));
9
+ const express_rate_limit_1 = require("express-rate-limit");
10
+ const api_key_authentication_1 = __importDefault(require("../middlewares/api-key-authentication"));
11
+ const limiter = (0, express_rate_limit_1.rateLimit)({
12
+ windowMs: 1000,
13
+ limit: 5,
14
+ standardHeaders: "draft-7",
15
+ legacyHeaders: false
16
+ });
17
+ const route = express_1.default.Router();
18
+ route.use(limiter);
19
+ route.use(api_key_authentication_1.default);
20
+ route.post("/ping", ping_message_validator_1.default, controllers_1.pingController);
21
+ exports.default = route;
dist/test/index.js ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const dotenv_1 = __importDefault(require("dotenv"));
7
+ dotenv_1.default.config();
8
+ console.log("✔ All test has been passed");
package.json ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "whatsapp-ping",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "vite-node ./src/test/index.ts",
8
+ "dev": "concurrently \"vite-node --watch src/index.ts\" \"npx tailwindcss -i ./public/input.css -o ./public/global.css --watch\"",
9
+ "start": "pm2 start ./dist/index.js",
10
+ "build": "tsc && tailwindcss -i ./public/input.css -o ./public/global.css"
11
+ },
12
+ "keywords": [
13
+ "whatsapp ping"
14
+ ],
15
+ "author": "Zain-ul-din",
16
+ "license": "MIT",
17
+ "devDependencies": {
18
+ "@types/body-parser": "^1.19.5",
19
+ "@types/cors": "^2.8.17",
20
+ "@types/dotenv": "^8.2.0",
21
+ "@types/express": "^4.17.21",
22
+ "concurrently": "^8.2.2",
23
+ "husky": "^9.1.5",
24
+ "tailwindcss": "^3.4.10",
25
+ "typescript": "^5.6.3",
26
+ "vite-node": "^2.0.5"
27
+ },
28
+ "dependencies": {
29
+ "@hapi/boom": "^10.0.1",
30
+ "@whiskeysockets/baileys": "^6.7.9",
31
+ "@whiskeysockets/libsignal-node": "github:WhiskeySockets/libsignal-node",
32
+ "body-parser": "^1.20.2",
33
+ "cors": "^2.8.5",
34
+ "dotenv": "^16.4.5",
35
+ "ejs": "^3.1.10",
36
+ "express": "^4.19.2",
37
+ "express-rate-limit": "^7.4.0",
38
+ "mongo-baileys": "^1.0.1",
39
+ "mongodb": "^6.8.1",
40
+ "platformsh-config": "^2.4.1",
41
+ "qrcode-terminal": "^0.12.0",
42
+ "whatsapp-ping": "file:",
43
+ "zod": "^3.23.8"
44
+ }
45
+ }
public/global.css ADDED
@@ -0,0 +1,817 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *, ::before, ::after {
2
+ --tw-border-spacing-x: 0;
3
+ --tw-border-spacing-y: 0;
4
+ --tw-translate-x: 0;
5
+ --tw-translate-y: 0;
6
+ --tw-rotate: 0;
7
+ --tw-skew-x: 0;
8
+ --tw-skew-y: 0;
9
+ --tw-scale-x: 1;
10
+ --tw-scale-y: 1;
11
+ --tw-pan-x: ;
12
+ --tw-pan-y: ;
13
+ --tw-pinch-zoom: ;
14
+ --tw-scroll-snap-strictness: proximity;
15
+ --tw-gradient-from-position: ;
16
+ --tw-gradient-via-position: ;
17
+ --tw-gradient-to-position: ;
18
+ --tw-ordinal: ;
19
+ --tw-slashed-zero: ;
20
+ --tw-numeric-figure: ;
21
+ --tw-numeric-spacing: ;
22
+ --tw-numeric-fraction: ;
23
+ --tw-ring-inset: ;
24
+ --tw-ring-offset-width: 0px;
25
+ --tw-ring-offset-color: #fff;
26
+ --tw-ring-color: rgb(59 130 246 / 0.5);
27
+ --tw-ring-offset-shadow: 0 0 #0000;
28
+ --tw-ring-shadow: 0 0 #0000;
29
+ --tw-shadow: 0 0 #0000;
30
+ --tw-shadow-colored: 0 0 #0000;
31
+ --tw-blur: ;
32
+ --tw-brightness: ;
33
+ --tw-contrast: ;
34
+ --tw-grayscale: ;
35
+ --tw-hue-rotate: ;
36
+ --tw-invert: ;
37
+ --tw-saturate: ;
38
+ --tw-sepia: ;
39
+ --tw-drop-shadow: ;
40
+ --tw-backdrop-blur: ;
41
+ --tw-backdrop-brightness: ;
42
+ --tw-backdrop-contrast: ;
43
+ --tw-backdrop-grayscale: ;
44
+ --tw-backdrop-hue-rotate: ;
45
+ --tw-backdrop-invert: ;
46
+ --tw-backdrop-opacity: ;
47
+ --tw-backdrop-saturate: ;
48
+ --tw-backdrop-sepia: ;
49
+ --tw-contain-size: ;
50
+ --tw-contain-layout: ;
51
+ --tw-contain-paint: ;
52
+ --tw-contain-style: ;
53
+ }
54
+
55
+ ::backdrop {
56
+ --tw-border-spacing-x: 0;
57
+ --tw-border-spacing-y: 0;
58
+ --tw-translate-x: 0;
59
+ --tw-translate-y: 0;
60
+ --tw-rotate: 0;
61
+ --tw-skew-x: 0;
62
+ --tw-skew-y: 0;
63
+ --tw-scale-x: 1;
64
+ --tw-scale-y: 1;
65
+ --tw-pan-x: ;
66
+ --tw-pan-y: ;
67
+ --tw-pinch-zoom: ;
68
+ --tw-scroll-snap-strictness: proximity;
69
+ --tw-gradient-from-position: ;
70
+ --tw-gradient-via-position: ;
71
+ --tw-gradient-to-position: ;
72
+ --tw-ordinal: ;
73
+ --tw-slashed-zero: ;
74
+ --tw-numeric-figure: ;
75
+ --tw-numeric-spacing: ;
76
+ --tw-numeric-fraction: ;
77
+ --tw-ring-inset: ;
78
+ --tw-ring-offset-width: 0px;
79
+ --tw-ring-offset-color: #fff;
80
+ --tw-ring-color: rgb(59 130 246 / 0.5);
81
+ --tw-ring-offset-shadow: 0 0 #0000;
82
+ --tw-ring-shadow: 0 0 #0000;
83
+ --tw-shadow: 0 0 #0000;
84
+ --tw-shadow-colored: 0 0 #0000;
85
+ --tw-blur: ;
86
+ --tw-brightness: ;
87
+ --tw-contrast: ;
88
+ --tw-grayscale: ;
89
+ --tw-hue-rotate: ;
90
+ --tw-invert: ;
91
+ --tw-saturate: ;
92
+ --tw-sepia: ;
93
+ --tw-drop-shadow: ;
94
+ --tw-backdrop-blur: ;
95
+ --tw-backdrop-brightness: ;
96
+ --tw-backdrop-contrast: ;
97
+ --tw-backdrop-grayscale: ;
98
+ --tw-backdrop-hue-rotate: ;
99
+ --tw-backdrop-invert: ;
100
+ --tw-backdrop-opacity: ;
101
+ --tw-backdrop-saturate: ;
102
+ --tw-backdrop-sepia: ;
103
+ --tw-contain-size: ;
104
+ --tw-contain-layout: ;
105
+ --tw-contain-paint: ;
106
+ --tw-contain-style: ;
107
+ }
108
+
109
+ /*
110
+ ! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com
111
+ */
112
+
113
+ /*
114
+ 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
115
+ 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
116
+ */
117
+
118
+ *,
119
+ ::before,
120
+ ::after {
121
+ box-sizing: border-box;
122
+ /* 1 */
123
+ border-width: 0;
124
+ /* 2 */
125
+ border-style: solid;
126
+ /* 2 */
127
+ border-color: #e5e7eb;
128
+ /* 2 */
129
+ }
130
+
131
+ ::before,
132
+ ::after {
133
+ --tw-content: '';
134
+ }
135
+
136
+ /*
137
+ 1. Use a consistent sensible line-height in all browsers.
138
+ 2. Prevent adjustments of font size after orientation changes in iOS.
139
+ 3. Use a more readable tab size.
140
+ 4. Use the user's configured `sans` font-family by default.
141
+ 5. Use the user's configured `sans` font-feature-settings by default.
142
+ 6. Use the user's configured `sans` font-variation-settings by default.
143
+ 7. Disable tap highlights on iOS
144
+ */
145
+
146
+ html,
147
+ :host {
148
+ line-height: 1.5;
149
+ /* 1 */
150
+ -webkit-text-size-adjust: 100%;
151
+ /* 2 */
152
+ -moz-tab-size: 4;
153
+ /* 3 */
154
+ -o-tab-size: 4;
155
+ tab-size: 4;
156
+ /* 3 */
157
+ font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
158
+ /* 4 */
159
+ font-feature-settings: normal;
160
+ /* 5 */
161
+ font-variation-settings: normal;
162
+ /* 6 */
163
+ -webkit-tap-highlight-color: transparent;
164
+ /* 7 */
165
+ }
166
+
167
+ /*
168
+ 1. Remove the margin in all browsers.
169
+ 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
170
+ */
171
+
172
+ body {
173
+ margin: 0;
174
+ /* 1 */
175
+ line-height: inherit;
176
+ /* 2 */
177
+ }
178
+
179
+ /*
180
+ 1. Add the correct height in Firefox.
181
+ 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
182
+ 3. Ensure horizontal rules are visible by default.
183
+ */
184
+
185
+ hr {
186
+ height: 0;
187
+ /* 1 */
188
+ color: inherit;
189
+ /* 2 */
190
+ border-top-width: 1px;
191
+ /* 3 */
192
+ }
193
+
194
+ /*
195
+ Add the correct text decoration in Chrome, Edge, and Safari.
196
+ */
197
+
198
+ abbr:where([title]) {
199
+ -webkit-text-decoration: underline dotted;
200
+ text-decoration: underline dotted;
201
+ }
202
+
203
+ /*
204
+ Remove the default font size and weight for headings.
205
+ */
206
+
207
+ h1,
208
+ h2,
209
+ h3,
210
+ h4,
211
+ h5,
212
+ h6 {
213
+ font-size: inherit;
214
+ font-weight: inherit;
215
+ }
216
+
217
+ /*
218
+ Reset links to optimize for opt-in styling instead of opt-out.
219
+ */
220
+
221
+ a {
222
+ color: inherit;
223
+ text-decoration: inherit;
224
+ }
225
+
226
+ /*
227
+ Add the correct font weight in Edge and Safari.
228
+ */
229
+
230
+ b,
231
+ strong {
232
+ font-weight: bolder;
233
+ }
234
+
235
+ /*
236
+ 1. Use the user's configured `mono` font-family by default.
237
+ 2. Use the user's configured `mono` font-feature-settings by default.
238
+ 3. Use the user's configured `mono` font-variation-settings by default.
239
+ 4. Correct the odd `em` font sizing in all browsers.
240
+ */
241
+
242
+ code,
243
+ kbd,
244
+ samp,
245
+ pre {
246
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
247
+ /* 1 */
248
+ font-feature-settings: normal;
249
+ /* 2 */
250
+ font-variation-settings: normal;
251
+ /* 3 */
252
+ font-size: 1em;
253
+ /* 4 */
254
+ }
255
+
256
+ /*
257
+ Add the correct font size in all browsers.
258
+ */
259
+
260
+ small {
261
+ font-size: 80%;
262
+ }
263
+
264
+ /*
265
+ Prevent `sub` and `sup` elements from affecting the line height in all browsers.
266
+ */
267
+
268
+ sub,
269
+ sup {
270
+ font-size: 75%;
271
+ line-height: 0;
272
+ position: relative;
273
+ vertical-align: baseline;
274
+ }
275
+
276
+ sub {
277
+ bottom: -0.25em;
278
+ }
279
+
280
+ sup {
281
+ top: -0.5em;
282
+ }
283
+
284
+ /*
285
+ 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
286
+ 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
287
+ 3. Remove gaps between table borders by default.
288
+ */
289
+
290
+ table {
291
+ text-indent: 0;
292
+ /* 1 */
293
+ border-color: inherit;
294
+ /* 2 */
295
+ border-collapse: collapse;
296
+ /* 3 */
297
+ }
298
+
299
+ /*
300
+ 1. Change the font styles in all browsers.
301
+ 2. Remove the margin in Firefox and Safari.
302
+ 3. Remove default padding in all browsers.
303
+ */
304
+
305
+ button,
306
+ input,
307
+ optgroup,
308
+ select,
309
+ textarea {
310
+ font-family: inherit;
311
+ /* 1 */
312
+ font-feature-settings: inherit;
313
+ /* 1 */
314
+ font-variation-settings: inherit;
315
+ /* 1 */
316
+ font-size: 100%;
317
+ /* 1 */
318
+ font-weight: inherit;
319
+ /* 1 */
320
+ line-height: inherit;
321
+ /* 1 */
322
+ letter-spacing: inherit;
323
+ /* 1 */
324
+ color: inherit;
325
+ /* 1 */
326
+ margin: 0;
327
+ /* 2 */
328
+ padding: 0;
329
+ /* 3 */
330
+ }
331
+
332
+ /*
333
+ Remove the inheritance of text transform in Edge and Firefox.
334
+ */
335
+
336
+ button,
337
+ select {
338
+ text-transform: none;
339
+ }
340
+
341
+ /*
342
+ 1. Correct the inability to style clickable types in iOS and Safari.
343
+ 2. Remove default button styles.
344
+ */
345
+
346
+ button,
347
+ input:where([type='button']),
348
+ input:where([type='reset']),
349
+ input:where([type='submit']) {
350
+ -webkit-appearance: button;
351
+ /* 1 */
352
+ background-color: transparent;
353
+ /* 2 */
354
+ background-image: none;
355
+ /* 2 */
356
+ }
357
+
358
+ /*
359
+ Use the modern Firefox focus style for all focusable elements.
360
+ */
361
+
362
+ :-moz-focusring {
363
+ outline: auto;
364
+ }
365
+
366
+ /*
367
+ Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
368
+ */
369
+
370
+ :-moz-ui-invalid {
371
+ box-shadow: none;
372
+ }
373
+
374
+ /*
375
+ Add the correct vertical alignment in Chrome and Firefox.
376
+ */
377
+
378
+ progress {
379
+ vertical-align: baseline;
380
+ }
381
+
382
+ /*
383
+ Correct the cursor style of increment and decrement buttons in Safari.
384
+ */
385
+
386
+ ::-webkit-inner-spin-button,
387
+ ::-webkit-outer-spin-button {
388
+ height: auto;
389
+ }
390
+
391
+ /*
392
+ 1. Correct the odd appearance in Chrome and Safari.
393
+ 2. Correct the outline style in Safari.
394
+ */
395
+
396
+ [type='search'] {
397
+ -webkit-appearance: textfield;
398
+ /* 1 */
399
+ outline-offset: -2px;
400
+ /* 2 */
401
+ }
402
+
403
+ /*
404
+ Remove the inner padding in Chrome and Safari on macOS.
405
+ */
406
+
407
+ ::-webkit-search-decoration {
408
+ -webkit-appearance: none;
409
+ }
410
+
411
+ /*
412
+ 1. Correct the inability to style clickable types in iOS and Safari.
413
+ 2. Change font properties to `inherit` in Safari.
414
+ */
415
+
416
+ ::-webkit-file-upload-button {
417
+ -webkit-appearance: button;
418
+ /* 1 */
419
+ font: inherit;
420
+ /* 2 */
421
+ }
422
+
423
+ /*
424
+ Add the correct display in Chrome and Safari.
425
+ */
426
+
427
+ summary {
428
+ display: list-item;
429
+ }
430
+
431
+ /*
432
+ Removes the default spacing and border for appropriate elements.
433
+ */
434
+
435
+ blockquote,
436
+ dl,
437
+ dd,
438
+ h1,
439
+ h2,
440
+ h3,
441
+ h4,
442
+ h5,
443
+ h6,
444
+ hr,
445
+ figure,
446
+ p,
447
+ pre {
448
+ margin: 0;
449
+ }
450
+
451
+ fieldset {
452
+ margin: 0;
453
+ padding: 0;
454
+ }
455
+
456
+ legend {
457
+ padding: 0;
458
+ }
459
+
460
+ ol,
461
+ ul,
462
+ menu {
463
+ list-style: none;
464
+ margin: 0;
465
+ padding: 0;
466
+ }
467
+
468
+ /*
469
+ Reset default styling for dialogs.
470
+ */
471
+
472
+ dialog {
473
+ padding: 0;
474
+ }
475
+
476
+ /*
477
+ Prevent resizing textareas horizontally by default.
478
+ */
479
+
480
+ textarea {
481
+ resize: vertical;
482
+ }
483
+
484
+ /*
485
+ 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
486
+ 2. Set the default placeholder color to the user's configured gray 400 color.
487
+ */
488
+
489
+ input::-moz-placeholder, textarea::-moz-placeholder {
490
+ opacity: 1;
491
+ /* 1 */
492
+ color: #9ca3af;
493
+ /* 2 */
494
+ }
495
+
496
+ input::placeholder,
497
+ textarea::placeholder {
498
+ opacity: 1;
499
+ /* 1 */
500
+ color: #9ca3af;
501
+ /* 2 */
502
+ }
503
+
504
+ /*
505
+ Set the default cursor for buttons.
506
+ */
507
+
508
+ button,
509
+ [role="button"] {
510
+ cursor: pointer;
511
+ }
512
+
513
+ /*
514
+ Make sure disabled buttons don't get the pointer cursor.
515
+ */
516
+
517
+ :disabled {
518
+ cursor: default;
519
+ }
520
+
521
+ /*
522
+ 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
523
+ 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
524
+ This can trigger a poorly considered lint error in some tools but is included by design.
525
+ */
526
+
527
+ img,
528
+ svg,
529
+ video,
530
+ canvas,
531
+ audio,
532
+ iframe,
533
+ embed,
534
+ object {
535
+ display: block;
536
+ /* 1 */
537
+ vertical-align: middle;
538
+ /* 2 */
539
+ }
540
+
541
+ /*
542
+ Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
543
+ */
544
+
545
+ img,
546
+ video {
547
+ max-width: 100%;
548
+ height: auto;
549
+ }
550
+
551
+ /* Make elements with the HTML hidden attribute stay hidden by default */
552
+
553
+ [hidden]:where(:not([hidden="until-found"])) {
554
+ display: none;
555
+ }
556
+
557
+ :root {
558
+ --background: 0 0% 100%;
559
+ --foreground: 240 10% 3.9%;
560
+ --card: 0 0% 100%;
561
+ --card-foreground: 240 10% 3.9%;
562
+ --popover: 0 0% 100%;
563
+ --popover-foreground: 240 10% 3.9%;
564
+ --primary: 240 5.9% 10%;
565
+ --primary-foreground: 0 0% 98%;
566
+ --secondary: 240 4.8% 95.9%;
567
+ --secondary-foreground: 240 5.9% 10%;
568
+ --muted: 240 4.8% 95.9%;
569
+ --muted-foreground: 240 3.8% 45%;
570
+ --accent: 240 4.8% 95.9%;
571
+ --accent-foreground: 240 5.9% 10%;
572
+ --destructive: 0 72% 51%;
573
+ --destructive-foreground: 0 0% 98%;
574
+ --border: 240 5.9% 90%;
575
+ --input: 240 5.9% 90%;
576
+ --ring: 240 5.9% 10%;
577
+ --radius: 0.5rem;
578
+ }
579
+
580
+ * {
581
+ border-color: hsl(var(--border));
582
+ }
583
+
584
+ body {
585
+ /* Your custom styles here */
586
+ font-family: "Inter", sans-serif;
587
+ background-color: hsl(var(--background));
588
+ color: hsl(var(--foreground));
589
+ }
590
+
591
+ h1,
592
+ h2,
593
+ h3,
594
+ h4,
595
+ h5,
596
+ h6 {
597
+ /* Your custom styles here */
598
+ font-family: "Inter", sans-serif;
599
+ }
600
+
601
+ .mx-auto {
602
+ margin-left: auto;
603
+ margin-right: auto;
604
+ }
605
+
606
+ .my-auto {
607
+ margin-top: auto;
608
+ margin-bottom: auto;
609
+ }
610
+
611
+ .mb-4 {
612
+ margin-bottom: 1rem;
613
+ }
614
+
615
+ .ml-auto {
616
+ margin-left: auto;
617
+ }
618
+
619
+ .mr-1 {
620
+ margin-right: 0.25rem;
621
+ }
622
+
623
+ .mt-2 {
624
+ margin-top: 0.5rem;
625
+ }
626
+
627
+ .mt-4 {
628
+ margin-top: 1rem;
629
+ }
630
+
631
+ .flex {
632
+ display: flex;
633
+ }
634
+
635
+ .hidden {
636
+ display: none;
637
+ }
638
+
639
+ .h-\[302px\] {
640
+ height: 302px;
641
+ }
642
+
643
+ .h-screen {
644
+ height: 100vh;
645
+ }
646
+
647
+ .w-\[302px\] {
648
+ width: 302px;
649
+ }
650
+
651
+ .w-full {
652
+ width: 100%;
653
+ }
654
+
655
+ .max-w-md {
656
+ max-width: 28rem;
657
+ }
658
+
659
+ .cursor-pointer {
660
+ cursor: pointer;
661
+ }
662
+
663
+ .flex-col {
664
+ flex-direction: column;
665
+ }
666
+
667
+ .items-center {
668
+ align-items: center;
669
+ }
670
+
671
+ .justify-center {
672
+ justify-content: center;
673
+ }
674
+
675
+ .text-balance {
676
+ text-wrap: balance;
677
+ }
678
+
679
+ .rounded-md {
680
+ border-radius: calc(var(--radius) - 2px);
681
+ }
682
+
683
+ .rounded-b-lg {
684
+ border-bottom-right-radius: var(--radius);
685
+ border-bottom-left-radius: var(--radius);
686
+ }
687
+
688
+ .border {
689
+ border-width: 1px;
690
+ }
691
+
692
+ .border-t {
693
+ border-top-width: 1px;
694
+ }
695
+
696
+ .border-gray-300 {
697
+ --tw-border-opacity: 1;
698
+ border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
699
+ }
700
+
701
+ .bg-blue-600 {
702
+ --tw-bg-opacity: 1;
703
+ background-color: rgb(37 99 235 / var(--tw-bg-opacity, 1));
704
+ }
705
+
706
+ .p-2 {
707
+ padding: 0.5rem;
708
+ }
709
+
710
+ .p-6 {
711
+ padding: 1.5rem;
712
+ }
713
+
714
+ .py-12 {
715
+ padding-top: 3rem;
716
+ padding-bottom: 3rem;
717
+ }
718
+
719
+ .py-2 {
720
+ padding-top: 0.5rem;
721
+ padding-bottom: 0.5rem;
722
+ }
723
+
724
+ .pt-2 {
725
+ padding-top: 0.5rem;
726
+ }
727
+
728
+ .text-center {
729
+ text-align: center;
730
+ }
731
+
732
+ .text-2xl {
733
+ font-size: 1.5rem;
734
+ line-height: 2rem;
735
+ }
736
+
737
+ .text-lg {
738
+ font-size: 1.125rem;
739
+ line-height: 1.75rem;
740
+ }
741
+
742
+ .font-medium {
743
+ font-weight: 500;
744
+ }
745
+
746
+ .font-semibold {
747
+ font-weight: 600;
748
+ }
749
+
750
+ .text-blue-400 {
751
+ --tw-text-opacity: 1;
752
+ color: rgb(96 165 250 / var(--tw-text-opacity, 1));
753
+ }
754
+
755
+ .text-gray-800 {
756
+ --tw-text-opacity: 1;
757
+ color: rgb(31 41 55 / var(--tw-text-opacity, 1));
758
+ }
759
+
760
+ .text-green-400 {
761
+ --tw-text-opacity: 1;
762
+ color: rgb(74 222 128 / var(--tw-text-opacity, 1));
763
+ }
764
+
765
+ .text-red-400 {
766
+ --tw-text-opacity: 1;
767
+ color: rgb(248 113 113 / var(--tw-text-opacity, 1));
768
+ }
769
+
770
+ .text-white {
771
+ --tw-text-opacity: 1;
772
+ color: rgb(255 255 255 / var(--tw-text-opacity, 1));
773
+ }
774
+
775
+ .shadow-sm {
776
+ --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
777
+ --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
778
+ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
779
+ }
780
+
781
+ .transition {
782
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
783
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
784
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
785
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
786
+ transition-duration: 150ms;
787
+ }
788
+
789
+ .duration-200 {
790
+ transition-duration: 200ms;
791
+ }
792
+
793
+ .hover\:bg-blue-700:hover {
794
+ --tw-bg-opacity: 1;
795
+ background-color: rgb(29 78 216 / var(--tw-bg-opacity, 1));
796
+ }
797
+
798
+ .hover\:text-blue-600:hover {
799
+ --tw-text-opacity: 1;
800
+ color: rgb(37 99 235 / var(--tw-text-opacity, 1));
801
+ }
802
+
803
+ .focus\:outline-none:focus {
804
+ outline: 2px solid transparent;
805
+ outline-offset: 2px;
806
+ }
807
+
808
+ .focus\:ring-2:focus {
809
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
810
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
811
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
812
+ }
813
+
814
+ .focus\:ring-blue-500:focus {
815
+ --tw-ring-opacity: 1;
816
+ --tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1));
817
+ }
public/input.css ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer base {
6
+ .font-body,
7
+ .font-heading {
8
+ /* Your custom styles here */
9
+ font-family: "Inter", sans-serif;
10
+ }
11
+
12
+ :root {
13
+ --background: 0 0% 100%;
14
+ --foreground: 240 10% 3.9%;
15
+ --card: 0 0% 100%;
16
+ --card-foreground: 240 10% 3.9%;
17
+ --popover: 0 0% 100%;
18
+ --popover-foreground: 240 10% 3.9%;
19
+ --primary: 240 5.9% 10%;
20
+ --primary-foreground: 0 0% 98%;
21
+ --secondary: 240 4.8% 95.9%;
22
+ --secondary-foreground: 240 5.9% 10%;
23
+ --muted: 240 4.8% 95.9%;
24
+ --muted-foreground: 240 3.8% 45%;
25
+ --accent: 240 4.8% 95.9%;
26
+ --accent-foreground: 240 5.9% 10%;
27
+ --destructive: 0 72% 51%;
28
+ --destructive-foreground: 0 0% 98%;
29
+ --border: 240 5.9% 90%;
30
+ --input: 240 5.9% 90%;
31
+ --ring: 240 5.9% 10%;
32
+ --radius: 0.5rem;
33
+ }
34
+ }
35
+
36
+ @layer base {
37
+ * {
38
+ @apply border-border;
39
+ }
40
+
41
+ body {
42
+ @apply bg-background text-foreground font-body;
43
+ }
44
+
45
+ h1,
46
+ h2,
47
+ h3,
48
+ h4,
49
+ h5,
50
+ h6 {
51
+ @apply font-heading;
52
+ }
53
+ }
readme.md ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div align="center">
2
+
3
+ <h1>
4
+ <img src="https://github.com/Zain-ul-din/whatsapp-ai-bot/assets/78583049/d31339cf-b4ae-450e-95b9-53d21e4641a0" width="35" height="35"/>
5
+ WhatsApp Ping 🔔</h1>
6
+ </div>
7
+
8
+ Catching messages on Gmail from clients always has me running late, and checking it over and over can be a real drag. That's why I created `whatsapp ping`! It lets me ping myself on WhatsApp using a webhook.
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+ https://github.com/user-attachments/assets/f46a909d-a0c8-4cd1-9fcb-56dce1e3d40b
18
+
19
+
20
+
21
+ ![Screenshot (2091)](https://github.com/user-attachments/assets/c8738a61-70da-48f9-b1c7-ab2a29c6271e)
22
+
23
+
24
+
25
+
26
+ ### Use Cases
27
+
28
+ few use cases of this project generated by gpt.
29
+
30
+ - 🔔 Instant Notifications: Get WhatsApp alerts for important client emails.
31
+ - 📬 Stay Connected: Easily send quick messages to your team or clients.
32
+ - 📅 Task Reminders: Schedule pings to remind yourself of upcoming tasks.
33
+ - 🎉 Timely Notification: Wish your friend a happy birthday right on time.
34
+ - 👨‍💻 Programmatic Use: Thanks to webhooks, integrate notifications into any app or service.
35
+
36
+ ### Usage
37
+
38
+ clone or fork the repo and install all dependencies using your favorite package manager. In my case, i'm using `pnpm`.
39
+
40
+ ```bash
41
+ pnpm i
42
+ ```
43
+
44
+ **Prepare environment variables:**
45
+
46
+ you need two env variables to make this work
47
+
48
+ ```env
49
+ API_KEY=YOUR_API_KEY
50
+ MONGO_URL=your_url
51
+
52
+ # optional
53
+
54
+ SELF_HOSTED=boolean -> set to true if running in self-hosted environment
55
+ LOCAL=boolean -> for development purposes
56
+ NEXT_MSG_DELAY=number > delays between next message
57
+
58
+ ```
59
+
60
+ We use `api-key` for authentication you can use any key you like to use but we recommend using strong key that is not vulnerable to `brute-force` attack.
61
+
62
+ ```bash
63
+ openssl rand -base64 256
64
+ ```
65
+
66
+ To run this bot in a serverless environment, we use MongoDB to manage the authentication state.
67
+
68
+ You can Get the MongoDB URL for free from their [website](https://www.mongodb.com/).
69
+
70
+ ```js
71
+ mongodb+srv://<user_name>:<password>@cluster0.usggwa4.mongodb.net/?retryWrites=true&w=majority
72
+ ```
73
+
74
+ **start the server using the following command:**
75
+
76
+ ```bash
77
+ pnpm dev # start development server
78
+ OR
79
+ pnpm start # start server in production
80
+ ```
81
+
82
+ After starting the server, it will print QR code in the terminal scan it using your phone to connect whatsapp.
83
+
84
+ ### ⚓ Web Hook Usage
85
+
86
+ Next use web hook as following to send a message,
87
+
88
+ ```c
89
+ curl -X POST localhost:8080/ping \
90
+ -H "Authorization: Bearer $YOUR_API_KEY"
91
+ -d '{
92
+ "message": "hello world",
93
+ "numbers": ["123456789012"]
94
+ }'
95
+ ```
96
+
97
+ using js,
98
+
99
+ ```js
100
+ fetch("http://localhost:8080/ping", {
101
+ headers: {
102
+ Authorization: "Bearer YOUR_KEY",
103
+ "Content-Type": "application/json"
104
+ },
105
+ method: "POST",
106
+ body: JSON.stringify({
107
+ message: "hey",
108
+ image: "optional BASE64 image",
109
+ numbers: ["<country_code_without_plus><...number>"]
110
+ })
111
+ })
112
+ .then((res) => res.text())
113
+ .then((res) => console.log(res));
114
+ ```
115
+
116
+ ### Caveats
117
+
118
+ ```diff
119
+ - If you plan to deploy this project, it currently works only on VPS and not in a serverless environment. This means you will not be able to deploy it to the Vercel or other serverless providers.
120
+ + We just added support for serverless deployment https://github.com/Zain-ul-din/whatsapp-ping/issues/2
121
+ ```
122
+
123
+ <br>
124
+
125
+ ## 😻 Sponsors
126
+
127
+ A big thank you to these people for supporting this project.
128
+
129
+ <table>
130
+ <thead>
131
+ <tr>
132
+ <th>
133
+ <img src="https://avatars.githubusercontent.com/u/97310455?v=4" width="150" height="150" />
134
+ </th>
135
+ <th>
136
+ <img src="https://avatars.githubusercontent.com/u/0?v=4" width="150" height="150"/>
137
+ </th>
138
+ </tr>
139
+ </thead>
140
+ <tbody>
141
+ <tr>
142
+ <td align="center">
143
+ <a target="_blank" href="https://github.com/levitco">Ahtisham Abbas Qureshi</a>
144
+ </td>
145
+ <td align="center">
146
+ <a target="_blank" href="https://www.buymeacoffee.com/zainuldin">YOU?</a>
147
+ </td>
148
+ </tr>
149
+ </tbody>
150
+ </table>
151
+
152
+
153
+
154
+ #### Thanks to These Awesome Projects:
155
+
156
+ - [@whiskeysockets/baileys](https://www.npmjs.com/package/@whiskeysockets/baileys)
157
+ - [mongo-baileys](https://www.npmjs.com/package/mongo-baileys)
158
+
159
+ ---
160
+
161
+ <div align="center">
162
+ <h4 font-weight="bold">This repository is maintained by <a href="https://github.com/Zain-ul-din">Zain-Ul-Din</a></h4>
163
+ <p> Show some ❤️ by starring this awesome repository! </p>
164
+ </div>
165
+
166
+ <div align="center">
167
+ <a href="https://www.buymeacoffee.com/zainuldin" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>
168
+
169
+ </div>
setup.bash ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ npm install
2
+ npm run build
src/baileys/db.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { MongoClient } from "mongodb";
2
+ import { AuthenticationCreds } from "@whiskeysockets/baileys";
3
+
4
+ const dbName = "whatsapp";
5
+ const collectionName = "authState";
6
+
7
+ interface AuthDocument extends Document {
8
+ _id: string;
9
+ creds?: AuthenticationCreds;
10
+ }
11
+
12
+ async function connectDB() {
13
+ const client = new MongoClient(process.env.MONGO_URL || "");
14
+ await client.connect();
15
+ const db = client.db(dbName);
16
+ const collection = db.collection<AuthDocument>(collectionName);
17
+ return { client, collection };
18
+ }
19
+
20
+ export { connectDB };
src/baileys/index.ts ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import makeWASocket, {
2
+ DisconnectReason,
3
+ useMultiFileAuthState
4
+ } from "@whiskeysockets/baileys";
5
+ import { Boom } from "@hapi/boom";
6
+ import * as fs from "fs";
7
+ import { connectDB } from "./db";
8
+ import { useMongoDBAuthState } from "mongo-baileys";
9
+
10
+ async function connectToWhatsApp(onStart?: () => void) {
11
+ const { state, saveCreds } = process.env.MONGO_URL
12
+ ? await useMongoDBAuthState((await connectDB()).collection as any)
13
+ : await useMultiFileAuthState("auth_info_baileys");
14
+
15
+ const sock = makeWASocket({
16
+ // can provide additional config here
17
+ printQRInTerminal: true,
18
+ mobile: false,
19
+ keepAliveIntervalMs: 10000,
20
+ syncFullHistory: false,
21
+ markOnlineOnConnect: true,
22
+ defaultQueryTimeoutMs: undefined,
23
+ auth: state
24
+ });
25
+
26
+ sock.ev.on("creds.update", saveCreds);
27
+
28
+ const setupAuth = new Promise(async (resolve, rej) => {
29
+ sock.ev.on("connection.update", (update) => {
30
+ const { connection, lastDisconnect, qr } = update;
31
+ global.waQrCode = qr || null;
32
+ try {
33
+ if (connection === "close" && lastDisconnect) {
34
+ const statusCode = (lastDisconnect.error as Boom)?.output?.statusCode;
35
+ const shouldReconnect =
36
+ (lastDisconnect.error as Boom)?.output?.statusCode !==
37
+ DisconnectReason.loggedOut;
38
+
39
+ console.log(
40
+ "connection closed due to ",
41
+ lastDisconnect.error,
42
+ ", status code: ",
43
+ statusCode,
44
+ ", reconnecting ",
45
+ shouldReconnect
46
+ );
47
+ // reconnect if not logged out
48
+ if (shouldReconnect) {
49
+ connectToWhatsApp();
50
+ } else {
51
+ // clear credentials
52
+ if (lastDisconnect.error) {
53
+ if (fs.existsSync("./auth_info_baileys")) {
54
+ fs.rmSync("./auth_info_baileys", {
55
+ force: true,
56
+ recursive: true
57
+ });
58
+ }
59
+ }
60
+ }
61
+ } else if (connection === "open") {
62
+ console.log("\n ✔ opened connection \n");
63
+ resolve(null);
64
+ }
65
+ } catch (err) {
66
+ console.log(err);
67
+ }
68
+ });
69
+ });
70
+
71
+ const FIVE_MIN_IN_MS = 1000 * 60 * 5;
72
+
73
+ await Promise.race([
74
+ setupAuth,
75
+ new Promise((_, rej) =>
76
+ setTimeout(
77
+ () => rej("Timeout while setting up connection to whatsapp"),
78
+ FIVE_MIN_IN_MS
79
+ )
80
+ )
81
+ ]);
82
+
83
+ global.waSock = sock;
84
+ }
85
+
86
+ // run in main file
87
+ export { connectToWhatsApp };
src/controllers/home.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Request, Response } from "express";
2
+
3
+ export const homeController = (req: Request, res: Response) => {
4
+ res.render("index");
5
+ };
6
+
7
+ export const getQrCodeController = (req: Request, res: Response) => {
8
+ const apiKey = req.body.api_key;
9
+
10
+ if (apiKey != process.env.API_KEY) {
11
+ res.render("error", { message: "Invalid Credentials" });
12
+ return;
13
+ }
14
+
15
+ if (global.waQrCode) {
16
+ res.render("qr", { qr: global.waQrCode });
17
+ } else {
18
+ res.render("qr", { qr: "" });
19
+ }
20
+ };
src/controllers/index.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ import pingController from "./ping";
2
+ import { getQrCodeController, homeController } from "./home";
3
+
4
+ export { pingController, homeController, getQrCodeController };
src/controllers/ping.ts ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Request, Response } from "express";
2
+ import { delay } from "../lib/delay";
3
+
4
+ const pingController = async (req: Request, res: Response): Promise<void> => {
5
+ if (global.waSock == null) {
6
+ res.status(500).send({ error: "WA is not connected" });
7
+ return;
8
+ }
9
+ const { message, numbers, image } = req.body;
10
+ for (let number of numbers) {
11
+ const id = `${number}@s.whatsapp.net`;
12
+ if (image) {
13
+ const imgToBase64 = Buffer.from(image, "base64");
14
+ await global.waSock.sendMessage(id, {
15
+ image: imgToBase64,
16
+ caption: message
17
+ });
18
+ } else {
19
+ await global.waSock.sendMessage(id, { text: message });
20
+ }
21
+ await delay(100 * Math.random());
22
+ }
23
+ res.status(200).send({ message: "success" });
24
+ };
25
+
26
+ export default pingController;
src/index.ts ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from "express";
2
+ import { homeRoute, pingRoute } from "./routes";
3
+ import bodyParser from "body-parser";
4
+ import dotenv from "dotenv";
5
+ import cors from "cors";
6
+ import { connectToWhatsApp } from "./baileys";
7
+
8
+ const app = express();
9
+ const PORT = process.env.PORT || 8080;
10
+
11
+ dotenv.config();
12
+
13
+ app.set("views", "./views");
14
+ app.set("view engine", "ejs");
15
+
16
+ app.use(cors({ origin: "*" }));
17
+ app.use(express.static("public"));
18
+
19
+ app.use(bodyParser.json({ limit: "20mb" }));
20
+ app.use(express.urlencoded({ extended: true }));
21
+
22
+ app.use(homeRoute);
23
+ app.use(pingRoute);
24
+
25
+ app.use((_, res) => {
26
+ res.status(404).json({ message: "Resource not found" });
27
+ });
28
+
29
+ (async () => {
30
+ await connectToWhatsApp();
31
+ })();
32
+
33
+ app.listen(PORT, () => {
34
+ console.log(`server is listening on localhost:${PORT}`);
35
+ });
src/lib/delay.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ export const delay = (ms: number) => new Promise((r) => setTimeout(r, ms));
src/middlewares/api-key-authentication.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Request, Response, NextFunction } from "express";
2
+
3
+ const authentication = (req: Request, res: Response, next: NextFunction) => {
4
+ const authHeader = req.headers["authorization"];
5
+ if (!authHeader) {
6
+ return res.status(401).json({ message: "Missing authorization header" });
7
+ }
8
+
9
+ const apiKey = authHeader.split("Bearer ").at(-1);
10
+ if (!apiKey) {
11
+ return res.status(401).json({ message: "Invalid authorization format" });
12
+ }
13
+
14
+ if (process.env.API_KEY && apiKey === process.env.API_KEY) {
15
+ next();
16
+ } else {
17
+ res.status(401).json({ message: "Unauthorized" });
18
+ }
19
+ };
20
+
21
+ export default authentication;
src/middlewares/ping-message-validator.ts ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Request, Response, NextFunction } from "express";
2
+ import { z } from "zod";
3
+
4
+ const pingMessageSchema = z.object({
5
+ message: z
6
+ .string()
7
+ .min(1, "Message is required and must be a non-empty string"),
8
+ numbers: z
9
+ .array(
10
+ z
11
+ .string()
12
+ .min(12, "Each number must be at least 12 characters long")
13
+ .regex(
14
+ /^\d{12}$/,
15
+ "Invalid phone number format. Correct example: 123456789012"
16
+ )
17
+ )
18
+ .max(5, "You can provide a maximum of 5 phone numbers"),
19
+ image: z.string().optional()
20
+ });
21
+
22
+ const validatePingMessage = (
23
+ req: Request,
24
+ res: Response,
25
+ next: NextFunction
26
+ ): void => {
27
+ try {
28
+ pingMessageSchema.parse(req.body);
29
+ next();
30
+ } catch (error) {
31
+ if (error instanceof z.ZodError) {
32
+ res.status(400).json({ errors: error.errors });
33
+ } else {
34
+ res.status(500).json({ message: "Internal Server Error" });
35
+ }
36
+ }
37
+ };
38
+
39
+ export default validatePingMessage;
src/routes/home.ts ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import express from "express";
2
+ import { getQrCodeController, homeController } from "../controllers";
3
+
4
+ const router = express.Router();
5
+
6
+ router.get("/", homeController);
7
+ router.post("/", getQrCodeController);
8
+
9
+ export default router;
src/routes/index.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ import pingRoute from "./ping";
2
+ import homeRoute from "./home";
3
+
4
+ export { pingRoute, homeRoute };
src/routes/ping.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from "express";
2
+ import { pingController } from "../controllers";
3
+ import validatePingMessage from "../middlewares/ping-message-validator";
4
+ import { rateLimit } from "express-rate-limit";
5
+ import authentication from "../middlewares/api-key-authentication";
6
+
7
+ const limiter = rateLimit({
8
+ windowMs: 1000,
9
+ limit: 5,
10
+ standardHeaders: "draft-7",
11
+ legacyHeaders: false
12
+ });
13
+
14
+ const route = express.Router();
15
+
16
+ route.use(limiter);
17
+ route.use(authentication);
18
+ route.post("/ping", validatePingMessage, pingController);
19
+
20
+ export default route;
src/test/index.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ import dotenv from "dotenv";
2
+ dotenv.config();
3
+ console.log("✔ All test has been passed");
src/types/AsyncReturnType.d.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ type AsyncReturnType<T extends (...args: any) => Promise<any>> = T extends (
2
+ ...args: any
3
+ ) => Promise<infer R>
4
+ ? R
5
+ : any;
6
+
7
+ export default AsyncReturnType;
src/types/globals.d.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import { WASocket } from "@whiskeysockets/baileys"; // Import the type for your socket
2
+
3
+ declare global {
4
+ var waSock: WASocket | null;
5
+ var waQrCode: string | null;
6
+ }
7
+
8
+ export {};
tailwind.config.js ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ module.exports = {
3
+ content: ["./views/**/*.{html,js,ejs}", "./public/**/*.{html,js}"],
4
+ theme: {
5
+ extend: {
6
+ colors: {
7
+ border: "hsl(var(--border))",
8
+ input: "hsl(var(--input))",
9
+ ring: "hsl(var(--ring))",
10
+ background: "hsl(var(--background))",
11
+ foreground: "hsl(var(--foreground))",
12
+ primary: {
13
+ DEFAULT: "hsl(var(--primary))",
14
+ foreground: "hsl(var(--primary-foreground))"
15
+ },
16
+ secondary: {
17
+ DEFAULT: "hsl(var(--secondary))",
18
+ foreground: "hsl(var(--secondary-foreground))"
19
+ },
20
+ destructive: {
21
+ DEFAULT: "hsl(var(--destructive))",
22
+ foreground: "hsl(var(--destructive-foreground))"
23
+ },
24
+ muted: {
25
+ DEFAULT: "hsl(var(--muted))",
26
+ foreground: "hsl(var(--muted-foreground))"
27
+ },
28
+ accent: {
29
+ DEFAULT: "hsl(var(--accent))",
30
+ foreground: "hsl(var(--accent-foreground))"
31
+ },
32
+ popover: {
33
+ DEFAULT: "hsl(var(--popover))",
34
+ foreground: "hsl(var(--popover-foreground))"
35
+ },
36
+ card: {
37
+ DEFAULT: "hsl(var(--card))",
38
+ foreground: "hsl(var(--card-foreground))"
39
+ }
40
+ },
41
+ borderRadius: {
42
+ xl: `calc(var(--radius) + 4px)`,
43
+ lg: `var(--radius)`,
44
+ md: `calc(var(--radius) - 2px)`,
45
+ sm: `calc(var(--radius) - 4px)`
46
+ },
47
+ keyframes: {
48
+ "accordion-down": {
49
+ from: { height: 0 },
50
+ to: { height: "var(--radix-accordion-content-height)" }
51
+ },
52
+ "accordion-up": {
53
+ from: { height: "var(--radix-accordion-content-height)" },
54
+ to: { height: 0 }
55
+ }
56
+ },
57
+ animation: {
58
+ "accordion-down": "accordion-down 0.2s ease-out",
59
+ "accordion-up": "accordion-up 0.2s ease-out"
60
+ }
61
+ }
62
+ },
63
+ plugins: []
64
+ };
tsconfig.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES6",
4
+ "module": "commonjs",
5
+ "outDir": "./dist",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true
9
+ },
10
+ "include": ["src/**/*.ts"],
11
+ "exclude": ["node_modules"]
12
+ }
views/error.ejs ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Error - Whats App Ping</title>
7
+ <link href="/global.css" rel="stylesheet" />
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ </head>
10
+ <body>
11
+ <main class="h-screen flex flex-col items-center justify-center">
12
+ <div class="border rounded-b-lg shadow-sm w-full max-w-md p-6">
13
+ <h1 class="text-red-400 text-lg font-medium text-center py-12">
14
+ Error: <%= message %>
15
+ </h1>
16
+ </div>
17
+ </main>
18
+ </body>
19
+ </html>
views/index.ejs ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Whats App Ping</title>
7
+ <link href="/global.css" rel="stylesheet" />
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ </head>
10
+ <body>
11
+ <main class="h-screen flex flex-col items-center justify-center">
12
+ <div
13
+ class="charming-header border rounded-b-lg shadow-sm w-full max-w-md p-6"
14
+ >
15
+ <h1 class="text-2xl font-semibold text-center mb-4">
16
+ 🔔 Whats App Ping
17
+ </h1>
18
+ <form method="post" action="/">
19
+ <label class="flex flex-col mb-4">
20
+ <span class="text-lg">API Key</span>
21
+ <input
22
+ type="text"
23
+ placeholder="Enter your API key"
24
+ class="mt-2 p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
25
+ name="api_key"
26
+ required
27
+ />
28
+ </label>
29
+ <button
30
+ type="submit"
31
+ class="w-full bg-blue-600 text-white py-2 rounded-md hover:bg-blue-700 transition duration-200"
32
+ >
33
+ Submit
34
+ </button>
35
+ </form>
36
+
37
+ <div class="mt-4 flex">
38
+ <a
39
+ class="ml-auto flex cursor-pointer items-center text-gray-800 hover:text-blue-600 transition duration-200"
40
+ href="https://github.com/Zain-ul-din/whatsapp-ping"
41
+ target="_blank"
42
+ >
43
+ <!-- GitHub SVG Icon -->
44
+ <svg
45
+ xmlns="http://www.w3.org/2000/svg"
46
+ width="20"
47
+ height="20"
48
+ viewBox="0 0 24 24"
49
+ fill="none"
50
+ stroke="currentColor"
51
+ stroke-width="2"
52
+ stroke-linecap="round"
53
+ stroke-linejoin="round"
54
+ class="lucide lucide-github mr-1"
55
+ >
56
+ <path
57
+ d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"
58
+ />
59
+ <path d="M9 18c-4.51 2-5-2-7-2" />
60
+ </svg>
61
+ View On GitHub
62
+ </a>
63
+ </div>
64
+ </div>
65
+ </main>
66
+ </body>
67
+ </html>
views/qr.ejs ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Scan Qr - Whats App Ping</title>
7
+ <link href="/global.css" rel="stylesheet" />
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script src="https://cdn.rawgit.com/davidshimjs/qrcodejs/gh-pages/qrcode.min.js"></script>
10
+ </head>
11
+ <body>
12
+ <main class="h-screen flex flex-col items-center justify-center">
13
+ <div
14
+ class="charming-header border rounded-b-lg flex flex-col justify-center items-center shadow-sm w-full max-w-md p-6"
15
+ >
16
+ <input id="qr_code" value="<%= qr %>" hidden />
17
+ <div id="qrcode" class="w-[302px] h-[302px] my-auto mx-auto"></div>
18
+
19
+ <% if (qr.length == 0) { %>
20
+ <p class="text-blue-400 text-balance font-medium border-t pt-2">
21
+ QR code generation is still in progress; please wait a minute and try
22
+ again.
23
+ </p>
24
+ <% } else { %>
25
+ <p class="text-green-400 text-balance font-medium border-t pt-2">
26
+ Use your phone to scan the QR code.
27
+ </p>
28
+ <% } %>
29
+ </div>
30
+ </main>
31
+
32
+ <script>
33
+ var qrcode = new QRCode("qrcode");
34
+
35
+ function makeCode() {
36
+ var elText = document.getElementById("qr_code");
37
+
38
+ if (!elText.value) return;
39
+
40
+ qrcode.makeCode(elText.value);
41
+ }
42
+
43
+ makeCode();
44
+ </script>
45
+ </body>
46
+ </html>