/* * functions for admintools * */ /* eslint-disable no-await-in-loop */ import sharp from 'sharp'; import Sequelize from 'sequelize'; import isIPAllowed from './isAllowed'; import { validateCoorRange } from '../utils/validation'; import CanvasCleaner from './CanvasCleaner'; import { RegUser } from '../data/sql'; import { cleanCacheForIP, } from '../data/redis/isAllowedCache'; import { forceCaptcha } from '../data/redis/captcha'; import { isWhitelisted, whitelistIP, unwhitelistIP, } from '../data/sql/Whitelist'; import { getBanInfo, banIP, unbanIP, } from '../data/sql/Ban'; import { getInfoToIp, getIPofIID, getIIDofIP, } from '../data/sql/IPInfo'; // eslint-disable-next-line import/no-unresolved import canvases from './canvases.json'; import { imageABGR2Canvas, protectCanvasArea, } from './Image'; import { getIIDSummary, getIIDPixels, getSummaryFromArea, getPixelsFromArea, } from './parsePixelLog'; import rollbackCanvasArea from './rollback'; /* * Execute IP based actions (banning, whitelist, etc.) * @param action what to do with the ip * @param ip already sanizized ip * @return text of success */ export async function executeIPAction(action, ips, logger = null) { const valueArray = ips.split('\n'); let out = ''; for (let i = 0; i < valueArray.length; i += 1) { const value = valueArray[i].trim(); if (!value) { continue; } if (logger) logger(`${action} ${value}`); if (action === 'iidtoip') { const ip = await getIPofIID(value); out += (ip) ? `${ip}\n` : `${value}\n`; continue; } if (action === 'iptoiid') { const iid = await getIIDofIP(value); out += (iid) ? `${iid}\n` : `${value}\n`; } } return out; } /* * Execute IID based actions * @param action what to do with the iid * @param iid already sanizized iid * @return text of success */ export async function executeIIDAction( action, iid, reason, expire, muid, logger = null, ) { const ip = await getIPofIID(iid); if (!ip) { return `Could not resolve ${iid}`; } const iidPart = iid.slice(0, iid.indexOf('-')); if (logger) logger(`${action} ${iid} ${ip}`); switch (action) { case 'status': { const allowed = await isIPAllowed(ip, true); let out = `Allowed to place: ${allowed.allowed}\n`; const info = await getInfoToIp(ip); out += `Country: ${info.country}\n` + `CIDR: ${info.cidr}\n` + `org: ${info.org || 'N/A'}\n` + `desc: ${info.descr || 'N/A'}\n` + `asn: ${info.asn}\n` + `proxy: ${info.isProxy}\n`; if (info.pcheck) { const { pcheck } = info; out += `pc: ${pcheck.slice(0, pcheck.indexOf(','))}\n`; } const whitelisted = await isWhitelisted(ip); out += `whitelisted: ${whitelisted}\n`; const ban = await getBanInfo(ip); if (!ban) { out += 'banned: false\n'; } else { out += 'banned: true\n' + `reason: ${ban.reason}\n`; if (ban.expires) { out += `expires: ${ban.expires.toLocaleString()}\n`; } if (ban.mod) { out += `by: @[${ban.mod.name}](${ban.mod.id})\n`; } } return out; } case 'givecaptcha': { const succ = await forceCaptcha(ip); if (succ === null) { return 'Captchas are deactivated on this server.'; } if (succ) { return `Forced captcha on ${iidPart}`; } return `${iidPart} would have gotten captcha anyway`; } case 'ban': { const expireTs = parseInt(expire, 10); if (Number.isNaN(expireTs) || (expireTs && expireTs < Date.now())) { return 'No valid expiration time'; } if (!reason || !reason.trim()) { return 'No reason specified'; } const ret = await banIP(ip, reason, expireTs || null, muid); if (ret) { return 'Successfully banned user'; } return 'Updated existing ban of user'; } case 'unban': { const ret = await unbanIP(ip); if (ret) { return 'Successfully unbanned user'; } return 'User is not banned'; } case 'whitelist': { const ret = await whitelistIP(ip); if (ret) { await cleanCacheForIP(ip); return 'Successfully whitelisted user'; } return 'User is already whitelisted'; } case 'unwhitelist': { const ret = await unwhitelistIP(ip); if (ret) { await cleanCacheForIP(ip); return 'Successfully removed user from whitelist'; } return 'User is not on whitelist'; } default: return `Failed to ${action} ${iid}`; } } /* * Execute Image based actions (upload, protect, etc.) * @param action what to do with the image * @param file imagefile * @param coords coord sin X_Y format * @param canvasid numerical canvas id as string * @return [ret, msg] http status code and message */ export async function executeImageAction( action, file, coords, canvasid, logger = null, ) { if (!coords) { return [403, 'Coordinates not defined']; } if (!canvasid) { return [403, 'canvasid not defined']; } const splitCoords = coords.trim().split('_'); if (splitCoords.length !== 2) { return [403, 'Invalid Coordinate Format']; } const [x, y] = splitCoords.map((z) => Math.floor(Number(z))); const canvas = canvases[canvasid]; let error = null; if (Number.isNaN(x)) { error = 'x is not a valid number'; } else if (Number.isNaN(y)) { error = 'y is not a valid number'; } else if (!action) { error = 'No imageaction given'; } else if (!canvas) { error = 'Invalid canvas selected'; } else if (canvas.v) { error = 'Can not upload Image to 3D canvas'; } 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 are outside of canvas']; } const protect = (action === 'protect'); const wipe = (action === 'wipe'); try { const { data, info } = await sharp(file.buffer) .ensureAlpha() .raw() .toBuffer({ resolveWithObject: true }); const pxlCount = await imageABGR2Canvas( canvasid, x, y, data, info.width, info.height, wipe, protect, ); // eslint-disable-next-line max-len if (logger) logger(`loaded image wth *${pxlCount}*pxls to #${canvas.ident},${x},${y} (+*${x}*+\\_+*${y}*+ - +*${x + info.width - 1}*+\\_+*${y + info.height - 1}*+)`); return [ 200, `Successfully loaded image wth ${pxlCount}pxls to ${x}/${y}`, ]; } catch { return [400, 'Can not read image file']; } } /* * 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 (!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 }; } let ret; if (!ulcoor && !brcoor && iid) { if (action === 'summary') { ret = await getIIDSummary( iid, time, ); } if (action === 'all') { ret = await getIIDPixels( iid, time, ); } if (typeof ret === 'string') { return { info: ret }; } if (typeof ret !== 'undefined') { return ret; } } 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) && Date.now() - ts > 5 * 60 * 1000 && !iid ) { return { info: 'Cann not watch so many pixels' }; } if (action === 'summary') { ret = await getSummaryFromArea( canvasid, x, y, u, v, time, iid, ); } if (action === 'all') { ret = await getPixelsFromArea( canvasid, x, y, u, v, time, iid, ); } if (typeof ret === 'string') { return { info: ret }; } if (typeof ret !== 'undefined') { return ret; } return { info: 'Invalid action given' }; } /* * 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 (!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}"* from #${canvas.ident},${x},${y} to #${canvas.ident},${u},${v}`; if (logger) logger(report); return [200, report]; } /* * Execute actions for protecting areas * @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 executeProtAction( action, ulcoor, brcoor, canvasid, logger = null, ) { if (!canvasid) { return [403, 'canvasid not defined']; } const canvas = canvases[canvasid]; let error = null; 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)'; } if (error !== null) { return [403, error]; } 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, x, y, width, height, protect, ); if (logger) { logger( (protect) // eslint-disable-next-line max-len ? `protected *${width}*x*${height}* area at #${canvas.ident},${x},${y} with *${pxlCount}*pxls (+*${x}*+\\_+*${y}*+ - +*${u}*+\\_+*${v}*+)` // eslint-disable-next-line max-len : `unprotect *${width}*x*${height}* area at #${canvas.ident},${x},${y} with *${pxlCount}*pxls (+*${x}*+\\_+*${y}*+ - +*${u}*+\\_+*${v}*+)`, ); } return [ 200, (protect) // eslint-disable-next-line max-len ? `Successfully protected ${width}x${height} area at #${canvas.ident},${x},${y} with ${pxlCount}pxls (${ulcoor} - ${brcoor})` // eslint-disable-next-line max-len : `Successfully unprotected ${width}x${height} area at #${canvas.ident},${x},${y} with ${pxlCount}pxls (${ulcoor} - ${brcoor})`, ]; } /* * Execute rollback * @param date in format YYYYMMdd * @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 executeRollback( date, ulcoor, brcoor, canvasid, logger = null, isAdmin = false, ) { if (!canvasid) { return [403, 'canvasid not defined']; } const canvas = canvases[canvasid]; let error = null; 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'; } if (error !== null) { return [403, error]; } 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 (!isAdmin && width * height > 1000000) { return [403, 'Can not rollback more than 1m pixels at once']; } const pxlCount = await rollbackCanvasArea( canvasid, x, y, width, height, date, ); if (logger) { logger( // eslint-disable-next-line max-len `rolled back to *${date}* for *${width}*x*${height}* area at #${canvas.ident},${x},${y} with *${pxlCount}*pxls (+*${x}*+\\_+*${y}*+ - +*${u}*+\\_+*${v}*+)`, ); } return [ 200, // eslint-disable-next-line max-len `Successfully rolled back to ${date} for ${width}x${height} area at #${canvas.ident},${x},${y} with ${pxlCount}pxls (${ulcoor} - ${brcoor})`, ]; } /* * Get list of mods * @return [[id1, name2], [id2, name2], ...] list */ export async function getModList() { const mods = await RegUser.findAll({ where: Sequelize.where(Sequelize.literal('roles & 1'), '!=', 0), attributes: ['id', 'name'], raw: true, }); return mods.map((mod) => [mod.id, mod.name]); } export async function removeMod(userId) { if (Number.isNaN(userId)) { throw new Error('Invalid userId'); } let user = null; try { user = await RegUser.findByPk(userId); } catch { throw new Error('Database error on remove mod'); } if (!user) { throw new Error('User not found'); } try { await user.update({ isMod: false, }); return `Moderation rights removed from user ${userId}`; } catch { throw new Error('Couldn\'t remove Mod from user'); } } export async function makeMod(name) { if (!name) { throw new Error('No username given'); } let user = null; try { user = await RegUser.findOne({ where: { name, }, }); } catch { throw new Error(`Invalid user ${name}`); } if (!user) { throw new Error(`User ${name} not found`); } try { await user.update({ isMod: true, }); return [user.id, user.name]; } catch { throw new Error('Couldn\'t remove Mod from user'); } }