diff --git a/controllers/graphsController.js b/controllers/graphsController.js new file mode 100644 index 0000000..f1ffa57 --- /dev/null +++ b/controllers/graphsController.js @@ -0,0 +1,484 @@ +import pool from '../config/db.js' +import { ChartJSNodeCanvas } from 'chartjs-node-canvas' +import ChartDataLabels from 'chartjs-plugin-datalabels' + +const width = 1760; +const height = 990; +const backgroundColour = 'white'; +const chartJSNodeCanvas = new ChartJSNodeCanvas({ + width, + height, + backgroundColour, + plugins: { + modern: [ChartDataLabels] + } +}); + +function get_date_str(date) { + const dd = String(date.getDate()).padStart(2, '0'); + const mm = String(date.getMonth() + 1).padStart(2, '0'); + const yy = String(date.getFullYear()).slice(-2); + + return dd + mm + yy; +} + +function formatDateStr(dateStr) { + if (dateStr.length !== 6) throw new Error("Date string must be 6 digits (DDMMYY)"); + const day = dateStr.slice(0, 2); + const month = dateStr.slice(2, 4); + const year = dateStr.slice(4, 6); + return `${day}/${month}/${year}`; +} + +function generateHSLColors(count) { + const colors = []; + const saturation = 75; + const lightness = 55; + + for (let i = 0; i < count; i++) { + const hue = Math.floor((360 / count) * i); + colors.push(`hsl(${hue}, ${saturation}%, ${lightness}%)`); + } + return colors; +} + +const singleBarColors = { + background: 'rgba(99, 102, 241, 0.8)', + border: 'rgba(99, 102, 241, 1)', + gradient: ['rgba(99, 102, 241, 0.8)', 'rgba(139, 92, 246, 0.8)'] +}; + +const multiBarColors = [ + 'rgba(99, 102, 241, 0.8)', + 'rgba(16, 185, 129, 0.8)', + 'rgba(245, 101, 101, 0.8)', + 'rgba(251, 191, 36, 0.8)', + 'rgba(139, 92, 246, 0.8)', + 'rgba(6, 182, 212, 0.8)', + 'rgba(236, 72, 153, 0.8)', + 'rgba(34, 197, 94, 0.8)', + 'rgba(249, 115, 22, 0.8)', + 'rgba(168, 85, 247, 0.8)' +]; + +const getBaseConfig = (type = 'bar') => ({ + type, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'top', + labels: { + font: { + size: 14, + weight: 'bold' + }, + color: '#374151', + padding: 20 + } + }, + tooltip: { + backgroundColor: 'rgba(0, 0, 0, 0.8)', + titleFont: { + size: 14, + weight: 'bold' + }, + bodyFont: { + size: 13 + }, + cornerRadius: 8, + padding: 12 + }, + datalabels: { + anchor: 'end', + align: 'top', + color: '#374151', + font: { + size: 12, + weight: 'bold' + }, + formatter: (value, context) => { + return value > 0 ? value : ''; + } + } + }, + scales: { + x: { + grid: { + display: false + }, + ticks: { + font: { + size: 12, + weight: '500' + }, + color: '#6B7280', + maxRotation: 45 + }, + title: { + display: true, + text: 'Date', + font: { + size: 14, + weight: 'bold' + }, + color: '#374151' + } + }, + y: { + beginAtZero: true, + suggestedMax: function(context) { + const max = Math.max(...context.chart.data.datasets.flatMap(d => d.data)); + return max * 1.15; + }, + grid: { + color: 'rgba(0, 0, 0, 0.1)', + lineWidth: 1 + }, + ticks: { + font: { + size: 12, + weight: '500' + }, + color: '#6B7280' + }, + title: { + display: true, + text: 'Hours', + font: { + size: 14, + weight: 'bold' + }, + color: '#374151' + } + } + }, + layout: { + padding: { + top: 20, + right: 30, + bottom: 20, + left: 30 + } + } + } +}); + +async function make_graph(config) { + const image = await chartJSNodeCanvas.renderToBuffer(config); + return image; +} + +export const myGraph = async (req, res) => { + const myName = req.params.slackname; + const timeframe = req.params.timeframe; + + if (timeframe == 'alltime') { + const initial = await pool.query( + 'SELECT labdata FROM members_info WHERE (slack_name)=($1)', [myName] + ); + if (initial.rows.length == 0) { + res.status(400).json({ error: 'Bad Name argument' }); + } + + let labtime = initial.rows[0].labdata['labTime']; + + let config = { + ...getBaseConfig('bar'), + data: { + labels: ['All time'], + datasets: [{ + label: `${myName}'s overall lab hours`, + data: [labtime], + backgroundColor: singleBarColors.background, + borderColor: singleBarColors.border, + borderWidth: 2, + borderRadius: 6, + borderSkipped: false, + maxBarThickness: 120, + minBarLength: 2 + }] + } + }; + + config.options.scales.x.title.text = 'Period'; + + res.type('image/png'); + let graph = await make_graph(config); + res.send(graph); + + } else if (timeframe == 'monthly') { + const initial = await pool.query( + 'SELECT labdata FROM members_info WHERE (slack_name)=($1)', [myName] + ); + if (initial.rows.length == 0) { + res.status(400).json({ error: 'Bad Name argument' }); + } + + let labData = initial.rows[0].labdata['dayWise']; + + var labtimes = []; + var dates = []; + + for (let i = 30; i > 0; i--) { + let date = new Date(); + date.setDate(date.getDate() - i) + let date_str = get_date_str(date); + + dates.push(formatDateStr(date_str)); + if (date_str in labData) { + labtimes.push(labData[date_str]); + } else { + labtimes.push(0); + } + } + + let config = { + ...getBaseConfig('bar'), + data: { + labels: dates, + datasets: [{ + label: `${myName}'s lab hours of the last month`, + data: labtimes, + backgroundColor: singleBarColors.background, + borderColor: singleBarColors.border, + borderWidth: 1, + borderRadius: 4, + borderSkipped: false, + maxBarThickness: 25, + minBarLength: 1 + }] + } + }; + + res.type('image/png'); + let graph = await make_graph(config); + res.send(graph); + + } else if (timeframe == 'weekly') { + const initial = await pool.query( + 'SELECT labdata FROM members_info WHERE (slack_name)=($1)', [myName] + ); + if (initial.rows.length == 0) { + res.status(400).json({ error: 'Bad Name argument' }); + } + + let labData = initial.rows[0].labdata['dayWise']; + + var labtimes = []; + var dates = []; + + for (let i = 7; i >0; i--) { + let date = new Date(); + date.setDate(date.getDate() - i) + let date_str = get_date_str(date); + + dates.push(formatDateStr(date_str)); + if (date_str in labData) { + labtimes.push(labData[date_str]); + } else { + labtimes.push(0); + } + } + + let config = { + ...getBaseConfig('bar'), + data: { + labels: dates, + datasets: [{ + label: `${myName}'s lab hours of the last week`, + data: labtimes, + backgroundColor: singleBarColors.background, + borderColor: singleBarColors.border, + borderWidth: 2, + borderRadius: 6, + borderSkipped: false, + maxBarThickness: 80, + minBarLength: 2 + }] + } + }; + + res.type('image/png'); + let graph = await make_graph(config); + res.send(graph); + + } else { + res.status(400).json({ error: 'Bad Timeframe argument' }); + } +} + +export const graphByTag = async (req, res) => { + const tag = req.params.tag; + const timeframe = req.params.timeframe; + + if (timeframe == 'alltime') { + const users = await pool.query( + 'SELECT * FROM members_info WHERE $1 = ANY(tags)', [tag] + ); + if (users.rows.length == 0) { + res.status(400).json({ error: 'Bad Tag argument' }); + } + + var labtimes = []; + var names = []; + var backgroundColors = []; + var borderColors = []; + + for (let [index, row] of users.rows.entries()) { + names.push(row.slack_name); + if (row.labdata['labTime'] != undefined) { + labtimes.push(row.labdata['labTime']); + } else { + labtimes.push(0); + } + backgroundColors.push(multiBarColors[index % multiBarColors.length]); + borderColors.push(multiBarColors[index % multiBarColors.length].replace('0.8', '1')); + } + + let config = { + ...getBaseConfig('bar'), + data: { + labels: names, + datasets: [{ + label: `${tag}'s overall lab hours`, + data: labtimes, + backgroundColor: backgroundColors, + borderColor: borderColors, + borderWidth: 2, + borderRadius: 6, + borderSkipped: false, + maxBarThickness: 60, + minBarLength: 2 + }] + } + }; + + config.options.scales.x.title.text = 'Members'; + + res.type('image/png'); + let graph = await make_graph(config); + res.send(graph); + + } else if (timeframe == 'weekly') { + const users = await pool.query( + 'SELECT * FROM members_info WHERE $1 = ANY(tags)', [tag] + ); + if (users.rows.length == 0) { + res.status(400).json({ error: 'Bad Tag argument' }); + } + + let config = { + ...getBaseConfig('bar'), + data: { + labels: [], + datasets: [] + } + }; + + var dates = []; + for (let i = 7; i > 0; i--) { + let date = new Date(); + date.setDate(date.getDate() - i) + let date_str = get_date_str(date); + + dates.push(formatDateStr(date_str)); + } + config.data.labels = dates; + + for (let [index, row] of users.rows.entries()) { + let dataset = { + label: row.slack_name, + backgroundColor: multiBarColors[index % multiBarColors.length], + borderColor: multiBarColors[index % multiBarColors.length].replace('0.8', '1'), + borderWidth: 1, + borderRadius: 4, + borderSkipped: false, + maxBarThickness: 40, + minBarLength: 1, + data: [] + }; + + let initial = row.labdata['dayWise']; + + for (let i = 7; i > 0; i--) { + let date = new Date(); + date.setDate(date.getDate() - i) + let date_str = get_date_str(date); + if (date_str in initial) { + dataset.data.push(initial[date_str]); + } else { + dataset.data.push(0); + } + } + config.data.datasets.push(dataset); + } + + res.type('image/png'); + let graph = await make_graph(config); + res.send(graph); + + } else if (timeframe == 'monthly') { + const users = await pool.query( + 'SELECT * FROM members_info WHERE $1 = ANY(tags)', [tag] + ); + + if (users.rows.length == 0) { + res.status(400).json({ error: 'Bad Tag argument' }); + } + + let config = { + ...getBaseConfig('bar'), + data: { + labels: [], + datasets: [] + } + }; + + var dates = []; + for (let i = 30; i >0; i--) { + let date = new Date(); + date.setDate(date.getDate() - i) + let date_str = get_date_str(date); + + dates.push(formatDateStr(date_str)); + } + config.data.labels = dates; + + for (let [index, row] of users.rows.entries()) { + let dataset = { + label: row.slack_name, + backgroundColor: multiBarColors[index % multiBarColors.length], + borderColor: multiBarColors[index % multiBarColors.length].replace('0.8', '1'), + borderWidth: 1, + borderRadius: 3, + borderSkipped: false, + maxBarThickness: 20, + minBarLength: 1, + data: [] + }; + + let initial = row.labdata['dayWise']; + + for (let i = 30; i >0; i--) { + let date = new Date(); + date.setDate(date.getDate() - i) + let date_str = get_date_str(date); + if (date_str in initial) { + dataset.data.push(initial[date_str]); + } else { + dataset.data.push(0); + } + } + config.data.datasets.push(dataset); + } + + res.type('image/png'); + let graph = await make_graph(config); + res.send(graph); + + } else { + res.status(400).json({ error: 'Bad Timeframe argument' }); + } +} \ No newline at end of file diff --git a/controllers/labBot.js b/controllers/labBot.js new file mode 100644 index 0000000..b25e577 --- /dev/null +++ b/controllers/labBot.js @@ -0,0 +1,103 @@ +import express from 'express'; +import axios from 'axios'; +import dotenv from 'dotenv'; +import { WebClient } from '@slack/web-api'; + +dotenv.config(); +const slackClient = new WebClient(process.env.LAB_BOT_TOKEN); +const BotToken = process.env.LAB_BOT_TOKEN; + +const LABOPENCLOSE_CHANNEL_ID = process.env.SLACK_CHANNEL_ID; + +const messageOpen = "Bro lab is open"; +const messageClose = "Bro lab is closed"; +let currentBroStatus = true; + +function reverseBroStatus() { + currentBroStatus = !currentBroStatus + return currentBroStatus ? messageOpen : messageClose +} + +export const listenToGraphReq = async (req, res) => { + +} + +export const handleAppMention = async (event) => { + const { text, channel, user, ts } = event; + + console.log(text); + + // const event_time = parseEventTime(text); + // console.log('Event time:', event_time); + // const emoji = parseEmoji(text); + + // if (!event_time) { + // console.log(' Message does not contain valid event time'); + // return; + // } + // if (!emoji) { + // console.log('Message does not contain valid emoji') + // return; + // } + + // const createEventObj = + // { + // message: text, + // channel: channel, + // created_by: user, + // event_time: event_time, + // emoji: emoji, + // ts: ts + // } + + // try { + // const result = await createEvent(createEventObj); + + // console.log(' Event stored in DB:', result.rows[0]); + // const message = "Event successfully created!"; + // postMessageToSlack(message, createEventObj.channel); + // } catch (err) { + // console.error(' Error storing event:', err); + // } +}; + + + +export const printFromBro = async (req, res) => { + let message = reverseBroStatus(); + console.log("Bro status changed to: " + message); + try { + const result = await postMessageToSlack(message); + + return res.status(200).json({ + success: true, + message: "Message posted to Slack successfully.", + slackResponse: result + }); + + } catch (error) { + console.error('Error posting message to Slack:', error); + return res.status(500).json({ error: 'Failed to post message to Slack' }); + } +} + + +const postMessageToSlack = async (message) => { + let response = null; + try { + response = await axios.post(process.env.BASE_URL, { + "channel": LABOPENCLOSE_CHANNEL_ID, + "text": message, + }, { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${SLACK_TOKEN}`, + }, + }); + } catch (error) { + console.error('Error posting message to Slack:', error); + } + + return response.data; +} + diff --git a/controllers/labHours.js b/controllers/labHours.js index 63fea19..a954b86 100644 --- a/controllers/labHours.js +++ b/controllers/labHours.js @@ -1,4 +1,3 @@ -import { response } from 'express'; import pool from '../config/db.js' const SAC_CLOSING_HOUR = 2.0 @@ -11,7 +10,7 @@ export const toggleInOut = async (req, res) => { try { const initial = await pool.query( - 'SELECT * FROM activeUsers WHERE (enroll_num)=($1)', [enroll_num] + 'SELECT * FROM members_info WHERE (enrollment_num)=($1)', [enroll_num] ); let initStatus = initial.rows[0]; @@ -161,7 +160,7 @@ export const toggleInOut = async (req, res) => { const response = await pool.query( - 'UPDATE activeUsers SET labdata=($1) WHERE enroll_num = ($2)', [labData, enroll_num] + 'UPDATE members_info SET labdata=($1) WHERE enrollment_num = ($2)', [labData, enroll_num] ); diff --git a/controllers/maintenance.js b/controllers/maintenance.js index 7e83cb1..1e8247a 100644 --- a/controllers/maintenance.js +++ b/controllers/maintenance.js @@ -2,12 +2,11 @@ import pool from '../config/db.js' export async function doMaintenance(){ - const initial = await pool.query( - 'SELECT * FROM activeUsers' + const initial = await pool.query( + 'SELECT * FROM members_info' ); - - + let date = new Date(); const dd = String(date.getDate()).padStart(2, '0'); @@ -22,14 +21,13 @@ export async function doMaintenance(){ for (let i = 0; i < initial.rows.length; i++) { const row = initial.rows[i]; - const enrollNo = row['enroll_num']; + const enrollNo = row['enrollment_num']; let labData = row['labdata']; if (labData == null) { labData = { 'logs': [], 'isInLab': false, 'labTime': 0, 'dayWise': {} }; } - console.log('pritimg labdaat') - console.log(labData) + if (!(datestr in labData.dayWise)) { labData.dayWise[datestr] = 0; } @@ -37,15 +35,21 @@ export async function doMaintenance(){ values.push(enrollNo, JSON.stringify(labData)); const idx = i * 2; placeholders.push(`($${idx + 1}, $${idx + 2})`); - } - await pool.query('TRUNCATE TABLE activeUsers'); + await pool.query( + `UPDATE members_info + SET labdata = $1 + WHERE enrollment_num = $2`, + [labData, enrollNo] + ); - const query = `INSERT INTO activeUsers (enroll_num, labdata) VALUES ${placeholders.join(', ')}`; - await pool.query(query, values); + + + } } + export const setupLogs = async(req, res) =>{ try{ diff --git a/controllers/printFromBro.js b/controllers/printFromBro.js deleted file mode 100644 index 1859f38..0000000 --- a/controllers/printFromBro.js +++ /dev/null @@ -1,63 +0,0 @@ -import express from 'express'; -import axios from 'axios'; -import dotenv from 'dotenv'; -dotenv.config(); - -const SLACK_TOKEN = process.env.SLACK_BOT_TOKEN; -const CHANNEL_ID = process.env.SLACK_CHANNEL_ID; - - - -const messageOpen = "Bro lab is open"; -const messageClose = "Bro lab is closed"; -let currentBroStatus = true; -let prevBroStatus = false; - -function reverseBroStatus() { - currentBroStatus = !currentBroStatus - return currentBroStatus ? messageOpen: messageClose -} - - -export const printFromBro = async (req, res) => -{ - let message = reverseBroStatus(); - console.log("Bro status changed to: " + message); - try - { - const result = await postMessageToSlack(message); - - return res.status(200).json({ - success: true, - message: "Message posted to Slack successfully.", - slackResponse: result - }); - - }catch (error) - { - console.error('Error posting message to Slack:', error); - return res.status(500).json({ error: 'Failed to post message to Slack' }); - } -} - -const postMessageToSlack = async (message) => { - let response = null; - try - { - response = await axios.post(process.env.BASE_URL, { - "channel": CHANNEL_ID, - "text": message, - }, { - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${SLACK_TOKEN}`, - }, - }); - }catch (error) - { - console.error('Error posting message to Slack:', error); - } - - return response.data; -} - diff --git a/index.js b/index.js index d18f45f..1f6d761 100644 --- a/index.js +++ b/index.js @@ -7,6 +7,8 @@ import keyHolderRoutes from './routes/keyHolderRoutes.js'; import labHoursRoutes from './routes/labHoursRoutes.js'; import slackRoutes from './routes/slackRoutes.js'; import eventApiRoutes from './routes/eventApiRoutes.js' +import graphRoutes from './routes/getGraphsRoute.js' + import { startScheduler } from './services/schedulerService.js'; import maintenanceRoute from './routes/maintenanceRoute.js' @@ -38,6 +40,7 @@ app.use('/api/message', sendMsgRoute); app.use('/api/keyHolders', keyHolderRoutes); app.use('/api/labHours', labHoursRoutes); app.use('/api/eventApi', eventApiRoutes); +app.use('/api/graphs/', graphRoutes); startScheduler(); app.use('/api/maintenance', maintenanceRoute) diff --git a/package-lock.json b/package-lock.json index 767c85f..f0df70b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,8 @@ "@slack/events-api": "^3.0.1", "@slack/web-api": "^7.9.2", "axios": "^1.9.0", + "chartjs-node-canvas": "^5.0.0", + "chartjs-plugin-datalabels": "^2.2.0", "chrono-node": "^2.8.2", "cors": "^2.8.5", "dotenv": "^16.5.0", @@ -20,6 +22,12 @@ "postgres": "^3.4.7" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "peer": true + }, "node_modules/@slack/events-api": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@slack/events-api/-/events-api-3.0.1.tgz", @@ -646,6 +654,35 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/body-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", @@ -666,6 +703,29 @@ "node": ">=18" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -713,6 +773,56 @@ "node": ">=6" } }, + "node_modules/canvas": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.1.2.tgz", + "integrity": "sha512-Z/tzFAcBzoCvJlOSlCnoekh1Gu8YMn0J51+UAuXJAbW1Z6I9l2mZgdD7738MepoeeIcUdDtbMnOg6cC7GJxy/g==", + "hasInstallScript": true, + "dependencies": { + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.3" + }, + "engines": { + "node": "^18.12.0 || >= 20.9.0" + } + }, + "node_modules/chart.js": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz", + "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", + "peer": true, + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chartjs-node-canvas": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chartjs-node-canvas/-/chartjs-node-canvas-5.0.0.tgz", + "integrity": "sha512-+Lc5phRWjb+UxAIiQpKgvOaG6Mw276YQx2jl2BrxoUtI3A4RYTZuGM5Dq+s4ReYmCY42WEPSR6viF3lDSTxpvw==", + "dependencies": { + "canvas": "^3.1.0", + "tslib": "^2.8.1" + }, + "peerDependencies": { + "chart.js": "^4.4.8" + } + }, + "node_modules/chartjs-plugin-datalabels": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chartjs-plugin-datalabels/-/chartjs-plugin-datalabels-2.2.0.tgz", + "integrity": "sha512-14ZU30lH7n89oq+A4bWaJPnAG8a7ZTk7dKf48YAzMvJjQtjrgg5Dpk9f+LbjCF6bpx3RAGTeL13IXpKQYyRvlw==", + "peerDependencies": { + "chart.js": ">=3.0.0" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, "node_modules/chrono-node": { "version": "2.8.2", "resolved": "https://registry.npmjs.org/chrono-node/-/chrono-node-2.8.2.tgz", @@ -849,6 +959,28 @@ "node": ">=0.10.0" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -878,6 +1010,14 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "engines": { + "node": ">=8" + } + }, "node_modules/dotenv": { "version": "16.5.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", @@ -925,6 +1065,14 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -991,6 +1139,14 @@ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "license": "MIT" }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "engines": { + "node": ">=6" + } + }, "node_modules/express": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", @@ -1137,6 +1293,11 @@ "node": ">= 0.8" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -1192,6 +1353,11 @@ "node": ">= 0.4" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -1271,12 +1437,36 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -1411,12 +1601,41 @@ "node": ">= 0.6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -1426,6 +1645,22 @@ "node": ">= 0.6" } }, + "node_modules/node-abi": { + "version": "3.75.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", + "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1727,6 +1962,31 @@ "node": ">=0.10.0" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1746,6 +2006,15 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", @@ -1785,6 +2054,33 @@ "node": ">= 0.8" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -1851,6 +2147,17 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", @@ -1972,6 +2279,49 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -1990,6 +2340,14 @@ "node": ">= 0.8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -2016,6 +2374,40 @@ "node": ">=8" } }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", + "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -2025,6 +2417,11 @@ "node": ">=0.6" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, "node_modules/tsscmp": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", @@ -2034,6 +2431,17 @@ "node": ">=0.6.x" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -2063,6 +2471,11 @@ "node": ">= 0.8" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", diff --git a/package.json b/package.json index 57a08a8..f823e00 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,8 @@ "@slack/events-api": "^3.0.1", "@slack/web-api": "^7.9.2", "axios": "^1.9.0", + "chartjs-node-canvas": "^5.0.0", + "chartjs-plugin-datalabels": "^2.2.0", "chrono-node": "^2.8.2", "cors": "^2.8.5", "dotenv": "^16.5.0", diff --git a/routes/getGraphsRoute.js b/routes/getGraphsRoute.js new file mode 100644 index 0000000..ef1a73c --- /dev/null +++ b/routes/getGraphsRoute.js @@ -0,0 +1,9 @@ +import express from 'express' +import { myGraph, graphByTag } from '../controllers/graphsController.js' + +const router = express.Router(); + +router.get('/me/:slackname/:timeframe', myGraph); // options for timeframe: weekly, monthly, alltime +router.get('/bytag/:tag/:timeframe', graphByTag); // options for timeframe: alltime, weekly, monthly + +export default router; \ No newline at end of file diff --git a/routes/sendMsgRoute.js b/routes/sendMsgRoute.js index 676343b..1e94fa7 100644 --- a/routes/sendMsgRoute.js +++ b/routes/sendMsgRoute.js @@ -1,5 +1,5 @@ import { Router } from 'express'; -import { printFromBro } from '../controllers/printFromBro.js'; +import { printFromBro } from '../controllers/labBot.js'; const router = Router(); diff --git a/routes/slackRoutes.js b/routes/slackRoutes.js index 526df5f..5317ac3 100644 --- a/routes/slackRoutes.js +++ b/routes/slackRoutes.js @@ -1,7 +1,8 @@ import express from 'express'; import { createEventAdapter } from '@slack/events-api'; import dotenv from 'dotenv'; -import { handleAppMention, handleReactionAdded, handleReactionRemoved } from '../controllers/eventController.js'; +import { handleAppMention_event, handleReactionAdded, handleReactionRemoved } from '../controllers/eventController.js'; +import { handleAppMention_labbot } from '../controllers/labBot.js'; dotenv.config(); @@ -11,8 +12,9 @@ const router = express.Router(); router.use('/events', slackEvents.expressMiddleware()); // Handle app mentions slackEvents.on('app_mention', async (event) => { - console.log('🔔 App mentioned:', event.text); - await handleAppMention(event); + console.log('App mentioned:', event.text); + await handleAppMention_event(event); + await handleAppMention_labbot(event); }); slackEvents.on('reaction_added', async (event) => { console.log('Reacted to app'); @@ -26,4 +28,4 @@ slackEvents.on('reaction_removed', async (event) => { slackEvents.on('error', console.error); -export default router; +export default router; \ No newline at end of file diff --git a/test.html b/test.html new file mode 100644 index 0000000..22ef72c --- /dev/null +++ b/test.html @@ -0,0 +1,28 @@ + + +
+ +