From 86adb162dc454af1627eb3acc16cd1e60b770464 Mon Sep 17 00:00:00 2001 From: HF Date: Tue, 2 Aug 2022 17:14:37 +0200 Subject: [PATCH] Add parser for current-day pixellog --- src/components/ModWatchtools.jsx | 1 + src/core/adminfunctions.js | 86 ++++++++++++++ src/core/parsePixelLog.js | 188 +++++++++++++++++++++++++++++++ src/data/sql/IPInfo.js | 23 +++- src/data/sql/RegUser.js | 7 +- src/routes/api/modtools.js | 16 +++ 6 files changed, 317 insertions(+), 4 deletions(-) create mode 100644 src/core/parsePixelLog.js diff --git a/src/components/ModWatchtools.jsx b/src/components/ModWatchtools.jsx index f6a3f3b..1b01b55 100644 --- a/src/components/ModWatchtools.jsx +++ b/src/components/ModWatchtools.jsx @@ -49,6 +49,7 @@ async function submitWatchAction( } const data = new FormData(); data.append('watchaction', action); + data.append('canvasid', canvas); data.append('ulcoor', tlcoords); data.append('brcoor', brcoords); data.append('time', time); diff --git a/src/core/adminfunctions.js b/src/core/adminfunctions.js index 854540f..2907f33 100644 --- a/src/core/adminfunctions.js +++ b/src/core/adminfunctions.js @@ -19,6 +19,10 @@ import { imageABGR2Canvas, protectCanvasArea, } from './Image'; +import { + getSummaryFromArea, + getPixelsFromArea, +} from './parsePixelLog'; import rollbackCanvasArea from './rollback'; /* @@ -165,6 +169,88 @@ export async function executeImageAction( } } +/* + * Check who placed on a canvas area + * @param action if every pixel or summary should be returned + * @param ulcoor coords of upper-left corner in X_Y format + * @param brcoor coords of bottom-right corner in X_Y format + * @param canvasid numerical canvas id as string + * @return Object with {info, cols, rows} + */ +export async function executeWatchAction( + action, + ulcoor, + brcoor, + time, + iid, + canvasid, +) { + if (!canvasid) { + return { info: 'canvasid not defined' }; + } + const ts = parseInt(time, 10); + const canvas = canvases[canvasid]; + let error = null; + if (!ulcoor || !brcoor) { + error = 'Not all coordinates defined'; + } else if (!canvas) { + error = 'Invalid canvas selected'; + } else if (!action) { + error = 'No cleanaction given'; + } else if (Number.isNaN(ts)) { + error = 'Invalid time given'; + } + if (error) { + return { info: error }; + } + + const parseCoords = validateCoorRange(ulcoor, brcoor, canvas.size); + if (typeof parseCoords === 'string') { + return { info: parseCoords }; + } + const [x, y, u, v] = parseCoords; + + if (u - x > 1000 || v - y > 1000) { + return { info: 'Cann not watch larger than 1000x1000 area' }; + } + + if (action === 'summary') { + const ret = await getSummaryFromArea( + canvasid, + x, y, u, v, + time, + iid, + ); + if (typeof ret === 'string') { + return { info: ret }; + } + return { + info: null, + columns: ['#pxls', 'IID', 'User', 'last', 'clr', 'ts'], + rows: (ret.length > 300) ? ret.slice(-300) : ret, + }; + } + + if (action === 'all') { + const ret = await getPixelsFromArea( + canvasid, + x, y, u, v, + time, + iid, + ); + if (typeof ret === 'string') { + return { info: ret }; + } + return { + info: null, + columns: ['IID', 'User', 'last', 'clr', 'ts'], + rows: ret, + }; + } + + return { info: 'Invalid action given' }; +} + /* * Execute actions for cleaning/filtering canvas * @param action what to do diff --git a/src/core/parsePixelLog.js b/src/core/parsePixelLog.js new file mode 100644 index 0000000..fba94fb --- /dev/null +++ b/src/core/parsePixelLog.js @@ -0,0 +1,188 @@ +import fs from 'fs'; +import readline from 'readline'; + +import { PIXELLOGGER_PREFIX } from './logger'; +import { getNamesToIds } from '../data/sql/RegUser'; +import { getIdsToIps, getIPofIID } from '../data/sql/IPInfo'; +import { getIPv6Subnet } from '../utils/ip'; + + +function parseFile(cb) { + const date = new Date(); + const year = date.getUTCFullYear(); + let month = date.getUTCMonth() + 1; + let day = date.getUTCDate(); + if (day < 10) day = `0${day}`; + if (month < 10) month = `0${month}`; + const filename = `${PIXELLOGGER_PREFIX}${year}-${month}-${day}.log`; + + return new Promise((resolve, reject) => { + const fileStream = fs.createReadStream(filename); + + const rl = readline.createInterface({ + input: fileStream, + }); + + rl.on('line', (line) => cb(line.split(' '))); + + rl.on('error', (err) => { + reject(err); + }); + + rl.on('close', () => { + resolve(); + }); + }); +} + +/* + * Get summary of users placing in area of current day + * @param canvasId id of canvas + * @param xUL, yUL, xBR, yBR area of canvs + * @param time timestamp of when to start + * @param iid Limit on one user (optional) + * @return array of parsed pixel log lines + * string if error + */ +export async function getSummaryFromArea( + canvasId, + xUL, + yUL, + xBR, + yBR, + time, + iid, +) { + const ips = {}; + const uids = []; + let filterIP = null; + if (iid) { + filterIP = await getIPofIID(iid); + if (!filterIP) { + return 'Could not resolve IID to IP'; + } + } + try { + await parseFile((parts) => { + const [ts, ipFull, uid, cid, x, y,, clr] = parts; + // eslint-disable-next-line eqeqeq + if (canvasId == cid + && ts >= time + && x >= xUL + && x <= xBR + && y >= yUL + && y <= yBR + ) { + const ip = getIPv6Subnet(ipFull); + if (filterIP && ip !== filterIP) { + return; + } + let curVals = ips[ip]; + if (!curVals) { + curVals = [0, ip, uid, 0, 0, 0, 0]; + ips[ip] = curVals; + uids.push(uid); + } + curVals[0] += 1; + curVals[3] = x; + curVals[4] = y; + curVals[5] = clr; + curVals[6] = ts; + } + }); + } catch (err) { + return `Could not parse logfile: ${err.message}`; + } + + const uid2Name = await getNamesToIds(uids); + + const ipKeys = Object.keys(ips); + const ip2Id = await getIdsToIps(ipKeys); + + const rows = []; + for (let i = 0; i < ipKeys.length; i += 1) { + const [pxls, ip, uid, x, y, clr, ts] = ips[ipKeys[i]]; + const userMd = (uid && uid2Name[uid]) + ? `@[${uid2Name[uid]}](${uid})` : 'N/A'; + rows.push([ + pxls, + ip2Id[ip] || 'N/A', + userMd, + `#d,${x},${y}`, + clr, + ts, + ]); + } + + return rows; +} + + +export async function getPixelsFromArea( + canvasId, + xUL, + yUL, + xBR, + yBR, + time, + iid, + maxRows = 300, +) { + const pixels = []; + const uids = []; + const ips = []; + let filterIP = null; + if (iid) { + filterIP = await getIPofIID(iid); + if (!filterIP) { + return 'Could not resolve IID to IP'; + } + } + try { + await parseFile((parts) => { + const [ts, ipFull, uid, cid, x, y,, clr] = parts; + // eslint-disable-next-line eqeqeq + if (canvasId == cid + && ts >= time + && x >= xUL + && x <= xBR + && y >= yUL + && y <= yBR + ) { + const ip = getIPv6Subnet(ipFull); + if (filterIP && ip !== filterIP) { + return; + } + pixels.push([ip, uid, x, y, clr, ts]); + if (!ips.includes(ip)) { + ips.push(ip); + uids.push(uid); + } + } + }); + } catch (err) { + return `Could not parse logfile: ${err.message}`; + } + + const uid2Name = await getNamesToIds(uids); + const ip2Id = await getIdsToIps(ips); + + const pixelF = (pixels.length > 300) ? pixels.slice(maxRows * -1) : pixels; + + const rows = []; + for (let i = 0; i < pixelF.length; i += 1) { + const [ip, uid, x, y, clr, ts] = pixelF[i]; + const userMd = (uid && uid2Name[uid]) + ? `@[${uid2Name[uid]}](${uid})` : 'N/A'; + const id = ip2Id[ip] || 'N/A'; + rows.push([ + id, + userMd, + `#d,${x},${y}`, + clr, + ts, + ]); + } + + return rows; +} diff --git a/src/data/sql/IPInfo.js b/src/data/sql/IPInfo.js index 048dcc3..1687e75 100644 --- a/src/data/sql/IPInfo.js +++ b/src/data/sql/IPInfo.js @@ -87,15 +87,34 @@ const IPInfo = sequelize.define('IPInfo', { }, }); +export async function getIPofIID(uuid) { + let result = null; + try { + result = IPInfo.findOne({ + attributes: ['ip'], + where: { uuid }, + }); + } catch { + return null; + } + if (result) { + return result.id; + } + return null; +} + export async function getIdsToIps(ips) { - const result = IPInfo.findAll({ + const ipToIdMap = {}; + if (!ips.length) { + return ipToIdMap; + } + const result = await IPInfo.findAll({ attributes: ['ip', 'uuid'], where: { ip: ips, }, raw: true, }); - const ipToIdMap = {}; result.forEach((obj) => { ipToIdMap[obj.ip] = obj.uuid; }); diff --git a/src/data/sql/RegUser.js b/src/data/sql/RegUser.js index 0f9c2cf..7f49c7e 100644 --- a/src/data/sql/RegUser.js +++ b/src/data/sql/RegUser.js @@ -182,14 +182,17 @@ export async function findIdByNameOrId(searchString) { } export async function getNamesToIds(ids) { - const result = RegUser.findAll({ + const idToNameMap = {}; + if (!ids.length) { + return idToNameMap; + } + const result = await RegUser.findAll({ attributes: ['id', 'name'], where: { id: ids, }, raw: true, }); - const idToNameMap = {}; result.forEach((obj) => { idToNameMap[obj.id] = obj.name; }); diff --git a/src/routes/api/modtools.js b/src/routes/api/modtools.js index fae0b3c..255f3f9 100644 --- a/src/routes/api/modtools.js +++ b/src/routes/api/modtools.js @@ -18,6 +18,7 @@ import { executeProtAction, executeRollback, executeCleanerAction, + executeWatchAction, getModList, removeMod, makeMod, @@ -99,6 +100,21 @@ router.post('/', upload.single('image'), async (req, res, next) => { res.status(200).send(ret); return; } + if (req.body.watchaction) { + const { + watchaction, ulcoor, brcoor, time, iid, canvasid, + } = req.body; + const ret = await executeWatchAction( + watchaction, + ulcoor, + brcoor, + time, + iid, + canvasid, + ); + res.status(200).json(ret); + return; + } if (req.body.cleaneraction) { const { cleaneraction, ulcoor, brcoor, canvasid,