├── .gitignore ├── package.json ├── functions ├── getWebsite.js ├── formatMessage.js ├── getCookie.js └── extractInformation.js ├── express.js ├── ReadMe.md └── app.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Node.js dependencies 2 | node_modules/ 3 | 4 | # Logs 5 | logs/ 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | # Environment variables 12 | .env 13 | 14 | # OS generated files 15 | .DS_Store 16 | Thumbs.db 17 | 18 | # Build output 19 | dist/ 20 | build/ 21 | 22 | # IDE specific files 23 | .vscode/ 24 | .idea/ 25 | *.swp 26 | *.swo 27 | *.iml 28 | 29 | example.txt -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "attendancebot", 3 | "version": "1.0.0", 4 | "main": "app.js", 5 | "scripts": { 6 | "start": "node express.js", 7 | "tunnel": "npx localtunnel --port 3000" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "description": "", 13 | "dependencies": { 14 | "axios": "^1.9.0", 15 | "cheerio": "^1.0.0", 16 | "dotenv": "^16.5.0", 17 | "express": "^5.1.0", 18 | "qs": "^6.14.0", 19 | "twilio": "^5.6.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /functions/getWebsite.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | 3 | async function getWebsite(cookie) { 4 | try { 5 | const url = 6 | "https://crce-students.contineo.in/parents/index.php?option=com_studentdashboard&controller=studentdashboard&task=dashboard"; 7 | 8 | const headers = { 9 | Cookie: cookie, // Set the cookie header from the function parameter 10 | }; 11 | 12 | const response = await axios.get(url, { headers: headers }); 13 | // console.log(response.data) 14 | return response.data; 15 | } catch (e) { 16 | console.log(`error in getting website ${e.message}`); 17 | return {}; 18 | } 19 | } 20 | 21 | module.exports = { getWebsite }; 22 | -------------------------------------------------------------------------------- /express.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const { MessagingResponse } = require("twilio").twiml; 3 | const { main } = require("./app"); 4 | const PORT = 3000; 5 | 6 | const app = express(); 7 | app.use(express.urlencoded({ extended: false })); 8 | 9 | app.post("/whatsaap", async (req, res) => { 10 | const twiml = new MessagingResponse(); 11 | try { 12 | const message = req.body.Body.trim().toLowerCase(); 13 | // console.log(message); 14 | 15 | if (message.includes("attendance")) { 16 | const message = await main(); 17 | // console.log(message); 18 | twiml.message(message); 19 | } else { 20 | twiml.message("Type < Attendance > to get attendance!"); 21 | } 22 | } catch (e) { 23 | console.log(`error in post method: ${e.message}`); 24 | twiml.message("there was an error"); 25 | } 26 | 27 | res.type("text/xml").send(twiml.toString()); 28 | console.log("reponse sent!") 29 | }); 30 | 31 | app.listen(PORT, "0.0.0.0", () => 32 | console.log(`server running on port ${PORT}`) 33 | ); 34 | -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | # Attendance Fetching Bot for Fr. Conceicao Rodrigues College of Engineering 2 | 3 | ## Overview 4 | 5 | This project is an automated attendance fetching bot designed specifically for Fr. Conceicao Rodrigues College of Engineering students. It allows users to query their attendance information through a WhatsApp interface by sending simple messages. 6 | 7 | The bot scrapes attendance data from the college’s portal and responds with up-to-date attendance details. 8 | 9 | --- 10 | 11 | ## Features 12 | 13 | - Fetches attendance data automatically by scraping the official portal. 14 | - WhatsApp integration for easy, on-the-go attendance checking. 15 | - Simple command interface (e.g., type `attendance` to receive your current attendance). 16 | - Handles HTML parsing and data extraction reliably using `cheerio`. 17 | 18 | --- 19 | 20 | ## Setup and Installation Local 21 | 22 | 1. *Clone the repository:* 23 | 24 | ```bash 25 | git clone https://github.com/yourusername/attendance-fetching-bot.git 26 | cd attendance-fetching-bot 27 | ``` 28 | 29 | 2. *Setup env* 30 | ```md 31 | username: 32 | dd: 33 | mm: 34 | yyyy: 35 | passwd: 36 | ``` 37 | 38 | 3. *Expose port* 39 | ```bash 40 | npm run tunnel 41 | ``` 42 | 43 | 2. *Setup twilio dashboard* 44 | 45 | 4. *Start server* 46 | ```bash 47 | npm start 48 | ``` -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | const qs = require("qs"); 3 | const env = require("dotenv"); 4 | const { getCookies } = require("./functions/getCookie"); 5 | const { getWebsite } = require("./functions/getWebsite"); 6 | const { extractAttendance, extractCourseMap, extractTitle } = require("./functions/extractInformation"); 7 | const { formatAttendanceForWhatsApp } = require("./functions/formatMessage"); 8 | 9 | env.config() 10 | 11 | 12 | const data = qs.stringify({ 13 | username: process.env.username, 14 | dd: process.env.dd, 15 | mm: process.env.mm, 16 | yyyy: process.env.yyyy, 17 | passwd: process.env.passwd, 18 | option: "com_user", 19 | task: "login", 20 | return: "�w^Ƙi", 21 | "return:": "", 22 | "0b9ce0ae5b5dfe9df30a4778f4395944": "1", 23 | "captcha-response": "", 24 | }); 25 | 26 | 27 | 28 | async function main() { 29 | const cookie = await getCookies(data) 30 | // console.log("my cookie" + cookie) 31 | 32 | const websiteHTML = await getWebsite(cookie) 33 | // console.log(typeof(websiteHTML)) 34 | 35 | const attendanceData = extractAttendance(websiteHTML) 36 | // console.log(attendanceData) 37 | 38 | const courseNameMap = extractCourseMap(websiteHTML); 39 | // console.log(courseNameMap) 40 | 41 | const title = extractTitle(websiteHTML) 42 | // console.log(title) 43 | 44 | const message = formatAttendanceForWhatsApp(attendanceData, courseNameMap, title) 45 | // console.log(message) 46 | return message 47 | } 48 | 49 | module.exports = {main} -------------------------------------------------------------------------------- /functions/formatMessage.js: -------------------------------------------------------------------------------- 1 | function formatAttendanceForWhatsApp(attendanceArray, courseNameMap, studentInfo) { 2 | let message = ""; 3 | 4 | if (studentInfo && studentInfo.title) { 5 | message += `*${studentInfo.title.trim()}*\n`; 6 | message += `-----------------------------------\n`; 7 | } 8 | 9 | message += "*Attendance Report:*\n\n"; 10 | 11 | if (!attendanceArray || attendanceArray.length === 0) { 12 | message += "_No attendance data available._\n"; 13 | return message; 14 | } 15 | 16 | // Determine max length for course code/name for alignment (optional, but nice) 17 | 18 | attendanceArray.forEach(item => { 19 | const courseCode = item[0]; 20 | const percentage = item[1]; 21 | const subjectName = courseNameMap[courseCode] || courseCode; // Use code if not available name 22 | 23 | message += `*${subjectName}* \n`; 24 | // message += ` Code: \`${courseCode}\`\n`; 25 | message += ` Attendance: *${percentage}%*\n\n`; 26 | }); 27 | 28 | // Add a summary for subjects with low attendance (e.g., below 75%) 29 | const lowAttendanceSubjects = attendanceArray.filter(item => item[1] < 75); 30 | if (lowAttendanceSubjects.length > 0) { 31 | message += `-----------------------------------\n`; 32 | message += "*Attention Required (Attendance < 75%):*\n"; 33 | lowAttendanceSubjects.forEach(item => { 34 | const courseCode = item[0]; 35 | const percentage = item[1]; 36 | const subjectName = courseNameMap[courseCode] || courseCode; 37 | message += ` - *${subjectName}*: ${percentage}%\n`; 38 | }); 39 | message += `\n`; 40 | } 41 | 42 | message += `_Last updated: ${studentInfo.lastUpdate}`; 43 | 44 | return message.trim(); // Trim any trailing newlines 45 | } 46 | 47 | module.exports = {formatAttendanceForWhatsApp} -------------------------------------------------------------------------------- /functions/getCookie.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | 3 | function trimAt(string, symbol) { 4 | const index = string.indexOf(symbol); 5 | if (index !== -1) { 6 | return string.substring(0, index); 7 | } else return string; 8 | } 9 | 10 | async function getCookies(data) { 11 | const url = "https://crce-students.contineo.in/parents/index.php"; 12 | 13 | const header = { 14 | headers: { 15 | Accept: 16 | "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", 17 | "Accept-Language": "en-US,en;q=0.9", 18 | "Cache-Control": "max-age=0", 19 | Connection: "keep-alive", 20 | "Content-Type": "application/x-www-form-urlencoded", 21 | DNT: "1", 22 | Origin: "https://crce-students.contineo.in", 23 | Referer: 24 | "https://crce-students.contineo.in/parents/index.php?option=com_studentdashboard&controller=studentdashboard&task=dashboard", 25 | "Sec-Fetch-Dest": "document", 26 | "Sec-Fetch-Mode": "navigate", 27 | "Sec-Fetch-Site": "same-origin", 28 | "Sec-Fetch-User": "?1", 29 | "Upgrade-Insecure-Requests": "1", 30 | "User-Agent": 31 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36", 32 | "sec-ch-ua": 33 | '"Chromium";v="136", "Google Chrome";v="136", "Not.A/Brand";v="99"', 34 | "sec-ch-ua-mobile": "?0", 35 | "sec-ch-ua-platform": '"macOS"', 36 | // 'Cookie': '5bd4aa82278a9392700cda732bf3f9eb=926cd64b8219cfd0c1d15dc7541f14a6' 37 | }, 38 | maxRedirects: 0, 39 | validateStatus: null, // allow 303 response through 40 | }; 41 | 42 | try { 43 | const response = await axios.post(url, data, header); 44 | 45 | const unformattedCookie = response.headers["set-cookie"][0]; 46 | 47 | const cookie = trimAt(unformattedCookie, ";"); 48 | // console.log(cookie); 49 | 50 | return cookie; 51 | } catch (error) { 52 | console.log(`error while getting cookie ${error}`) 53 | } 54 | } 55 | 56 | module.exports = { getCookies }; 57 | -------------------------------------------------------------------------------- /functions/extractInformation.js: -------------------------------------------------------------------------------- 1 | const cheerio = require("cheerio"); 2 | 3 | function extractAttendance(htmlString) { 4 | const $ = cheerio.load(htmlString); 5 | const scripts = $("script"); 6 | let attendanceDataColumns = null; 7 | 8 | // Regex to find the 'columns' array. 9 | // It captures the content *inside* the main brackets of the 'columns' array. 10 | // This assumes the 'columns' property is followed by a 'type' property in the data object. 11 | const regex = /columns:\s*\[\s*([\s\S]*?)\s*\]\s*,\s*type:\s*"gauge"/; 12 | // ^^^^^^^^^^^^^^^^^ match[1]: captures the content inside the brackets 13 | 14 | scripts.each((index, element) => { 15 | const scriptText = $(element).html(); 16 | 17 | // First, ensure we are looking at the correct script block 18 | if (scriptText && scriptText.includes('bindto: "#gaugeTypeMulti"')) { 19 | const match = scriptText.match(regex); 20 | 21 | if (match && typeof match[1] === "string") { 22 | // match[1] is the string content *inside* the columns array's brackets 23 | // e.g., " [\"ACSC601\",76],[\"ACSC602\",83], ... ,[\"CSDLO6013\",76], " 24 | const arrayInternalsString = match[1]; 25 | 26 | // We need to reconstruct the full array string for parsing 27 | const arrayStringToParse = "[" + arrayInternalsString + "]"; 28 | 29 | try { 30 | // Using Function constructor for leniency (e.g., handles trailing commas within arrayInternalsString) 31 | attendanceDataColumns = new Function( 32 | "return " + arrayStringToParse 33 | )(); 34 | return false; // Exit the .each loop since we found and parsed it 35 | } catch (e) { 36 | console.error( 37 | "Error parsing columns data with Function constructor:", 38 | e 39 | ); 40 | console.warn( 41 | "Problematic array string for Function constructor (after wrapping):", 42 | arrayStringToParse 43 | ); 44 | 45 | // Fallback: try to clean for JSON.parse 46 | // JSON.parse is stricter and doesn't allow trailing commas like [el1, el2,] 47 | // This regex removes a trailing comma if it's right before the final ']' 48 | const cleanedArrayStringToParse = arrayStringToParse.replace( 49 | /,\s*\]$/, 50 | "]" 51 | ); 52 | try { 53 | attendanceDataColumns = JSON.parse(cleanedArrayStringToParse); 54 | return false; // Exit the .each loop 55 | } catch (e2) { 56 | console.error( 57 | "Error parsing columns data with JSON.parse after cleaning:", 58 | e2 59 | ); 60 | console.warn( 61 | "String that failed JSON.parse:", 62 | cleanedArrayStringToParse 63 | ); 64 | } 65 | } 66 | } 67 | } 68 | }); 69 | 70 | return attendanceDataColumns; 71 | } 72 | 73 | function extractCourseMap(htmlString) { 74 | const $ = cheerio.load(htmlString); 75 | const courseMap = {}; 76 | 77 | // Find the table by its caption 78 | // Then navigate to the tbody and iterate over its rows 79 | $('caption:contains("Course registration - CIE and attendance status")') 80 | .closest("table") // Get the parent table of the caption 81 | .find("tbody tr") // Find all 'tr' elements within the 'tbody' 82 | .each((index, rowElement) => { 83 | const cells = $(rowElement).find("td"); // Get all 'td' elements in the current row 84 | 85 | if (cells.length >= 2) { 86 | // Ensure there are at least two cells 87 | const courseCode = $(cells[0]).text().trim(); // Text of the first cell 88 | const courseName = $(cells[1]).text().trim(); // Text of the second cell 89 | 90 | if (courseCode && courseName) { 91 | // Make sure both are not empty 92 | courseMap[courseCode] = courseName; 93 | } 94 | } 95 | }); 96 | 97 | return courseMap; 98 | } 99 | 100 | function extractTitle(htmlString) { 101 | try { 102 | const $ = cheerio.load(htmlString); 103 | const title = $( 104 | "div.cn-student-header.cn-grey-bg div.cn-stu-data1.uk-text-right.cn-mobile-text p" 105 | ) 106 | .first() 107 | .text() 108 | .trim(); 109 | 110 | const lastUpdate = $("p.uk-text-right.cn-last-update") 111 | .first() 112 | .text() 113 | .trim(); 114 | return { 115 | title: title, 116 | lastUpdate: lastUpdate, 117 | }; 118 | } catch (e) { 119 | console.log(`error in extractTitle ${e.message}`); 120 | return { 121 | title: null, 122 | lastUpdate: null, 123 | }; 124 | } 125 | } 126 | module.exports = { extractAttendance, extractCourseMap, extractTitle }; 127 | --------------------------------------------------------------------------------