diff --git a/src/components/Modtools.jsx b/src/components/Modtools.jsx
index c70c3cc..8d94c09 100644
--- a/src/components/Modtools.jsx
+++ b/src/components/Modtools.jsx
@@ -7,14 +7,17 @@ import React, { useState, useEffect } from 'react';
import { useSelector, shallowEqual } from 'react-redux';
import { t } from 'ttag';
+import useInterval from './hooks/useInterval';
import { getToday, dateToString } from '../core/utils';
const keptState = {
- coords: null,
- tlcoords: null,
- brcoords: null,
- tlrcoords: null,
- brrcoords: null,
+ coords: '',
+ tlcoords: '',
+ brcoords: '',
+ tlrcoords: '',
+ brrcoords: '',
+ tlccoords: '',
+ brccoords: '',
};
async function submitImageAction(
@@ -80,6 +83,26 @@ async function submitRollback(
callback(await resp.text());
}
+async function submitCanvasCleaner(
+ action,
+ canvas,
+ tlcoords,
+ brcoords,
+ callback,
+) {
+ const data = new FormData();
+ data.append('cleaneraction', action);
+ data.append('canvasid', canvas);
+ data.append('ulcoor', tlcoords);
+ data.append('brcoor', brcoords);
+ const resp = await fetch('./api/modtools', {
+ credentials: 'include',
+ method: 'POST',
+ body: data,
+ });
+ callback(await resp.text());
+}
+
async function submitIPAction(
action,
callback,
@@ -113,6 +136,41 @@ async function getModList(
}
}
+async function getCleanerStats(
+ callback,
+) {
+ const data = new FormData();
+ data.append('cleanerstat', true);
+ const resp = await fetch('./api/modtools', {
+ credentials: 'include',
+ method: 'POST',
+ body: data,
+ });
+ if (resp.ok) {
+ callback(await resp.json());
+ } else {
+ callback({
+ });
+ }
+}
+
+async function getCleanerCancel(
+ callback,
+) {
+ const data = new FormData();
+ data.append('cleanercancel', true);
+ const resp = await fetch('./api/modtools', {
+ credentials: 'include',
+ method: 'POST',
+ body: data,
+ });
+ if (resp.ok) {
+ callback(await resp.text());
+ } else {
+ callback('');
+ }
+}
+
async function submitRemMod(
userId,
callback,
@@ -151,6 +209,7 @@ function Modtools() {
const [selectedCanvas, selectCanvas] = useState(0);
const [imageAction, selectImageAction] = useState('build');
+ const [cleanAction, selectCleanAction] = useState('spare');
const [iPAction, selectIPAction] = useState('ban');
const [protAction, selectProtAction] = useState('protect');
const [date, selectDate] = useState(maxDate);
@@ -159,9 +218,12 @@ function Modtools() {
const [brcoords, selectBRCoords] = useState(keptState.brcoords);
const [tlrcoords, selectTLRCoords] = useState(keptState.tlrcoords);
const [brrcoords, selectBRRCoords] = useState(keptState.brrcoords);
- const [modName, selectModName] = useState(null);
+ const [tlccoords, selectTLCCoords] = useState(keptState.tlrcoords);
+ const [brccoords, selectBRCCoords] = useState(keptState.brrcoords);
+ const [modName, selectModName] = useState('');
const [resp, setResp] = useState(null);
const [modlist, setModList] = useState([]);
+ const [cleanerstats, setCleanerStats] = useState({});
const [submitting, setSubmitting] = useState(false);
const [
@@ -193,12 +255,40 @@ function Modtools() {
// nothing
}
+ let descCleanAction;
+ switch (cleanAction) {
+ case 'spare':
+ // eslint-disable-next-line max-len
+ descCleanAction = t`Clean spare pixels that are surrounded by unset pixels`;
+ break;
+ case 'spareext':
+ // eslint-disable-next-line max-len
+ descCleanAction = t`Clean spare pixels that are surrounded by a single other color`;
+ break;
+ default:
+ // nothing
+ }
+
useEffect(() => {
if (userlvl === 1) {
getModList((mods) => setModList(mods));
}
+ if (userlvl > 0) {
+ getCleanerStats((stats) => setCleanerStats(stats));
+ }
}, []);
+ useInterval(() => {
+ if (userlvl > 0) {
+ getCleanerStats((stats) => setCleanerStats(stats));
+ }
+ }, 10000);
+
+ const cleanerStatusString = (!cleanerstats.running)
+ ? t`Status: Not running`
+ // eslint-disable-next-line max-len
+ : `Status: ${cleanerstats.method} from ${cleanerstats.tl} to ${cleanerstats.br} on canvas ${canvases[cleanerstats.canvasId].ident} to ${cleanerstats.percent} done`;
+
return (
{resp && (
@@ -227,6 +317,7 @@ function Modtools() {
)}
Choose Canvas:
)}
+
+
+ {t`Canvas Cleaner`}
+
+ {t`Apply a filter to clean trash in large canvas areas.`}
+
+ {
+ const sel = e.target;
+ selectCleanAction(sel.options[sel.selectedIndex].value);
+ }}
+ >
+ {['spare', 'spareext'].map((opt) => (
+
+ ))}
+
+ {descCleanAction}
+
+ {cleanerStatusString}
+
+
+ {t`Top-left corner`} (X_Y):
+ {
+ const co = evt.target.value.trim();
+ selectTLCCoords(co);
+ keptState.tlccoords = co;
+ }}
+ />
+
+
+ {t`Bottom-right corner`} (X_Y):
+ {
+ const co = evt.target.value.trim();
+ selectBRCCoords(co);
+ keptState.brccoords = co;
+ }}
+ />
+
+
+
+
{(userlvl === 1) && (
@@ -477,6 +676,7 @@ function Modtools() {
{t`Do stuff with IPs (one IP per line)`}
{
const sel = e.target;
selectIPAction(sel.options[sel.selectedIndex].value);
@@ -485,7 +685,6 @@ function Modtools() {
{['ban', 'unban', 'whitelist', 'unwhitelist'].map((opt) => (
diff --git a/src/components/hooks/useInterval.js b/src/components/hooks/useInterval.js
new file mode 100644
index 0000000..6fe50f6
--- /dev/null
+++ b/src/components/hooks/useInterval.js
@@ -0,0 +1,24 @@
+import { useEffect, useRef } from 'react';
+
+function useInterval(callback, delay) {
+ const savedCallback = useRef();
+
+ // Remember the latest callback.
+ useEffect(() => {
+ savedCallback.current = callback;
+ }, [callback]);
+
+ // Set up the interval.
+ // eslint-disable-next-line consistent-return
+ useEffect(() => {
+ function tick() {
+ savedCallback.current();
+ }
+ if (delay !== null) {
+ const id = setInterval(tick, delay);
+ return () => clearInterval(id);
+ }
+ }, [delay]);
+}
+
+export default useInterval;
diff --git a/src/controls/PixelPainterControls.js b/src/controls/PixelPainterControls.js
index 113516f..3231880 100644
--- a/src/controls/PixelPainterControls.js
+++ b/src/controls/PixelPainterControls.js
@@ -80,7 +80,7 @@ class PixelPlainterControls {
viewport.addEventListener('mousemove', this.onMouseMove, false);
viewport.addEventListener('mouseup', this.onMouseUp, false);
// TODO check if we can go passive here
- //viewport.addEventListener('wheel', this.onWheel, { passive: true });
+ // viewport.addEventListener('wheel', this.onWheel, { passive: true });
viewport.addEventListener('wheel', this.onWheel, false);
viewport.addEventListener('touchstart', this.onTouchStart, false);
viewport.addEventListener('touchend', this.onTouchEnd, false);
diff --git a/src/core/CanvasCleaner.js b/src/core/CanvasCleaner.js
new file mode 100644
index 0000000..87f75de
--- /dev/null
+++ b/src/core/CanvasCleaner.js
@@ -0,0 +1,406 @@
+/*
+ * runs a filter over a larger canvas area over time
+ *
+ */
+
+import {
+ setData,
+ getData,
+ setStatus,
+ getStatus,
+} from '../data/models/CanvasCleaner';
+import RedisCanvas from '../data/models/RedisCanvas';
+import {
+ getChunkOfPixel,
+ getCornerOfChunk,
+} from './utils';
+import { setPixelByOffset } from './setPixel';
+import {
+ TILE_SIZE,
+} from './constants';
+import logger from './logger';
+// eslint-disable-next-line import/no-unresolved
+import canvases from './canvases.json';
+
+const METHODS = {
+ /*
+ * @param xc, yc chunk coordinates of pixel relative to center chunk
+ * of chunk area
+ */
+ spare: (xc, yc, clrIgnore, canvasCleaner) => {
+ let rplPxl = null;
+ for (let u = -1; u <= 1; u += 1) {
+ for (let v = -1; v <= 1; v += 1) {
+ const pxl = canvasCleaner.getPixelInChunkArea(xc + u, yc + v);
+ if (pxl === null) {
+ continue;
+ }
+ if (u === 0 && v === 0) {
+ if (pxl < clrIgnore) {
+ return null;
+ }
+ } else {
+ if (pxl >= clrIgnore) {
+ return null;
+ }
+ if (rplPxl === null) {
+ rplPxl = pxl;
+ }
+ }
+ }
+ }
+ return rplPxl;
+ },
+
+ spareext: (xc, yc, clrIgnore, canvasCleaner) => {
+ let rplPxl = null;
+ let origPxl = null;
+ for (let u = -1; u <= 1; u += 1) {
+ for (let v = -1; v <= 1; v += 1) {
+ const pxl = canvasCleaner.getPixelInChunkArea(xc + u, yc + v);
+ if (pxl === null) {
+ continue;
+ }
+ if (u === 0 && v === 0) {
+ if (pxl < clrIgnore || pxl === rplPxl) {
+ return null;
+ }
+ origPxl = pxl;
+ } else {
+ if (rplPxl === null) {
+ rplPxl = pxl;
+ }
+ if (pxl >= clrIgnore) {
+ if (rplPxl < clrIgnore) {
+ rplPxl = pxl;
+ continue;
+ }
+ if (pxl !== rplPxl) {
+ return null;
+ }
+ }
+ }
+ }
+ }
+ if (rplPxl === origPxl) {
+ return null;
+ }
+ return rplPxl;
+ },
+};
+
+class CanvasCleaner {
+ // canvas id: integer
+ canvasId;
+ // coords of top left and bottom right corner of area: integer
+ x;
+ y;
+ u;
+ v;
+ // name of filter method, string
+ methodName;
+ // 3x3 canvas area
+ // [
+ // [AA, AB, AC],
+ // [BA, BB, BC],
+ // [CA, CB, CC],
+ // ]
+ chunks;
+ // chunk coordinates of center BB of chunks
+ centerChunk;
+ // iterator over chunks
+ cIter;
+ // info about chunks of total affected area
+ // cx, cy: top right chunk coords
+ // cw, ch: height and width in chunks
+ // amountChunks: cw * ch
+ cx; cy;
+ cw; ch;
+ amountChunks;
+ // current setTimeout index
+ tick;
+ // if running: boolean
+ running;
+ // stats
+ pxlProcessed;
+ pxlCleaned;
+
+ constructor() {
+ this.logger = (text) => {
+ logger.warn(`[CanvasCleaner] ${text}`);
+ };
+ this.cleanChunk = this.cleanChunk.bind(this);
+ this.clearValues();
+ this.loadArgs();
+ }
+
+ clearValues() {
+ this.running = false;
+ this.chunks = [
+ [null, null, null],
+ [null, null, null],
+ [null, null, null],
+ ];
+ this.centerChunk = [null, null];
+ this.cIter = 0;
+ this.cx = 0;
+ this.cy = 0;
+ this.cw = 0;
+ this.ch = 0;
+ this.amountChunks = 0;
+ this.pxlProcessed = 0;
+ this.pxlCleaned = 0;
+ this.tick = null;
+ }
+
+ async loadArgs() {
+ const [cIter, running] = await getStatus();
+ if (running) {
+ const [canvasId, x, y, u, v, methodName] = await getData();
+ this.set(canvasId, x, y, u, v, methodName, cIter);
+ }
+ }
+
+ stop() {
+ this.running = false;
+ const str = 'Stopped CanvasCleaner';
+ this.logger(str);
+ return str;
+ }
+
+ async cleanChunk() {
+ this.tick = null;
+ const {
+ canvasId, cIter, cw, cx, cy,
+ } = this;
+ const method = METHODS[this.methodName];
+ if (cIter >= this.amountChunks || !this.running) {
+ // finished
+ // eslint-disable-next-line max-len
+ this.logger(`Finished Cleaning on ${this.x},${this.y}, cleaned ${this.pxlCleaned} / ${this.pxlProcessed} pixels`);
+ this.clearValues();
+ this.saveStatus();
+ return;
+ }
+ const canvas = canvases[canvasId];
+ let i = (cIter % cw);
+ const j = ((cIter - i) / cw) + cy;
+ i += cx;
+ const clrIgnore = canvas.cli || 0;
+
+ await this.loadChunkArea(i, j);
+ if (this.checkIfChunkInArea(i, j)) {
+ const [xCor, yCor] = getCornerOfChunk(canvas.size, i, j);
+ const xLow = (xCor > this.x) ? 0 : (this.x - xCor);
+ const yLow = (yCor > this.y) ? 0 : (this.y - yCor);
+ const xHigh = (xCor + TILE_SIZE <= this.u) ? TILE_SIZE
+ : (this.u - xCor + 1);
+ const yHigh = (yCor + TILE_SIZE <= this.v) ? TILE_SIZE
+ : (this.v - yCor + 1);
+ for (let xc = xLow; xc < xHigh; xc += 1) {
+ for (let yc = yLow; yc < yHigh; yc += 1) {
+ // eslint-disable-next-line no-await-in-loop
+ const rplPxl = await method(xc, yc, clrIgnore, this);
+ this.pxlProcessed += 1;
+ if (rplPxl !== null) {
+ this.pxlCleaned += 1;
+ setPixelByOffset(
+ canvasId,
+ rplPxl,
+ i, j,
+ yc * TILE_SIZE + xc,
+ );
+ }
+ }
+ }
+ }
+
+ this.saveStatus();
+ this.cIter += 1;
+
+ this.tick = setTimeout(this.cleanChunk, 500);
+ }
+
+ set(canvasId, x, y, u, v, methodName, cIter = 0) {
+ if (!METHODS[methodName]) {
+ const str = `Method ${methodName} not available`;
+ this.logger(str);
+ return str;
+ }
+ const canvas = canvases[canvasId];
+ if (!canvas) {
+ const str = `Canvas ${canvasId} invalid`;
+ this.logger(str);
+ return str;
+ }
+ if (canvas.v) {
+ const str = 'Can not clean 3D canvas';
+ this.logger(str);
+ return str;
+ }
+ if (x > u || y > v) {
+ const str = 'Invalid area';
+ this.logger(str);
+ return str;
+ }
+ const canvasSize = canvas.size;
+ const canvasMaxXY = canvasSize / 2;
+ const canvasMinXY = -canvasMaxXY;
+ if (x < canvasMinXY || y < canvasMinXY
+ || x >= canvasMaxXY || y >= canvasMaxXY
+ || u < canvasMinXY || v < canvasMinXY
+ || u >= canvasMaxXY || v >= canvasMaxXY) {
+ const str = 'Coordinates out of bounds';
+ this.logger(str);
+ return str;
+ }
+
+ if (this.tick) {
+ this.running = false;
+ clearTimeout(this.tick);
+ }
+ this.canvasId = canvasId;
+ this.x = x;
+ this.y = y;
+ this.u = u;
+ this.v = v;
+ this.cIter = cIter;
+ this.methodName = methodName;
+ const [cx, cy] = getChunkOfPixel(canvas.size, this.x, this.y);
+ this.cx = cx;
+ this.cy = cy;
+ const [cu, cv] = getChunkOfPixel(canvas.size, this.u, this.v);
+ this.cw = cu - cx + 1;
+ this.ch = cv - cy + 1;
+ this.amountChunks = this.cw * this.ch;
+
+ this.running = true;
+ this.tick = setTimeout(this.cleanChunk, 500);
+ // eslint-disable-next-line max-len
+ this.logger(`Start Cleaning on #${canvas.ident},${this.x},${this.y} till #${canvas.ident},${this.u},${this.v} with method ${methodName}`);
+ this.saveData();
+ return null;
+ }
+
+ /*
+ * get pixel out of 3x3 chunk area
+ * @param x, y coordinates relative to center chunk
+ * @return integer color index or null if chunk is empty
+ */
+ getPixelInChunkArea(x, y) {
+ const { chunks } = this;
+ let col;
+ let xc = x;
+ if (x >= 0 && x < TILE_SIZE) {
+ col = 1;
+ } else if (x < 0) {
+ col = 0;
+ xc += TILE_SIZE;
+ } else {
+ col = 2;
+ xc -= TILE_SIZE;
+ }
+ let row;
+ let yc = y;
+ if (y >= 0 && y < TILE_SIZE) {
+ row = 1;
+ } else if (y < 0) {
+ row = 0;
+ yc += TILE_SIZE;
+ } else {
+ row = 2;
+ yc -= TILE_SIZE;
+ }
+ const chunk = chunks[row][col];
+ if (!chunk) return null;
+ // get rid of protection
+ return chunk[yc * TILE_SIZE + xc] & 0x3F;
+ }
+
+ /*
+ * load 3x3 chunk area
+ * @param i, j chunk coordinates of center chunk
+ */
+ async loadChunkArea(i, j) {
+ const { chunks, centerChunk, canvasId } = this;
+ const [io, jo] = centerChunk;
+ const newChunks = [
+ [null, null, null],
+ [null, null, null],
+ [null, null, null],
+ ];
+ for (let iRel = -1; iRel <= 1; iRel += 1) {
+ for (let jRel = -1; jRel <= 1; jRel += 1) {
+ let chunk = null;
+ const iAbs = iRel + i;
+ const jAbs = jRel + j;
+ if (
+ io && jo
+ && iAbs >= io - 1
+ && iAbs <= io + 1
+ && jAbs >= jo - 1
+ && jAbs <= jo + 1
+ ) {
+ chunk = chunks[jAbs - jo + 1][iAbs - io + 1];
+ } else {
+ // eslint-disable-next-line no-await-in-loop
+ chunk = await RedisCanvas.getChunk(canvasId, iAbs, jAbs);
+ if (!chunk || chunk.length !== TILE_SIZE * TILE_SIZE) {
+ chunk = null;
+ if (chunk) {
+ // eslint-disable-next-line max-len
+ this.logger(`Chunk ch:${canvasId}:${iAbs}:${jAbs} has invalid size ${chunk.length}.`);
+ }
+ }
+ }
+ newChunks[jRel + 1][iRel + 1] = chunk;
+ }
+ }
+ this.chunks = newChunks;
+ this.centerChunk = [i, j];
+ }
+
+ /*
+ * check if chunk exists in area and is not empty
+ * @param i, j chunk to check
+ */
+ checkIfChunkInArea(i, j) {
+ const { chunks, centerChunk } = this;
+ const [io, jo] = centerChunk;
+ if (
+ io && jo
+ && i >= io - 1
+ && i <= io + 1
+ && j >= jo - 1
+ && j <= jo + 1
+ ) {
+ const col = i - io + 1;
+ const row = j - jo + 1;
+ if (chunks[row][col] !== null) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ reportStatus() {
+ return {
+ running: this.running,
+ canvasId: this.canvasId,
+ percent: `${this.cIter} / ${this.amountChunks}`,
+ tl: `${this.x}_${this.y}`,
+ br: `${this.u}_${this.v}`,
+ method: this.methodName,
+ };
+ }
+
+ saveData() {
+ setData(this.canvasId, this.x, this.y, this.u, this.v, this.methodName);
+ }
+
+ saveStatus() {
+ setStatus(this.cIter, this.running);
+ }
+}
+
+export default new CanvasCleaner();
diff --git a/src/core/adminfunctions.js b/src/core/adminfunctions.js
index 1c9856c..106c350 100644
--- a/src/core/adminfunctions.js
+++ b/src/core/adminfunctions.js
@@ -1,7 +1,6 @@
/*
* functions for admintools
*
- * @flow
*/
/* eslint-disable no-await-in-loop */
@@ -10,8 +9,9 @@ import sharp from 'sharp';
import Sequelize from 'sequelize';
import redis from '../data/redis';
-import { modtoolsLogger } from './logger';
import { getIPv6Subnet } from '../utils/ip';
+import { validateCoorRange } from '../utils/validation';
+import CanvasCleaner from './CanvasCleaner';
import { Blacklist, Whitelist, RegUser } from '../data/models';
// eslint-disable-next-line import/no-unresolved
import canvases from './canvases.json';
@@ -27,7 +27,7 @@ import rollbackCanvasArea from './rollback';
* @param ip already sanizized ip
* @return true if successful
*/
-export async function executeIPAction(action: string, ips: string): string {
+export async function executeIPAction(action, ips, logger = null) {
const ipArray = ips.split('\n');
let out = '';
const splitRegExp = /\s+/;
@@ -46,7 +46,7 @@ export async function executeIPAction(action: string, ips: string): string {
const ipKey = getIPv6Subnet(ip);
const key = `isprox:${ipKey}`;
- modtoolsLogger.info(`ADMINTOOLS: ${action} ${ip}`);
+ if (logger) logger(`${action} ${ip}`);
switch (action) {
case 'ban':
await Blacklist.findOrCreate({
@@ -89,10 +89,11 @@ export async function executeIPAction(action: string, ips: string): string {
* @return [ret, msg] http status code and message
*/
export async function executeImageAction(
- action: string,
- file: Object,
- coords: string,
- canvasid: string,
+ action,
+ file,
+ coords,
+ canvasid,
+ logger = null,
) {
if (!coords) {
return [403, 'Coordinates not defined'];
@@ -150,7 +151,7 @@ export async function executeImageAction(
);
// eslint-disable-next-line max-len
- modtoolsLogger.info(`ADMINTOOLS: Loaded image wth ${pxlCount} pixels to ${x}/${y}`);
+ if (logger) logger(`Loaded image wth ${pxlCount} pixels to #${canvas.ident},${x},${y}`);
return [
200,
`Successfully loaded image wth ${pxlCount} pixels to ${x}/${y}`,
@@ -160,6 +161,53 @@ export async function executeImageAction(
}
}
+/*
+ * Execute actions for cleaning/filtering canvas
+ * @param action what to do
+ * @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 [ret, msg] http status code and message
+ */
+export async function executeCleanerAction(
+ action,
+ ulcoor,
+ brcoor,
+ canvasid,
+ logger = null,
+) {
+ if (!canvasid) {
+ return [403, 'canvasid not defined'];
+ }
+ 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';
+ }
+ if (error) {
+ return [403, error];
+ }
+
+ const parseCoords = validateCoorRange(ulcoor, brcoor, canvas.size);
+ if (typeof parseCoords === 'string') {
+ return [403, parseCoords];
+ }
+ const [x, y, u, v] = parseCoords;
+
+ error = CanvasCleaner.set(canvasid, x, y, u, v, action);
+ if (error) {
+ return [403, error];
+ }
+ // eslint-disable-next-line max-len
+ const report = `Set Canvas Cleaner to ${action} canvas ${canvas.ident} from ${ulcoor} to ${brcoor}`;
+ if (logger) logger(report);
+ return [200, report];
+}
+
/*
* Execute actions for protecting areas
* @param action what to do
@@ -169,46 +217,23 @@ export async function executeImageAction(
* @return [ret, msg] http status code and message
*/
export async function executeProtAction(
- action: string,
- ulcoor: string,
- brcoor: string,
- canvasid: number,
+ action,
+ ulcoor,
+ brcoor,
+ canvasid,
+ logger = null,
) {
- if (!ulcoor || !brcoor) {
- return [403, 'Not all coordinates defined'];
- }
if (!canvasid) {
return [403, 'canvasid not defined'];
}
-
- let splitCoords = ulcoor.trim().split('_');
- if (splitCoords.length !== 2) {
- return [403, 'Invalid Coordinate Format for top-left corner'];
- }
- const [x, y] = splitCoords.map((z) => Math.floor(Number(z)));
- splitCoords = brcoor.trim().split('_');
- if (splitCoords.length !== 2) {
- return [403, 'Invalid Coordinate Format for bottom-right corner'];
- }
- const [u, v] = splitCoords.map((z) => Math.floor(Number(z)));
-
const canvas = canvases[canvasid];
-
let error = null;
- if (Number.isNaN(x)) {
- error = 'x of top-left corner is not a valid number';
- } else if (Number.isNaN(y)) {
- error = 'y of top-left corner is not a valid number';
- } else if (Number.isNaN(u)) {
- error = 'x of bottom-right corner is not a valid number';
- } else if (Number.isNaN(v)) {
- error = 'y of bottom-right corner is not a valid number';
- } else if (u < x || v < y) {
- error = 'Corner coordinates are alligned wrong';
- } else if (!action) {
- error = 'No imageaction given';
+ if (!ulcoor || !brcoor) {
+ error = 'Not all coordinates defined';
} else if (!canvas) {
error = 'Invalid canvas selected';
+ } else if (!action) {
+ error = 'No imageaction given';
} else if (action !== 'protect' && action !== 'unprotect') {
error = 'Invalid action (must be protect or unprotect)';
}
@@ -216,19 +241,17 @@ export async function executeProtAction(
return [403, error];
}
- const canvasMaxXY = canvas.size / 2;
- const canvasMinXY = -canvasMaxXY;
- if (x < canvasMinXY || y < canvasMinXY
- || x >= canvasMaxXY || y >= canvasMaxXY) {
- return [403, 'Coordinates of top-left corner are outside of canvas'];
- }
- if (u < canvasMinXY || v < canvasMinXY
- || u >= canvasMaxXY || v >= canvasMaxXY) {
- return [403, 'Coordinates of bottom-right corner are outside of canvas'];
+ const parseCoords = validateCoorRange(ulcoor, brcoor, canvas.size);
+ if (typeof parseCoords === 'string') {
+ return [403, parseCoords];
}
+ const [x, y, u, v] = parseCoords;
const width = u - x + 1;
const height = v - y + 1;
+ if (width * height > 10000000) {
+ return [403, 'Can not set protection to more than 10m pixels at onec'];
+ }
const protect = action === 'protect';
const pxlCount = await protectCanvasArea(
canvasid,
@@ -238,10 +261,13 @@ export async function executeProtAction(
height,
protect,
);
- modtoolsLogger.info(
- // eslint-disable-next-line max-len
- `ADMINTOOLS: Set protect to ${protect} for ${pxlCount} pixels at ${x} / ${y} with dimension ${width}x${height}`,
- );
+ if (logger) {
+ logger(
+ (protect)
+ ? `Protect ${width}x${height} area at #${canvas.ident},${x},${y}`
+ : `Unprotect ${width}x${height} area at #${canvas.ident},${x},${y}`,
+ );
+ }
return [
200,
(protect)
@@ -261,63 +287,36 @@ export async function executeProtAction(
* @return [ret, msg] http status code and message
*/
export async function executeRollback(
- date: string,
- ulcoor: string,
- brcoor: string,
- canvasid: number,
+ date,
+ ulcoor,
+ brcoor,
+ canvasid,
+ logger = null,
) {
- if (!ulcoor || !brcoor) {
- return [403, 'Not all coordinates defined'];
- }
if (!canvasid) {
return [403, 'canvasid not defined'];
}
-
- let splitCoords = ulcoor.trim().split('_');
- if (splitCoords.length !== 2) {
- return [403, 'Invalid Coordinate Format for top-left corner'];
- }
- const [x, y] = splitCoords.map((z) => Math.floor(Number(z)));
- splitCoords = brcoor.trim().split('_');
- if (splitCoords.length !== 2) {
- return [403, 'Invalid Coordinate Format for bottom-right corner'];
- }
- const [u, v] = splitCoords.map((z) => Math.floor(Number(z)));
-
const canvas = canvases[canvasid];
-
let error = null;
- if (Number.isNaN(x)) {
- error = 'x of top-left corner is not a valid number';
- } else if (Number.isNaN(y)) {
- error = 'y of top-left corner is not a valid number';
- } else if (Number.isNaN(u)) {
- error = 'x of bottom-right corner is not a valid number';
- } else if (Number.isNaN(v)) {
- error = 'y of bottom-right corner is not a valid number';
- } else if (u < x || v < y) {
- error = 'Corner coordinates are alligned wrong';
+ if (!ulcoor || !brcoor) {
+ error = 'Not all coordinates defined';
+ } else if (!canvas) {
+ error = 'Invalid canvas selected';
} else if (!date) {
error = 'No date given';
} else if (Number.isNaN(Number(date)) || date.length !== 8) {
error = 'Invalid date';
- } else if (!canvas) {
- error = 'Invalid canvas selected';
}
if (error !== null) {
return [403, error];
}
- const canvasMaxXY = canvas.size / 2;
- const canvasMinXY = -canvasMaxXY;
- if (x < canvasMinXY || y < canvasMinXY
- || x >= canvasMaxXY || y >= canvasMaxXY) {
- return [403, 'Coordinates of top-left corner are outside of canvas'];
- }
- if (u < canvasMinXY || v < canvasMinXY
- || u >= canvasMaxXY || v >= canvasMaxXY) {
- return [403, 'Coordinates of bottom-right corner are outside of canvas'];
+
+ const parseCoords = validateCoorRange(ulcoor, brcoor, canvas.size);
+ if (typeof parseCoords === 'string') {
+ return [403, parseCoords];
}
+ const [x, y, u, v] = parseCoords;
const width = u - x + 1;
const height = v - y + 1;
@@ -333,10 +332,12 @@ export async function executeRollback(
height,
date,
);
- modtoolsLogger.info(
+ if (logger) {
+ logger(
// eslint-disable-next-line max-len
- `ADMINTOOLS: Rollback to ${date} for ${pxlCount} pixels at ${x} / ${y} with dimension ${width}x${height}`,
- );
+ `Rollback to ${date} for ${pxlCount} pixels with dimension ${width}x${height} at #${canvas.ident},${x},${y}`,
+ );
+ }
return [
200,
// eslint-disable-next-line max-len
@@ -381,6 +382,9 @@ export async function removeMod(userId) {
}
export async function makeMod(name) {
+ if (!name) {
+ throw new Error('No username given');
+ }
let user = null;
try {
user = await RegUser.findOne({
diff --git a/src/core/utils.js b/src/core/utils.js
index 4dda499..8980edc 100644
--- a/src/core/utils.js
+++ b/src/core/utils.js
@@ -77,6 +77,19 @@ export function getChunkOfPixel(
return [cx, cy];
}
+// get coordinates of top-left corner of chunk
+export function getCornerOfChunk(
+ canvasSize,
+ i,
+ j,
+ is3d = false,
+) {
+ const tileSize = (is3d) ? THREE_TILE_SIZE : TILE_SIZE;
+ const x = (i * tileSize) - (canvasSize / 2);
+ const y = (j * tileSize) - (canvasSize / 2);
+ return [x, y, 0];
+}
+
export function getTileOfPixel(
tileScale,
pixel,
diff --git a/src/data/models/CanvasCleaner.js b/src/data/models/CanvasCleaner.js
new file mode 100644
index 0000000..44f2576
--- /dev/null
+++ b/src/data/models/CanvasCleaner.js
@@ -0,0 +1,107 @@
+/*
+ * storing Event data
+ */
+
+import redis from '../redis';
+import logger from '../../core/logger';
+
+const DATA_KEY = 'clr:dat';
+const STAT_KEY = 'clr:sta';
+
+/*
+ * Gets data of CanvasCleaner from redis
+ * @return Array with [canvasId, x, y, u, v, methodName] (all int except Name)
+ * (check core/CanvasCleaner for the meaning)
+*/
+export async function getData() {
+ const data = await redis.getAsync(DATA_KEY);
+ if (data) {
+ const parsedData = data.toString().split(':');
+ for (let i = 0; i < parsedData.length - 1; i += 1) {
+ const num = parseInt(parsedData[i], 10);
+ if (Number.isNaN(num)) {
+ logger.warn(
+ // eslint-disable-next-line max-len
+ `[CanvasCleaner] ${DATA_KEY} in redis does not seem legit (int conversion).`,
+ );
+ return [0, 0, 0, 0, 0, 0, ''];
+ }
+ parsedData[i] = num;
+ }
+ if (parsedData.length === 6) {
+ return parsedData;
+ }
+ logger.warn(
+ `[CanvasCleaner] ${DATA_KEY} in redis does not seem legit.`,
+ );
+ }
+ return [0, 0, 0, 0, 0, 0, ''];
+}
+
+/*
+ * Writes data of CanvasCleaner to redis
+ * @param check out core/CanvasCleaner
+ */
+export async function setData(canvasId, x, y, u, v, methodName) {
+ const dataStr = `${canvasId}:${x}:${y}:${u}:${v}:${methodName}`;
+ if (
+ Number.isNaN(parseInt(canvasId, 10))
+ || Number.isNaN(parseInt(x, 10))
+ || Number.isNaN(parseInt(y, 10))
+ || Number.isNaN(parseInt(u, 10))
+ || Number.isNaN(parseInt(v, 10))
+ ) {
+ logger.warn(
+ `[CanvasCleaner] can not write ${dataStr} to redis, seems not legit.`,
+ );
+ return null;
+ }
+ return redis.setAsync(DATA_KEY, dataStr);
+}
+
+/*
+ * Gets status of CanvasCleaner from redis
+ * @return Array with [cIter, running]
+ * cIter: current chunk iterator integer
+ * running: boolean if filter is running
+ */
+export async function getStatus() {
+ const stat = await redis.getAsync(STAT_KEY);
+ if (stat) {
+ const parsedStat = stat.toString().split(':');
+ if (parsedStat.length !== 2) {
+ logger.warn(
+ `[CanvasCleaner] ${STAT_KEY} in redis is incomplete.`,
+ );
+ } else {
+ const cIter = parseInt(parsedStat[0], 10);
+ const running = !!parseInt(parsedStat[1], 10);
+ if (!Number.isNaN(cIter)) {
+ return [cIter, running];
+ }
+ logger.warn(
+ `[CanvasCleaner] ${STAT_KEY} in redis does not seem legit.`,
+ );
+ }
+ }
+ return [0, false];
+}
+
+/*
+ * Writes status of CanvasCleaner to redis
+ * @param cIter current chunk iterator integer
+ * @param running Boolean if running or not
+ */
+export async function setStatus(cIter, running) {
+ const runningInt = (running) ? 1 : 0;
+ const statString = `${cIter}:${runningInt}`;
+ if (
+ Number.isNaN(parseInt(cIter, 10))
+ ) {
+ logger.warn(
+ `[CanvasCleaner] can not write ${statString} to redis, seems not legit.`,
+ );
+ return null;
+ }
+ return redis.setAsync(STAT_KEY, statString);
+}
diff --git a/src/data/models/Event.js b/src/data/models/Event.js
index d65ce91..3829aca 100644
--- a/src/data/models/Event.js
+++ b/src/data/models/Event.js
@@ -2,7 +2,6 @@
*
* data saving for hourly events
*
- * @flow
*/
// its ok if its slow
@@ -101,7 +100,7 @@ export async function clearOldEvent() {
* @param minutes minutes till next event
* @param i, j chunk coordinates of center of event
*/
-export async function setNextEvent(minutes: number, i: number, j: number) {
+export async function setNextEvent(minutes, i, j) {
await clearOldEvent();
for (let jc = j - 1; jc <= j + 1; jc += 1) {
for (let ic = i - 1; ic <= i + 1; ic += 1) {
diff --git a/src/routes/api/modtools.js b/src/routes/api/modtools.js
index 1558a82..97db245 100644
--- a/src/routes/api/modtools.js
+++ b/src/routes/api/modtools.js
@@ -10,13 +10,15 @@ import express from 'express';
import type { Request, Response } from 'express';
import multer from 'multer';
+import CanvasCleaner from '../../core/CanvasCleaner';
import { getIPFromRequest } from '../../utils/ip';
-import { modtoolsLogger } from '../../core/logger';
+import logger, { modtoolsLogger } from '../../core/logger';
import {
executeIPAction,
executeImageAction,
executeProtAction,
executeRollback,
+ executeCleanerAction,
getModList,
removeMod,
makeMod,
@@ -43,7 +45,7 @@ const upload = multer({
router.use(async (req, res, next) => {
const ip = getIPFromRequest(req);
if (!req.user) {
- modtoolsLogger.info(
+ logger.warn(
`MODTOOLS: ${ip} tried to access modtools without login`,
);
const { t } = req.ttag;
@@ -55,16 +57,13 @@ router.use(async (req, res, next) => {
* 2 = Mod
*/
if (!req.user.userlvl) {
- modtoolsLogger.info(
+ logger.warn(
`MODTOOLS: ${ip} / ${req.user.id} tried to access modtools`,
);
const { t } = req.ttag;
res.status(403).send(t`You are not allowed to access this page`);
return;
}
- modtoolsLogger.info(
- `MODTOOLS: ${req.user.id} / ${req.user.regUser.name} is using modtools`,
- );
next();
});
@@ -74,7 +73,40 @@ router.use(async (req, res, next) => {
* Post for mod + admin
*/
router.post('/', upload.single('image'), async (req, res, next) => {
+ const aLogger = (text) => {
+ const timeString = new Date().toLocaleTimeString();
+ modtoolsLogger.info(
+ // eslint-disable-next-line max-len
+ `${timeString} | MODTOOLS> ${req.user.regUser.name}[${req.user.id}]> ${text}`,
+ );
+ };
+
try {
+ if (req.body.cleanerstat) {
+ const ret = CanvasCleaner.reportStatus();
+ res.status(200);
+ res.json(ret);
+ return;
+ }
+ if (req.body.cleanercancel) {
+ const ret = CanvasCleaner.stop();
+ res.status(200).send(ret);
+ return;
+ }
+ if (req.body.cleaneraction) {
+ const {
+ cleaneraction, ulcoor, brcoor, canvasid,
+ } = req.body;
+ const [ret, msg] = await executeCleanerAction(
+ cleaneraction,
+ ulcoor,
+ brcoor,
+ canvasid,
+ aLogger,
+ );
+ res.status(ret).send(msg);
+ return;
+ }
if (req.body.imageaction) {
const { imageaction, coords, canvasid } = req.body;
const [ret, msg] = await executeImageAction(
@@ -82,10 +114,12 @@ router.post('/', upload.single('image'), async (req, res, next) => {
req.file,
coords,
canvasid,
+ aLogger,
);
res.status(ret).send(msg);
return;
- } if (req.body.protaction) {
+ }
+ if (req.body.protaction) {
const {
protaction, ulcoor, brcoor, canvasid,
} = req.body;
@@ -94,10 +128,12 @@ router.post('/', upload.single('image'), async (req, res, next) => {
ulcoor,
brcoor,
canvasid,
+ aLogger,
);
res.status(ret).send(msg);
return;
- } if (req.body.rollback) {
+ }
+ if (req.body.rollback) {
// rollback is date as YYYYMMdd
const {
rollback, ulcoor, brcoor, canvasid,
@@ -107,6 +143,7 @@ router.post('/', upload.single('image'), async (req, res, next) => {
ulcoor,
brcoor,
canvasid,
+ aLogger,
);
res.status(ret).send(msg);
return;
@@ -135,9 +172,17 @@ router.use(async (req, res, next) => {
* Post just for admin
*/
router.post('/', async (req, res, next) => {
+ const aLogger = (text) => {
+ logger.info(`ADMIN> ${req.user.regUser.name}[${req.user.id}]> ${text}`);
+ };
+
try {
if (req.body.ipaction) {
- const ret = await executeIPAction(req.body.ipaction, req.body.ip);
+ const ret = await executeIPAction(
+ req.body.ipaction,
+ req.body.ip,
+ aLogger,
+ );
res.status(200).send(ret);
return;
}