const express = require('express') const session = require('express-session') const fs = require('fs') const path = require('path') const morgan = require('morgan') const bodyParser = require('body-parser') const sqlite3 = require('sqlite3').verbose() const bole = require('bole') const config = require('./config') const log = bole('app') const app = express() // Create an SQLite database and initialize tables const db = new sqlite3.Database(config.db_path, (err) => { if (err) { log.error('Error opening SQLite database:', err.message) } else { log.info('Connected to SQLite database', config.db_path) db.run(` CREATE TABLE IF NOT EXISTS button_clicks ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT, animal_name TEXT, button_name TEXT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, time_difference INTEGER -- Add this column for time difference ) `) db.run(` CREATE TABLE IF NOT EXISTS animals ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL ) `) } }) if (config.PRODUCTION) { let accessLogStream = fs.createWriteStream( path.join(__dirname, 'log', 'access.log'), { flags: 'a' } ) app.use(morgan('combined', { stream: accessLogStream })) } else { app.use(morgan('combined')) } app.use(bodyParser.json()) app.use(session({ resave: false, saveUninitialized: false, cookie: { maxAge: 3600000 }, secret: config.session_token })) let animals // check and load animals into redis try { fs.readFile('./animals.json', function (err, data) { if (err) { throw err } const jsondata = JSON.parse(data) animals = jsondata.animals for (const animal of animals) { db.run(` INSERT INTO animals(name) SELECT ? WHERE NOT EXISTS(SELECT 1 FROM animals WHERE name = ?); `, [animal, animal], (err) => { if (err) { log.error(`Error inserting animal ${animal}: `, err.message) } else { log.info(`Success inserting animal ${animal}`) } } ) } }) } catch (error) { log.error('Error loading animals:', error) animals = ['Dog', 'Cat', 'Elephant', 'Lion', 'Giraffe'] } // Serve the HTML file app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'index.html')) }) app.get('/asset/frontend.js', (req, res) => { res.sendFile(path.join(__dirname, 'asset/frontend.js')) }) app.get('/newSession', (req, res) => { log.info(req.session.id) req.session.regenerate((error) => { if (error) { log.error(error) } }) log.info(req.session.id) }) // Route to get a random animal name app.get('/getNextAnimal', async (req, res) => { try { // TODO this is currently random, and should have a bit of reasoning behind the next choice const randomIndex = Math.floor(Math.random() * animals.length) const randomAnimal = animals[randomIndex] res.json({ animalName: randomAnimal }) } catch (error) { log.error('Error fetching random animal:', error) res.status(500).json({ error: 'Internal server error' }) } }) // Route to record button clicks along with session IDs in SQLite app.post('/recordButtonClick', (req, res) => { try { // const { buttonName, sessionId } = req.body; const result = req.body db.run( 'INSERT INTO button_clicks (session_id, animal_name, button_name, timestamp, time_difference) VALUES (?, ?, ?, ?, ?)', [ req.session.id, result.animal, result.button, result.time, result.difference ], (err) => { if (err) { log.error('Error recording button click:', err.message) res.status(500).json({ error: 'Internal server error' }) } else { res.sendStatus(200) } } ) } catch (error) { log.error('Error recording button click:', error) res.status(500).json({ error: 'Internal server error' }) } }) // Route to show the current results from SQLite app.get('/results', async (req, res) => { try { const results = { count: {}, avgTimes: {} } const getCount = new Promise((resolve, reject) => { db.all( ` SELECT animal_name, button_name, COUNT(*) AS count FROM button_clicks GROUP BY button_name, animal_name `, (err, rows) => { if (err) { reject(new Error('getCount: ' + err.message)) } else { rows.forEach((row) => { if (typeof results.count[row.animal_name] === 'undefined') { results.count[row.animal_name] = {} } results.count[row.animal_name][row.button_name] = row.count }) resolve() } } ) }) const getAvgTime = new Promise((resolve, reject) => { db.all( ` SELECT animal_name, button_name, AVG(time_difference) AS time FROM button_clicks GROUP BY animal_name, button_name; `, (err, rows) => { if (err) { reject(new Error('getAvgTime: ' + err.message)) } else { rows.forEach((row) => { if (typeof results.avgTimes[row.animal_name] === 'undefined') { results.avgTimes[row.animal_name] = {} } results.avgTimes[row.animal_name][row.button_name] = row.time }) resolve() } } ) }) const getTotalAvgTime = new Promise((resolve, reject) => { db.all( ` SELECT animal_name, AVG(time_difference) AS time FROM button_clicks GROUP BY animal_name; `, (err, rows) => { if (err) { reject(new Error('getTotalAvgTime: ' + err.message)) } else { rows.forEach((row) => { if (typeof results.avgTimes[row.animal_name] === 'undefined') { results.avgTimes[row.animal_name] = {} } results.avgTimes[row.animal_name].total = row.time }) resolve() } } ) }) await Promise.all([getCount, getTotalAvgTime, getAvgTime]) res.json(results) } catch (error) { log.error('Error fetching results:', error) res.status(500).json({ error: 'Internal server error' }) } }) module.exports = app // vim:set sts=2 sw=2 et: