/* ---------------------------------------------------------------------------- Question Server GitLab: This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ------------------------------------------------------------------------- */ // package requires import express from 'express' import bodyParser from 'body-parser' import busboy from 'connect-busboy' import { v4 as uuidv4 } from 'uuid' import fs from 'fs' // other requires import logger from '../../utils/logger' import utils from '../../utils/utils' import { processIncomingRequest, logResult, backupData, shouldSaveDataFile, shouldSearchDataFile, loadJSON, Result, } from '../../utils/actions' import dbtools from '../../utils/dbtools' import auth from '../../middlewares/auth.middleware' import { dataToString, getSubjNameWithoutYear } from '../../utils/classes' import { initWorkerPool, doALongTask, msgAllWorker, } from '../../utils/workerPool' import { SetupData } from '../../server' import { ModuleType, User, DataFile, Request, QuestionDb, } from '../../types/basicTypes' // files const msgFile = 'stats/msgs' const passwordFile = 'data/dataEditorPasswords.json' const dataEditsLog = 'stats/dataEdits' const dailyDataCountFile = 'stats/dailyDataCount' const usersDbBackupPath = 'data/dbs/backup' const quickVoteResultsDir = 'stats/qvote' const quickVotes = 'stats/qvote/votes.json' const testUsersFile = 'data/testUsers.json' const idStatFile = 'stats/idstats' const idvStatFile = 'stats/idvstats' const todosFile = 'data/todos.json' const userScriptFile = 'submodules/moodle-test-userscript/stable.user.js' const rootRedirectToFile = 'data/apiRootRedirectTo' const recievedQuestionFile = 'stats/recievedQuestions' const registeredScriptsFile = 'stats/registeredScripts.json' const savedQuestionsFileName = 'savedQuestions.json' const oldMotdFile = 'publicDirs/qminingPublic/oldMotd' // other constants const line = '====================================================' // lol const addPWPerDay = 3 // every x day a user can give a pw const maxPWCount = 6 // maximum pw give opportunities a user can have at once const addPWCount = 1 // how many pw gen opportunities to add each time const daysAfterUserGetsPWs = 5 // days after user gets pw-s const minimumAlowwedSessions = 2 // how many sessions are allowed for a user // stuff gotten from server.js let userDB let url // eslint-disable-line let publicdirs = [] function GetApp(): ModuleType { const app = express() const publicDir = publicdirs[0] if (!publicDir) { throw new Error(`No public dir! ( API )`) } // files in public dirs const dbsFile = publicDir + 'questionDbs.json' const savedQuestionsDir = publicDir + 'savedQuestions' const recivedFiles = publicDir + 'recivedfiles' const uloadFiles = publicDir + 'f' const motdFile = publicDir + 'motd' const userSpecificMotdFile = publicDir + 'userSpecificMotd.json' let domain = url.split('.') // [ "https://api", "frylabs", "net" ] domain.shift() // [ "frylabs", "net" ] domain = domain.join('.') // "frylabs.net" logger.DebugLog(`Cookie domain: ${domain}`, 'cookie', 1) app.use( bodyParser.urlencoded({ limit: '10mb', extended: true, }) ) app.use( bodyParser.json({ limit: '10mb', }) ) app.set('view engine', 'ejs') app.set('views', ['./src/modules/api/views', './src/sharedViews']) app.use( auth({ userDB: userDB, jsonResponse: true, exceptions: [ '/register', '/favicon.ico', '/login', '/postfeedback', '/fosuploader', '/badtestsender', ], }) ) publicdirs.forEach((pdir) => { logger.Log(`Using public dir: ${pdir}`) app.use(express.static(pdir)) }) app.use( busboy({ limits: { fileSize: 50000 * 1024 * 1024, }, }) ) const dataFiles: Array = utils.ReadJSON(dbsFile) const questionDbs = loadJSON(dataFiles, publicDir) let version = '' let rootRedirectURL = '' let motd = '' let userSpecificMotd = {} // FIXME: check type from file let testUsers: any = [] initWorkerPool(questionDbs) function mergeObjSum(a, b) { const res = { ...b } Object.keys(a).forEach((key) => { if (res[key]) { res[key] += a[key] } else { res[key] = a[key] } }) return res } function LoadVersion() { const scriptContent = utils.ReadFile(userScriptFile) let temp: any = scriptContent.split('\n').find((x) => { return x.includes('@version') }) temp = temp.split(' ') temp = temp[temp.length - 1] version = temp } function LoadMOTD() { motd = utils.ReadFile(motdFile) } function LoadUserSpecificMOTD() { try { userSpecificMotd = utils.ReadJSON(userSpecificMotdFile) } catch (err) { logger.Log('Couldnt parse user specific motd!', logger.GetColor('redbg')) console.error(err) } } function LoadTestUsers() { testUsers = utils.ReadJSON(testUsersFile) if (testUsers) { testUsers = testUsers.userIds } } function reloadRootRedirectURL() { if (utils.FileExists(rootRedirectToFile)) { rootRedirectURL = utils.ReadFile(rootRedirectToFile) } } const filesToWatch = [ { fname: userSpecificMotdFile, logMsg: 'User Specific Motd updated', action: LoadUserSpecificMOTD, }, { fname: motdFile, logMsg: 'Motd updated', action: LoadMOTD, }, { fname: testUsersFile, logMsg: 'Test Users file changed', action: LoadTestUsers, }, { fname: userScriptFile, logMsg: 'User script file changed', action: LoadVersion, }, { fname: rootRedirectToFile, logMsg: 'Root redirect URL changed', action: reloadRootRedirectURL, }, ] function Load() { backupData(questionDbs) filesToWatch.forEach((ftw) => { if (utils.FileExists(ftw.fname)) { utils.WatchFile(ftw.fname, () => { logger.Log(ftw.logMsg) ftw.action() }) ftw.action() } else { logger.Log( `File ${ftw.fname} does not exists to watch!`, logger.GetColor('redbg') ) } }) } Load() // ------------------------------------------------------------- app.get('/getDbs', (req: Request, res: any) => { logger.LogReq(req) res.json( questionDbs.map((qdb) => { return { path: qdb.path.replace(publicDir, ''), name: qdb.name, } }) ) }) app.get('/voteTodo', (req: Request, res: any) => { logger.LogReq(req) const userId = req.session.user.id const id: any = req.query.id const todos = utils.ReadJSON(todosFile) if (!id) { res.json({ msg: 'id query undefined', result: 'not ok', }) } const cardIndex = todos.cards.findIndex((currcard) => { return currcard.id === parseInt(id) }) if (cardIndex === -1) { res.json({ msg: 'card not found', result: 'not ok', }) return } const ind = todos.cards[cardIndex].votes.indexOf(userId) if (ind === -1) { todos.cards[cardIndex].votes.push(userId) } else { todos.cards[cardIndex].votes.splice(ind, 1) } utils.WriteFile(JSON.stringify(todos, null, 2), todosFile) res.json({ todos: todos, userId: userId, msg: 'updated', result: 'ok', }) }) app.get('/todos', (req: Request, res: any) => { logger.LogReq(req) const userId = req.session.user.id const todos = utils.ReadJSON(todosFile) res.json({ todos: todos, userId: userId, result: 'ok', }) }) app.get('/ranklist', (req: Request, res: any) => { logger.LogReq(req) let result const querySince: any = req.query.since const user: User = req.session.user if (!querySince) { result = utils.ReadJSON(idStatFile) } else { try { const since = new Date(querySince) if (!(since instanceof Date) || isNaN(since.getTime())) { throw new Error('Not a date') } const data = utils.ReadJSON(idvStatFile) result = {} Object.keys(data).forEach((key) => { const dailyStat = data[key] if (new Date(key) > since) { Object.keys(dailyStat).forEach((userId) => { const userStat = dailyStat[userId] const uidRes = result[userId] if (!uidRes) { result[userId] = userStat } else { result[userId] = { count: uidRes.count + userStat.count, newQuestions: uidRes.newQuestions + userStat.newQuestions, allQuestions: uidRes.allQuestions + userStat.allQuestions, subjs: mergeObjSum(uidRes.subjs, userStat.subjs), } } }) } }) } catch (err) { res.json({ msg: 'invalid date format, or other error occured', }) } } const list = [] const sum = { count: 0, newQuestions: 0, allQuestions: 0, } Object.keys(result).forEach((key) => { list.push({ userId: parseInt(key), ...result[key], }) sum.count = sum.count + result[key].count sum.newQuestions = sum.newQuestions + result[key].newQuestions sum.allQuestions = sum.allQuestions + result[key].allQuestions }) if (list.length === 0) { res.json({ msg: 'There are no users in the stats db :c', }) return } res.json({ since: querySince, sum: sum, list: list, selfuserId: user.id, }) }) app.get('/quickvote', (req: Request, res: any) => { const key = req.query.key const val: any = req.query.val const user: User = req.session.user if (!key || !val) { res.render('votethank', { results: 'error', msg: 'no key or val query param!', }) return } // FIXME: check vote type in file let votes: any = {} if (utils.FileExists(quickVotes)) { votes = utils.ReadJSON(quickVotes) } else { logger.Log( `No such vote "${key}", and quickVotes.json is missing ( #${user.id}: ${key}-${val} )`, logger.GetColor('blue') ) res.render('votethank', { result: 'no such pool', }) return } if (!votes.voteNames.includes(key)) { logger.Log( `No such vote "${key}" ( #${user.id}: ${key}-${val} )`, logger.GetColor('blue') ) res.render('votethank', { result: 'no such pool', }) return } const voteFile = quickVoteResultsDir + '/' + key + '.json' let voteData = { votes: {}, sum: {}, } if (utils.FileExists(voteFile)) { voteData = utils.ReadJSON(voteFile) } else { utils.CreatePath(quickVoteResultsDir) } const prevVote = voteData.votes[user.id] voteData.votes[user.id] = val if (voteData.sum[val]) { voteData.sum[val]++ } else { voteData.sum[val] = 1 } if (prevVote) { if (voteData.sum[prevVote]) { voteData.sum[prevVote] -= 1 } } logger.Log(`Vote from #${user.id}: ${key}: ${val}`, logger.GetColor('blue')) res.render('votethank', { result: prevVote ? 'already voted' : 'success', prevVote: prevVote, msg: 'vote added', }) utils.WriteFile(JSON.stringify(voteData), voteFile) }) app.get('/avaiblePWS', (req: Request, res: any) => { logger.LogReq(req) const user: User = req.session.user res.json({ result: 'success', userCreated: user.created, avaiblePWS: user.avaiblePWRequests, requestedPWS: user.pwRequestCount, maxPWCount: maxPWCount, daysAfterUserGetsPWs: daysAfterUserGetsPWs, addPWPerDay: addPWPerDay, addPWCount: addPWCount, dayDiff: getDayDiff(user.created), }) }) app.post('/getpw', function(req: Request, res: any) { logger.LogReq(req) const requestingUser = req.session.user if (requestingUser.avaiblePWRequests <= 0) { res.json({ result: 'error', msg: 'Too many passwords requested or cant request password yet, try later', }) logger.Log( `User #${requestingUser.id} requested too much passwords`, logger.GetColor('cyan') ) return } dbtools.Update( userDB, 'users', { avaiblePWRequests: requestingUser.avaiblePWRequests - 1, pwRequestCount: requestingUser.pwRequestCount + 1, }, { id: requestingUser.id, } ) const pw = uuidv4() const insertRes = dbtools.Insert(userDB, 'users', { pw: pw, avaiblePWRequests: 0, created: utils.GetDateString(), }) logger.Log( `User #${requestingUser.id} created new user #${insertRes.lastInsertRowid}`, logger.GetColor('cyan') ) res.json({ result: 'success', pw: pw, requestedPWS: requestingUser.pwRequestCount + 1, remaining: requestingUser.avaiblePWRequests - 1, }) }) app.post('/login', (req: Request, res: any) => { logger.LogReq(req) const pw = req.body.pw ? req.body.pw .replace(/'/g, '') .replace(/"/g, '') .replace(/;/g, '') : false const isScript = req.body.script const ip = req.headers['cf-connecting-ip'] || req.connection.remoteAddress const user: User = dbtools.Select(userDB, 'users', { pw: pw, })[0] if (user) { const sessionID = uuidv4() const existingSessions = dbtools .Select(userDB, 'sessions', { userID: user.id, isScript: isScript ? 1 : 0, }) .sort((a, b) => { return new Date(a).getTime() - new Date(b).getTime() }) const diff = existingSessions.length - minimumAlowwedSessions if (diff > 0) { logger.Log( `Multiple ${isScript ? 'script' : 'website'} sessions ( ${ existingSessions.length } ) for #${user.id}, deleting olds`, logger.GetColor('cyan') ) for (let i = 0; i < diff; i++) { const id = existingSessions[i].id dbtools.Delete(userDB, 'sessions', { id: id, isScript: isScript ? 1 : 0, }) } } dbtools.Update( userDB, 'users', { loginCount: user.loginCount + 1, lastIP: ip, lastLogin: utils.GetDateString(), }, { id: user.id, } ) dbtools.Insert(userDB, 'sessions', { id: sessionID, ip: ip, userID: user.id, isScript: isScript ? 1 : 0, createDate: utils.GetDateString(), }) // https://www.npmjs.com/package/cookie // FIXME: cookies are not configured coorectly res.cookie('sessionID', sessionID, { domain: domain, expires: new Date( new Date().getTime() + 10 * 365 * 24 * 60 * 60 * 1000 ), sameSite: 'none', secure: true, }) res.cookie('sessionID', sessionID, { expires: new Date( new Date().getTime() + 10 * 365 * 24 * 60 * 60 * 1000 ), sameSite: 'none', secure: true, }) res.json({ result: 'success', msg: 'you are now logged in', }) logger.Log( `Successfull login to ${ isScript ? 'script' : 'website' } with user ID: #${user.id}`, logger.GetColor('cyan') ) } else { logger.Log( `Login attempt with invalid pw: ${pw} to ${ isScript ? 'script' : 'website' }`, logger.GetColor('cyan') ) res.json({ result: 'error', msg: 'Invalid password', }) } }) app.post('/logout', (req: Request, res: any) => { logger.LogReq(req) const sessionID = req.cookies.sessionID // removing session from db dbtools.Delete(userDB, 'sessions', { id: sessionID, }) res.clearCookie('sessionID').json({ result: 'success', }) }) // -------------------------------------------------------------- app.get('/', function(req: Request, res: any) { logger.LogReq(req) if (reloadRootRedirectURL) { res.redirect(rootRedirectURL) } else { res.json({ msg: 'hi c:' }) } }) app.post('/postfeedbackfile', function(req: Request, res: any) { UploadFile(req, res, uloadFiles, () => { res.json({ success: true }) }) logger.LogReq(req) logger.Log('New feedback file', logger.GetColor('bluebg')) }) app.post('/postfeedback', function(req: Request, res: any) { logger.LogReq(req) if (req.body.fromLogin) { logger.Log( 'New feedback message from Login page', logger.GetColor('bluebg') ) } else { logger.Log( 'New feedback message from feedback page', logger.GetColor('bluebg') ) } const ip = req.headers['cf-connecting-ip'] || req.connection.remoteAddress const user: User = req.session.user utils.AppendToFile( utils.GetDateString() + ':\n' + JSON.stringify( { ...req.body, userID: user ? user.id : 'no user', ip: ip, }, null, 2 ), msgFile ) res.json({ success: true }) }) function UploadFile(req: Request, res: any, path, next) { try { req.pipe(req.busboy) req.busboy.on('file', function(fieldname, file, filename) { logger.Log('Uploading: ' + filename, logger.GetColor('blue')) utils.CreatePath(path, true) const date = new Date() const fn = date.getHours() + '' + date.getMinutes() + '' + date.getSeconds() + '_' + filename const fstream = fs.createWriteStream(path + '/' + fn) file.pipe(fstream) fstream.on('close', function() { logger.Log( 'Upload Finished of ' + path + '/' + fn, logger.GetColor('blue') ) next(fn) }) fstream.on('error', function(err) { console.error(err) res.end('something bad happened :s') }) }) } catch (err) { logger.Log(`Unable to upload file!`, logger.GetColor('redbg')) console.error(err) } } app.route('/fosuploader').post(function(req: Request, res: any) { UploadFile(req, res, uloadFiles, (fn) => { res.redirect('/f/' + fn) }) }) app.route('/badtestsender').post(function(req: Request, res: any) { UploadFile(req, res, recivedFiles, () => { res.redirect('back') }) logger.LogReq(req) }) app.get('/allqr.txt', function(req: Request, res: any) { logger.LogReq(req) const db: any = req.query.db let stringifiedData = '' res.setHeader('content-type', 'text/plain; charset=utf-8') if (db) { const requestedDb = questionDbs.find((qdb) => { return qdb.name === db }) if (!requestedDb) { res.end(`No such db ${db}`) return } stringifiedData = '\n' + line stringifiedData += ` Questions in ${requestedDb.name}: ` stringifiedData += line + '\n' stringifiedData += dataToString(requestedDb.data) stringifiedData += '\n' + line + line + '\n' } else { stringifiedData = questionDbs .map((qdb) => { let result = '' result += '\n' + line result += ` Questions in ${qdb.name}: ` result += line + '\n' result += dataToString(qdb.data) result += '\n' + line + line + '\n' return result }) .join('\n\n') } res.end(stringifiedData) }) // ------------------------------------------------------------------------------------------- // API app.post('/uploaddata', (req: Request, res: any) => { // body: JSON.stringify({ // newData: data, // count: getCount(data), // initialCount: initialCount, // password: password, // editedQuestions: editedQuestions // }) const { count, initialCount, editedQuestions, password /*, newData*/, } = req.body const respStatuses = { invalidPass: 'invalidPass', ok: 'ok', error: 'error', } logger.LogReq(req) try { // finding user const pwds = JSON.parse(utils.ReadFile(passwordFile)) const userKey = Object.keys(pwds).find((key) => { const userKey = pwds[key] return userKey.password === password }) // FIXME: check user type in dataeditorPW-s json const user: any = pwds[userKey] // logging and stuff logger.Log(`Data upload`, logger.GetColor('bluebg')) logger.Log(`PWD: ${password}`, logger.GetColor('bluebg')) // returning if user password is not ok if (!user) { logger.Log( `Data upload: invalid password ${password}`, logger.GetColor('red') ) utils.AppendToFile( utils.GetDateString() + '\n' + password + '(FAILED PASSWORD)\n' + JSON.stringify(editedQuestions) + '\n\n', dataEditsLog ) res.json({ status: respStatuses.invalidPass }) return } logger.Log( `Password accepted for ${user.name}`, logger.GetColor('bluebg') ) logger.Log( `Old Subjects/Questions: ${initialCount.subjectCount} / ${ initialCount.questionCount } | New: ${count.subjectCount} / ${ count.questionCount } | Edited question count: ${Object.keys(editedQuestions).length}`, logger.GetColor('bluebg') ) // saving detailed editedCount utils.AppendToFile( utils.GetDateString() + '\n' + JSON.stringify(user) + '\n' + JSON.stringify(editedQuestions) + '\n\n', dataEditsLog ) // making backup // TODO // utils.CopyFile( // './' + dataFile, // `./publicDirs/qminingPublic/backs/data_before_${ // user.name // }_${utils.GetDateString().replace(/ /g, '_')}` // ) // TODO: rewrite to dinamyc public!!! // logger.Log('Backup made') // // writing data // utils.WriteFile(JSON.stringify(newData), dataFile) // logger.Log('New data file written') // // reloading data file // data = [...newData] // data = newData logger.Log('Data set to newData') res.json({ status: respStatuses.ok, user: user.name, }) logger.Log('Data updating done!', logger.GetColor('bluebg')) } catch (error) { logger.Log(`Data upload error! `, logger.GetColor('redbg')) console.error(error) res.json({ status: respStatuses.error, msg: error.message }) } }) function getNewQdb(location, maxIndex) { logger.Log( `No suitable questiondbs found for ${location}, creating a new one...` ) const newDb: DataFile = { path: `questionDbs/${location}.json`, name: location, shouldSearch: { location: { val: location, }, }, shouldSave: { location: { val: location, }, }, } utils.WriteFile( JSON.stringify( [ ...utils.ReadJSON(dbsFile), newDb, // stored as 'data.json', but is './publicDirs/.../data.json' runtime ], null, 2 ), dbsFile ) // "loading" new db const loadedNewDb: QuestionDb = { ...newDb, data: [], path: publicDir + newDb.path, index: maxIndex, } utils.WriteFile('[]', loadedNewDb.path) questionDbs.push(loadedNewDb) msgAllWorker({ newdb: loadedNewDb, type: 'newdb', }) return loadedNewDb } app.post('/isAdding', function(req: Request, res: any) { logger.LogReq(req) const user: User = req.session.user const dryRun = testUsers.includes(user.id) if (!req.body.location) { logger.Log( '\tbody.location is missing, client version:' + req.body.version ) res.json({ msg: 'body.location is missing' }) return } const location = req.body.location.split('/')[2] try { let maxIndex = -1 const suitedQuestionDbs = questionDbs.filter((qdb) => { if (maxIndex < qdb.index) { maxIndex = qdb.index } return shouldSaveDataFile(qdb, req.body) }, []) if (suitedQuestionDbs.length === 0) { suitedQuestionDbs.push(getNewQdb(location, maxIndex)) } processIncomingRequest(req.body, suitedQuestionDbs, dryRun, user) .then((resultArray: Array) => { logResult(req.body, resultArray, user.id, dryRun) const totalNewQuestions = resultArray.reduce((acc, sres) => { return acc + sres.newQuestions }, 0) res.json({ success: resultArray.length > 0, newQuestions: resultArray, totalNewQuestions: totalNewQuestions, }) if (totalNewQuestions > 0) { msgAllWorker({ qdbs: questionDbs, type: 'update', }) } }) .catch((err) => { logger.Log( 'Error during processing incoming request', logger.GetColor('redbg') ) console.error(err) res.json({ success: false, }) }) } catch (err) { logger.Log( 'Error during getting incoming request processor promises ', logger.GetColor('redbg') ) console.error(err) res.json({ success: false, }) } }) function writeAskData(body) { try { let towrite = utils.GetDateString() + '\n' towrite += '------------------------------------------------------------------------------\n' towrite += JSON.stringify(body) towrite += '\n------------------------------------------------------------------------------\n' utils.AppendToFile(towrite, recievedQuestionFile) } catch (err) { logger.Log('Error writing revieved /ask POST data') console.error(err) } } function saveQuestion(questions, subj, testUrl, userid) { // TODO: clear folder every now and then, check if saved questions exist const questionsToSave = { questions: questions, subj: subj, userid: userid, testUrl: testUrl, date: new Date(), } const fname = `${utils.GetDateString()}_${userid}_${testUrl}.json` const subject = getSubjNameWithoutYear(subj) const subjPath = `${savedQuestionsDir}/${subject}` const savedSubjQuestionsFilePath = `${subjPath}/${savedQuestionsFileName}` utils.CreatePath(subjPath, true) if (!utils.FileExists(savedSubjQuestionsFilePath)) { utils.WriteFile('[]', savedSubjQuestionsFilePath) } const savedQuestions = utils.ReadJSON(savedSubjQuestionsFilePath) savedQuestions.push({ fname: fname, subj: subj, userid: userid, testUrl: testUrl, date: new Date(), }) utils.WriteFile(JSON.stringify(savedQuestions), savedSubjQuestionsFilePath) utils.WriteFile(JSON.stringify(questionsToSave), `${subjPath}/${fname}`) } app.post('/ask', function(req: Request, res) { const user: User = req.session.user if (!req.body.questions) { res.json({ message: 'ask something! ?q=[question]&subj=[subject]&data=[question data]. "subj" is optimal for faster result', result: [], recievedData: JSON.stringify(req.body), success: false, }) return } const subj: any = req.body.subj || '' // TODO: test if testUrl is undefined (it shouldnt) const testUrl = req.body.testUrl ? req.body.testUrl.split('/')[2] : undefined writeAskData(req.body) // every question in a different thread const resultPromises = req.body.questions.map((question) => { return getResult(question, subj, null, req.body, testUrl) }) Promise.all(resultPromises).then((results) => { const response = results.map((result: any) => { return { answers: result.result, question: result.question, } }) res.json(response) const saveableQuestions = response.reduce((acc, res) => { if (res.answers.length === 0) { acc.push(res.question) } return acc }, []) if (saveableQuestions.length > 0) { saveQuestion(saveableQuestions, subj, testUrl, user.id) } }) }) app.get('/ask', function(req: Request, res) { if (Object.keys(req.query).length === 0) { logger.DebugLog(`No query params /ask GET`, 'ask', 1) res.json({ message: 'ask something! ?q=[question]&subj=[subject]&data=[question data]. "subj" is optimal for faster result', result: [], recievedData: JSON.stringify(req.query), success: false, }) return } if (req.query.q && req.query.data) { const subj: any = req.query.subj || '' const question = req.query.q const recData: any = req.query.data getResult(question, subj, recData, req.query).then((result) => { res.json(result) }) } else { logger.DebugLog(`Invalid question`, 'ask', 1) res.json({ message: `Invalid question :(`, result: [], recievedData: JSON.stringify(req.query), success: false, }) } }) function getDbIndexesToSearchIn(testUrl, trueIfAlways?) { return testUrl ? questionDbs.reduce((acc, qdb, i) => { if (shouldSearchDataFile(qdb, testUrl, trueIfAlways)) { acc.push(i) } return acc }, []) : [] } function getResult(question, subj, recData, recievedData, testUrl?) { return new Promise((resolve) => { const searchIn = getDbIndexesToSearchIn(testUrl, false) searchInDbs( question, subj, recData, recievedData, searchIn, testUrl ).then((res: any) => { if (res.result.length === 0) { logger.DebugLog( `No result while searching specific question db ${testUrl}`, 'ask', 1 ) const searchInMore = getDbIndexesToSearchIn(testUrl, true).filter( (x) => { return !searchIn.includes(x) } ) searchInDbs( question, subj, recData, recievedData, searchInMore, testUrl ).then((res) => { resolve(res) }) } else { resolve(res) } }) }) } function searchInDbs( question, subj, recData, recievedData, searchIn, testUrl? ) { // searchIn could be [0], [1], ... to search every db in different thread. Put this into a // forEach(qdbs) to achieve this return new Promise((resolve) => { doALongTask({ type: 'work', data: { searchIn: searchIn, testUrl: testUrl, question: question, subjName: subj, questionData: recData, searchInAllIfNoResult: true, }, }) .then((taskResult) => { try { logger.DebugLog( `Question result length: ${taskResult.length}`, 'ask', 1 ) logger.DebugLog(taskResult, 'ask', 2) resolve({ question: question, result: taskResult.result, success: true, }) } catch (err) { console.error(err) logger.Log( 'Error while sending ask results', logger.GetColor('redbg') ) } }) .catch((err) => { logger.Log('Search Data error!', logger.GetColor('redbg')) console.error(err) resolve({ mesage: `There was an error processing the question: ${err.message}`, result: [], recievedData: JSON.stringify(recievedData), success: false, }) }) }) } function getSubjCount(qdbs) { return qdbs.reduce((acc, qdb) => { return acc + qdb.data.length }, 0) } function getQuestionCount(qdbs) { return qdbs.reduce((acc, qdb) => { return ( acc + qdb.data.reduce((qacc, subject) => { return qacc + subject.Questions.length }, 0) ) }, 0) } function getSimplreRes() { return { subjects: getSubjCount(questionDbs), questions: getQuestionCount(questionDbs), } } function getDetailedRes() { return questionDbs.map((qdb) => { return { dbName: qdb.name, subjs: qdb.data.map((subj) => { return { name: subj.Name, count: subj.Questions.length, } }), } }) } app.get('/datacount', function(req: Request, res: any) { logger.LogReq(req) if (req.query.detailed === 'all') { res.json({ detailed: getDetailedRes(), simple: getSimplreRes(), }) } else if (req.query.detailed) { res.json(getDetailedRes()) } else { res.json(getSimplreRes()) } }) function getMotd(version) { if (version) { if (version.startsWith('2.0.')) { if (utils.FileExists(oldMotdFile)) { return utils.ReadFile(oldMotdFile) } } } return motd } app.get('/infos', function(req: Request, res) { const user: User = req.session.user const result: any = { result: 'success', uid: user.id, } if (req.query.subjinfo) { result.subjinfo = getSimplreRes() } if (req.query.version) { result.version = version } if (req.query.motd) { result.motd = getMotd(req.query.cversion) if (userSpecificMotd[user.id]) { result.userSpecificMotd = { msg: userSpecificMotd[user.id].msg, seen: userSpecificMotd[user.id].seen, } } } res.json(result) }) app.post('/infos', (req: Request, res) => { const user: User = req.session.user if (req.body.userSpecificMotdSeen) { userSpecificMotd[user.id].seen = true logger.Log( `User #${user.id}'s user specific motd is now seen.`, logger.GetColor('bluebg') ) utils.WriteFile( JSON.stringify(userSpecificMotd, null, 2), userSpecificMotdFile ) } res.json({ msg: 'done' }) }) app.post('/registerscript', function(req: Request, res) { logger.LogReq(req) if (!utils.FileExists(registeredScriptsFile)) { utils.WriteFile('[]', registeredScriptsFile) } const ip: any = req.headers['cf-connecting-ip'] || req.connection.remoteAddress const ua: any = req.headers['user-agent'] const registeredScripts = utils.ReadJSON(registeredScriptsFile) const { cid, uid, version, installSource, date } = req.body const index = registeredScripts.findIndex((registration) => { return registration.cid === cid }) if (index === -1) { const x: any = { cid: cid, version: version, installSource: installSource, date: date, ip: ip, userAgent: ua, } if (uid) { x.uid = uid x.loginDate = date } registeredScripts.push(x) } else { const currRegistration = registeredScripts[index] if (!currRegistration.uid && uid) { registeredScripts[index] = { ...registeredScripts[index], uid: uid, loginDate: date, } } else { logger.DebugLog( `cid: ${cid}, uid: ${uid} tried to register multiple times`, 'register', 1 ) } } utils.WriteFile( JSON.stringify(registeredScripts, null, 2), registeredScriptsFile ) res.json({ msg: 'done' }) }) // ------------------------------------------------------------------------------------------- app.get('*', function(req: Request, res: any) { res.status(404).render('404') }) app.post('*', function(req: Request, res: any) { res.status(404).render('404') }) function ExportDailyDataCount() { logger.Log('Saving daily data count ...') utils.AppendToFile( JSON.stringify({ date: utils.GetDateString(), subjectCount: getSubjCount(questionDbs), questionCount: getQuestionCount(questionDbs), userCount: dbtools.TableInfo(userDB, 'users').dataCount, }), dailyDataCountFile ) } function BackupDB() { logger.Log('Backing up auth DB ...') utils.CreatePath(usersDbBackupPath, true) userDB .backup( `${usersDbBackupPath}/users.${utils .GetDateString() .replace(/ /g, '_')}.db` ) .then(() => { logger.Log('Auth DB backup complete!') }) .catch((err) => { logger.Log('Auth DB backup failed!', logger.GetColor('redbg')) console.error(err) }) } function getDayDiff(dateString) { const msdiff = new Date().getTime() - new Date(dateString).getTime() return Math.floor(msdiff / (1000 * 3600 * 24)) } function IncrementAvaiblePWs() { // FIXME: check this if this is legit and works logger.Log('Incrementing avaible PW-s ...') const users: Array = dbtools.SelectAll(userDB, 'users') users.forEach((user) => { if (user.avaiblePWRequests >= maxPWCount) { return } const dayDiff = getDayDiff(user.created) if (dayDiff < daysAfterUserGetsPWs) { logger.Log( `User #${user.id} is not registered long enough to get password ( ${dayDiff} days, ${daysAfterUserGetsPWs} needed)`, logger.GetColor('cyan') ) return } if (dayDiff % addPWPerDay === 0) { logger.Log( `Incrementing avaible PW-s for user #${user.id}: ${ user.avaiblePWRequests } -> ${user.avaiblePWRequests + addPWCount}`, logger.GetColor('cyan') ) dbtools.Update( userDB, 'users', { avaiblePWRequests: user.avaiblePWRequests + addPWCount, }, { id: user.id, } ) } }) } function DailyAction() { backupData(questionDbs) BackupDB() ExportDailyDataCount() IncrementAvaiblePWs() } return { dailyAction: DailyAction, app: app, } } export default { name: 'API', getApp: GetApp, setup: (data: SetupData): void => { userDB = data.userDB url = data.url // eslint-disable-line publicdirs = data.publicdirs }, }