commit 7f1682c04f5245bb3edfe1bd3ca4039e0fae030f Author: Raido Kalbre Date: Sat Aug 25 18:12:15 2018 +0300 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..a8fc851 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/db.sqlite b/db.sqlite new file mode 100755 index 0000000..8472f69 Binary files /dev/null and b/db.sqlite differ diff --git a/db.sqlite.201701010102 b/db.sqlite.201701010102 new file mode 100755 index 0000000..3c7eabb Binary files /dev/null and b/db.sqlite.201701010102 differ diff --git a/db.sqlite.201701012214 b/db.sqlite.201701012214 new file mode 100755 index 0000000..9e186af Binary files /dev/null and b/db.sqlite.201701012214 differ diff --git a/index.js b/index.js new file mode 100755 index 0000000..25e74da --- /dev/null +++ b/index.js @@ -0,0 +1,146 @@ +var express = require('express'); +var app = express(); +var cors = require('cors'); +var escape = require('escape-html') +var bodyParser = require('body-parser'); +var sqlite3 = require('sqlite3').verbose(); +var db = new sqlite3.Database('db.sqlite', sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, function (err) { + if(err) { + console.log('error opening db:', err); + } else { + db.run("CREATE TABLE IF NOT EXISTS images (id INTEGER PRIMARY KEY, src TEXT, dataUri TEXT)"); + db.run("CREATE TABLE IF NOT EXISTS questions (id INTEGER PRIMARY KEY, updated DATETIME DEFAULT CURRENT_TIMESTAMP, text TEXT, explanation TEXT)"); + db.run("ALTER TABLE questions ADD COLUMN explanation TEXT", function (err) {}); + db.run("CREATE TABLE IF NOT EXISTS answers (id INTEGER PRIMARY KEY, aid INTEGER, qid INTEGER, text TEXT, correct INTEGER, checked INTEGER)"); + db.run("CREATE TABLE IF NOT EXISTS answering (id INTEGER PRIMARY KEY, created DATETIME DEFAULT CURRENT_TIMESTAMP, version TEXT, href TEXT)"); + } +}); + + +app.use( bodyParser.json() ); +app.use(cors()); +app.set('view engine', 'pug'); + + + + +var corsOptions = { + origin: "*", + methods: "POST" +}; + +app.get('/', function (req, res) { + db.all("SELECT q.id AS qid, q.text AS question, q.explanation AS explanation, i.dataUri AS image, T2.id AS aid, T2.text AS answer, T2.correct FROM questions q " + + "LEFT JOIN (SELECT qid, MAX(aid) AS maxid FROM answers GROUP BY qid) AS T1 ON q.id = T1.qid " + + "LEFT JOIN answers AS T2 ON T2.aid = T1.maxid AND T2.qid = q.id " + + "LEFT JOIN images AS i ON i.id = q.id " + + "ORDER BY T2.id DESC", + function (err, rows) { + //console.log('selected rows:', rows.length); + if (err) { + console.log('error:', err); + res.sendStatus(500); + } + var prevQid, index = -1; + var questions = rows.reduce(function(questions, row) { + var a = { answer: row.answer, correct: row.correct }; + if (prevQid != row.qid) { + questions[++index] = { id: row.qid, question: row.question, explanation: row.explanation, image: row.image, answers: [a] }; + } else { + questions[index].answers.push(a); + } + prevQid = row.qid; + return questions; + }, []); + console.log('total questions:', questions.length); + res.render('index', { questions: questions }); + }); +}); + +app.post('/explanation/:id', function (req, res) { + var id = req.params.id; + var data = req.body; + if (id && data.explanation) { + console.log('updating', id, 'with', data); + db.run('UPDATE questions SET explanation = $explanation WHERE id = $id', { + $id: id, + $explanation: escape(data.explanation) + }, function (err) { + if (err) { + console.log('error updating explanation for', id, ':', err); + res.sendStatus(500); + } else { + res.sendStatus(200); + } + }); + } else { + console.log('invalid explanation for', id, ':', data); + res.sendStatus(400); + } +}); + +app.post('/qa', cors(corsOptions), function (req, res) { + var data = req.body; + console.log('NEW QA DATA:', JSON.stringify(data)); + if (!data.version || !data.data || data.data.length < 15) { + console.log('invalid qa data', !data.version, !data.data, data.data.length < 15); + return res.sendStatus(400); + } + db.serialize(function() { + var aid = null; + db.run("INSERT INTO answering (version, href) VALUES ($version,$href)", { + $version: data.version, + $href: data.href + }, function(err) { + if (err) { + console.log('inserting answering failed:', err); + return; + } + console.log('inserted:', this.changes, 'lastID:', this.lastID); + aid = this.lastID; + if (aid) { + var qstmt = db.prepare("REPLACE INTO questions (id, text) VALUES (?,?)"); + var astmt = db.prepare("INSERT INTO answers (aid, qid, text, correct, checked) VALUES (?,?,?,?,?)"); + for (var i = 0; i < data.data.length; i++) { + var q = data.data[i]; + qstmt.run(q.id, q.question); + for (var j = 0; j < q.answers.length; j++) { + var a = q.answers[j]; + astmt.run(aid, q.id, a.text, a.correct, a.checked); + } + } + qstmt.finalize(); + astmt.finalize(); + + res.sendStatus(200); + } else { + console.log('no answering id'); + res.sendStatus(400); + } + }); + }); +}); + +app.post('/img', cors(corsOptions), function (req, res) { + var data = req.body; + if (!data.version || !data.data.id || !data.data.src || !data.data.dataUri) { + console.log('invalid img data', !data.version, !data.data.id, !data.data.src, !data.data.dataUri); + return res.sendStatus(400); + } + var img = data.data; + console.log('NEW IMG DATA:', JSON.stringify(data).substring(0,200)); + + var istmt = db.run("REPLACE INTO images (id, src, dataUri) VALUES (?,?,?)", img.id, img.src, img.dataUri, function (err) { + if (err) { + console.log('saving image ' + img.id + 'failed:', err); + res.sendStatus(400); + } else { + res.sendStatus(200); + } + }); + +}); + +app.listen(3000, function () { + console.log('Example app listening on port 3000!') +}); diff --git a/package.json b/package.json new file mode 100755 index 0000000..85d7a37 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "qadigger", + "version": "0.0.1", + "description": "Tampermonkey script and Node.js Express server to gather questions and answers for later analysis, hobby project", + "main": "index.js", + "scripts": { + "start": "node index", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Raidok", + "license": "MIT", + "dependencies": { + "body-parser": "^1.15.2", + "cors": "^2.8.1", + "escape-html": "^1.0.3", + "express": "^4.14.0", + "pug": "^2.0.0-beta6", + "sqlite3": "^3.1.8" + } +} diff --git a/sql.js b/sql.js new file mode 100755 index 0000000..7efdfe7 --- /dev/null +++ b/sql.js @@ -0,0 +1,25 @@ + +const CREATE_TABLE_IMAGES = "CREATE TABLE IF NOT EXISTS images (id INTEGER PRIMARY KEY, src TEXT, dataUri TEXT)"; +const CREATE_TABLE_QUESTIONS = "CREATE TABLE IF NOT EXISTS questions (id INTEGER PRIMARY KEY, updated DATETIME DEFAULT CURRENT_TIMESTAMP, text TEXT)"; +const CREATE_TABLE_ANSWERS = "CREATE TABLE IF NOT EXISTS answers (id INTEGER PRIMARY KEY, aid INTEGER, qid INTEGER, text TEXT, correct INTEGER, checked INTEGER)"; +const CREATE_TABLE_ANSWERING = "CREATE TABLE IF NOT EXISTS answering (id INTEGER PRIMARY KEY, created DATETIME DEFAULT CURRENT_TIMESTAMP, version TEXT, href TEXT)"; + +const SELECT_ALL = "SELECT q.id AS qid, q.text AS question, i.dataUri AS image, T2.id AS aid, T2.text AS answer, T2.correct FROM questions q \ +LEFT JOIN (SELECT qid, MAX(aid) AS maxid FROM answers GROUP BY qid) AS T1 ON q.id = T1.qid \ +LEFT JOIN answers AS T2 ON T2.aid = T1.maxid AND T2.qid = q.id \ +LEFT JOIN images AS i ON i.id = q.id ORDER BY T2.id DESC"; + + + +module.exports = { + SELECT_ALL: SELECT_ALL +} + + +app.get('/top-mistakes', function (req, res) { + db.all("SELECT q.id AS qid, q.text as question, a.acount AS mistakes FROM questions q " + + "JOIN (SELECT qid, count(*) as acount FROM answers WHERE correct != checked GROUP BY aid,qid) AS a ON a.qid = q.id ORDER BY mistakes DESC", + function (err, rows) { + res.render('index', { questions: rows }); + }); +}); diff --git a/tampermonkey.js b/tampermonkey.js new file mode 100755 index 0000000..1e1f038 --- /dev/null +++ b/tampermonkey.js @@ -0,0 +1,105 @@ +// ==UserScript== +// @name AKparse +// @namespace http://tampermonkey.net/ +// @version 0.1 +// @description try to take over the world! +// @author Raidok +// @match http://localhost:8080/* +// @grant none +// ==/UserScript== +(function() { + 'use strict'; + + if (document.querySelectorAll('td.popup-contents')[0].childNodes[0].textContent.indexOf('viga') === -1) { + console.log('script aborted'); + return; + } + + function getDataUri(url, callback) { + var image = new Image(); + + image.onload = function () { + var canvas = document.createElement('canvas'); + canvas.width = this.naturalWidth; // or 'width' if you want a special/scaled size + canvas.height = this.naturalHeight; // or 'height' if you want a special/scaled size + + canvas.getContext('2d').drawImage(this, 0, 0); + + console.log('loaded', url); + callback(canvas.toDataURL('image/jpeg')); + }; + + image.src = url; + console.log('loading', url); + } + + function nodeListToArray(list) { + var array= new Array(list.length); + for (var i= 0, n= list.length; i { + var tds = el.querySelectorAll('td'); + if (!tds) { + console.log('no tds!'); + } + var q = tds[0].innerText; + console.log(q, tds); + var img = tds[1].querySelector('img'); + var imgSrc = img ? img.getAttribute('src') : undefined; + var i = imgSrc ? { src: imgSrc } : undefined; + var aHtml = tds[2]; + //console.log('vastused:', tds[2].innerHTML); + var id = aHtml.querySelector('input[type=hidden]').getAttribute("value"); + var as = nodeListToArray(aHtml.querySelectorAll('input[type=checkbox]')).map(x => { + var checked = !!x.getAttribute('checked'); + var green = !!(x.nextElementSibling.getAttribute('style') || '').match(/color:.?green/); + var correct = green ? checked : !checked; + return { checked: checked, green: green, correct: correct, text: x.nextElementSibling.innerText }; + }); + console.log(q,i,as); + return { id: id, question: q, img: i, answers: as }; + }); + } + + function send(what, data) { + var xhr = new XMLHttpRequest(); + + xhr.open('POST', 'http://localhost:3000/' + what); + xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.onload = function() { + if (xhr.status === 200 || xhr.status === 204) { + console.log('Success! Response:' + xhr.responseText); + } else { + console.log('Request failed. Responded ' + xhr.status + ' with content:' + xhr.responseText); + } + }; + + xhr.send(JSON.stringify({ + version: GM_info.script.version, + href: window.location.href, + userAgent: window.agent, + data: data + })); + } + + + var data = parse(); + send('qa', data); + + var itemsProcessed = 0; + + data.forEach((item, index, array) => { + + if (item.img && item.img.src) { + getDataUri(item.img.src, function(dataUri) { + send('img', { id: item.id, src: item.img.src, dataUri: dataUri }); + }); + } + }); + + +})(); diff --git a/views/index.pug b/views/index.pug new file mode 100755 index 0000000..609a7e0 --- /dev/null +++ b/views/index.pug @@ -0,0 +1,80 @@ +html + head + title QA + style(media='screen', type='text/css'). + h4 { + margin: 40px 0 10px; + } + textarea { + width: 100%; + height: 50px; + } + .true:hover { + color: green; + } + .false:hover { + color: red; + } + .explanation { + color: #999; + } + script. + function postExplanation(id, explanation) { + if (!id) { + console.log('no id!'); + return; + } + var xhr = new XMLHttpRequest(); + + xhr.open('POST', 'http://localhost:3000/explanation/' + id); + xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.onload = function() { + if (xhr.status === 200 || xhr.status === 204) { + console.log('Success! Response:' + xhr.responseText); + } else { + console.log('Request failed. Responded ' + xhr.status + ' with content:' + xhr.responseText); + } + }; + + xhr.send(JSON.stringify({explanation: explanation})); + } + function edit(editLink) { + var saveLink = editLink.nextSibling + , textArea = editLink.previousSibling + , div = textArea.previousSibling + , text = div.innerText; + console.log('copying', text); + textArea.value = text; + + editLink.setAttribute('style', 'display:none'); + saveLink.removeAttribute('style'); + textArea.removeAttribute('style'); + } + function save(saveLink) { + var id = saveLink.parentNode.getAttribute('data-qid') + , editLink = saveLink.previousSibling + , textArea = editLink.previousSibling + , div = textArea.previousSibling + , text = textArea.value; + console.log('posting', id, text); + div.innerText = text; + + editLink.removeAttribute('style'); + saveLink.setAttribute('style', 'display:none'); + textArea.setAttribute('style', 'display:none'); + postExplanation(id, text); + } + body + ul(style='list-style-type:none') + each q in questions + li(data-qid=q.id) + h4= q.question + p.explanation !{ q.explanation ? q.explanation.replace(/\n/g, "
") : "" } + textarea(style='display:none') + a(href='javascript:void(0)',onclick='edit(this)') edit + a(href='javascript:void(0)',onclick='save(this)',style='display:none') save + p + img(src=q.image) + ul(style='list-style-type:decimal') + each a in q.answers + li(class=a.correct?'true':'false')= a.answer