diff --git a/src/modules/api/api.ts b/src/modules/api/api.ts index 452f426..a2377d0 100644 --- a/src/modules/api/api.ts +++ b/src/modules/api/api.ts @@ -22,75 +22,19 @@ 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, - writeData, - shouldSaveDataFile, - shouldSearchDataFile, - loadJSON, - Result, - isQuestionValid, -} from '../../utils/actions' -import dbtools from '../../utils/dbtools' import auth from '../../middlewares/auth.middleware' -import { - dataToString, - getSubjNameWithoutYear, - createQuestion, - // compareQuestionObj, -} from '../../utils/classes' -import { - initWorkerPool, - doALongTask, - msgAllWorker, -} from '../../utils/workerPool' - import { SetupData } from '../../server' -import { - ModuleType, - User, - DataFile, - Request, - QuestionDb, -} from '../../types/basicTypes' +import { ModuleType, Request } from '../../types/basicTypes' // files -const msgFile = 'stats/msgs' -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 adminUsersFile = 'data/admins.json' -const oldMotdFile = 'publicDirs/qminingPublic/oldMotd' -const uloadFiles = 'data/f' // other constants -const line = '====================================================' // lol const moduleName = 'API' -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 @@ -105,14 +49,6 @@ function GetApp(): ModuleType { 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 motdFile = publicDir + 'motd' - const userSpecificMotdFile = publicDir + 'userSpecificMotd.json' - const newsFile = publicDir + 'news.json' - let domain = url.split('.') // [ "https://api", "frylabs", "net" ] domain.shift() // [ "frylabs", "net" ] domain = domain.join('.') // "frylabs.net" @@ -157,61 +93,7 @@ function GetApp(): ModuleType { }) ) - const dataFiles: Array = utils.ReadJSON(dbsFile) - const questionDbs: Array = 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)) { @@ -220,26 +102,6 @@ function GetApp(): ModuleType { } 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', @@ -248,8 +110,6 @@ function GetApp(): ModuleType { ] function Load() { - backupData(questionDbs) - filesToWatch.forEach((ftw) => { if (utils.FileExists(ftw.fname)) { utils.WatchFile(ftw.fname, () => { @@ -268,428 +128,6 @@ function GetApp(): ModuleType { 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, - locked: qdb.locked, - } - }) - ) - }) - - 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.get('/logout', (req: Request, res: any) => { - logger.LogReq(req) - const sessionID = req.cookies.sessionID - const user: User = req.session.user - - if (!user) { - res.json({ - msg: 'You are not logged in', - success: false, - }) - return - } - - logger.Log( - `Successfull logout with user ID: #${user.id}`, - logger.GetColor('cyan') - ) - - // removing session from db - dbtools.Delete(userDB, 'sessions', { - id: sessionID, - }) - res.clearCookie('sessionID').json({ - msg: 'Successfull logout', - result: 'success', - }) - }) - // -------------------------------------------------------------- app.get('/', function(req: Request, res: any) { @@ -701,1258 +139,9 @@ function GetApp(): ModuleType { } }) - 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: string, 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) - }) - // ------------------------------------------------------------------------------------------- - 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 - } - - function dbExists(location, qdbs: Array) { - return qdbs.some((qdb) => { - return qdb.name === location - }) - } - - 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) { - if (!dbExists(location, questionDbs)) { - suitedQuestionDbs.push(getNewQdb(location, maxIndex)) - } else { - logger.Log( - `Tried to add existing db named ${location}!`, - logger.GetColor('red') - ) - } - } - if (suitedQuestionDbs.length === 0) { - res.json({ - status: 'fail', - msg: 'No suitable dbs to add questions to', - }) - return - } - - 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 - if (!subj) { - logger.Log('No subj name to save test question') - return - } - const questionsToSave = { - questions: questions, - subj: subj, - userid: userid, - testUrl: testUrl, - date: new Date(), - } - const fname = `${utils.GetDateString()}_${userid}_${testUrl}.json` - const subject = getSubjNameWithoutYear(subj).replace(/\//g, '-') - const subjPath = `${savedQuestionsDir}/${subject}` - const savedSubjQuestionsFilePath = `${subjPath}/${savedQuestionsFileName}` - - utils.CreatePath(subjPath, true) - if (!utils.FileExists(savedSubjQuestionsFilePath)) { - utils.WriteFile('[]', savedSubjQuestionsFilePath) - } - - const savedQuestions = utils.ReadJSON(savedSubjQuestionsFilePath) - - const testExists = false - // TODO: do this on another thread? - // const testExists = savedQuestions.some((savedQuestion) => { - // const data = utils.ReadJSON(`${subjPath}/${savedQuestion.fname}`) - - // return data.questions.some((dQuestion) => { - // return questions.some((question) => { - // const percent = compareQuestionObj( - // createQuestion(question), - // '', - // createQuestion(dQuestion), - // '' - // ) - - // return percent.avg === 100 - // }) - // }) - // }) - - if (testExists) { - return - } - - 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! { question:'' ,subject:'', location:'' }`, - 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: string, trueIfAlways?: boolean) { - 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) { - userSpecificMotd[user.id].seen = true - - logger.Log( - `User #${user.id}'s user specific motd is now seen:`, - logger.GetColor('bluebg') - ) - logger.Log(userSpecificMotd[user.id].msg) - utils.WriteFile( - JSON.stringify(userSpecificMotd, null, 2), - userSpecificMotdFile - ) - } - - res.json({ msg: 'done' }) - }) - - function addReaction(obj, path, { reaction, isDelete, uid }) { - if (path.length === 1) { - const index = path[0] - if (!obj[index].reacts) { - obj[index].reacts = {} - } - if (isDelete) { - if (obj[index].reacts[reaction]) { - obj[index].reacts[reaction] = obj[index].reacts[reaction].filter( - (uid) => { - return uid !== uid - } - ) - if (obj[index].reacts[reaction].length === 0) { - delete obj[index].reacts[reaction] - } - } - } else { - if (!obj[index].reacts[reaction]) { - obj[index].reacts[reaction] = [uid] - } else { - if (!obj[index].reacts[reaction].includes(uid)) { - obj[index].reacts[reaction].push(uid) - } - } - } - } else { - const i = path.pop() - addReaction(obj[i].subComments, path, { - reaction: reaction, - isDelete: isDelete, - uid: uid, - }) - } - } - - app.post('/react', (req: Request, res) => { - logger.LogReq(req) - - const user: User = req.session.user - const news: any = utils.ReadJSON(newsFile) - - const { newsKey, path, reaction, isDelete } = req.body - if (!newsKey || !reaction) { - res.json({ status: 'fail', msg: 'no newskey or reaction' }) - return - } - if (!path || path.length === 0) { - if (news[newsKey]) { - if (isDelete) { - if (news[newsKey].reacts) { - news[newsKey].reacts[reaction] = news[newsKey].reacts[ - reaction - ].filter((uid) => { - return uid !== user.id - }) - if (news[newsKey].reacts[reaction].length === 0) { - delete news[newsKey].reacts[reaction] - } - } - } else { - if (!news[newsKey].reacts) { - news[newsKey].reacts = { [reaction]: [user.id] } - } else { - if (Array.isArray(news[newsKey].reacts[reaction])) { - if (!news[newsKey].reacts[reaction].includes(user.id)) { - news[newsKey].reacts[reaction].push(user.id) - } - } else { - news[newsKey].reacts[reaction] = [user.id] - } - } - } - } - } else { - addReaction(news[newsKey].comments, path, { - reaction: reaction, - isDelete: isDelete, - uid: user.id, - }) - } - - utils.WriteFile(JSON.stringify(news, null, 2), newsFile) - res.json({ status: 'ok', news: news }) - }) - - function addComment(obj, path, comment) { - if (path.length === 0) { - obj.push(comment) - } else { - const i = path.pop() - if (!obj[i].subComments) { - obj[i].subComments = [] - } - addComment(obj[i].subComments, path, comment) - } - } - - function deleteComment( - obj: any, - path: Array, - userid: number - ): boolean { - if (path.length === 1) { - if (obj[path[0]].user === userid) { - obj.splice(path[0], 1) - return true - } else { - return false - } - } else { - const i = path.pop() - deleteComment(obj[i].subComments, path, userid) - return true - } - } - - app.post('/comment', (req: Request, res) => { - logger.LogReq(req) - - const user: User = req.session.user - const news: any = utils.ReadJSON(newsFile) - const admins: any = utils.FileExists(adminUsersFile) - ? utils.ReadJSON(adminUsersFile) - : [] - const { type, path, newsKey } = req.body - if (!type || !path || !newsKey) { - res.json({ status: 'fail', msg: ' type or path or newsKey is undefined' }) - return - } - - if (type === 'add') { - const { content } = req.body - const comment = { - date: utils.GetDateString(), - user: user.id, - content: content, - admin: admins.includes(user.id), - } - if (!news[newsKey].comments) { - news[newsKey].comments = [] - } - addComment(news[newsKey].comments, path, comment) - } else if (type === 'delete') { - if (news[newsKey].comments) { - const success = deleteComment(news[newsKey].comments, path, user.id) - if (!success) { - res.json({ - status: 'fail', - msg: 'you cant delete other users comments', - news: news, - }) - return - } - } - } else { - res.json({ status: 'fail', msg: 'no such type', news: news }) - return - } - utils.WriteFile(JSON.stringify(news, null, 2), newsFile) - res.json({ status: 'ok', news: news }) - }) - - 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.post('/rmPost', (req: Request, res) => { - logger.LogReq(req) - const user: User = req.session.user - const news: any = utils.ReadJSON(newsFile) - const { newsKey } = req.body - - if (news[newsKey].user === user.id) { - delete news[newsKey] - } else { - res.json({ - status: 'fail', - msg: 'u cant delete other users posts!', - news: news, - }) - return - } - - utils.WriteFile(JSON.stringify(news, null, 2), newsFile) - res.json({ status: 'ok', news: news }) - }) - - app.post('/addPost', (req: Request, res) => { - logger.LogReq(req) - const user: User = req.session.user - const news: any = utils.ReadJSON(newsFile) - const admins: any = utils.FileExists(adminUsersFile) - ? utils.ReadJSON(adminUsersFile) - : [] - const { title, content } = req.body - - news[uuidv4()] = { - date: utils.GetDateString(), - user: user.id, - title: title, - content: content, - admin: admins.includes(user.id), - } - - utils.WriteFile(JSON.stringify(news, null, 2), newsFile) - res.json({ status: 'ok', news: news }) - }) - - app.get('/possibleAnswers', (req: Request, res: any) => { - logger.LogReq(req) - const files = utils.ReadDir(savedQuestionsDir) - - files.sort(function(a, b) { - return ( - fs.statSync(savedQuestionsDir + '/' + b).mtime.getTime() - - fs.statSync(savedQuestionsDir + '/' + a).mtime.getTime() - ) - }) - - res.json({ - savedQuestionsFileName: savedQuestionsFileName, - subjects: files.map((subj) => { - return { - name: subj, - path: `savedQuestions/${subj}/`, - } - }), - }) - }) - - app.post('/rmPossibleAnswer', (req: Request, res: any) => { - logger.LogReq(req) - const user: User = req.session.user - - const subj = req.body.subj - const file = req.body.file - const savedQuestionsPath = `${savedQuestionsDir}/${subj}/${savedQuestionsFileName}` - const savedQuestions = utils.ReadJSON(savedQuestionsPath) - let path = `${savedQuestionsDir}/${subj}/${file}` - // to prevent deleting ../../../ ... /etc/shadow - while (path.includes('..')) { - path = path.replace(/\.\./g, '.') - } - - if (utils.FileExists(path)) { - utils.deleteFile(path) - - utils.WriteFile( - JSON.stringify( - savedQuestions.filter((sq) => { - return sq.fname !== file - }) - ), - savedQuestionsPath - ) - - logger.Log( - `User #${user.id} deleted '${file}' from subject '${subj}'`, - logger.GetColor('cyan') - ) - res.json({ - res: 'ok', - }) - } else { - logger.Log( - `User #${user.id} tried to delete '${file}' from subject '${subj}', but failed`, - logger.GetColor('red') - ) - res.json({ - res: 'fail', - }) - } - }) - - app.post('/updateQuestion', (req: Request, res) => { - logger.LogReq(req) - const user: User = req.session.user - const date = utils.GetDateString() - let saveDb = false - - const editType = req.body.type - const selectedDb = req.body.selectedDb - if (!editType || !selectedDb) { - res.json({ - status: 'fail', - msg: 'No .editType or .selectedDb !', - }) - return - } - - const dbIndex = questionDbs.findIndex((qdb) => { - return qdb.name === selectedDb.name - }) - const currDb = questionDbs[dbIndex] - - if (dbIndex === -1) { - res.json({ - status: 'fail', - msg: `No question db named like ${selectedDb.name}!`, - }) - return - } - - // { - // "index": 0, - // "subjName": "VHDL programozás", - // "type": "delete", - // "selectedDb": { - // "path": "questionDbs/elearning.uni-obuda.hu.json", - // "name": "elearning.uni-obuda.hu" - // } - // } - if (editType === 'delete') { - const { index, subjName } = req.body - let deletedQuestion = {} - if (isNaN(index) || !subjName) { - res.json({ - status: 'fail', - msg: 'No .index or .subjName !', - }) - return - } - - questionDbs[dbIndex].data = currDb.data.map((subj) => { - if (subj.Name !== subjName) { - return subj - } else { - return { - ...subj, - Questions: subj.Questions.filter((question, i) => { - if (index === i) { - deletedQuestion = question - return false - } else { - return true - } - }), - } - } - }) - - logger.Log( - `User #${user.id} deleted a question from '${subjName}'`, - logger.GetColor('cyan') - ) - utils.AppendToFile( - `${date}: User ${user.id} deleted a question from '${subjName}' (index: ${index})`, - dataEditsLog - ) - utils.AppendToFile(JSON.stringify(deletedQuestion, null, 2), dataEditsLog) - saveDb = true - } - - // { - // "index": 0, - // "subjName": "Elektronika", - // "type": "edit", - // "newVal": { - // "Q": "Analóg műszer esetén az érzékenység az a legkisebb mennyiség, amely a műszer kijelzőjén meghatározott mértékű változást okoz.", - // "A": "Igaz", - // "data": { - // "type": "simple", - // "possibleAnswers": [ - // "Igaz" - // ] - // }, - // "possibleAnswers": [ - // "Igaz" - // ] - // }, - // "selectedDb": { - // "path": "questionDbs/elearning.uni-obuda.hu.json", - // "name": "elearning.uni-obuda.hu" - // } - // } - if (editType === 'edit') { - const { index, subjName, newVal } = req.body - let oldVal = {} - if (isNaN(index) || !subjName) { - res.json({ - status: 'fail', - msg: 'No .index or .subjName !', - }) - return - } - if (!isQuestionValid(newVal)) { - res.json({ - status: 'fail', - msg: 'edited question is not valid', - question: newVal, - }) - return - } - - questionDbs[dbIndex].data = currDb.data.map((subj) => { - if (subj.Name !== subjName) { - return subj - } else { - return { - ...subj, - Questions: subj.Questions.map((question, i) => { - if (index === i) { - oldVal = question - return createQuestion(newVal) - } else { - return question - } - }), - } - } - }) - - logger.Log( - `User #${user.id} edited a question in '${subjName}'`, - logger.GetColor('cyan') - ) - utils.AppendToFile( - `${date}: User ${user.id} edited a question in '${subjName}' (index: ${index})`, - dataEditsLog - ) - utils.AppendToFile( - JSON.stringify( - { - newVal: newVal, - oldVal: oldVal, - }, - null, - 2 - ), - dataEditsLog - ) - saveDb = true - } - - // { - // "subjName": "Elektronika", - // "changedQuestions": [ - // { - // "index": 1, - // "value": { - // "Q": "A műszer pontosságát a hibájával fejezzük ki, melyet az osztályjel (osztálypontosság ) mutat meg.", - // "A": "Hamis", - // "data": { - // "type": "simple", - // "possibleAnswers": [ - // "Igaz", - // "Hamis" - // ] - // } - // } - // } - // ], - // "deletedQuestions": [ - // 0 - // ], - // "type": "subjEdit", - // "selectedDb": { - // "path": "questionDbs/elearning.uni-obuda.hu.json", - // "name": "elearning.uni-obuda.hu" - // } - // } - if (editType === 'subjEdit') { - const { subjName, changedQuestions, deletedQuestions } = req.body - const deletedQuestionsToWrite = [] - const changedQuestionsToWrite = [] - if ( - !Array.isArray(changedQuestions) || - !Array.isArray(deletedQuestions) - ) { - res.json({ - status: 'fail', - msg: 'no changedQuestions or deletedQuestions!', - }) - return - } - - // processing changed questions - questionDbs[dbIndex].data = currDb.data.map((subj) => { - if (subj.Name !== subjName) { - return subj - } else { - return { - ...subj, - Questions: subj.Questions.map((question, i) => { - const changedTo = changedQuestions.find((cq) => { - return cq.index === i - }) - if (changedTo) { - changedQuestionsToWrite.push({ - oldVal: question, - newVal: changedTo.value, - }) - return createQuestion(changedTo.value) - } else { - return question - } - }), - } - } - }) - - // processing deletedQuestions - questionDbs[dbIndex].data = currDb.data.map((subj) => { - if (subj.Name !== subjName) { - return subj - } else { - return { - ...subj, - Questions: subj.Questions.filter((question, i) => { - const isDeleted = deletedQuestions.includes(i) - if (isDeleted) { - deletedQuestionsToWrite.push(question) - return false - } else { - return true - } - }), - } - } - }) - - logger.Log( - `User #${user.id} modified '${subjName}'. Edited: ${deletedQuestionsToWrite.length}, deleted: ${deletedQuestionsToWrite.length}`, - logger.GetColor('cyan') - ) - utils.AppendToFile( - `${date} User #${user.id} modified '${subjName}'. Edited: ${deletedQuestionsToWrite.length}, deleted: ${deletedQuestionsToWrite.length}`, - dataEditsLog - ) - utils.AppendToFile( - JSON.stringify( - { - deletedQuestions: deletedQuestionsToWrite, - changedQuestions: changedQuestionsToWrite, - }, - null, - 2 - ), - dataEditsLog - ) - saveDb = true - } - - if (saveDb) { - writeData(currDb.data, currDb.path) - msgAllWorker({ - qdbs: questionDbs, - type: 'update', - }) - } - - res.json({ - status: 'OK', - }) - }) - - setupSubModules(app) + const submoduleDatas = setupSubModules(app) // ------------------------------------------------------------------------------------------- @@ -1964,102 +153,31 @@ function GetApp(): ModuleType { 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() { + submoduleDatas.forEach((data) => { + if (data.dailyAction) { + data.dailyAction() } }) } - function DailyAction() { - backupData(questionDbs) - BackupDB() - ExportDailyDataCount() - IncrementAvaiblePWs() - } - return { dailyAction: DailyAction, app: app, } } -function setupSubModules(parentApp) { +function setupSubModules( + parentApp: express.Application, + moduleSpecificData?: any +): any { const submoduleDir = './submodules/' const absolutePath = __dirname + '/' + submoduleDir if (!utils.FileExists(absolutePath)) { return } const files = utils.ReadDir(absolutePath) + const moduleDatas = [] files.forEach((file) => { if (!file.endsWith('.js')) { return @@ -2067,19 +185,23 @@ function setupSubModules(parentApp) { const submodulePath = submoduleDir + file try { + logger.Log(`Loading submodule '${file}' for '${moduleName}'...`) const mod = require(submodulePath).default // eslint-disable-line - mod.setup({ + const loadedModData = mod.setup({ app: parentApp, userDB: userDB, url: url, publicdirs: publicdirs, + moduleSpecificData: moduleSpecificData, }) - logger.Log(`Submodule '${file}' loaded for '${moduleName}'`) + moduleDatas.push(loadedModData || {}) } catch (e) { logger.Log(`Error loading submodule from ${submodulePath}`) console.error(e) } }) + + return moduleDatas } export default { diff --git a/src/modules/api/submodules/feedback.ts b/src/modules/api/submodules/feedback.ts new file mode 100644 index 0000000..2908bae --- /dev/null +++ b/src/modules/api/submodules/feedback.ts @@ -0,0 +1,102 @@ +import fs from 'fs' + +import logger from '../../../utils/logger' +import utils from '../../../utils/utils' +import { Request, SubmoduleData, User } from '../../../types/basicTypes' + +const msgFile = 'stats/msgs' +const uloadFiles = 'data/f' + +function setup(data: SubmoduleData): void { + const { app /* userDB, url, publicdirs, moduleSpecificData */ } = data + + 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: string, 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) + }) + }) +} + +export default { + setup: setup, +} diff --git a/src/modules/api/submodules/forum.ts b/src/modules/api/submodules/forum.ts new file mode 100644 index 0000000..9ef878b --- /dev/null +++ b/src/modules/api/submodules/forum.ts @@ -0,0 +1,220 @@ +import { v4 as uuidv4 } from 'uuid' + +import logger from '../../../utils/logger' +import utils from '../../../utils/utils' +import { Request, SubmoduleData, User } from '../../../types/basicTypes' + +const adminUsersFile = 'data/admins.json' + +function addComment(obj, path, comment) { + if (path.length === 0) { + obj.push(comment) + } else { + const i = path.pop() + if (!obj[i].subComments) { + obj[i].subComments = [] + } + addComment(obj[i].subComments, path, comment) + } +} + +function deleteComment(obj: any, path: Array, userid: number): boolean { + if (path.length === 1) { + if (obj[path[0]].user === userid) { + obj.splice(path[0], 1) + return true + } else { + return false + } + } else { + const i = path.pop() + deleteComment(obj[i].subComments, path, userid) + return true + } +} + +function addReaction(obj, path, { reaction, isDelete, uid }) { + if (path.length === 1) { + const index = path[0] + if (!obj[index].reacts) { + obj[index].reacts = {} + } + if (isDelete) { + if (obj[index].reacts[reaction]) { + obj[index].reacts[reaction] = obj[index].reacts[reaction].filter( + (uid) => { + return uid !== uid + } + ) + if (obj[index].reacts[reaction].length === 0) { + delete obj[index].reacts[reaction] + } + } + } else { + if (!obj[index].reacts[reaction]) { + obj[index].reacts[reaction] = [uid] + } else { + if (!obj[index].reacts[reaction].includes(uid)) { + obj[index].reacts[reaction].push(uid) + } + } + } + } else { + const i = path.pop() + addReaction(obj[i].subComments, path, { + reaction: reaction, + isDelete: isDelete, + uid: uid, + }) + } +} + +function setup(data: SubmoduleData): void { + const { app, /* userDB, url, */ publicdirs /*, moduleSpecificData */ } = data + + const publicDir = publicdirs[0] + + const newsFile = publicDir + 'news.json' + + app.post('/react', (req: Request, res) => { + logger.LogReq(req) + + const user: User = req.session.user + const news: any = utils.ReadJSON(newsFile) + + const { newsKey, path, reaction, isDelete } = req.body + if (!newsKey || !reaction) { + res.json({ status: 'fail', msg: 'no newskey or reaction' }) + return + } + if (!path || path.length === 0) { + if (news[newsKey]) { + if (isDelete) { + if (news[newsKey].reacts) { + news[newsKey].reacts[reaction] = news[newsKey].reacts[ + reaction + ].filter((uid) => { + return uid !== user.id + }) + if (news[newsKey].reacts[reaction].length === 0) { + delete news[newsKey].reacts[reaction] + } + } + } else { + if (!news[newsKey].reacts) { + news[newsKey].reacts = { [reaction]: [user.id] } + } else { + if (Array.isArray(news[newsKey].reacts[reaction])) { + if (!news[newsKey].reacts[reaction].includes(user.id)) { + news[newsKey].reacts[reaction].push(user.id) + } + } else { + news[newsKey].reacts[reaction] = [user.id] + } + } + } + } + } else { + addReaction(news[newsKey].comments, path, { + reaction: reaction, + isDelete: isDelete, + uid: user.id, + }) + } + + utils.WriteFile(JSON.stringify(news, null, 2), newsFile) + res.json({ status: 'ok', news: news }) + }) + + app.post('/comment', (req: Request, res) => { + logger.LogReq(req) + + const user: User = req.session.user + const news: any = utils.ReadJSON(newsFile) + const admins: any = utils.FileExists(adminUsersFile) + ? utils.ReadJSON(adminUsersFile) + : [] + const { type, path, newsKey } = req.body + if (!type || !path || !newsKey) { + res.json({ status: 'fail', msg: ' type or path or newsKey is undefined' }) + return + } + + if (type === 'add') { + const { content } = req.body + const comment = { + date: utils.GetDateString(), + user: user.id, + content: content, + admin: admins.includes(user.id), + } + if (!news[newsKey].comments) { + news[newsKey].comments = [] + } + addComment(news[newsKey].comments, path, comment) + } else if (type === 'delete') { + if (news[newsKey].comments) { + const success = deleteComment(news[newsKey].comments, path, user.id) + if (!success) { + res.json({ + status: 'fail', + msg: 'you cant delete other users comments', + news: news, + }) + return + } + } + } else { + res.json({ status: 'fail', msg: 'no such type', news: news }) + return + } + utils.WriteFile(JSON.stringify(news, null, 2), newsFile) + res.json({ status: 'ok', news: news }) + }) + + app.post('/rmPost', (req: Request, res) => { + logger.LogReq(req) + const user: User = req.session.user + const news: any = utils.ReadJSON(newsFile) + const { newsKey } = req.body + + if (news[newsKey].user === user.id) { + delete news[newsKey] + } else { + res.json({ + status: 'fail', + msg: 'u cant delete other users posts!', + news: news, + }) + return + } + + utils.WriteFile(JSON.stringify(news, null, 2), newsFile) + res.json({ status: 'ok', news: news }) + }) + + app.post('/addPost', (req: Request, res) => { + logger.LogReq(req) + const user: User = req.session.user + const news: any = utils.ReadJSON(newsFile) + const admins: any = utils.FileExists(adminUsersFile) + ? utils.ReadJSON(adminUsersFile) + : [] + const { title, content } = req.body + + news[uuidv4()] = { + date: utils.GetDateString(), + user: user.id, + title: title, + content: content, + admin: admins.includes(user.id), + } + + utils.WriteFile(JSON.stringify(news, null, 2), newsFile) + res.json({ status: 'ok', news: news }) + }) +} + +export default { + setup: setup, +} diff --git a/src/modules/api/submodules/qmining.ts b/src/modules/api/submodules/qmining.ts new file mode 100644 index 0000000..0d370a8 --- /dev/null +++ b/src/modules/api/submodules/qmining.ts @@ -0,0 +1,1147 @@ +import fs from 'fs' + +import logger from '../../../utils/logger' +import utils from '../../../utils/utils' +import { + User, + DataFile, + Request, + QuestionDb, + SubmoduleData, +} from '../../../types/basicTypes' +import { + processIncomingRequest, + logResult, + shouldSaveDataFile, + Result, + isQuestionValid, + backupData, + shouldSearchDataFile, + loadJSON, + writeData, +} from '../../../utils/actions' +import { + createQuestion, + dataToString, + getSubjNameWithoutYear, + // compareQuestionObj, +} from '../../../utils/classes' +import { + doALongTask, + msgAllWorker, + initWorkerPool, +} from '../../../utils/workerPool' +import dbtools from '../../../utils/dbtools' + +const line = '====================================================' // lol +const registeredScriptsFile = 'stats/registeredScripts.json' +const testUsersFile = 'data/testUsers.json' +const userScriptFile = 'submodules/moodle-test-userscript/stable.user.js' +const recievedQuestionFile = 'stats/recievedQuestions' +const savedQuestionsFileName = 'savedQuestions.json' +const oldMotdFile = 'publicDirs/qminingPublic/oldMotd' +const dailyDataCountFile = 'stats/dailyDataCount' +const dataEditsLog = 'stats/dataEdits' + +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 ExportDailyDataCount(questionDbs, userDB) { + 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 getDbIndexesToSearchIn( + testUrl: string, + questionDbs, + trueIfAlways?: boolean +) { + return testUrl + ? questionDbs.reduce((acc, qdb, i) => { + if (shouldSearchDataFile(qdb, testUrl, trueIfAlways)) { + acc.push(i) + } + return acc + }, []) + : [] +} + +function getSimplreRes(questionDbs) { + return { + subjects: getSubjCount(questionDbs), + questions: getQuestionCount(questionDbs), + } +} + +function getDetailedRes(questionDbs) { + return questionDbs.map((qdb) => { + return { + dbName: qdb.name, + subjs: qdb.data.map((subj) => { + return { + name: subj.Name, + count: subj.Questions.length, + } + }), + } + }) +} + +function getMotd(version, motd) { + if (version) { + if (version.startsWith('2.0.')) { + if (utils.FileExists(oldMotdFile)) { + return utils.ReadFile(oldMotdFile) + } + } + } + return motd +} + +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 getResult( + question, + subj, + recData, + recievedData, + questionDbs, + testUrl? +) { + return new Promise((resolve) => { + const searchIn = getDbIndexesToSearchIn(testUrl, questionDbs, 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, + questionDbs, + true + ).filter((x) => { + return !searchIn.includes(x) + }) + searchInDbs( + question, + subj, + recData, + recievedData, + searchInMore, + testUrl + ).then((res) => { + resolve(res) + }) + } else { + resolve(res) + } + } + ) + }) +} + +function dbExists(location, qdbs: Array) { + return qdbs.some((qdb) => { + return qdb.name === location + }) +} + +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, savedQuestionsDir) { + // TODO: clear folder every now and then, check if saved questions exist + if (!subj) { + logger.Log('No subj name to save test question') + return + } + const questionsToSave = { + questions: questions, + subj: subj, + userid: userid, + testUrl: testUrl, + date: new Date(), + } + const fname = `${utils.GetDateString()}_${userid}_${testUrl}.json` + const subject = getSubjNameWithoutYear(subj).replace(/\//g, '-') + const subjPath = `${savedQuestionsDir}/${subject}` + const savedSubjQuestionsFilePath = `${subjPath}/${savedQuestionsFileName}` + + utils.CreatePath(subjPath, true) + if (!utils.FileExists(savedSubjQuestionsFilePath)) { + utils.WriteFile('[]', savedSubjQuestionsFilePath) + } + + const savedQuestions = utils.ReadJSON(savedSubjQuestionsFilePath) + + const testExists = false + // TODO: do this on another thread? + // const testExists = savedQuestions.some((savedQuestion) => { + // const data = utils.ReadJSON(`${subjPath}/${savedQuestion.fname}`) + + // return data.questions.some((dQuestion) => { + // return questions.some((question) => { + // const percent = compareQuestionObj( + // createQuestion(question), + // '', + // createQuestion(dQuestion), + // '' + // ) + + // return percent.avg === 100 + // }) + // }) + // }) + + if (testExists) { + return + } + + 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}`) +} + +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] + + return temp +} + +function LoadMOTD(motdFile) { + return utils.ReadFile(motdFile) +} + +function LoadUserSpecificMOTD(userSpecificMotdFile) { + try { + return utils.ReadJSON(userSpecificMotdFile) + } catch (err) { + logger.Log('Couldnt parse user specific motd!', logger.GetColor('redbg')) + console.error(err) + } +} + +function LoadTestUsers() { + let testUsers = utils.ReadJSON(testUsersFile) + if (testUsers) { + testUsers = testUsers.userIds + } + return testUsers +} + +function getNewQdb(location, maxIndex, dbsFile, publicDir, questionDbs) { + 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 +} + +function setup(data: SubmoduleData): any { + const { app, userDB, /* url */ publicdirs /* moduleSpecificData */ } = data + + const publicDir = publicdirs[0] + const motdFile = publicDir + 'motd' + const userSpecificMotdFile = publicDir + 'userSpecificMotd.json' + const dbsFile = publicDir + 'questionDbs.json' + const savedQuestionsDir = publicDir + 'savedQuestions' + + let version = LoadVersion() + let motd = LoadMOTD(motdFile) + let userSpecificMotd = LoadUserSpecificMOTD(userSpecificMotdFile) + let testUsers: any = LoadTestUsers() + + const dataFiles: Array = utils.ReadJSON(dbsFile) + const questionDbs: Array = loadJSON(dataFiles, publicDir) + initWorkerPool(questionDbs) + + const filesToWatch = [ + { + fname: userSpecificMotdFile, + logMsg: 'User Specific Motd updated', + action: () => { + userSpecificMotd = LoadUserSpecificMOTD(userSpecificMotdFile) + }, + }, + { + fname: motdFile, + logMsg: 'Motd updated', + action: () => { + motd = LoadMOTD(motdFile) + }, + }, + { + fname: testUsersFile, + logMsg: 'Test Users file changed', + action: () => { + testUsers = LoadTestUsers() + }, + }, + { + fname: userScriptFile, + logMsg: 'User script file changed', + action: () => { + version = LoadVersion() + }, + }, + ] + + app.get('/getDbs', (req: Request, res: any) => { + logger.LogReq(req) + res.json( + questionDbs.map((qdb) => { + return { + path: qdb.path.replace(publicDir, ''), + name: qdb.name, + locked: qdb.locked, + } + }) + ) + }) + + 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) + }) + + 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) { + if (!dbExists(location, questionDbs)) { + suitedQuestionDbs.push( + getNewQdb(location, maxIndex, dbsFile, publicDir, questionDbs) + ) + } else { + logger.Log( + `Tried to add existing db named ${location}!`, + logger.GetColor('red') + ) + } + } + if (suitedQuestionDbs.length === 0) { + res.json({ + status: 'fail', + msg: 'No suitable dbs to add questions to', + }) + return + } + + 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, + }) + } + }) + + app.post('/ask', function(req: Request, res) { + const user: User = req.session.user + + if (!req.body.questions) { + res.json({ + message: `ask something! { question:'' ,subject:'', location:'' }`, + 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, questionDbs) + }) + + 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, + savedQuestionsDir + ) + } + }) + }) + 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, questionDbs).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, + }) + } + }) + app.get('/datacount', function(req: Request, res: any) { + logger.LogReq(req) + if (req.query.detailed === 'all') { + res.json({ + detailed: getDetailedRes(questionDbs), + simple: getSimplreRes(questionDbs), + }) + } else if (req.query.detailed) { + res.json(getDetailedRes(questionDbs)) + } else { + res.json(getSimplreRes(questionDbs)) + } + }) + 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(questionDbs) + } + if (req.query.version) { + result.version = version + } + if (req.query.motd) { + result.motd = getMotd(req.query.cversion, motd) + 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) { + userSpecificMotd[user.id].seen = true + + logger.Log( + `User #${user.id}'s user specific motd is now seen:`, + logger.GetColor('bluebg') + ) + logger.Log(userSpecificMotd[user.id].msg) + 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('/possibleAnswers', (req: Request, res: any) => { + logger.LogReq(req) + const files = utils.ReadDir(savedQuestionsDir) + + files.sort(function(a, b) { + return ( + fs.statSync(savedQuestionsDir + '/' + b).mtime.getTime() - + fs.statSync(savedQuestionsDir + '/' + a).mtime.getTime() + ) + }) + + res.json({ + savedQuestionsFileName: savedQuestionsFileName, + subjects: files.map((subj) => { + return { + name: subj, + path: `savedQuestions/${subj}/`, + } + }), + }) + }) + + app.post('/rmPossibleAnswer', (req: Request, res: any) => { + logger.LogReq(req) + const user: User = req.session.user + + const subj = req.body.subj + const file = req.body.file + const savedQuestionsPath = `${savedQuestionsDir}/${subj}/${savedQuestionsFileName}` + const savedQuestions = utils.ReadJSON(savedQuestionsPath) + let path = `${savedQuestionsDir}/${subj}/${file}` + // to prevent deleting ../../../ ... /etc/shadow + while (path.includes('..')) { + path = path.replace(/\.\./g, '.') + } + + if (utils.FileExists(path)) { + utils.deleteFile(path) + + utils.WriteFile( + JSON.stringify( + savedQuestions.filter((sq) => { + return sq.fname !== file + }) + ), + savedQuestionsPath + ) + + logger.Log( + `User #${user.id} deleted '${file}' from subject '${subj}'`, + logger.GetColor('cyan') + ) + res.json({ + res: 'ok', + }) + } else { + logger.Log( + `User #${user.id} tried to delete '${file}' from subject '${subj}', but failed`, + logger.GetColor('red') + ) + res.json({ + res: 'fail', + }) + } + }) + + app.post('/updateQuestion', (req: Request, res) => { + logger.LogReq(req) + const user: User = req.session.user + const date = utils.GetDateString() + let saveDb = false + + const editType = req.body.type + const selectedDb = req.body.selectedDb + if (!editType || !selectedDb) { + res.json({ + status: 'fail', + msg: 'No .editType or .selectedDb !', + }) + return + } + + const dbIndex = questionDbs.findIndex((qdb) => { + return qdb.name === selectedDb.name + }) + const currDb = questionDbs[dbIndex] + + if (dbIndex === -1) { + res.json({ + status: 'fail', + msg: `No question db named like ${selectedDb.name}!`, + }) + return + } + + // { + // "index": 0, + // "subjName": "VHDL programozás", + // "type": "delete", + // "selectedDb": { + // "path": "questionDbs/elearning.uni-obuda.hu.json", + // "name": "elearning.uni-obuda.hu" + // } + // } + if (editType === 'delete') { + const { index, subjName } = req.body + let deletedQuestion = {} + if (isNaN(index) || !subjName) { + res.json({ + status: 'fail', + msg: 'No .index or .subjName !', + }) + return + } + + questionDbs[dbIndex].data = currDb.data.map((subj) => { + if (subj.Name !== subjName) { + return subj + } else { + return { + ...subj, + Questions: subj.Questions.filter((question, i) => { + if (index === i) { + deletedQuestion = question + return false + } else { + return true + } + }), + } + } + }) + + logger.Log( + `User #${user.id} deleted a question from '${subjName}'`, + logger.GetColor('cyan') + ) + utils.AppendToFile( + `${date}: User ${user.id} deleted a question from '${subjName}' (index: ${index})`, + dataEditsLog + ) + utils.AppendToFile(JSON.stringify(deletedQuestion, null, 2), dataEditsLog) + saveDb = true + } + + // { + // "index": 0, + // "subjName": "Elektronika", + // "type": "edit", + // "newVal": { + // "Q": "Analóg műszer esetén az érzékenység az a legkisebb mennyiség, amely a műszer kijelzőjén meghatározott mértékű változást okoz.", + // "A": "Igaz", + // "data": { + // "type": "simple", + // "possibleAnswers": [ + // "Igaz" + // ] + // }, + // "possibleAnswers": [ + // "Igaz" + // ] + // }, + // "selectedDb": { + // "path": "questionDbs/elearning.uni-obuda.hu.json", + // "name": "elearning.uni-obuda.hu" + // } + // } + if (editType === 'edit') { + const { index, subjName, newVal } = req.body + let oldVal = {} + if (isNaN(index) || !subjName) { + res.json({ + status: 'fail', + msg: 'No .index or .subjName !', + }) + return + } + if (!isQuestionValid(newVal)) { + res.json({ + status: 'fail', + msg: 'edited question is not valid', + question: newVal, + }) + return + } + + questionDbs[dbIndex].data = currDb.data.map((subj) => { + if (subj.Name !== subjName) { + return subj + } else { + return { + ...subj, + Questions: subj.Questions.map((question, i) => { + if (index === i) { + oldVal = question + return createQuestion(newVal) + } else { + return question + } + }), + } + } + }) + + logger.Log( + `User #${user.id} edited a question in '${subjName}'`, + logger.GetColor('cyan') + ) + utils.AppendToFile( + `${date}: User ${user.id} edited a question in '${subjName}' (index: ${index})`, + dataEditsLog + ) + utils.AppendToFile( + JSON.stringify( + { + newVal: newVal, + oldVal: oldVal, + }, + null, + 2 + ), + dataEditsLog + ) + saveDb = true + } + + // { + // "subjName": "Elektronika", + // "changedQuestions": [ + // { + // "index": 1, + // "value": { + // "Q": "A műszer pontosságát a hibájával fejezzük ki, melyet az osztályjel (osztálypontosság ) mutat meg.", + // "A": "Hamis", + // "data": { + // "type": "simple", + // "possibleAnswers": [ + // "Igaz", + // "Hamis" + // ] + // } + // } + // } + // ], + // "deletedQuestions": [ + // 0 + // ], + // "type": "subjEdit", + // "selectedDb": { + // "path": "questionDbs/elearning.uni-obuda.hu.json", + // "name": "elearning.uni-obuda.hu" + // } + // } + if (editType === 'subjEdit') { + const { subjName, changedQuestions, deletedQuestions } = req.body + const deletedQuestionsToWrite = [] + const changedQuestionsToWrite = [] + if ( + !Array.isArray(changedQuestions) || + !Array.isArray(deletedQuestions) + ) { + res.json({ + status: 'fail', + msg: 'no changedQuestions or deletedQuestions!', + }) + return + } + + // processing changed questions + questionDbs[dbIndex].data = currDb.data.map((subj) => { + if (subj.Name !== subjName) { + return subj + } else { + return { + ...subj, + Questions: subj.Questions.map((question, i) => { + const changedTo = changedQuestions.find((cq) => { + return cq.index === i + }) + if (changedTo) { + changedQuestionsToWrite.push({ + oldVal: question, + newVal: changedTo.value, + }) + return createQuestion(changedTo.value) + } else { + return question + } + }), + } + } + }) + + // processing deletedQuestions + questionDbs[dbIndex].data = currDb.data.map((subj) => { + if (subj.Name !== subjName) { + return subj + } else { + return { + ...subj, + Questions: subj.Questions.filter((question, i) => { + const isDeleted = deletedQuestions.includes(i) + if (isDeleted) { + deletedQuestionsToWrite.push(question) + return false + } else { + return true + } + }), + } + } + }) + + logger.Log( + `User #${user.id} modified '${subjName}'. Edited: ${deletedQuestionsToWrite.length}, deleted: ${deletedQuestionsToWrite.length}`, + logger.GetColor('cyan') + ) + utils.AppendToFile( + `${date} User #${user.id} modified '${subjName}'. Edited: ${deletedQuestionsToWrite.length}, deleted: ${deletedQuestionsToWrite.length}`, + dataEditsLog + ) + utils.AppendToFile( + JSON.stringify( + { + deletedQuestions: deletedQuestionsToWrite, + changedQuestions: changedQuestionsToWrite, + }, + null, + 2 + ), + dataEditsLog + ) + saveDb = true + } + + if (saveDb) { + writeData(currDb.data, currDb.path) + msgAllWorker({ + qdbs: questionDbs, + type: 'update', + }) + } + + res.json({ + status: 'OK', + }) + }) + + return { + dailyAction: () => { + backupData(questionDbs) + ExportDailyDataCount(questionDbs, userDB) + }, + 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') + ) + } + }) + }, + } +} + +export default { + setup: setup, +} diff --git a/src/modules/api/submodules/quickvote.ts b/src/modules/api/submodules/quickvote.ts new file mode 100644 index 0000000..a7faa50 --- /dev/null +++ b/src/modules/api/submodules/quickvote.ts @@ -0,0 +1,90 @@ +import logger from '../../../utils/logger' +import utils from '../../../utils/utils' +import { Request, SubmoduleData, User } from '../../../types/basicTypes' + +const quickVoteResultsDir = 'stats/qvote' +const quickVotes = 'stats/qvote/votes.json' + +function setup(data: SubmoduleData): void { + const { app /* userDB, url, publicdirs, moduleSpecificData */ } = data + + 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) + }) +} + +export default { + setup: setup, +} diff --git a/src/modules/api/submodules/ranklist.ts b/src/modules/api/submodules/ranklist.ts new file mode 100644 index 0000000..0ce76bd --- /dev/null +++ b/src/modules/api/submodules/ranklist.ts @@ -0,0 +1,104 @@ +import logger from '../../../utils/logger' +import utils from '../../../utils/utils' +import { Request, SubmoduleData, User } from '../../../types/basicTypes' + +const idStatFile = 'stats/idstats' +const idvStatFile = 'stats/idvstats' + +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 setup(data: SubmoduleData): void { + const { app /* userDB, url, publicdirs, moduleSpecificData */ } = data + + 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, + }) + }) +} + +export default { + setup: setup, +} diff --git a/src/modules/api/submodules/todos.ts b/src/modules/api/submodules/todos.ts new file mode 100644 index 0000000..f59b992 --- /dev/null +++ b/src/modules/api/submodules/todos.ts @@ -0,0 +1,65 @@ +import logger from '../../../utils/logger' +import utils from '../../../utils/utils' +import { Request, SubmoduleData } from '../../../types/basicTypes' + +const todosFile = 'data/todos.json' + +function setup(data: SubmoduleData): void { + const { app /* userDB, url, publicdirs, moduleSpecificData */ } = data + + 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', + }) + }) +} + +export default { + setup: setup, +} diff --git a/src/modules/api/submodules/userFiles.ts b/src/modules/api/submodules/userFiles.ts new file mode 100644 index 0000000..d7b4679 --- /dev/null +++ b/src/modules/api/submodules/userFiles.ts @@ -0,0 +1,27 @@ +import logger from '../../../utils/logger' +import utils from '../../../utils/utils' +import { Request, SubmoduleData } from '../../../types/basicTypes' + +function setup(data: SubmoduleData): void { + const { app, /* userDB, url, */ publicdirs /* moduleSpecificData */ } = data + + const publicDir = publicdirs[0] + + const userFilesDir = publicDir + 'userFiles' + + app.get('/listUserFiles', (req: Request, res) => { + logger.LogReq(req) + + if (!utils.FileExists(userFilesDir)) { + utils.CreatePath(userFilesDir, true) + } + + res.json({ + files: utils.ReadDir(userFilesDir), + }) + }) +} + +export default { + setup: setup, +} diff --git a/src/modules/api/submodules/userManagement.ts b/src/modules/api/submodules/userManagement.ts new file mode 100644 index 0000000..bbd6eed --- /dev/null +++ b/src/modules/api/submodules/userManagement.ts @@ -0,0 +1,297 @@ +import { v4 as uuidv4 } from 'uuid' + +import logger from '../../../utils/logger' +import utils from '../../../utils/utils' +import { Request, SubmoduleData, User } from '../../../types/basicTypes' +import dbtools from '../../../utils/dbtools' + +const minimumAlowwedSessions = 2 // how many sessions are allowed for a user +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 usersDbBackupPath = 'data/dbs/backup' + +function BackupDB(usersDbBackupPath, userDB) { + 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 setup(data: SubmoduleData): any { + const { app, userDB, url /* publicdirs, moduleSpecificData */ } = data + let domain: any = url.split('.') // [ "https://api", "frylabs", "net" ] + domain.shift() // [ "frylabs", "net" ] + domain = domain.join('.') // "frylabs.net" + logger.DebugLog(`Cookie domain: ${domain}`, 'cookie', 1) + + 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.get('/logout', (req: Request, res: any) => { + logger.LogReq(req) + const sessionID = req.cookies.sessionID + const user: User = req.session.user + + if (!user) { + res.json({ + msg: 'You are not logged in', + success: false, + }) + return + } + + logger.Log( + `Successfull logout with user ID: #${user.id}`, + logger.GetColor('cyan') + ) + + // removing session from db + dbtools.Delete(userDB, 'sessions', { + id: sessionID, + }) + res.clearCookie('sessionID').json({ + msg: 'Successfull logout', + result: 'success', + }) + }) + + 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, + } + ) + } + }) + } + + return { + dailyAction: () => { + BackupDB(usersDbBackupPath, userDB) + IncrementAvaiblePWs() + }, + } +} + +export default { + setup: setup, +} diff --git a/src/standaloneUtils/rmDuplicates.js b/src/standaloneUtils/rmDuplicates.js index de7c99b..1e503fe 100644 --- a/src/standaloneUtils/rmDuplicates.js +++ b/src/standaloneUtils/rmDuplicates.js @@ -94,6 +94,8 @@ if (stat.isDirectory()) { // possible answers duplicate removing // --------------------------------------------------------------------------------- +// TODO: dont check every file, only check per directorires +// only compare questions of same subjects function removePossibleAnswersDuplicates(path) { let count = 0 let currIndex = 1 diff --git a/src/types/basicTypes.ts b/src/types/basicTypes.ts index 1f8d57d..3364435 100644 --- a/src/types/basicTypes.ts +++ b/src/types/basicTypes.ts @@ -80,4 +80,5 @@ export interface SubmoduleData { publicdirs: Array userDB?: any nextdir?: string + moduleSpecificData?: any }