add canvas cleaner

This commit is contained in:
HF 2022-03-31 15:10:50 +02:00
parent 9122f3e0a0
commit 3a14699c6b
9 changed files with 921 additions and 124 deletions

View File

@ -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 (
<div style={{ textAlign: 'center', paddingLeft: '5%', paddingRight: '5%' }}>
{resp && (
@ -227,6 +317,7 @@ function Modtools() {
)}
<p className="modalcotext">Choose Canvas:&nbsp;
<select
value={selectedCanvas}
onChange={(e) => {
const sel = e.target;
selectCanvas(sel.options[sel.selectedIndex].value);
@ -237,7 +328,6 @@ function Modtools() {
? null
: (
<option
selected={canvas === selectedCanvas}
value={canvas}
>
{
@ -257,6 +347,7 @@ function Modtools() {
<input type="file" name="image" id="imgfile" />
</p>
<select
value={imageAction}
onChange={(e) => {
const sel = e.target;
selectImageAction(sel.options[sel.selectedIndex].value);
@ -265,7 +356,6 @@ function Modtools() {
{['build', 'protect', 'wipe'].map((opt) => (
<option
value={opt}
selected={imageAction === opt}
>
{opt}
</option>
@ -320,6 +410,7 @@ function Modtools() {
use protect with image upload and alpha layers)`}
</p>
<select
value={protAction}
onChange={(e) => {
const sel = e.target;
selectProtAction(sel.options[sel.selectedIndex].value);
@ -328,14 +419,13 @@ function Modtools() {
{['protect', 'unprotect'].map((opt) => (
<option
value={opt}
selected={protAction === opt}
>
{opt}
</option>
))}
</select>
<p className="modalcotext">
Top-left corner (X_Y):&nbsp;
{t`Top-left corner`} (X_Y):&nbsp;
<input
value={tlcoords}
style={{
@ -353,7 +443,7 @@ function Modtools() {
/>
</p>
<p className="modalcotext">
Bottom-right corner (X_Y):&nbsp;
{t`Bottom-right corner`} (X_Y):&nbsp;
<input
value={brcoords}
style={{
@ -410,7 +500,7 @@ function Modtools() {
}}
/>
<p className="modalcotext">
Top-left corner (X_Y):&nbsp;
{t`Top-left corner`} (X_Y):&nbsp;
<input
value={tlrcoords}
style={{
@ -428,7 +518,7 @@ function Modtools() {
/>
</p>
<p className="modalcotext">
Bottom-right corner (X_Y):&nbsp;
{t`Bottom-right corner`} (X_Y):&nbsp;
<input
value={brrcoords}
style={{
@ -468,6 +558,115 @@ function Modtools() {
</button>
</div>
)}
<br />
<div className="modaldivider" />
<h3 className="modaltitle">{t`Canvas Cleaner`}</h3>
<p className="modalcotext">
{t`Apply a filter to clean trash in large canvas areas.`}
</p>
<select
value={cleanAction}
onChange={(e) => {
const sel = e.target;
selectCleanAction(sel.options[sel.selectedIndex].value);
}}
>
{['spare', 'spareext'].map((opt) => (
<option
value={opt}
>
{opt}
</option>
))}
</select>
<p className="modalcotext">{descCleanAction}</p>
<p className="modalcotext" style={{ fontWeight: 'bold' }}>
{cleanerStatusString}
</p>
<p className="modalcotext">
{t`Top-left corner`} (X_Y):&nbsp;
<input
value={tlccoords}
style={{
display: 'inline-block',
width: '100%',
maxWidth: '15em',
}}
type="text"
placeholder="X_Y"
onChange={(evt) => {
const co = evt.target.value.trim();
selectTLCCoords(co);
keptState.tlccoords = co;
}}
/>
</p>
<p className="modalcotext">
{t`Bottom-right corner`} (X_Y):&nbsp;
<input
value={brccoords}
style={{
display: 'inline-block',
width: '100%',
maxWidth: '15em',
}}
type="text"
placeholder="X_Y"
onChange={(evt) => {
const co = evt.target.value.trim();
selectBRCCoords(co);
keptState.brccoords = co;
}}
/>
</p>
<button
type="button"
onClick={() => {
if (submitting) {
return;
}
setSubmitting(true);
submitCanvasCleaner(
cleanAction,
selectedCanvas,
tlccoords,
brccoords,
(ret) => {
setCleanerStats({
running: true,
percent: 'N/A',
method: cleanAction,
tl: tlccoords,
br: brccoords,
canvasId: selectedCanvas,
});
setSubmitting(false);
setResp(ret);
},
);
}}
>
{(submitting) ? '...' : t`Submit`}
</button>
<button
type="button"
onClick={() => {
if (submitting) {
return;
}
setSubmitting(true);
getCleanerCancel(
(ret) => {
setCleanerStats({});
setSubmitting(false);
setResp(ret);
},
);
}}
>
{(submitting) ? '...' : t`Stop Cleaner`}
</button>
{(userlvl === 1) && (
<div>
<br />
@ -477,6 +676,7 @@ function Modtools() {
{t`Do stuff with IPs (one IP per line)`}
</p>
<select
value={iPAction}
onChange={(e) => {
const sel = e.target;
selectIPAction(sel.options[sel.selectedIndex].value);
@ -485,7 +685,6 @@ function Modtools() {
{['ban', 'unban', 'whitelist', 'unwhitelist'].map((opt) => (
<option
value={opt}
selected={iPAction === opt}
>
{opt}
</option>

View File

@ -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;

View File

@ -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);

406
src/core/CanvasCleaner.js Normal file
View File

@ -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();

View File

@ -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({

View File

@ -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,

View File

@ -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);
}

View File

@ -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) {

View File

@ -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;
}