[
  {
    "path": ".gitignore",
    "content": "/node_modules\n.env"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\"><img src=\"https://cdn.shopify.com/s/files/1/2579/7072/articles/puppeteer-cover_1200x630.png?v=1521812467\" align=\"center\" width=\"250\"></p>\n<h2 align=\"center\">Google Meet Scheduler</h2>\n<p align=\"center\"><b>Join's meet link for you 😴</b></p>\n\nBot for scheduling and entering google meet sessions automatically.\n\n##### New Features\n\n- Saving RAM: Using same browser for all meet pages\n- Multiple meet logic added\n\n### Installation Guide\n\n1. Open terminal on your PC\n2. Clone the repo `git clone https://github.com/AmanRaj1608/Google-Meet-Scheduler.git`\n3. Go inside the project directory\n4. Rename `.env-example` file to `.env` and replace your email and password there\n5. Install dependencies `npm install`\n6. Start the application `npm start`\n\nNow the project has started on `localhost:3000`\n\n### Usage Guide\n\nNow when you visit the page you will see things to add\n\n- Meet Link\n- Start Time\n- End time\n\nThen submit and do what you wanted to, it will log in and join meet for you.\n\nYou can add more links there to add it to the queue.\n\n### Requirements\n\n- [Node.js](https://nodejs.org/en/download/) should be installed\n- [Google Chrome](https://www.google.com/intl/en_in/chrome/) with version 70+\n- Works only on windows (see [Issue #2](https://github.com/AmanRaj1608/Google-Meet-Scheduler/issues/8) for more info)\n\n### If you want to see the whole process\n\nOn line `16` of `server.js` file you can see a variable name head=false;\n\nIf you want to see bot automatically opening the page and filling login values and joining meet link then you can set the headless as flase.\n\nBut while for deployment we need headless as true.\n\n### Deployment\n\nIf you want to deploy your instance of app you need it to set it up properly.\nThe main problem on deployment is that after deployment it will be hosted on different IP and when bot tries to sign in Google will ask to login again with `one time password`.\n\nMore details here [Issue #1](https://github.com/AmanRaj1608/Google-Meet-Scheduler/issues/1)\n\nI recommend using [digitalocean](https://m.do.co/c/92e13bfef66f)\n\n### Todo\n\nYou can however deploy it by creating an API that will ask for OTP and while sign-in you give that info to the server.\nThis can be implemented as a new branch especially for deployment purpose\n\n### How it works\n\nProject is made using [Puppeteer](https://developers.google.com/web/tools/puppeteer) which is a Node library which provides a high-level API to control headless Chrome or Chromium. We open a chromium app on server where we can add create open tabs see browser versions and everything.\n\nSo here we are using `puppeteer-extra` and `puppeteer-extra-plugin-stealth` which helps in creating an instance of chrome where google don't able to detect that it is created by puppeteer. So using this plugin we can login into google without filling capcha.\n\n---\n\n<p align=\"center\"> Made with ❤️ by <a href=\"https://amanraj.dev/\">Aman Raj</a></p>\n```\n"
  },
  {
    "path": "google-meet.js",
    "content": "// const puppeteer = require('puppeteer');\nconst puppeteer = require('puppeteer-extra')\nconst StealthPlugin = require('puppeteer-extra-plugin-stealth')\n\npuppeteer.use(StealthPlugin())\n\nclass GoogleMeet {\n\tconstructor(email, pass, head, strict) {\n\t\tthis.email = email;\n\t\tthis.pass = pass;\n\t\tthis.head = head;\n\t\tthis.strict = strict;\n\t\tthis.browser;\n\t\tthis.page = {};\n\t\tthis.browserIsActive = false;\n\t}\n\n\tasync createBrowser() {\n\t\tthis.browser = await puppeteer.launch({\n\t\t\theadless: this.head,\n\t\t\targs: [\n\t\t\t\t'--no-sandbox',\n\t\t\t\t'--disable-setuid-sandbox',\n\t\t\t\t'--use-fake-ui-for-media-stream',\n\t\t\t\t'--disable-audio-output'\n\t\t\t],\n\t\t});\n\t}\n\n\tasync accountLogin(newPage) {\n\t\tawait newPage.goto('https://accounts.google.com/signin/v2/identifier?flowName=GlifWebSignIn&flowEntry=ServiceLogin');\n\t\t// Login Start\n\t\tawait newPage.type(\"input#identifierId\", this.email, {\n\t\t\tdelay: 0\n\t\t})\n\t\tawait newPage.click(\"div#identifierNext\");\n\n\t\tawait newPage.waitForTimeout(7000);\n\n\t\tawait newPage.type(\"input[name=password]\", this.pass, {\n\t\t\tdelay: 0\n\t\t})\n\t\tawait newPage.click(\"div#passwordNext\");\n\t\tawait newPage.waitForTimeout(5000);\n\t}\n\n\tasync schedule(url, meetId) {\n\t\ttry {\n\t\t\t// Open new browser only if not req\n\t\t\tif (!this.browserIsActive) {\n\t\t\t\tawait this.createBrowser();\n\t\t\t}\n\t\t\t// open new tab on browser\n\t\t\tconst newPage = await this.browser.newPage();\n\n\t\t\tif (!this.browserIsActive) {\n\t\t\t\tawait this.accountLogin(newPage);\n\t\t\t}\n\n\t\t\t// open meet in tab\n\t\t\tawait newPage.goto(url);\n\t\t\tconsole.log(\"inside meet page\");\n\t\t\tawait newPage.waitForTimeout(7000);\n\t\t\ttry {\n\t\t\t\tawait newPage.click(\"div.IYwVEf.HotEze.uB7U9e.nAZzG\");\n\t\t\t} catch (e) {\n\t\t\t\tconsole.log(\"\\naudio seems to disabled already\", e.message);\n\t\t\t}\n\t\t\tawait newPage.waitForTimeout(1000);\n\t\t\ttry {\n\t\t\t\tawait newPage.click(\"div.IYwVEf.HotEze.nAZzG\");\n\t\t\t} catch (e) {\n\t\t\t\tconsole.log(\"\\nvideo seems to be disabled already\", e.message);\n\t\t\t}\n\n\t\t\t// sanity check (connect only if both audio and video are muted) :P\n\t\t\tif (this.strict) {\n\t\t\t\tlet audio = await newPage.evaluate('document.querySelectorAll(\"div.sUZ4id\")[0].children[0].getAttribute(\"data-is-muted\")')\n\t\t\t\tlet video = await newPage.evaluate('document.querySelectorAll(\"div.sUZ4id\")[1].children[0].getAttribute(\"data-is-muted\")')\n\n\t\t\t\tif (audio === \"false\" || video === \"false\") {\n\t\t\t\t\tconsole.log(\"Not joining meeting. We couldn't disable either audio or video from the device.\\nYou may try again.\")\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconsole.log(\"all set!!\")\n\t\t\t}\n\n\t\t\tawait newPage.waitForTimeout(1000);\n\t\t\tconsole.log('clicking on join');\n\t\t\tawait newPage.click(\"span.NPEfkd.RveJvd.snByac\");\n\n\t\t\tthis.page[meetId] = newPage;\n\t\t\tthis.browserIsActive = true;\n\t\t\tconsole.log(\"Successfully joined/Sent join request\");\n\t\t\treturn true;\n\t\t}\n\t\tcatch (err) {\n\t\t\tconsole.log(err);\n\t\t\tthis.browserIsActive = false;\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tasync closeTab(ind) {\n\t\tawait this.page[ind].close();\n\t}\n\n\tasync closeBrowser() {\n\t\tawait this.browser.close();\n\t\tthis.browserIsActive = false;\n\t}\n\n\tgetBrowserIsActive() {\n\t\treturn this.browserIsActive;\n\t}\n}\n\nmodule.exports = GoogleMeet;\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"attendance\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"app.js\",\n  \"scripts\": {\n    \"start\": \"node server.js\",\n    \"dev\": \"nodemon server.js\"\n  },\n  \"author\": \"Aman Raj\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"body-parser\": \"^1.19.0\",\n    \"dotenv\": \"^10.0.0\",\n    \"ejs\": \"^3.1.6\",\n    \"express\": \"^4.17.1\",\n    \"nodemon\": \"^2.0.12\",\n    \"puppeteer\": \"^10.1.0\",\n    \"puppeteer-extra\": \"^3.1.18\",\n    \"puppeteer-extra-plugin-stealth\": \"^2.7.8\"\n  }\n}"
  },
  {
    "path": "server.js",
    "content": "require('dotenv').config()\nconst express = require('express');\nconst path = require('path');\nconst GoogleMeet = require('./google-meet');\n\nconst app = express();\napp.use(express.urlencoded({ extended: true }));\napp.use(express.static(path.join(__dirname, 'public')));\napp.set('views', path.join(__dirname, 'views'));\napp.set('view engine', 'ejs');\n\n// Values\nlet email = process.env.EMAIL;\nlet password = process.env.PASSWORD;\n\nlet head = false;\nlet strict = false;\n\nmeetObj = new GoogleMeet(email, password, head, strict);\n\n// cache store\n// can be moved to db\nlet url = {};\nlet ind = 0;\n\napp.get('/', (req, res) => {\n\tres.render('index', { url, email, password })\n});\napp.post('/postlink', (req, res) => {\n\tind++;\n\turl[ind] = {};\n\turl[ind].id = ind;\n\turl[ind].url = req.body.url;\n\turl[ind].startTime = Date.parse(req.body.startDate);\n\turl[ind].endTime = Date.parse(req.body.endDate);\n\tres.redirect(\"/\");\n});\n\nconst listener = app.listen(3000 || process.env.PORT, () => {\n\n\tsetInterval(() => {\n\t\t// when no scheduled links\n\t\tif (Object.keys(url).length === 0) {\n\t\t\tif (meetObj.getBrowserIsActive())\n\t\t\t\tmeetObj.closeBrowser();\n\t\t\telse\n\t\t\t\treturn;\n\t\t}\n\t\t// check meet array every 10sec\n\t\tfor (x in url) {\n\t\t\t// join period\n\t\t\tif (url[x].startTime < Date.now() && url[x].endTime > Date.now()) {\n\t\t\t\tconsole.log(`Request for joining meet ${url[x].url}`);\n\t\t\t\tmeetObj.schedule(url[x].url, url[x].id);\n\t\t\t\t// hack: set above endTime so that it will not come for \n\t\t\t\t// \t\t\t\tsame meetId in this block\n\t\t\t\turl[x].startTime = url[x].endTime + 2000; \n\t\t\t}\n\t\t\t// leave period\n\t\t\tif (url[x].endTime < Date.now()) {\n\t\t\t\tconsole.log(`Request for leaving meet ${url[x].url}`);\n\t\t\t\tmeetObj.closeTab(url[x].id);\n\t\t\t\tdelete url[x];\n\t\t\t}\n\t\t}\n\t}, 10000)\n\n\tconsole.log(`App listening on port ${listener.address().port}`)\n})"
  },
  {
    "path": "views/index.ejs",
    "content": "<!doctype html>\n<html lang=\"en\">\n\n<head>\n\t<!-- Required meta tags -->\n\t<meta charset=\"utf-8\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n\n\t<!-- Bootstrap CSS -->\n\t<link rel=\"stylesheet\" href=\"https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css\"\n\t\tintegrity=\"sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO\" crossorigin=\"anonymous\">\n\n\t<title>Google Meet Scheduler</title>\n</head>\n\n<body>\n\t<nav class=\"navbar navbar-dark bg-primary\">\n\t\t<a class=\"navbar-brand\" href=\"#\">\n\t\t\t<img src=\"https://i.ytimg.com/vi/yNxPVj0hejg/hqdefault.jpg\" width=\"60\" height=\"40\"\n\t\t\t\tclass=\"d-inline-block align-top\" alt=\"\">\n\t\t\tGoogle Meet Scheduler\n\t\t</a>\n\t</nav>\n\t<div class=\"container\" style=\"text-align: center; max-width: 500px;\">\n\t\t<h1 class=\"display-4\" style=\"font-size: 40px;\">Schedule Google Meet</h1>\n\t\t<div>\n\t\t\t<% if(!password || 0===password.length || !email || 0===email.length) { %>\n\t\t\t\t<p style=\" color : red\"> Password or Email not set as environment variables or in <mark>.env</mark>\n\t\t\t\t\tfile. Refer <a href=https://github.com/AmanRaj1608/Google-Meet-Scheduler#readme>here</a> for\n\t\t\t\t\tinstructions.</p>\n\t\t\t\t<% } %>\n\t\t</div>\n\t\t<br>\n\t\t<form style=\"width: 100%;\" method=\"POST\" action=\"/postlink\" enctype=\"application/x-www-form-urlencoded\">\n\t\t\t<div class=\"form-group\">\n\t\t\t\t<label for=\"url\">Meet Url</label>\n\t\t\t\t<input name=\"url\" type=\"text\" class=\"form-control\" id=\"url\" placeholder=\"https://meet.google.com/kgh-hwtg-vus\"\n\t\t\t\t\trequired>\n\t\t\t</div>\n\t\t\t<div class=\"form-group\">\n\t\t\t\t<label for=\"startDate\">Class Start Time</label>\n\t\t\t\t<input name=\"startDate\" type=\"datetime-local\" class=\"form-control\" id=\"startDate\" required>\n\t\t\t</div>\n\t\t\t<div class=\"form-group\">\n\t\t\t\t<label for=\"endDate\">Class End Time</label>\n\t\t\t\t<input name=\"endDate\" type=\"datetime-local\" class=\"form-control\" id=\"endDate\" required>\n\t\t\t</div>\n\t\t\t<div class=\"form-group\">\n\t\t\t\t<input type=\"submit\" class=\"btn btn-primary\" value=\"Schedule\" id=\"submit\" style=\"width: 100px\">\n\t\t\t</div>\n\t\t</form>\n\t\t<br>\n\t</div>\n\n\t<div class=\"container\" style=\"text-align: center; max-width: 850px;\">\n\t\t<h1 class=\"display-4\" style=\"font-size: 40px;\">Scheduled</h1>\n\t\t<div class=\"card-columns\">\n\t\t\t<% for(i in url) {%>\n\t\t\t\t<div class=\"card text-white bg-dark mb-3\" style=\"max-width: 18rem;\">\n\t\t\t\t\t<div class=\"card-header\">Id: <%= i %>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"card-body\">\n\t\t\t\t\t\t<p class=\"card-title\">meetUrl: <br>\n\t\t\t\t\t\t\t<%= url[i].url %>\n\t\t\t\t\t\t</p>\n\t\t\t\t\t\t<p class=\"card-title\">startTime: <br>\n\t\t\t\t\t\t\t<%= new Date(url[i].startTime).toLocaleString().replace(',','') %>\n\t\t\t\t\t\t</p>\n\t\t\t\t\t\t<p class=\"card-title\">endTime: <br>\n\t\t\t\t\t\t\t<%= new Date(url[i].endTime).toLocaleString().replace(',','') %>\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<% } %>\n\t\t</div>\n\t</div>\n\n\n\t<!-- Optional JavaScript -->\n\t<!-- jQuery first, then Popper.js, then Bootstrap JS -->\n\t<script src=\"https://code.jquery.com/jquery-3.3.1.slim.min.js\"\n\t\tintegrity=\"sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo\"\n\t\tcrossorigin=\"anonymous\"></script>\n\t<script src=\"https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js\"\n\t\tintegrity=\"sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49\"\n\t\tcrossorigin=\"anonymous\"></script>\n\t<script src=\"https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js\"\n\t\tintegrity=\"sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy\"\n\t\tcrossorigin=\"anonymous\"></script>\n</body>\n\n</html>"
  }
]