├── .gitignore ├── CHANGELOG.md ├── README.md ├── backend ├── app.mjs └── tools │ └── scraper.js ├── index.js ├── package.json └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | /.wwebjs_auth 2 | /node_modules 3 | /package-lock.json 4 | /*.csv 5 | /.env -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v1.0.1 (2022-12-10) 2 | 3 | ### Highlights 4 | - Added Job Scraping from Linkedin 5 | 6 | 7 | ## v1.1.0 (2023-01-11) 8 | 9 | ### Highlights 10 | #### Removed: 11 | - Job Scraping from Linkedin 12 | 13 | #### Added 14 | + Connection to frontend 15 | + Resolved issue with whatsapp-web.js 16 | + [View Resolution](https://github.com/pedroslopez/whatsapp-web.js/issues/1913#issuecomment-1377194341) 17 | + Message sending from frontend 18 | + Message scheduling from frontend -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WhatScheduler by **DreadedHippy** 2 | ![GitHub last commit](https://img.shields.io/github/last-commit/DreadedHippy/WhatScheduler_backend?color=%23ffbb00&logo=Github&logoColor=%23ffbb00&style=for-the-badge) 3 | ![GitHub package.json version](https://img.shields.io/github/package-json/v/DreadedHippy/WhatScheduler_backend?color=%23ffbb00&logo=Github&logoColor=%23ffdd00&style=for-the-badge) 4 | 5 | ##### This is a simple NodeJS program to automate sending and replying to whatsapp messages using *node-cron*,*qrcode-terminal* and *whatsapp-web* NPM packages. I hope to implement scheduled job-postings using Puppeteer. Buckle up for that 😉 6 | 7 | #### My motivation behind this project: 8 | Over the long period of time I've been using whatsapp (3+ Years), there were moments when I forgot 9 | to send messages such as birthday messages or messages requesting permission from someone and I thought: I've been coding since 2019, why don't I just *automate* it? Here we are 😅 10 | 11 | 12 | #### [ChangeLogs](CHANGELOG.md) 13 | 14 | #### How to Install and run: 15 | *Disclaimer: This was made with Node.js v18.12.1* 16 | - Clone the repository 17 | - Run these commands in the terminal: `npm i` and then `npm run start` 18 | - You're all set 19 | 20 | #### Using the project: 21 | When you authenticate for the first time, you are required to login by scanning the QR code using *Whatsapp* installed on your phone. 22 | ***Note*: This is the default configuration, you can check out more authentication methods with the *whatsapp-web.js* package at [Whatsapp-Web website]('https://wwebjs.dev/guide/')** 23 | 24 | After the first authentication, a folder named '.wwebjs_auth' will be created in the pwd, which will store session information. Basically, you only have to login *once*. 25 | 26 | By default, while the program is running, it sends the message `Hello from the other side!` every 30 secondss. This was done using with the help of *node-cron* package, 27 | **More information at [node-cron repo page]('https://github.com/node-cron/node-cron')** 28 | 29 | 30 | #### Errors and solutions: 31 | | Error | Possible cause | Solution | 32 | |----|------|--------| 33 | | Could not find expected browser (chrome) locally. Run `npm install` to download the correct Chromium revision (982053).| The program is unable to detect a browser usable by the puppeteer package dependency of whatsapp-web.js package| Run the command `node ./node_modules/whatsapp-web.js/node_modules/puppeteer/install.js`| 34 | 35 | 36 | #### License: 37 | MIT License 38 | 39 | Copyright (c) 2022 Onotioese Izormen 40 | 41 | Permission is hereby granted, free of charge, to any person obtaining a copy 42 | of this software and associated documentation files (the "Software"), to deal 43 | in the Software without restriction, including without limitation the rights 44 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 45 | copies of the Software, and to permit persons to whom the Software is 46 | furnished to do so, subject to the following conditions: 47 | 48 | The above copyright notice and this permission notice shall be included in all 49 | copies or substantial portions of the Software. 50 | 51 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 52 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 53 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 54 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 55 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 56 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 57 | SOFTWARE. 58 | -------------------------------------------------------------------------------- /backend/app.mjs: -------------------------------------------------------------------------------- 1 | import qrcode from 'qrcode-terminal' 2 | import wweb from 'whatsapp-web.js'; 3 | const { Client, LocalAuth } = wweb 4 | import schedule from 'node-schedule'; 5 | import nodeCron from 'node-cron'; 6 | // import scraper from './tools/scraper.js'; 7 | import dotenv from 'dotenv' 8 | dotenv.config(); 9 | import express from 'express'; 10 | import cors from 'cors' 11 | const app = express(); 12 | import bodyParser from 'body-parser' 13 | // const index = require("../index") 14 | let myGroup = {} 15 | let isReady = false; 16 | 17 | 18 | const mainGroupName = process.env.MAIN_GROUP_NAME 19 | const testGroupName = 'Whatsapp' 20 | 21 | 22 | 23 | // Use the saved values 24 | const client = new Client({ 25 | authStrategy: new LocalAuth() 26 | }); 27 | 28 | client.on('qr', (qr) => { 29 | qrcode.generate(qr, {small: true}); 30 | }); 31 | 32 | client.on('ready', () => { 33 | console.log('Client is ready!'); 34 | isReady = true 35 | client.getChats().then( chats => { 36 | // console.log(chats) 37 | myGroup = chats.find((chat) => chat.name === testGroupName); 38 | client.sendMessage( 39 | myGroup.id._serialized, 'Hello from the other side!' 40 | ) 41 | 42 | let position = 0 43 | }) 44 | }); 45 | 46 | client.on('message', message => { 47 | console.log(message.body); 48 | }); 49 | 50 | client.initialize() 51 | 52 | 53 | 54 | app.use(cors()); 55 | app.use(bodyParser.json()); 56 | app.use(bodyParser.urlencoded({ extended: false})); 57 | 58 | app.get('/', (req, res) => { 59 | res.send('Hello World'); 60 | }); 61 | 62 | app.post('/simplesend', (req, res, next) => { 63 | const message = req.body 64 | const date = new Date 65 | 66 | function sendMessage(){ 67 | client.sendMessage( 68 | myGroup.id._serialized, message.content 69 | ).then(() => { 70 | console.log('Message sent!', 'Is instant?', message.isInstant) 71 | res.status(200).json({ 72 | message: 'Sent!', 73 | }) 74 | }).catch(err => { 75 | console.log('Message not sent!',err) 76 | res.status(400).json({ 77 | message: 'Something went wrong...', 78 | }) 79 | }) 80 | } 81 | 82 | if(!message.isInstant){ 83 | const job = schedule.scheduleJob(message.date, function(){ 84 | client.sendMessage( 85 | myGroup.id._serialized, message.content 86 | ) 87 | }) 88 | console.log('Scheduled message') 89 | res.status(200).json({ 90 | message: 'Message Scheduled!', 91 | }) 92 | return; 93 | } 94 | 95 | sendMessage() 96 | 97 | }) 98 | 99 | function getState(){ 100 | // console.log(isReady) 101 | return isReady 102 | } 103 | 104 | export default {getState, app}; -------------------------------------------------------------------------------- /backend/tools/scraper.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const cheerio = require('cheerio'); 3 | const ObjectsToCsv = require('objects-to-csv'); 4 | 5 | linkedinJobs = []; 6 | let pageNumber = 0; 7 | 8 | function getInfo() { 9 | return new Promise((resolve, reject) => { 10 | setTimeout(() => { 11 | let url = `https://www.linkedin.com/jobs/search?keywords=Web%20Development&location=Lagos%2C%20Lagos%20State%2C%20Nigeria&geoId=105693087&trk=public_jobs_jobs-search-bar_search-submit&position=1&pageNum=0&start=${pageNumber}`; 12 | axios(url).then(response => { 13 | const html = response.data; 14 | const $ = cheerio.load(html); 15 | const jobs = $('li') 16 | jobs.each((index, element) => { 17 | const jobTitle = $(element).find('h3.base-search-card__title').text().trim() 18 | const company = $(element).find('h4.base-search-card__subtitle').text().trim() 19 | const location = $(element).find('span.job-search-card__location').text().trim() 20 | const link = $(element).find('a.base-card__full-link').attr('href') 21 | 22 | linkedinJobs.push({ 23 | 'Title': jobTitle, 24 | 'Company': company, 25 | 'Location': location, 26 | 'Link': link, 27 | }) 28 | 29 | if(!jobTitle || !company || !location || !link){ 30 | linkedinJobs.pop() 31 | } 32 | resolve(linkedinJobs) 33 | }); 34 | // const csv = new ObjectsToCsv(linkedinJobs) 35 | // csv.toDisk('./linkedInJobs.csv', { append: true }) 36 | }) 37 | .catch(console.error) 38 | 39 | pageNumber+= 25; 40 | if(pageNumber < 50){ 41 | getInfo() 42 | } 43 | }, 5000); 44 | }) 45 | } 46 | 47 | 48 | let jobs = new Promise(function showResult(resolve, reject){ 49 | getInfo().then((result) =>{ 50 | resolve(result) 51 | }).catch(err => console.log(err)) 52 | }) 53 | 54 | 55 | 56 | module.exports = {jobs} 57 | 58 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const qrcode = require('qrcode-terminal') 2 | const { Client, LocalAuth } = require('whatsapp-web.js'); 3 | const nodeCron = require('node-cron'); 4 | const scraper = require('./backend/tools/scraper'); 5 | require('dotenv').config() 6 | 7 | 8 | const mainGroupName = process.env.MAIN_GROUP_NAME 9 | const testGroupName = 'Whatsapp' 10 | 11 | // Use the saved values 12 | const client = new Client({ 13 | authStrategy: new LocalAuth() 14 | }); 15 | 16 | client.on('qr', (qr) => { 17 | qrcode.generate(qr, {small: true}); 18 | }); 19 | 20 | client.on('ready', () => { 21 | console.log('Client is ready!'); 22 | isReady = true 23 | client.getChats().then( chats => { 24 | const myGroup = chats.find((chat) => chat.name === testGroupName); 25 | client.sendMessage( 26 | myGroup.id._serialized, 'Hello from the other side!' 27 | ) 28 | 29 | let position = 0 30 | 31 | scraper.jobs.then((result) => { 32 | const job = nodeCron.schedule("*/30 * * * * *", () => { 33 | let title = result[position].Title 34 | let company = result[position].Company 35 | let location = result[position].Location 36 | let link = result[position].Link 37 | 38 | if(position > 50){ 39 | return client.sendMessage( 40 | myGroup.id._serialized, `Hello, this is a scheduled message(position ${position})!` 41 | ) 42 | } 43 | 44 | client.sendMessage( 45 | myGroup.id._serialized, ` 💡\`\`\`LINKEDIN JOB ALERTS\`\`\`💡 46 | \n *Title*: ${title} 47 | \n *Company*: ${company} 48 | \n *Location*: ${location} 49 | \n *Link*: ${link} 50 | ` 51 | ) 52 | position++ 53 | }); 54 | }).catch( err => {console.log(err)}) 55 | 56 | }) 57 | }); 58 | 59 | client.on('message', message => { 60 | console.log(message.body); 61 | }); 62 | 63 | function sendMessage(message){ 64 | client.sendMessage( 65 | myGroup.id._serialized, ` 💡\`\`\`LINKEDIN JOB ALERTS\`\`\`💡 66 | \n *Title*: ${title} 67 | \n *Company*: ${company} 68 | \n *Location*: ${location} 69 | \n *Link*: ${link} 70 | ` 71 | ) 72 | } 73 | 74 | client.initialize(); 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "dependencies": { 4 | "axios": "^1.2.1", 5 | "cheerio": "^1.0.0-rc.12", 6 | "cors": "^2.8.5", 7 | "dotenv": "^16.0.3", 8 | "esm": "^3.2.25", 9 | "express": "^4.18.2", 10 | "http": "^0.0.1-security", 11 | "node-cron": "^3.0.2", 12 | "node-schedule": "^2.1.0", 13 | "objects-to-csv": "^1.3.6", 14 | "puppeteer": "^19.3.0", 15 | "qrcode-terminal": "^0.12.0", 16 | "whatsapp-web.js": "^1.18.4" 17 | }, 18 | "name": "whatscheduler_backend", 19 | "version": "1.1.0", 20 | "main": "server.js", 21 | "scripts": { 22 | "test": "echo \"Error: no test specified\" && exit 1", 23 | "start": "node index.js" 24 | }, 25 | "keywords": [], 26 | "author": "", 27 | "license": "ISC", 28 | "description": "" 29 | } 30 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | import http from 'http' 2 | import main from './backend/app.mjs'; 3 | // import debug from ('debug')('node-angular') 4 | const server = http.createServer(main.app); 5 | const port = 3000; 6 | 7 | main.app.set('port', port); 8 | let flag = false 9 | 10 | function waitFor(conditionFunction) { 11 | const poll = resolve => { 12 | if(conditionFunction()) resolve(); 13 | else { 14 | setTimeout(_ => poll(resolve), 4000) 15 | flag = main.getState() 16 | }; 17 | } 18 | 19 | return new Promise(poll); 20 | } 21 | 22 | waitFor(_ => flag === true) 23 | .then(_ => { 24 | server.listen(process.env.PORT || port); 25 | console.log('Listening for messages') 26 | }) 27 | ; 28 | --------------------------------------------------------------------------------