Browse Source

Fixes #3 link stats
Fixes #2 QR codes

isundil 2 years ago
parent
commit
4e5a1f3e7a
10 changed files with 203 additions and 25 deletions
  1. 1 1
      main.js
  2. 1 1
      models/access.js
  3. 39 0
      models/origin.js
  4. 1 1
      models/pasteContent.js
  5. 3 1
      package.json
  6. 15 5
      router/input.js
  7. 55 0
      router/qrcode.js
  8. 3 1
      src/databaseHelper.js
  9. 73 14
      static/public/js/stats.js
  10. 12 1
      templates/stats.js

+ 1 - 1
main.js

@@ -26,8 +26,8 @@ App.prototype.init = async function() {
         fs.mkdirSync(this.dataDir);
 
     require('./router/mdi.js').register(this);
-
     require('./router/input.js').register(this);
+    require('./router/qrcode.js').register(this);
 
     await this.databaseHelper.init();
 }

+ 1 - 1
models/access.js

@@ -59,7 +59,7 @@ AccessModel.prototype.describe = function() {
 
 AccessModel.prototype.fromDb = function(dbObj) {
     this.privId = dbObj['privId'];
-    this.publicId = dbObj['publicId'];
+    this.publicId = "" + dbObj['publicId'];
     this.accessTime = new Date(dbObj['accessTime']);
     this.ipAddress = dbObj['ipAddress'];
     this.reverseIp = dbObj['reverseIp'];

+ 39 - 0
models/origin.js

@@ -0,0 +1,39 @@
+
+const DatabaseModel = require("./DatabaseModel.js").DatabaseModel;
+const Security = require('../src/security.js');
+
+function OriginModel(privId) {
+    this.privId = privId;
+    this.name = null;
+}
+
+Object.setPrototypeOf(OriginModel.prototype, DatabaseModel.prototype);
+
+OriginModel.prototype.getTableName = function() {
+    return "origin";
+}
+
+OriginModel.prototype.createOrUpdateBase = async function(dbHelper) {
+    await dbHelper.runSql(`CREATE TABLE IF NOT EXISTS 'origin' (
+        privId string not null,
+        name string not null,
+
+        PRIMARY KEY(privId, name),
+        FOREIGN KEY (privId) REFERENCES pasteContent(privId)
+        )`);
+}
+
+OriginModel.prototype.describe = function() {
+    return {
+        "privId": this.privId,
+        "name": this.name
+    };
+}
+
+OriginModel.prototype.fromDb = function(dbObj) {
+    this.privId = dbObj['privId'];
+    this.name = "" + dbObj['name'];
+}
+
+module.exports.OriginModel = OriginModel;
+

+ 1 - 1
models/pasteContent.js

@@ -31,7 +31,7 @@ PasteContent.prototype.getTableName = function() {
 PasteContent.prototype.createOrUpdateBase = async function(dbHelper) {
     await dbHelper.runSql(`CREATE TABLE IF NOT EXISTS 'pasteContent' (
         privId string not null PRIMARY KEY,
-        publicId string not null,
+        publicId string not null UNIQUE,
         created datetime not null,
         expire datetime,
         type string not null,

+ 3 - 1
package.json

@@ -1,10 +1,12 @@
 {
   "dependencies": {
     "@mdi/font": "^7.1.96",
-    "dns": "^0.2.2",
+    "canvas": "^2.11.2",
+    "dns": "^0.1.2",
     "geoip-lite": "^1.4.7",
     "mime-types": "^2.1.35",
     "node-simple-router": "^0.10.2",
+    "qrcode": "^1.5.3",
     "request": "^2.88.2",
     "sqlite3": "^5.1.4",
     "whiskers": "^0.4.0"

+ 15 - 5
router/input.js

@@ -2,6 +2,7 @@
 const whiskers = require('whiskers');
 const fs = require('fs');
 
+const OriginModel = require('../models/origin.js').OriginModel;
 const ApiKeyModel = require('../models/apiKey.js').ApiKeyModel;
 const PasteContent = require('../models/pasteContent.js').PasteContent;
 const AccessModel = require('../models/access.js').AccessModel;
@@ -10,9 +11,9 @@ const Security = require('../src/security.js');
 const CONFIG = require('../src/config.js');
 
 async function onAccessContent(app, req, entity) {
+    const rel = "" + (req.body.rel || req.body.r || "");
     if (entity.privId !== req.params.id) {
-        //FIXME ?rel
-        let accessEntry = new AccessModel(entity.privId, req.params.id, Security.getRequestIp(req));
+        let accessEntry = new AccessModel(entity.privId, rel, Security.getRequestIp(req));
         await accessEntry.resolveIp();
         await app.databaseHelper.insertOne(accessEntry);
     }
@@ -51,14 +52,23 @@ async function _readAccess(app, entityId) {
             .map(x => { delete x.ipAddress; delete x.privId; x.ipRegion = JSON.parse(x.ipRegion); return x; });
 }
 
-async function renderPrivatePage(app, res, entity) {
+async function renderPrivatePage(app, req, res, entity) {
     let stat;
     try { stat = fs.statSync(app.dataDir+entity.privId); } catch (e) { stat = { error: e }; }
     const access = await _readAccess(app, entity.privId);
+    const origins = await app.databaseHelper.fetch(OriginModel, { privId: entity.privId });
 
     let context = app.routerUtils.commonRenderInfos();
     context.page_title += " - Pastit";
-    res.end(whiskers.render(require('../templates/stats.js'), { ...context, ...{size: stat.size}, ...{ access: JSON.stringify(access) }}));
+    entity.createdTime = entity.created.getTime();
+    entity.expireTime = entity.expire?.getTime() || 0;
+    res.end(whiskers.render(require('../templates/stats.js'), {
+        ...context,
+        ...{size: stat.size || 0},
+        ...{ access: JSON.stringify(access)},
+        ...{entity: entity},
+        ...{origins: JSON.stringify(origins.map(i => i.name))}
+    }));
 }
 
 module.exports = { register: app => {
@@ -71,7 +81,7 @@ module.exports = { register: app => {
     app.router.get("/x/:id", async (req, res) => {
         let entity = await app.databaseHelper.findOne(PasteContent, { privId: req.params.id, publicId: req.params.id }, " or ");
         if (entity && entity.privId === req.params.id)
-            return renderPrivatePage(app, res, entity);
+            return renderPrivatePage(app, req, res, entity);
         if (entity && !entity.expired)
             return renderPublicPage(app, req, res, entity);
         app.routerUtils.onPageNotFound(res);

+ 55 - 0
router/qrcode.js

@@ -0,0 +1,55 @@
+const CONFIG = require('../src/config.js');
+const PasteContent = require('../models/pasteContent.js').PasteContent;
+const QRCode = require("qrcode");
+const { createCanvas, loadImage } = require("canvas");
+
+async function createQrCode(url, params) {
+    const width = params?.width || 500;
+    const canvas = createCanvas(width, width);
+
+    QRCode.toCanvas(
+        canvas,
+        url,
+        {
+            width: width,
+            errorCorrectionLevel: "H",
+            margin: 1,
+            color: {
+                dark: params?.dark || "#333",
+                light: params?.light || "#eee"
+            },
+    });
+
+    if (params?.centerImage) {
+        const ctx = canvas.getContext("2d");
+        const img = await loadImage(params.centerImage);
+        const sizeFactor = (width/3.5)/Math.max(img.width, img.height);
+        ctx.drawImage(img, 0, 0, img.width, img.height, (width - img.width*sizeFactor) /2, (width - img.height*sizeFactor) /2, img.width * sizeFactor, img.height * sizeFactor);
+    }
+    return canvas.toBuffer();
+}
+
+module.exports = {
+    register: app => {
+        // Params:
+        //  - s: size (in px)
+        //  - bg: bg color
+        //  - fg: fg color
+        //  - origin: to append to the url
+        app.router.get("/:id/qrcode.png", async (req, res) => {
+            if (!req.body.origin)
+                return app.routerUtils.onPageNotFound(res);
+            let entity = await app.databaseHelper.findOne(PasteContent, { publicId: req.params.id });
+            if (!entity)
+                return app.routerUtils.onPageNotFound(res);
+            const qrcode = await createQrCode(decodeURIComponent(req.body.origin) +"/x/" +entity.publicId +(req.body.rel ? ("?rel="+decodeURIComponent(req.body.rel)) : ""), {
+                width: req.body.s ? Number.parseInt(req.body.s) : undefined,
+                light: req.body.bg ? decodeURIComponent(req.body.bg) : undefined,
+                dark: req.body.fg ? decodeURIComponent(req.body.fg) : undefined,
+            });
+            res.writeHead(200, { "Content-Type": "image/png", "Content-Length": qrcode.length, "Cache-Control": "max-age=604800" }); // Cache=1 week
+            res.end(qrcode);
+        });
+    }
+};
+

+ 3 - 1
src/databaseHelper.js

@@ -5,6 +5,7 @@ const CONFIG = require('./config.js');
 const PasteContentModel = require('../models/pasteContent.js').PasteContent;
 const ApiKeyModel = require('../models/apiKey.js').ApiKeyModel;
 const AccessModel = require('../models/access.js').AccessModel;
+const OriginModel = require('../models/origin.js').OriginModel;
 
 function DatabaseHelper() {
     this.db = null;
@@ -17,11 +18,12 @@ DatabaseHelper.prototype.init = function() {
                 ko(err);
                 return;
             }
-            let types = [ PasteContentModel, ApiKeyModel, AccessModel ];
+            let types = [ PasteContentModel, ApiKeyModel, AccessModel, OriginModel ];
             for (let i =0; i < types.length; ++i) {
                 let instance = new types[i]();
                 await instance.createOrUpdateBase(this);
             }
+            await this.runSql("PRAGMA foreign_key=1");
             console.log("Database is ready");
             ok();
         });

+ 73 - 14
static/public/js/stats.js

@@ -1,7 +1,7 @@
 
 (()=>{
-    const NOW = Date.now();
     let tmp = new Date();
+    const NOW = tmp.getTime();
     tmp.setHours(0);
     tmp.setMinutes(0);
     tmp.setSeconds(0);
@@ -11,27 +11,86 @@
     if (tmp.getDay() !== 1)
         tmp.setDate(tmp.getDate()-tmp.getDay() +1);
     const ONE_WEEK = tmp.getTime();
-    console.log(tmp);
     tmp.setTime(today);
     tmp.setDate(1);
     const ONE_MONTH = tmp.getTime();
-    console.log(tmp);
+    const relSource = document.getElementById("relSource");
 
-    for (let i of ["All"].concat(Object.keys(ACCESS.reduce((acc, i) => { acc[i.publicId] = true; return acc; }, {})).sort())) {
-        let node = document.createElement("option");
-        node.textContent = i;
-        document.getElementById("relSource").appendChild(node);
+    /* INIT DROPDOWN */ (() => {
+        let frag = document.createDocumentFragment();
+        let origins = ACCESS.reduce((acc, i) => { acc[i.publicId] = true; return acc; }, {});
+        for (let i of ORIGINS) origins[i] = true;
+        for (let i of (["All"].concat(Object.keys(origins).sort())).map(i => {
+            let node = document.createElement("option");
+            node.textContent = i;
+            return node;
+        }))
+            frag.appendChild(i);
+        relSource.appendChild(frag);
+    })();
+
+    function formatDate(d) {
+        if (!d || isNaN(d.getTime()))
+            return "";
+        return d.toLocaleDateString() + " " + d.toLocaleTimeString();
+    }
+
+    function prependDomain(xmlElement) {
+        xmlElement.textContent = document.location.origin + xmlElement.textContent;
     }
+
+    function generateQrCode(size, rel) {
+        if (rel)
+            rel = "&rel="+encodeURIComponent(rel);
+        return "/" +PUBLIC_ID +"/qrcode.png?origin="+encodeURIComponent(document.location.origin)+'&s='+size +(rel || "");
+    }
+
+    let HASH_CHANGING = false;
+
     function selectSource(source) {
         let _access = source === "All" ? ACCESS : ACCESS.filter(i => i.publicId === source);
-        let lastAccess = _access.reduce((prev, i) => (prev && prev.accessTime > i.accessTime) ? prev : i, null)
-        document.getElementById("size").textContent = FILE_SIZE +'o';
+        let lastAccess = _access.reduce((prev, i) => (prev && prev.accessTime > i.accessTime) ? prev : i, null);
         document.getElementById('totalAccessCount').textContent = _access.length;
-        document.getElementById('weekAccess').textContent = _access.reduce((acc, i) => acc + (i.accessTime >= NOW -ONE_WEEK ? 1 : 0), 0);
-        document.getElementById('monthAccess').textContent = _access.reduce((acc, i) => acc + (i.accessTime >= NOW -ONE_MONTH ? 1 : 0), 0);
-        document.getElementById('lastAccess').textContent = new Date(lastAccess?.accessTime);
+        document.getElementById('weekAccess').textContent = _access.reduce((acc, i) => acc + (i.accessTime >= ONE_WEEK ? 1 : 0), 0);
+        document.getElementById('monthAccess').textContent = _access.reduce((acc, i) => acc + (i.accessTime >= ONE_MONTH ? 1 : 0), 0);
+        document.getElementById('lastAccess').textContent = formatDate(new Date(lastAccess?.accessTime));
+        document.getElementById("publicLinkQr").innerHTML = "<img src='" +generateQrCode(150, !source || source === "All" ? null : source) +"'/>";
+        document.getElementById("publicLinkQrBig").href = generateQrCode(1024, !source || source === "All" ? null : source);
+        const linkEnd = PUBLIC_ID + (!source || source === "All" ? "" : ("?r=" +source));
+        document.getElementById("publicLink").textContent = document.location.origin + "/x/" + linkEnd;
+        document.getElementById("embedLink").textContent = document.location.origin + "/x/raw/" + linkEnd;
+        document.getElementById("publicLinkA").href = "/x/" +linkEnd;
+        document.getElementById("embedLinkA").href = "/x/raw/" +linkEnd;
+        HASH_CHANGING = true;
+        document.location.hash = encodeURIComponent(source);
+        HASH_CHANGING = false;
+    }
+
+    function updateSource() {
+        const selected = relSource.selectedOptions?.[0]?.textContent;
+        selectSource(selected || selected === '' ? selected : "All");
     }
-    selectSource(document.getElementById("relSource").selectedOptions?.[0]?.textContent || "All");
-    document.getElementById("relSource").addEventListener("change", () => selectSource(document.getElementById("relSource").selectedOptions?.[0]?.textContent || "All"));
+    function hashChanged() {
+        if (HASH_CHANGING) return;
+        if (document.location.hash) {
+            const hash = (decodeURIComponent(document.location.hash.substring(1)));
+            selectSource(hash);
+            for (let i =0; i < relSource.children.length; ++i) {
+                if (relSource.children[i].textContent === hash) {
+                    relSource.selectedIndex = i;
+                    break;
+                }
+            }
+        } else {
+            updateSource();
+        }
+    }
+    hashChanged();
+
+    addEventListener("hashchange", hashChanged);
+    relSource.addEventListener("change", () => updateSource());
+    document.getElementById("size").textContent = FILE_SIZE +'o';
+    document.getElementById("creationTime").textContent = formatDate(new Date(CREATION_TIME));
+    document.getElementById("expiralTime").textContent = formatDate(new Date(EXPIRAL_TIME));
 })();
 

+ 12 - 1
templates/stats.js

@@ -1,14 +1,25 @@
 module.exports = require('./header.js') +`
-<select id="relSource"></select>
 <table>
 <tr><td>Size</td><td><span id="size"></span></td>
+<tr><td>Created</td><td><span id="creationTime"></span></td>
+<tr><td>Expire</td><td><span id="expiralTime"></span></td>
+</table>
+<select id="relSource"></select>
+<table>
 <tr><td>Last Access: </td><td><span id="lastAccess"></span></td></tr>
 <tr><td>Access this week: </td><td><span id="weekAccess"></span></td></tr>
 <tr><td>Access this month: </td><td><span id="monthAccess"></span></td></tr>
 <tr><td>Total Access Count: </td><td><span id="totalAccessCount"></span></td></tr>
+<tr><td>Public Link</td><td><a id="publicLinkA" target="_blank"><span id="publicLink">/x/{entity.publicId}</span></a></td>
+<tr><td>Public Link</td><td><a id="publicLinkQrBig" target="_blank"><span id="publicLinkQr"></span></a></td>
+<tr><td>Public Embed Link</td><td><a id="embedLinkA" target="_blank"><span id="embedLink">/x/raw/{entity.publicId}</span></a></td>
 </table>
 <script>
+const PUBLIC_ID="{entity.publicId}";
 const ACCESS=JSON.parse('{access}');
 const FILE_SIZE={size};
+const CREATION_TIME={entity.createdTime};
+const EXPIRAL_TIME={entity.expireTime};
+const ORIGINS=JSON.parse('{origins}');
 </script><script src="/public/js/stats.js"></script>
 ` + require('./footer.js');