diff --git a/src/actions/index.js b/src/actions/index.js index 52ff9a2..e1f3eb7 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -163,19 +163,15 @@ export function selectCanvas(canvasId: number): Action { }; } -export function placePixel(coordinates: Cell, color: ColorIndex): Action { +export function placedPixel(): Action { return { type: 'PLACE_PIXEL', - coordinates, - color, }; } -export function pixelWait(coordinates: Cell, color: ColorIndex): Action { +export function pixelWait(): Action { return { type: 'PIXEL_WAIT', - coordinates, - color, }; } @@ -230,92 +226,120 @@ export function notify(notification: string) { }; } -export function requestPlacePixel( - canvasId: number, - coordinates: Cell, +let pixelTimeout = null; + +export function tryPlacePixel( + i: number, + j: number, + offset: number, color: ColorIndex, - token: ?string = null, ): ThunkAction { - const [x, y, z] = coordinates; - return async (dispatch) => { - const body = JSON.stringify({ - cn: canvasId, - x, - y, - z, - clr: color, - token, + pixelTimeout = Date.now() + 5000; + await dispatch(setPlaceAllowed(false)); + + // TODO: + // this is for resending after captcha returned + // window is ugly, put it into redux or something + window.pixel = { + i, + j, + offset, + color, + }; + + dispatch({ + type: 'REQUEST_PLACE_PIXEL', + i, + j, + offset, + color, }); - - dispatch(setPlaceAllowed(false)); - try { - const response = await fetch('/api/pixel', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body, - // https://github.com/github/fetch/issues/349 - credentials: 'include', - }); - const { - success, - waitSeconds, - coolDownSeconds, - errors, - errorTitle, - } = await response.json(); - - if (waitSeconds) { - dispatch(setWait(waitSeconds * 1000)); - } - const coolDownNotify = Math.round(coolDownSeconds); - if (coolDownSeconds) { - dispatch(notify(coolDownNotify)); - } - if (response.ok) { - if (success) { - dispatch(placePixel(coordinates, color)); - } else { - dispatch(pixelWait(coordinates, color)); - } - return; - } - - if (response.status === 422) { - window.pixel = { canvasId, coordinates, color }; - window.grecaptcha.execute(); - return; - } - - dispatch(pixelFailure()); - dispatch(sweetAlert( - (errorTitle || `Error ${response.status}`), - errors[0].msg, - 'error', - 'OK', - )); - } finally { - dispatch(setPlaceAllowed(true)); - } }; } -export function tryPlacePixel( - coordinates: Cell, - color: ?ColorIndex = null, +export function receivePixelReturn( + retCode: number, + wait: number, + coolDownSeconds: number, ): ThunkAction { - return (dispatch, getState) => { - const state = getState(); - const { - canvasId, - } = state.canvas; - const selectedColor = (color === undefined || color === null) - ? state.canvas.selectedColor - : color; + return (dispatch) => { + try { + if (wait) { + dispatch(setWait(wait)); + } + if (coolDownSeconds) { + dispatch(notify(coolDownSeconds)); + } - dispatch(requestPlacePixel(canvasId, coordinates, selectedColor)); + let errorTitle = null; + let msg = null; + switch (retCode) { + case 0: + dispatch(placedPixel()); + break; + case 1: + errorTitle = 'Invalid Canvas'; + msg = 'This canvas doesn\'t exist'; + break; + case 2: + errorTitle = 'Invalid Coordinates'; + msg = 'x out of bounds'; + break; + case 3: + errorTitle = 'Invalid Coordinates'; + msg = 'y out of bounds'; + break; + case 4: + errorTitle = 'Invalid Coordinates'; + msg = 'z out of bounds'; + break; + case 5: + errorTitle = 'Wrong Color'; + msg = 'Invalid color selected'; + break; + case 6: + errorTitle = 'Just for registered Users'; + msg = 'You have to be logged in to place on this canvas'; + break; + case 7: + errorTitle = 'Place more :)'; + // eslint-disable-next-line max-len + msg = 'You can not access this canvas yet. You need to place more pixels'; + break; + case 8: + errorTitle = 'Oww noo'; + msg = 'This pixel is protected.'; + break; + case 9: + // pixestack used up + dispatch(pixelWait()); + break; + case 10: + // captcha + window.grecaptcha.execute(); + break; + case 11: + errorTitle = 'No Proxies Allowed :('; + msg = 'You are using a Proxy.'; + break; + default: + errorTitle = 'Weird'; + msg = 'Couldn\'t set Pixel'; + } + if (msg) { + dispatch(pixelFailure()); + dispatch(sweetAlert( + (errorTitle || `Error ${retCode}`), + msg, + 'error', + 'OK', + )); + } + } finally { + pixelTimeout = null; + dispatch(setPlaceAllowed(true)); + } }; } @@ -428,11 +452,11 @@ export function receiveBigChunkFailure(center: Cell, error: Error): Action { } export function receiveCoolDown( - waitSeconds: number, + wait: number, ): Action { return { type: 'RECEIVE_COOLDOWN', - waitSeconds, + wait, }; } @@ -565,14 +589,28 @@ function endCoolDown(): Action { function getPendingActions(state): Array { const actions = []; + const now = Date.now(); const { wait } = state.user; - if (wait === null || wait === undefined) return actions; - const coolDown = wait - Date.now(); + const coolDown = wait - now; - if (coolDown > 0) actions.push(setCoolDown(coolDown)); - else actions.push(endCoolDown()); + if (wait !== null && wait !== undefined) { + if (coolDown > 0) actions.push(setCoolDown(coolDown)); + else actions.push(endCoolDown()); + } + + if (pixelTimeout && now > pixelTimeout) { + actions.push(pixelFailure()); + pixelTimeout = null; + actions.push(setPlaceAllowed(true)); + actions.push(sweetAlert( + 'Error :(', + 'Didn\'t get an answer from pixelplanet. Maybe try to refresh?', + 'error', + 'OK', + )); + } return actions; } diff --git a/src/actions/types.js b/src/actions/types.js index 66ebbfc..d1abd80 100644 --- a/src/actions/types.js +++ b/src/actions/types.js @@ -32,13 +32,20 @@ export type Action = | { type: 'SET_HOVER', hover: Cell } | { type: 'UNSET_HOVER' } | { type: 'SET_WAIT', wait: ?number } + | { type: 'RECEIVE_COOLDOWN', wait: number } | { type: 'SET_MOBILE', mobile: boolean } | { type: 'COOLDOWN_END' } | { type: 'COOLDOWN_SET', coolDown: number } | { type: 'SELECT_COLOR', color: ColorIndex } | { type: 'SELECT_CANVAS', canvasId: number } - | { type: 'PLACE_PIXEL', coordinates: Cell, color: ColorIndex, wait: string } - | { type: 'PIXEL_WAIT', coordinates: Cell, color: ColorIndex, wait: string } + | { type: 'REQUEST_PLACE_PIXEL', + i: number, + j: number, + offset: number, + color: ColorIndex, + } + | { type: 'PLACE_PIXEL' } + | { type: 'PIXEL_WAIT' } | { type: 'PIXEL_FAILURE' } | { type: 'SET_VIEW_COORDINATES', view: Cell } | { type: 'SET_SCALE', scale: number, zoompoint: Cell } @@ -46,7 +53,6 @@ export type Action = | { type: 'PRE_LOADED_BIG_CHUNK', center: Cell } | { type: 'RECEIVE_BIG_CHUNK', center: Cell } | { type: 'RECEIVE_BIG_CHUNK_FAILURE', center: Cell, error: Error } - | { type: 'RECEIVE_COOLDOWN', waitSeconds: number } | { type: 'RECEIVE_PIXEL_UPDATE', i: number, j: number, diff --git a/src/client.js b/src/client.js index 6ddb9b4..1222fbe 100644 --- a/src/client.js +++ b/src/client.js @@ -1,5 +1,6 @@ /* @flow */ +// eslint-disable-next-line no-unused-vars import fetch from 'isomorphic-fetch'; // TODO put in the beggining with webpack! import './styles/font.css'; @@ -8,14 +9,15 @@ import './styles/font.css'; import onKeyPress from './controls/keypress'; import { receivePixelUpdate, - receiveCoolDown, fetchMe, fetchStats, initTimer, urlChange, receiveOnline, + receiveCoolDown, receiveChatMessage, receiveChatHistory, + receivePixelReturn, setMobile, } from './actions'; import store from './ui/store'; @@ -35,8 +37,13 @@ function init() { }) => { store.dispatch(receivePixelUpdate(i, j, offset, color)); }); - ProtocolClient.on('cooldownPacket', (waitSeconds) => { - store.dispatch(receiveCoolDown(waitSeconds)); + ProtocolClient.on('pixelReturn', ({ + retCode, wait, coolDownSeconds, + }) => { + store.dispatch(receivePixelReturn(retCode, wait, coolDownSeconds)); + }); + ProtocolClient.on('cooldownPacket', (coolDown) => { + store.dispatch(receiveCoolDown(coolDown)); }); ProtocolClient.on('onlineCounter', ({ online }) => { store.dispatch(receiveOnline(online)); @@ -74,27 +81,6 @@ function init() { store.dispatch(fetchStats()); setInterval(() => { store.dispatch(fetchStats()); }, 300000); - - // mess with void bot :) - function ayylmao() { - let cnt = 0; - for (let i = 0; i < document.body.children.length; i += 1) { - const node = document.body.children[i]; - if (node.nodeName === 'SCRIPT' && node.src === '') { - cnt += 1; - } - } - if (cnt > 1) { - document.body.style.setProperty( - '-webkit-transform', 'rotate(-180deg)', - null, - ); - fetch('https://assets.pixelplanet.fun/iamabot'); - window.fetch = () => true; - } - } - ayylmao(); - setInterval(ayylmao, 120000); } init(); diff --git a/src/components/Chat.jsx b/src/components/Chat.jsx index 36c1fe7..6fdbe79 100644 --- a/src/components/Chat.jsx +++ b/src/components/Chat.jsx @@ -18,6 +18,9 @@ import ProtocolClient from '../socket/ProtocolClient'; import { saveSelection, restoreSelection } from '../utils/storeSelection'; import splitChatMessage from '../core/chatMessageFilter'; +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} const Chat = ({ chatMessages, @@ -50,7 +53,7 @@ const Chat = ({ useEffect(() => { const regExp = (ownName) - ? new RegExp(`(^|\\s+)(@${ownName})(\\s+|$)`, 'g') + ? new RegExp(`(^|\\s+)(@${escapeRegExp(ownName)})(\\s+|$)`, 'g') : null; setNameRegExp(regExp); }, [ownName]); diff --git a/src/components/ReCaptcha.jsx b/src/components/ReCaptcha.jsx index 0f6303b..e301d79 100644 --- a/src/components/ReCaptcha.jsx +++ b/src/components/ReCaptcha.jsx @@ -8,13 +8,28 @@ import React from 'react'; import store from '../ui/store'; -import { requestPlacePixel } from '../actions'; +import { tryPlacePixel } from '../actions'; -function onCaptcha(token: string) { - const { canvasId, coordinates, color } = window.pixel; +async function onCaptcha(token: string) { + const body = JSON.stringify({ + token, + }); + await fetch('/api/captcha', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body, + // https://github.com/github/fetch/issues/349 + credentials: 'include', + }); + + const { + i, j, offset, color, + } = window.pixel; + store.dispatch(tryPlacePixel(i, j, offset, color)); - store.dispatch(requestPlacePixel(canvasId, coordinates, color, token)); window.grecaptcha.reset(); } // https://stackoverflow.com/questions/41717304/recaptcha-google-data-callback-with-angularjs diff --git a/src/components/UserAreaModal.jsx b/src/components/UserAreaModal.jsx index 303f6d1..86ba7fd 100644 --- a/src/components/UserAreaModal.jsx +++ b/src/components/UserAreaModal.jsx @@ -65,7 +65,7 @@ const LogInArea = ({ register, forgotPassword, me }) => ( style={logoStyle} width={32} src={`${window.assetserver}/vklogo.svg`} - alt="vk" + alt="VK" /> @@ -73,7 +73,7 @@ const LogInArea = ({ register, forgotPassword, me }) => ( style={logoStyle} width={32} src={`${window.assetserver}/redditlogo.svg`} - alt="vk" + alt="Reddit" />

or register here:

diff --git a/src/controls/PixelPainterControls.js b/src/controls/PixelPainterControls.js index 2589cb9..ff1228e 100644 --- a/src/controls/PixelPainterControls.js +++ b/src/controls/PixelPainterControls.js @@ -24,6 +24,8 @@ import { } from '../actions'; import { screenToWorld, + getChunkOfPixel, + getOffsetOfPixel, } from '../core/utils'; let store = null; @@ -145,11 +147,14 @@ export function initControls(renderer, viewport: HTMLCanvasElement, curStore) { if (!placeAllowed) return; - // dirty trick: to fetch only before multiple 3 AND on user action - // if (pixelsPlaced % 3 === 0) requestAds(); - if (selectedColor !== renderer.getColorIndexOfPixel(...cell)) { - store.dispatch(tryPlacePixel(cell)); + const { canvasSize } = state.canvas; + const [i, j] = getChunkOfPixel(canvasSize, ...cell); + const offset = getOffsetOfPixel(canvasSize, ...cell); + store.dispatch(tryPlacePixel( + i, j, offset, + selectedColor, + )); } }); diff --git a/src/core/ChatProvider.js b/src/core/ChatProvider.js index 6fa594f..e3d9c8c 100644 --- a/src/core/ChatProvider.js +++ b/src/core/ChatProvider.js @@ -7,11 +7,6 @@ import User from '../data/models/User'; import webSockets from '../socket/websockets'; import { CHAT_CHANNELS } from './constants'; -import { cheapDetector } from './isProxy'; -import { - USE_PROXYCHECK, -} from './config'; - export class ChatProvider { /* @@ -93,14 +88,6 @@ export class ChatProvider { } } - if (USE_PROXYCHECK && user.ip && await cheapDetector(user.ip)) { - logger.info( - `${name} / ${user.ip} tried to send chat message with proxy`, - ); - return 'You can not send chat messages with a proxy'; - } - - for (let i = 0; i < this.substitutes.length; i += 1) { const subsitute = this.substitutes[i]; message = message.replace(subsitute.regexp, subsitute.replace); diff --git a/src/core/draw.js b/src/core/draw.js index 8626f65..0543558 100644 --- a/src/core/draw.js +++ b/src/core/draw.js @@ -4,24 +4,30 @@ import { using } from 'bluebird'; import type { User } from '../data/models'; import { redlock } from '../data/redis'; -import { getChunkOfPixel, getOffsetOfPixel } from './utils'; +import { + getChunkOfPixel, + getOffsetOfPixel, + getPixelFromChunkOffset, +} from './utils'; import webSockets from '../socket/websockets'; -import logger from './logger'; +import logger, { pixelLogger } from './logger'; import RedisCanvas from '../data/models/RedisCanvas'; // eslint-disable-next-line import/no-unresolved import canvases from './canvases.json'; -import { THREE_CANVAS_HEIGHT } from './constants'; +import { THREE_CANVAS_HEIGHT, THREE_TILE_SIZE, TILE_SIZE } from './constants'; /** * * @param canvasId + * @param canvasId + * @param color * @param x * @param y - * @param color + * @param z optional, if given its 3d canvas */ -export function setPixel( +export function setPixelByCoords( canvasId: number, color: ColorIndex, x: number, @@ -37,6 +43,150 @@ export function setPixel( /** * + * By Offset is prefered on server side + * @param canvasId + * @param i Chunk coordinates + * @param j + * @param offset Offset of pixel withing chunk + */ +export function setPixelByOffset( + canvasId: number, + color: ColorIndex, + i: number, + j: number, + offset: number, +) { + RedisCanvas.setPixelInChunk(i, j, offset, color, canvasId); + webSockets.broadcastPixel(canvasId, i, j, offset, color); +} + +/** + * + * By Offset is prefered on server side + * This gets used by websocket pixel placing requests + * @param user user that can be registered, but doesn't have to + * @param canvasId + * @param i Chunk coordinates + * @param j + * @param offset Offset of pixel withing chunk + */ +export async function drawByOffset( + user: User, + canvasId: number, + color: ColorIndex, + i: number, + j: number, + offset: number, +): Promise { + let wait = 0; + let coolDown = 0; + let retCode = 0; + + logger.info(`Got request for ${canvasId} ${i} ${j} ${offset} ${color}`); + + const canvas = canvases[canvasId]; + if (!canvas) { + // canvas doesn't exist + return { + wait, + coolDown, + retCode: 1, + }; + } + const { size: canvasSize, v: is3d } = canvas; + + try { + const tileSize = (is3d) ? THREE_TILE_SIZE : TILE_SIZE; + if (i >= canvasSize / tileSize) { + // x out of bounds + throw new Error(2); + } + if (j >= canvasSize / tileSize) { + // y out of bounds + throw new Error(3); + } + const maxSize = (is3d) ? tileSize * tileSize * THREE_CANVAS_HEIGHT + : tileSize * tileSize; + if (offset >= maxSize) { + // z out of bounds or weird stuff + throw new Error(4); + } + if (color >= canvas.colors.length) { + // color out of bounds + throw new Error(5); + } + + if (canvas.req !== -1) { + if (user.id === null) { + // not logged in + throw new Error(6); + } + const totalPixels = await user.getTotalPixels(); + if (totalPixels < canvas.req) { + // not enough pixels placed yet + throw new Error(7); + } + } + + const setColor = await RedisCanvas.getPixelByOffset(canvasId, i, j, offset); + if (setColor & 0x80 + /* 3D Canvas Minecraft Avatars */ + // && x >= 96 && x <= 128 && z >= 35 && z <= 100 + // 96 - 128 on x + // 32 - 128 on z + || (canvas.v && i === 19 && j >= 17 && j < 20 && !user.isAdmin()) + ) { + // protected pixel + throw new Error(8); + } + + coolDown = (setColor & 0x3F) < canvas.cli ? canvas.bcd : canvas.pcd; + if (user.isAdmin()) { + coolDown = 0.0; + } + + const now = Date.now(); + wait = await user.getWait(canvasId); + if (!wait) wait = now; + wait += coolDown; + const waitLeft = wait - now; + if (waitLeft > canvas.cds) { + // cooldown stack used + wait = waitLeft - coolDown; + coolDown = canvas.cds - waitLeft; + throw new Error(9); + } + + setPixelByOffset(canvasId, color, i, j, offset); + + user.setWait(waitLeft, canvasId); + if (canvas.ranked) { + user.incrementPixelcount(); + } + wait = waitLeft; + } catch (e) { + retCode = parseInt(e.message, 10); + if (Number.isNaN(retCode)) { + throw e; + } + } + + const [x, y, z] = getPixelFromChunkOffset(i, j, offset, canvasSize, is3d); + // eslint-disable-next-line max-len + pixelLogger.info(`${user.ip} ${user.id} ${canvasId} ${x} ${y} ${z} ${color} ${retCode}`); + + return { + wait, + coolDown, + retCode, + }; +} + + +/** + * + * Old version of draw that returns explicit error messages + * used for http json api/pixel, used with coordinates * @param user * @param canvasId * @param x @@ -44,7 +194,7 @@ export function setPixel( * @param color * @returns {Promise.} */ -async function draw( +export async function drawByCoords( user: User, canvasId: number, color: ColorIndex, @@ -178,7 +328,7 @@ async function draw( }; } - setPixel(canvasId, color, x, y, z); + setPixelByCoords(canvasId, color, x, y, z); user.setWait(waitLeft, canvasId); if (canvas.ranked) { @@ -191,18 +341,20 @@ async function draw( }; } + /** * This function is a wrapper for draw. It fixes race condition exploits * It permits just placing one pixel at a time per user. * * @param user * @param canvasId + * @param color * @param x * @param y - * @param color + * @param z (optional for 3d canvas) * @returns {Promise.} */ -function drawSafe( +export function drawSafeByCoords( user: User, canvasId: number, color: ColorIndex, @@ -211,7 +363,7 @@ function drawSafe( z: number = null, ): Promise { if (user.isAdmin()) { - return draw(user, canvasId, color, x, y, z); + return drawByCoords(user, canvasId, color, x, y, z); } // can just check for one unique occurence, @@ -223,7 +375,7 @@ function drawSafe( using( redlock.disposer(`locks:${userId}`, 5000, logger.error), async () => { - const ret = await draw(user, canvasId, color, x, y, z); + const ret = await drawByCoords(user, canvasId, color, x, y, z); resolve(ret); }, ); // <-- unlock is automatically handled by bluebird @@ -231,6 +383,42 @@ function drawSafe( } -export const drawUnsafe = draw; +/** + * This function is a wrapper for draw. It fixes race condition exploits + * It permits just placing one pixel at a time per user. + * + * @param user + * @param canvasId + * @param color + * @param i Chunk coordinates + * @param j + * @param offset Offset of pixel withing chunk + * @returns {Promise.} + */ +export function drawSafeByOffset( + user: User, + canvasId: number, + color: ColorIndex, + i: number, + j: number, + offset: number, +): Promise { + if (user.isAdmin()) { + return drawByOffset(user, canvasId, color, i, j, offset); + } -export default drawSafe; + // can just check for one unique occurence, + // we use ip, because id for logged out users is + // always null + const userId = user.ip; + + return new Promise((resolve) => { + using( + redlock.disposer(`locks:${userId}`, 5000, logger.error), + async () => { + const ret = await drawByOffset(user, canvasId, color, i, j, offset); + resolve(ret); + }, + ); // <-- unlock is automatically handled by bluebird + }); +} diff --git a/src/core/isProxy.js b/src/core/isProxy.js index 7d7bd15..ed6a4fd 100644 --- a/src/core/isProxy.js +++ b/src/core/isProxy.js @@ -162,7 +162,7 @@ async function withoutCache(f, ip) { let lock = 4; const checking = []; async function withCache(f, ip) { - if (!ip) return true; + if (!ip || ip === '0.0.0.1') return true; // get from cache, if there const ipKey = getIPv6Subnet(ip); const key = `isprox:${ipKey}`; diff --git a/src/core/utils.js b/src/core/utils.js index c2a2fb7..be9d130 100644 --- a/src/core/utils.js +++ b/src/core/utils.js @@ -38,6 +38,8 @@ export function clamp(n: number, min: number, max: number): number { return Math.max(min, Math.min(n, max)); } +// z is assumed to be height here +// in ui and rendeer, y is height export function getChunkOfPixel( canvasSize: number, x: number, @@ -109,18 +111,26 @@ export function getIdFromObject(obj: Object, ident: string): number { return null; } +// z is returned as height here +// in ui and rendeer, y is height export function getPixelFromChunkOffset( i: number, j: number, offset: number, canvasSize: number, + is3d: boolean = false, ): Cell { - const cx = mod(offset, TILE_SIZE); - const cy = Math.floor(offset / TILE_SIZE); - const devOffset = canvasSize / 2 / TILE_SIZE; - const x = ((i - devOffset) * TILE_SIZE) + cx; - const y = ((j - devOffset) * TILE_SIZE) + cy; - return [x, y]; + const tileSize = (is3d) ? THREE_TILE_SIZE : TILE_SIZE; + const cx = offset % tileSize; + const off = offset - cx; + let cy = off % (tileSize * tileSize); + const z = (is3d) ? (off - cy) / tileSize / tileSize : null; + cy /= tileSize; + + const devOffset = canvasSize / 2 / tileSize; + const x = ((i - devOffset) * tileSize) + cx; + const y = ((j - devOffset) * tileSize) + cy; + return [x, y, z]; } export function getCellInsideChunk( diff --git a/src/data/models/RedisCanvas.js b/src/data/models/RedisCanvas.js index f9fc591..d5f3069 100644 --- a/src/data/models/RedisCanvas.js +++ b/src/data/models/RedisCanvas.js @@ -94,16 +94,13 @@ class RedisCanvas { static async getPixelIfExists( canvasId: number, - x: number, - y: number, - z: number = null, + i: number, + j: number, + offset: number, ): Promise { // 1st bit -> protected or not // 2nd bit -> unused // rest (6 bits) -> index of color - const canvasSize = canvases[canvasId].size; - const [i, j] = getChunkOfPixel(canvasSize, x, y, z); - const offset = getOffsetOfPixel(canvasSize, x, y, z); const args = [ `ch:${canvasId}:${i}:${j}`, 'GET', @@ -116,13 +113,27 @@ class RedisCanvas { return color; } + static async getPixelByOffset( + canvasId: number, + i: number, + j: number, + offset: number, + ): Promise { + const clr = RedisCanvas.getPixelIfExists(canvasId, i, j, offset); + return (clr == null) ? 0 : clr; + } + static async getPixel( canvasId: number, x: number, y: number, z: number = null, ): Promise { - const clr = RedisCanvas.getPixelIfExists(canvasId, x, y, z); + const canvasSize = canvases[canvasId].size; + const [i, j] = getChunkOfPixel(canvasSize, x, y, z); + const offset = getOffsetOfPixel(canvasSize, x, y, z); + + const clr = RedisCanvas.getPixelIfExists(canvasId, i, j, offset); return (clr == null) ? 0 : clr; } } diff --git a/src/reducers/user.js b/src/reducers/user.js index 4598161..87bfd8f 100644 --- a/src/reducers/user.js +++ b/src/reducers/user.js @@ -64,7 +64,7 @@ export default function user( const { coolDown } = action; return { ...state, - coolDown, + coolDown: coolDown || null, }; } @@ -96,6 +96,18 @@ export default function user( }; } + case 'RECEIVE_COOLDOWN': { + const { wait: duration } = action; + const wait = duration + ? new Date(Date.now() + duration) + : null; + return { + ...state, + wait, + coolDown: null, + }; + } + case 'SET_MOBILE': { const { mobile: isOnMobile } = action; return { @@ -150,18 +162,6 @@ export default function user( }; } - case 'RECEIVE_COOLDOWN': { - const { waitSeconds } = action; - const wait = waitSeconds - ? new Date(Date.now() + waitSeconds * 1000) - : null; - return { - ...state, - wait, - coolDown: null, - }; - } - case 'RECEIVE_ME': { const { name, diff --git a/src/routes/api/captcha.js b/src/routes/api/captcha.js new file mode 100644 index 0000000..8b43847 --- /dev/null +++ b/src/routes/api/captcha.js @@ -0,0 +1,85 @@ +/* + * This is just for verifying captcha tokens, + * the actual notification that a captcha is needed is sent + * with the pixel return answer when sending apixel on websocket + * + * @flow + */ +import type { Request, Response } from 'express'; + +import logger from '../../core/logger'; +import redis from '../../data/redis'; +import verifyCaptcha from '../../utils/recaptcha'; + +import { + RECAPTCHA_SECRET, + RECAPTCHA_TIME, +} from '../../core/config'; + +const TTL_CACHE = RECAPTCHA_TIME * 60; // seconds + +export default async (req: Request, res: Response) => { + if (!RECAPTCHA_SECRET) { + res.status(200) + .json({ + errors: [{ + msg: + 'No need for a captcha here', + }], + }); + return; + } + + const user = req.user || req.noauthUser; + const { ip } = user; + + try { + const { token } = req.body; + if (!token) { + res.status(400) + .json({ errors: [{ msg: 'No token given' }] }); + return; + } + + const key = `human:${ip}`; + + const ttl: number = await redis.ttlAsync(key); + if (ttl > 0) { + res.status(400) + .json({ + errors: [{ + msg: + 'Why would you even want to solve a captcha?', + }], + }); + return; + } + + if (!await verifyCaptcha(token, ip)) { + logger.info(`CAPTCHA ${ip} failed his captcha`); + res.status(422) + .json({ + errors: [{ + msg: + 'You failed your captcha', + }], + }); + return; + } + + // save to cache + await redis.setAsync(key, 'y', 'EX', TTL_CACHE); + + res.status(200) + .json({ success: true }); + } catch (error) { + logger.error('checkHuman', error); + res.status(500) + .json({ + errors: [{ + msg: + 'Server error occured', + }], + }); + } +}; diff --git a/src/routes/api/index.js b/src/routes/api/index.js index d857843..ba9143b 100644 --- a/src/routes/api/index.js +++ b/src/routes/api/index.js @@ -13,7 +13,8 @@ import { getIPFromRequest, getIPv6Subnet } from '../../utils/ip'; import me from './me'; import mctp from './mctp'; -import pixel from './pixel'; +// import pixel from './pixel'; +import captcha from './captcha'; import auth from './auth'; import ranking from './ranking'; import history from './history'; @@ -63,7 +64,11 @@ router.use((err, req, res, next) => { * rate limiting should occure outside, * with nginx or whatever */ -router.post('/pixel', pixel); +/* api pixel got deactivated in favor of websocket */ +/* keeping it still here to enable it again if needed */ +// router.post('/pixel', pixel); + +router.post('/captcha', captcha); /* * passport authenticate diff --git a/src/routes/api/pixel.js b/src/routes/api/pixel.js index 982bae6..086843a 100644 --- a/src/routes/api/pixel.js +++ b/src/routes/api/pixel.js @@ -5,7 +5,7 @@ import type { Request, Response } from 'express'; -import draw from '../../core/draw'; +import { drawSafeByCoords } from '../../core/draw'; import { blacklistDetector, cheapDetector, @@ -197,7 +197,7 @@ async function place(req: Request, res: Response) { const { errorTitle, error, success, waitSeconds, coolDownSeconds, - } = await draw(user, cn, clr, x, y, z); + } = await drawSafeByCoords(user, cn, clr, x, y, z); logger.log('debug', success); if (success) { diff --git a/src/socket/APISocketServer.js b/src/socket/APISocketServer.js index ed45d63..8e68d26 100644 --- a/src/socket/APISocketServer.js +++ b/src/socket/APISocketServer.js @@ -15,7 +15,7 @@ import WebSocketEvents from './WebSocketEvents'; import webSockets from './websockets'; import { getIPFromRequest } from '../utils/ip'; import Minecraft from '../core/minecraft'; -import { drawUnsafe, setPixel } from '../core/draw'; +import { drawByCoords, setPixelByCoords } from '../core/draw'; import logger from '../core/logger'; import { APISOCKET_KEY } from '../core/config'; import chatProvider from '../core/ChatProvider'; @@ -192,7 +192,7 @@ class APISocketServer extends WebSocketEvents { if (clr < 0 || clr > 32) return; // be aware that user null has no cd if (!minecraftid && !ip) { - setPixel('0', clr, x, y); + setPixelByCoords('0', clr, x, y); ws.send(JSON.stringify(['retpxl', null, null, true, 0, 0])); return; } @@ -200,7 +200,7 @@ class APISocketServer extends WebSocketEvents { user.ip = ip; const { error, success, waitSeconds, coolDownSeconds, - } = await drawUnsafe(user, '0', clr, x, y, null); + } = await drawByCoords(user, '0', clr, x, y, null); ws.send(JSON.stringify([ 'retpxl', (minecraftid) || ip, diff --git a/src/socket/ProtocolClient.js b/src/socket/ProtocolClient.js index 7e22ddb..0d8453d 100644 --- a/src/socket/ProtocolClient.js +++ b/src/socket/ProtocolClient.js @@ -9,7 +9,8 @@ import EventEmitter from 'events'; import CoolDownPacket from './packets/CoolDownPacket'; -import PixelUpdate from './packets/PixelUpdate'; +import PixelUpdate from './packets/PixelUpdateClient'; +import PixelReturn from './packets/PixelReturn'; import OnlineCounter from './packets/OnlineCounter'; import RegisterCanvas from './packets/RegisterCanvas'; import RegisterChunk from './packets/RegisterChunk'; @@ -130,6 +131,14 @@ class ProtocolClient extends EventEmitter { if (~pos) chunks.splice(pos, 1); } + requestPlacePixel( + i, j, offset, + color, + ) { + const buffer = PixelUpdate.dehydrate(i, j, offset, color); + this.sendWhenReady(buffer); + } + requestChatHistory() { const buffer = RequestChatHistory.dehydrate(); if (this.isConnected) this.ws.send(buffer); @@ -186,6 +195,9 @@ class ProtocolClient extends EventEmitter { case PixelUpdate.OP_CODE: this.emit('pixelUpdate', PixelUpdate.hydrate(data)); break; + case PixelReturn.OP_CODE: + this.emit('pixelReturn', PixelReturn.hydrate(data)); + break; case OnlineCounter.OP_CODE: this.emit('onlineCounter', OnlineCounter.hydrate(data)); break; diff --git a/src/socket/SocketServer.js b/src/socket/SocketServer.js index 3a2f925..f25400e 100644 --- a/src/socket/SocketServer.js +++ b/src/socket/SocketServer.js @@ -8,13 +8,15 @@ import Counter from '../utils/Counter'; import RateLimiter from '../utils/RateLimiter'; import { getIPFromRequest } from '../utils/ip'; +import CoolDownPacket from './packets/CoolDownPacket'; +import PixelUpdate from './packets/PixelUpdateServer'; +import PixelReturn from './packets/PixelReturn'; import RegisterCanvas from './packets/RegisterCanvas'; import RegisterChunk from './packets/RegisterChunk'; import RegisterMultipleChunks from './packets/RegisterMultipleChunks'; import DeRegisterChunk from './packets/DeRegisterChunk'; import DeRegisterMultipleChunks from './packets/DeRegisterMultipleChunks'; import RequestChatHistory from './packets/RequestChatHistory'; -import CoolDownPacket from './packets/CoolDownPacket'; import ChangedMe from './packets/ChangedMe'; import OnlineCounter from './packets/OnlineCounter'; @@ -22,6 +24,14 @@ import chatProvider, { ChatProvider } from '../core/ChatProvider'; import authenticateClient from './verifyClient'; import WebSocketEvents from './WebSocketEvents'; import webSockets from './websockets'; +import { drawSafeByOffset } from '../core/draw'; + +import redis from '../data/redis'; +import { cheapDetector, blacklistDetector } from '../core/isProxy'; +import { + USE_PROXYCHECK, + RECAPTCHA_SECRET, +} from '../core/config'; const ipCounter: Counter = new Counter(); @@ -32,10 +42,11 @@ function heartbeat() { async function verifyClient(info, done) { const { req } = info; + const { headers } = req; // Limiting socket connections per ip const ip = await getIPFromRequest(req); - logger.info(`Got ws request from ${ip}`); + logger.info(`Got ws request from ${ip} via ${headers.origin}`); if (ipCounter.get(ip) > 50) { logger.info(`Client ${ip} has more than 50 connections open.`); return done(false); @@ -81,7 +92,7 @@ class SocketServer extends WebSocketEvents { ws.user = user; ws.name = (user.regUser) ? user.regUser.name : null; ws.rateLimiter = new RateLimiter(20, 15, true); - const ip = await getIPFromRequest(req); + SocketServer.checkIfProxy(ws); if (ws.name) { ws.send(`"${ws.name}"`); @@ -91,6 +102,7 @@ class SocketServer extends WebSocketEvents { online: this.wss.clients.size || 0, })); + const ip = await getIPFromRequest(req); ws.on('error', (e) => { logger.error(`WebSocket Client Error for ${ws.name}: ${e.message}`); }); @@ -111,9 +123,15 @@ class SocketServer extends WebSocketEvents { this.onlineCounterBroadcast = this.onlineCounterBroadcast.bind(this); this.ping = this.ping.bind(this); - this.killOld = this.killOld.bind(this); - setInterval(this.killOld, 10 * 60 * 1000); + /* + * i don't tink that we really need that, it just stresses the server + * with lots of reconnects at once, the overhead of having a few idle + * connections isn't too bad in comparison + */ + // this.killOld = this.killOld.bind(this); + // setInterval(this.killOld, 10 * 60 * 1000); + setInterval(this.onlineCounterBroadcast, 10 * 1000); // https://github.com/websockets/ws#how-to-detect-and-close-broken-connections setInterval(this.ping, 45 * 1000); @@ -207,7 +225,7 @@ class SocketServer extends WebSocketEvents { const now = Date.now(); this.wss.clients.forEach((ws) => { const lifetime = now - ws.startDate; - if (lifetime > 30 * 60 * 1000) ws.terminate(); + if (lifetime > 30 * 60 * 1000 && Math.random() < 0.3) ws.terminate(); }); } @@ -227,6 +245,16 @@ class SocketServer extends WebSocketEvents { webSockets.broadcastOnlineCounter(online); } + static async checkIfProxy(ws) { + const { ip } = ws.user; + if (USE_PROXYCHECK && ip && await cheapDetector(ip)) { + return true; + } if (await blacklistDetector(ip)) { + return true; + } + return false; + } + static async onTextMessage(text, ws) { try { let message; @@ -247,6 +275,7 @@ class SocketServer extends WebSocketEvents { message = message.trim(); if (ws.name && message) { + const { user } = ws; const waitLeft = ws.rateLimiter.tick(); if (waitLeft) { ws.send(JSON.stringify([ @@ -258,8 +287,22 @@ class SocketServer extends WebSocketEvents { ])); return; } + // check proxy + if (await SocketServer.checkIfProxy(ws)) { + logger.info( + `${ws.name} / ${user.ip} tried to send chat message with proxy`, + ); + ws.send(JSON.stringify([ + 'info', + 'You can not send chat messages with a proxy', + 'il', + channelId, + ])); + return; + } + // const errorMsg = await chatProvider.sendMessage( - ws.user, + user, message, channelId, ); @@ -286,7 +329,7 @@ class SocketServer extends WebSocketEvents { logger.info('Got empty message or message from unidentified ws'); } } catch { - logger.info('Got invalid ws message'); + logger.info('Got invalid ws text message'); } } @@ -294,52 +337,98 @@ class SocketServer extends WebSocketEvents { if (buffer.byteLength === 0) return; const opcode = buffer[0]; - switch (opcode) { - case RegisterCanvas.OP_CODE: { - const canvasId = RegisterCanvas.hydrate(buffer); - if (ws.canvasId !== null && ws.canvasId !== canvasId) { - this.deleteAllChunks(ws); + try { + switch (opcode) { + case PixelUpdate.OP_CODE: { + const { canvasId, user } = ws; + if (canvasId === null) { + return; + } + const { ip } = user; + // check if captcha needed + if (RECAPTCHA_SECRET) { + const key = `human:${ip}`; + const ttl: number = await redis.ttlAsync(key); + if (ttl <= 0) { + // need captcha + logger.info(`CAPTCHA ${ip} / ${ws.name} got captcha`); + ws.send(PixelReturn.dehydrate(10, 0, 0)); + break; + } + } + // (re)check for Proxy + if (await SocketServer.checkIfProxy(ws)) { + ws.send(PixelReturn.dehydrate(11, 0, 0)); + break; + } + // receive pixels here + const { + i, j, offset, + color, + } = PixelUpdate.hydrate(buffer); + const { + wait, + coolDown, + retCode, + } = await drawSafeByOffset( + ws.user, + ws.canvasId, + color, + i, j, offset, + ); + logger.info(`send: ${wait}, ${coolDown}, ${retCode}`); + ws.send(PixelReturn.dehydrate(retCode, wait, coolDown)); + break; } - ws.canvasId = canvasId; - const wait = await ws.user.getWait(canvasId); - const waitSeconds = (wait) ? Math.ceil((wait - Date.now()) / 1000) : 0; - ws.send(CoolDownPacket.dehydrate(waitSeconds)); - break; - } - case RegisterChunk.OP_CODE: { - const chunkid = RegisterChunk.hydrate(buffer); - this.pushChunk(chunkid, ws); - break; - } - case RegisterMultipleChunks.OP_CODE: { - this.deleteAllChunks(ws); - let posu = 2; - while (posu < buffer.length) { - const chunkid = buffer[posu++] | buffer[posu++] << 8; + case RegisterCanvas.OP_CODE: { + const canvasId = RegisterCanvas.hydrate(buffer); + if (ws.canvasId !== null && ws.canvasId !== canvasId) { + this.deleteAllChunks(ws); + } + ws.canvasId = canvasId; + const wait = await ws.user.getWait(canvasId); + const waitMs = (wait) ? wait - Date.now() : 0; + ws.send(CoolDownPacket.dehydrate(waitMs)); + break; + } + case RegisterChunk.OP_CODE: { + const chunkid = RegisterChunk.hydrate(buffer); this.pushChunk(chunkid, ws); + break; } - break; - } - case DeRegisterChunk.OP_CODE: { - const chunkidn = DeRegisterChunk.hydrate(buffer); - this.deleteChunk(chunkidn, ws); - break; - } - case DeRegisterMultipleChunks.OP_CODE: { - let posl = 2; - while (posl < buffer.length) { - const chunkid = buffer[posl++] | buffer[posl++] << 8; - this.deleteChunk(chunkid, ws); + case RegisterMultipleChunks.OP_CODE: { + this.deleteAllChunks(ws); + let posu = 2; + while (posu < buffer.length) { + const chunkid = buffer[posu++] | buffer[posu++] << 8; + this.pushChunk(chunkid, ws); + } + break; } - break; + case DeRegisterChunk.OP_CODE: { + const chunkidn = DeRegisterChunk.hydrate(buffer); + this.deleteChunk(chunkidn, ws); + break; + } + case DeRegisterMultipleChunks.OP_CODE: { + let posl = 2; + while (posl < buffer.length) { + const chunkid = buffer[posl++] | buffer[posl++] << 8; + this.deleteChunk(chunkid, ws); + } + break; + } + case RequestChatHistory.OP_CODE: { + const history = JSON.stringify(chatProvider.history); + ws.send(history); + break; + } + default: + break; } - case RequestChatHistory.OP_CODE: { - const history = JSON.stringify(chatProvider.history); - ws.send(history); - break; - } - default: - break; + } catch (e) { + logger.info('Got invalid ws binary message'); + throw e; } } diff --git a/src/socket/packets/CoolDownPacket.js b/src/socket/packets/CoolDownPacket.js index 21c55ae..4bdb207 100644 --- a/src/socket/packets/CoolDownPacket.js +++ b/src/socket/packets/CoolDownPacket.js @@ -6,16 +6,12 @@ const OP_CODE = 0xC2; export default { OP_CODE, hydrate(data: DataView) { - // SERVER (Client) - const waitSeconds = data.getUint16(1); - return waitSeconds; + return data.getUint32(1); }, - dehydrate(waitSeconds): Buffer { - // CLIENT (Sender) - const buffer = Buffer.allocUnsafe(1 + 2); + dehydrate(wait): Buffer { + const buffer = Buffer.allocUnsafe(1 + 4); buffer.writeUInt8(OP_CODE, 0); - - buffer.writeUInt16BE(waitSeconds, 1); + buffer.writeUInt32BE(wait, 1); return buffer; }, }; diff --git a/src/socket/packets/PixelReturn.js b/src/socket/packets/PixelReturn.js new file mode 100644 index 0000000..44002b7 --- /dev/null +++ b/src/socket/packets/PixelReturn.js @@ -0,0 +1,27 @@ +/* @flow */ + + +const OP_CODE = 0xC3; + +export default { + OP_CODE, + hydrate(data: DataView) { + const retCode = data.getUint8(1); + const wait = data.getUint32(2); + const coolDownSeconds = data.getInt16(6); + return { + retCode, + wait, + coolDownSeconds, + }; + }, + dehydrate(retCode, wait, coolDown): Buffer { + const buffer = Buffer.allocUnsafe(1 + 1 + 4 + 1 + 2); + buffer.writeUInt8(OP_CODE, 0); + buffer.writeUInt8(retCode, 1); + buffer.writeUInt32BE(wait, 2); + const coolDownSeconds = Math.round(coolDown / 1000); + buffer.writeInt16BE(coolDownSeconds, 6); + return buffer; + }, +}; diff --git a/src/socket/packets/PixelUpdate.js b/src/socket/packets/PixelUpdate.js deleted file mode 100644 index 9d4cc2a..0000000 --- a/src/socket/packets/PixelUpdate.js +++ /dev/null @@ -1,46 +0,0 @@ -/* @flow */ - - -import type { ColorIndex } from '../../core/Palette'; - -type PixelUpdatePacket = { - x: number, - y: number, - color: ColorIndex, -}; - -const OP_CODE = 0xC1; // Chunk Update - -export default { - OP_CODE, - hydrate(data: DataView): PixelUpdatePacket { - // CLIENT - const i = data.getUint8(1); - const j = data.getUint8(2); - const offset = (data.getUint8(3) << 16) | data.getUint16(4); - const color = data.getUint8(6); - // const offset = data.getUint16(5); - // const color = data.getUint8(7); - return { - i, j, offset, color, - }; - }, - dehydrate(i, j, offset, color): Buffer { - // SERVER - if (!process.env.BROWSER) { - const buffer = Buffer.allocUnsafe(1 + 1 + 1 + 1 + 2 + 1); - buffer.writeUInt8(OP_CODE, 0); - - buffer.writeUInt8(i, 1); - buffer.writeUInt8(j, 2); - buffer.writeUInt8(offset >>> 16, 3); - buffer.writeUInt16BE(offset & 0x00FFFF, 4); - buffer.writeUInt8(color, 6); - // buffer.writeUInt16BE(offset, 5); - // buffer.writeUInt8(color, 7); - - return buffer; - } - return null; - }, -}; diff --git a/src/socket/packets/PixelUpdateClient.js b/src/socket/packets/PixelUpdateClient.js new file mode 100644 index 0000000..79d5206 --- /dev/null +++ b/src/socket/packets/PixelUpdateClient.js @@ -0,0 +1,40 @@ +/* @flow */ + + +import type { ColorIndex } from '../../core/Palette'; + +type PixelUpdatePacket = { + x: number, + y: number, + color: ColorIndex, +}; + +const OP_CODE = 0xC1; + +export default { + OP_CODE, + hydrate(data: DataView): PixelUpdatePacket { + const i = data.getUint8(1); + const j = data.getUint8(2); + const offset = (data.getUint8(3) << 16) | data.getUint16(4); + const color = data.getUint8(6); + return { + i, j, offset, color, + }; + }, + + dehydrate(i, j, offset, color): Buffer { + const buffer = new ArrayBuffer(1 + 1 + 1 + 1 + 2 + 1); + const view = new DataView(buffer); + view.setUint8(0, OP_CODE); + + view.setUint8(1, i); + view.setUint8(2, j); + view.setUint8(3, offset >>> 16); + view.setUint16(4, offset & 0x00FFFF); + view.setUint8(6, color); + + return buffer; + }, + +}; diff --git a/src/socket/packets/PixelUpdateServer.js b/src/socket/packets/PixelUpdateServer.js new file mode 100644 index 0000000..6d0ce47 --- /dev/null +++ b/src/socket/packets/PixelUpdateServer.js @@ -0,0 +1,37 @@ +/* @flow */ + + +import type { ColorIndex } from '../../core/Palette'; + +type PixelUpdatePacket = { + x: number, + y: number, + color: ColorIndex, +}; + +const OP_CODE = 0xC1; + +export default { + OP_CODE, + hydrate(data: Buffer): PixelUpdatePacket { + const i = data.readUInt8(1); + const j = data.readUInt8(2); + const offset = (data.readUInt8(3) << 16) | data.readUInt16BE(4); + const color = data.readUInt8(6); + return { + i, j, offset, color, + }; + }, + dehydrate(i, j, offset, color): Buffer { + const buffer = Buffer.allocUnsafe(1 + 1 + 1 + 1 + 2 + 1); + buffer.writeUInt8(OP_CODE, 0); + + buffer.writeUInt8(i, 1); + buffer.writeUInt8(j, 2); + buffer.writeUInt8(offset >>> 16, 3); + buffer.writeUInt16BE(offset & 0x00FFFF, 4); + buffer.writeUInt8(color, 6); + + return buffer; + }, +}; diff --git a/src/socket/websockets.js b/src/socket/websockets.js index c49b6fb..62fbb59 100644 --- a/src/socket/websockets.js +++ b/src/socket/websockets.js @@ -6,7 +6,7 @@ */ import OnlineCounter from './packets/OnlineCounter'; -import PixelUpdate from './packets/PixelUpdate'; +import PixelUpdate from './packets/PixelUpdateServer'; class WebSockets { diff --git a/src/store/analytics.js b/src/store/analytics.js deleted file mode 100644 index 03fe3f7..0000000 --- a/src/store/analytics.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright 2016 Facebook, Inc. - * - * You are hereby granted a non-exclusive, worldwide, royalty-free license to - * use, copy, modify, and distribute this software in source code or binary - * form for use in connection with the web services and APIs provided by - * Facebook. - * - * As with any software that integrates with the Facebook platform, your use - * of this software is subject to the Facebook Developer Principles and - * Policies [http://developers.facebook.com/policy/]. This copyright notice - * shall be included in all copies or substantial portions of the software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE - */ - -import track from './track'; - -export default () => (next) => (action) => { - track(action); - return next(action); -}; diff --git a/src/store/audio.js b/src/store/audio.js index b87f0af..9c66fdc 100644 --- a/src/store/audio.js +++ b/src/store/audio.js @@ -105,8 +105,7 @@ export default (store) => (next) => (action) => { case 'PLACE_PIXEL': { if (mute) break; - const { color } = action; - const { palette } = state.canvas; + const { palette, selectedColor: color } = state.canvas; const colorsAmount = palette.colors.length; const clrFreq = 100 + Math.log(color / colorsAmount + 1) * 300; diff --git a/src/store/configureStore.js b/src/store/configureStore.js index d8956c7..1ac4f8c 100644 --- a/src/store/configureStore.js +++ b/src/store/configureStore.js @@ -10,7 +10,6 @@ import swal from './sweetAlert'; import protocolClientHook from './protocolClientHook'; import rendererHook from './rendererHook'; // import ads from './ads'; -// import analytics from './analytics'; import array from './array'; import promise from './promise'; import notifications from './notifications'; @@ -41,7 +40,6 @@ const store = createStore( protocolClientHook, rendererHook, // ads, - // analytics, logger, ), ), diff --git a/src/store/protocolClientHook.js b/src/store/protocolClientHook.js index 18bb0ef..3827ee7 100644 --- a/src/store/protocolClientHook.js +++ b/src/store/protocolClientHook.js @@ -30,6 +30,17 @@ export default (store) => (next) => (action) => { break; } + case 'REQUEST_PLACE_PIXEL': { + const { + i, j, offset, color, + } = action; + ProtocolClient.requestPlacePixel( + i, j, offset, + color, + ); + break; + } + default: // nothing } diff --git a/src/store/rendererHook.js b/src/store/rendererHook.js index 2797e0d..00b1f4e 100644 --- a/src/store/rendererHook.js +++ b/src/store/rendererHook.js @@ -47,6 +47,12 @@ export default (store) => (next) => (action) => { break; } + case 'SET_PLACE_ALLOWED': { + const renderer = getRenderer(); + renderer.forceNextSubRender = true; + break; + } + case 'TOGGLE_HISTORICAL_VIEW': case 'SET_SCALE': { const { diff --git a/src/store/track.js b/src/store/track.js deleted file mode 100644 index 07d96b6..0000000 --- a/src/store/track.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Copyright 2016 Facebook, Inc. - * - * You are hereby granted a non-exclusive, worldwide, royalty-free license to - * use, copy, modify, and distribute this software in source code or binary - * form for use in connection with the web services and APIs provided by - * Facebook. - * - * As with any software that integrates with the Facebook platform, your use - * of this software is subject to the Facebook Developer Principles and - * Policies [http://developers.facebook.com/policy/]. This copyright notice - * shall be included in all copies or substantial portions of the software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE - * - * @flow - */ - -import type { Action } from '../actions/types'; - - -export default function track(action: Action): void { - if (typeof window.ga === 'undefined') return; - - switch (action.type) { - case 'PLACE_PIXEL': { - const [x, y] = action.coordinates; - window.ga('send', { - hitType: 'event', - eventCategory: 'Place', - eventAction: action.color, - eventLabel: `${x},${y}`, - }); - break; - } - - default: - // nothing - } -} diff --git a/src/ui/ChunkRGB3D.js b/src/ui/ChunkRGB3D.js index bcc12ab..283b4bd 100644 --- a/src/ui/ChunkRGB3D.js +++ b/src/ui/ChunkRGB3D.js @@ -87,7 +87,7 @@ class Chunk { timestamp: number; constructor(palette, key, xc, zc) { - this.array = [0, xc, zc]; + this.cell = [0, xc, zc]; this.key = key; this.palette = palette; this.timestamp = Date.now(); diff --git a/src/ui/Renderer3D.js b/src/ui/Renderer3D.js index 38e6224..c582e8f 100644 --- a/src/ui/Renderer3D.js +++ b/src/ui/Renderer3D.js @@ -12,6 +12,7 @@ import VoxelPainterControls from '../controls/VoxelPainterControls'; import ChunkLoader from './ChunkLoader3D'; import { getChunkOfPixel, + getOffsetOfPixel, } from '../core/utils'; import { THREE_TILE_SIZE, @@ -458,6 +459,21 @@ class Renderer { } } + placeVoxel(x: number, y: number, z: number, color: number = null) { + const { + store, + } = this; + const state = store.getState(); + const { + canvasSize, + selectedColor, + } = state.canvas; + const chClr = (color === null) ? selectedColor : color; + const [i, j] = getChunkOfPixel(canvasSize, x, y, z); + const offset = getOffsetOfPixel(canvasSize, x, y, z); + store.dispatch(tryPlacePixel(i, j, offset, chClr)); + } + multiTapEnd() { const { store, @@ -486,7 +502,7 @@ class Renderer { if (this.rollOverMesh.position.y < 0) { return; } - store.dispatch(tryPlacePixel([px, py, pz])); + this.placeVoxel(px, py, pz); break; } case 2: { @@ -512,8 +528,8 @@ class Renderer { return; } if (target.clone().sub(camera.position).length() <= 50) { - const cell = target.toArray(); - store.dispatch(tryPlacePixel(cell, 0)); + const [x, y, z] = target.toArray(); + this.placeVoxel(x, y, z, 0); } } break; @@ -602,8 +618,8 @@ class Renderer { .addScalar(0.5) .floor(); if (target.clone().sub(camera.position).length() < 120) { - const cell = target.toArray(); - store.dispatch(tryPlacePixel(cell)); + const [x, y, z] = target.toArray(); + this.placeVoxel(x, y, z); } } else if (button === 1) { // middle mouse button @@ -635,8 +651,8 @@ class Renderer { return; } if (target.clone().sub(camera.position).length() < 120) { - const cell = target.toArray(); - store.dispatch(tryPlacePixel(cell, 0)); + const [x, y, z] = target.toArray(); + this.placeVoxel(x, y, z, 0); } } }