diff --git a/src/client.js b/src/client.js index ef01ecb..c595561 100644 --- a/src/client.js +++ b/src/client.js @@ -8,11 +8,6 @@ import createKeyPressHandler from './controls/keypress'; import { initTimer, urlChange, - receiveOnline, - receiveCoolDown, - receiveChatMessage, - addChatChannel, - removeChatChannel, setMobile, windowResize, } from './store/actions'; @@ -47,50 +42,6 @@ persistStore(store, {}, () => { SocketClient.on('pixelReturn', (args) => { receivePixelReturn(store, getRenderer(), args); }); - SocketClient.on('cooldownPacket', (coolDown) => { - store.dispatch(receiveCoolDown(coolDown)); - }); - SocketClient.on('onlineCounter', (online) => { - store.dispatch(receiveOnline(online)); - }); - SocketClient.on('chatMessage', ( - name, - text, - country, - channelId, - userId, - ) => { - const state = store.getState(); - - // assume that if one chat window is not hidden, all are - const isRead = state.windows.showWindows - // eslint-disable-next-line max-len - && state.windows.windows.find((win) => win.windowType === 'CHAT' && !win.hidden) - // eslint-disable-next-line max-len - && Object.values(state.windows.args).find((args) => args.chatChannel === channelId); - - // TODO ping doesn't work since update - // const { nameRegExp } = state.user; - // const isPing = (nameRegExp && text.match(nameRegExp)); - store.dispatch(receiveChatMessage( - name, - text, - country, - channelId, - userId, - false, - !!isRead, - )); - }); - SocketClient.on('changedMe', () => { - store.dispatch(fetchMe()); - }); - SocketClient.on('remch', (cid) => { - store.dispatch(removeChatChannel(cid)); - }); - SocketClient.on('addch', (channel) => { - store.dispatch(addChatChannel(channel)); - }); window.addEventListener('hashchange', () => { store.dispatch(urlChange()); diff --git a/src/components/GlobalCaptcha.jsx b/src/components/GlobalCaptcha.jsx index f902864..18ba687 100644 --- a/src/components/GlobalCaptcha.jsx +++ b/src/components/GlobalCaptcha.jsx @@ -8,13 +8,14 @@ import React, { useState } from 'react'; import { t } from 'ttag'; import Captcha from './Captcha'; +import socketClient from '../socket/SocketClient'; import { - requestSolveCaptcha, requestBanMe, } from '../store/actions/fetch'; const GlobalCaptcha = ({ close }) => { - const [errors, setErrors] = useState([]); + const [error, setError] = useState(null); + const [submitting, setSubmitting] = useState(false); const [legit, setLegit] = useState(false); // used to be able to force Captcha rerender on error const [captKey, setCaptKey] = useState(Date.now()); @@ -24,7 +25,7 @@ const GlobalCaptcha = ({ close }) => { onSubmit={async (e) => { e.preventDefault(); const text = e.target.captcha.value.slice(0, 6); - if (!text || text.length < 4) { + if (submitting || !text) { return; } // ---- @@ -36,23 +37,44 @@ const GlobalCaptcha = ({ close }) => { } // ---- const captchaid = e.target.captchaid.value; - const { errors: resErrors } = await requestSolveCaptcha( - text, - captchaid, - ); - if (resErrors) { - setCaptKey(Date.now()); - setErrors(resErrors); - } else { - close(); + let errorText; + try { + setSubmitting(true); + const retCode = await socketClient + .sendCaptchaSolution(text, captchaid); + console.log('Captcha return:', retCode); + switch (retCode) { + case 0: + close(); + return; + case 1: + errorText = t`You took too long, try again.`; + break; + case 2: + errorText = t`You failed your captcha`; + break; + case 3: + errorText = t`No or invalid captcha text`; + break; + case 4: + errorText = t`No captcha id given`; + break; + default: + errorText = t`Unknown Captcha Error`; + } + } catch (err) { + errorText = `${err.message}`; } + setSubmitting(false); + setCaptKey(Date.now()); + setError(errorText); }} > - {errors.map((error) => ( + {(error) && (

{t`Error`}: {error}

- ))} + )}

diff --git a/src/controls/PixelPainterControls.js b/src/controls/PixelPainterControls.js index c543f62..5e24b73 100644 --- a/src/controls/PixelPainterControls.js +++ b/src/controls/PixelPainterControls.js @@ -233,8 +233,7 @@ class PixelPlainterControls { // 5275_-8713 // 5398_-8614 if (x > 5275 && y > -8713 && x < 5398 && y < -8614) { - // eslint-disable-next-line eqeqeq - if (state.canvas.canvasId == 0) { + if (state.canvas.canvasId === '0') { window.location.href = 'https://files.catbox.moe/gh2wtr.mp4'; return; } diff --git a/src/controls/keypress.js b/src/controls/keypress.js index 7537b21..671fa9c 100644 --- a/src/controls/keypress.js +++ b/src/controls/keypress.js @@ -35,8 +35,7 @@ function createKeyPressHandler(store) { const canvasIds = Object.keys(canvases).filter((id) => !canvases[id].hid); if (num <= canvasIds.length) { const canvasId = canvasIds[num - 1]; - // eslint-disable-next-line eqeqeq - if (canvasId != curCanvasId) { + if (canvasId !== curCanvasId) { store.dispatch(selectCanvas(canvasId)); const canvasName = canvases[canvasId].title; store.dispatch(notify(t`Switched to ${canvasName}`)); diff --git a/src/core/ChatProvider.js b/src/core/ChatProvider.js index 18385fd..f99b791 100644 --- a/src/core/ChatProvider.js +++ b/src/core/ChatProvider.js @@ -268,7 +268,7 @@ export class ChatProvider { const cmdArr = message.split(' '); const cmd = cmdArr[0].substring(1); const args = cmdArr.slice(1); - const initiator = `@[${escapeMd(user.getName())}](${user.id})`; + const initiator = `@[${escapeMd(user.name)}](${user.id})`; switch (cmd) { case 'mute': { const timeMin = Number(args.slice(-1)); @@ -398,7 +398,7 @@ export class ChatProvider { ) { const { id } = user; const { t } = user.ttag; - const name = user.getName(); + const { name } = user; if (!name || !id) { return null; diff --git a/src/core/constants.js b/src/core/constants.js index 4a8da21..ad47325 100644 --- a/src/core/constants.js +++ b/src/core/constants.js @@ -11,7 +11,7 @@ export const DEFAULT_SCALE = 3; // default canvas that is first assumed, before real canvas data // gets fetched via api/me -export const DEFAULT_CANVAS_ID = 0; +export const DEFAULT_CANVAS_ID = '0'; export const DEFAULT_CANVASES = { 0: { ident: 'd', diff --git a/src/core/utils.js b/src/core/utils.js index 612c6b3..dd61159 100644 --- a/src/core/utils.js +++ b/src/core/utils.js @@ -160,7 +160,7 @@ export function getIdFromObject(obj, ident) { for (let i = 0; i < ids.length; i += 1) { const key = ids[i]; if (obj[key].ident === ident) { - return parseInt(key, 10); + return key; } } return null; diff --git a/src/data/User.js b/src/data/User.js index 7286a37..a847ce1 100644 --- a/src/data/User.js +++ b/src/data/User.js @@ -158,6 +158,14 @@ class User { this.setRegUser(this.regUser); } + get name() { + return (this.regUser) ? this.regUser.name : null; + } + + get isRegistered() { + return !!this.id; + } + addChannel(cid, channelArray) { this.channels[cid] = channelArray; } @@ -166,10 +174,6 @@ class User { delete this.channels[cid]; } - getName() { - return (this.regUser) ? this.regUser.name : null; - } - setWait(wait, canvasId) { return setCoolDown(this.ipSub, this.id, canvasId, wait); } diff --git a/src/data/redis/captcha.js b/src/data/redis/captcha.js index 4ae295e..b2abb54 100644 --- a/src/data/redis/captcha.js +++ b/src/data/redis/captcha.js @@ -68,20 +68,14 @@ function evaluateResult(captchaText, userText) { * set captcha solution * * @param text Solution of captcha - * @param ip * @param captchaid */ export async function setCaptchaSolution( text, - ip, - captchaid = null, + captchaid, ) { - let key = `capt:${ip}`; - if (captchaid) { - key += `:${captchaid}`; - } try { - await client.set(key, text, { + await client.set(`capt:${captchaid}`, text, { EX: CAPTCHA_TIMEOUT, }); } catch (error) { @@ -105,22 +99,24 @@ export async function setCaptchaSolution( export async function checkCaptchaSolution( text, ip, - onetime = false, - captchaid = null, + onetime, + captchaid, wrongCallback = null, ) { - const ipn = getIPv6Subnet(ip); - let key = `capt:${ip}`; - if (captchaid) { - key += `:${captchaid}`; + if (!text || text.length > 10) { + return 3; } - const solution = await client.get(key); + if (!captchaid) { + return 4; + } + const solution = await client.get(`capt:${captchaid}`); if (solution) { if (evaluateResult(solution, text)) { if (Math.random() < 0.1) { return 2; } if (!onetime) { + const ipn = getIPv6Subnet(ip); const solvkey = `${PREFIX}:${ipn}`; await client.set(solvkey, '', { EX: TTL_CACHE, diff --git a/src/popup.js b/src/popup.js index 847f028..d084c14 100644 --- a/src/popup.js +++ b/src/popup.js @@ -8,10 +8,6 @@ import { parentExists } from './core/utils'; import store from './store/storePopUp'; import { urlChange, - receiveOnline, - receiveChatMessage, - removeChatChannel, - addChatChannel, } from './store/actions'; import { fetchMe, @@ -28,45 +24,6 @@ persistStore(store, {}, () => { store.dispatch(urlChange()); }); - SocketClient.on('onlineCounter', (online) => { - store.dispatch(receiveOnline(online)); - }); - SocketClient.on('chatMessage', ( - name, - text, - country, - channelId, - userId, - ) => { - const state = store.getState(); - - // assume that if one chat window is not hidden, all are - const isRead = state.popup.windowType === 'CHAT' - && state.popup.args.chatChannel === channelId; - - // TODO ping doesn't work since update - // const { nameRegExp } = state.user; - // const isPing = (nameRegExp && text.match(nameRegExp)); - store.dispatch(receiveChatMessage( - name, - text, - country, - channelId, - userId, - false, - !!isRead, - )); - }); - SocketClient.on('changedMe', () => { - store.dispatch(fetchMe()); - }); - SocketClient.on('remch', (cid) => { - store.dispatch(removeChatChannel(cid)); - }); - SocketClient.on('addch', (channel) => { - store.dispatch(addChatChannel(channel)); - }); - if (!parentExists()) { store.dispatch(fetchMe()); SocketClient.initialize(store); diff --git a/src/routes/api/block.js b/src/routes/api/block.js index 24e7778..e359fe1 100644 --- a/src/routes/api/block.js +++ b/src/routes/api/block.js @@ -119,7 +119,7 @@ async function block(req, res) { errors: ['Could not (un)block user'], }); logger.info( - `User ${user.getName()} (un)blocked ${userName}`, + `User ${user.name} (un)blocked ${userName}`, ); } } diff --git a/src/routes/api/blockdm.js b/src/routes/api/blockdm.js index 652c3a9..357fb56 100644 --- a/src/routes/api/blockdm.js +++ b/src/routes/api/blockdm.js @@ -26,7 +26,7 @@ async function blockdm(req, res) { } logger.info( - `User ${user.getName()} (un)blocked all dms`, + `User ${user.name} (un)blocked all dms`, ); await user.regUser.update({ diff --git a/src/routes/api/captcha.js b/src/routes/api/captcha.js index c1eff75..00a34af 100644 --- a/src/routes/api/captcha.js +++ b/src/routes/api/captcha.js @@ -15,16 +15,6 @@ export default async (req, res) => { try { const { text, id } = req.body; - if (!text) { - res.status(400) - .json({ errors: [t`No captcha text given`] }); - return; - } - if (!id) { - res.status(400) - .json({ errors: [t`No captcha id given`] }); - return; - } const ret = await checkCaptchaSolution( text, @@ -46,15 +36,19 @@ export default async (req, res) => { break; case 2: res.status(422) - .json({ - errors: [t`You failed your captcha`], - }); + .json({ errors: [t`You failed your captcha`] }); + break; + case 3: + res.status(400) + .json({ errors: [t`No captcha text given`] }); + break; + case 4: + res.status(400) + .json({ errors: [t`No captcha id given`] }); break; default: res.status(422) - .json({ - errors: [t`Unknown Captcha Error`], - }); + .json({ errors: [t`Unknown Captcha Error`] }); } } catch (error) { logger.error('CAPTCHA', error); diff --git a/src/routes/api/leavechan.js b/src/routes/api/leavechan.js index a7d34b2..792ebbd 100644 --- a/src/routes/api/leavechan.js +++ b/src/routes/api/leavechan.js @@ -57,7 +57,7 @@ async function leaveChan(req, res) { } logger.info( - `Removing user ${user.getName()} from channel ${channel.name || channelId}`, + `Removing user ${user.name} from channel ${channel.name || channelId}`, ); user.regUser.removeChannel(channel); diff --git a/src/routes/api/privatize.js b/src/routes/api/privatize.js index 70f567f..e283431 100644 --- a/src/routes/api/privatize.js +++ b/src/routes/api/privatize.js @@ -25,7 +25,7 @@ async function privatize(req, res) { } logger.info( - `User ${user.getName()} set private status to ${priv}`, + `User ${user.name} set private status to ${priv}`, ); await user.regUser.update({ diff --git a/src/routes/api/startdm.js b/src/routes/api/startdm.js index d383e9e..d31d141 100644 --- a/src/routes/api/startdm.js +++ b/src/routes/api/startdm.js @@ -108,7 +108,7 @@ async function startDm(req, res) { ChatProvider.addUserToChannel( userId, ChannelId, - [user.getName(), 1, curTime, user.id], + [user.name, 1, curTime, user.id], ), ]; await Promise.all(promises); diff --git a/src/routes/captcha.js b/src/routes/captcha.js index db0b8fa..6f41387 100644 --- a/src/routes/captcha.js +++ b/src/routes/captcha.js @@ -27,7 +27,7 @@ export default (req, res) => { } const ip = getIPFromRequest(req); - setCaptchaSolution(text, ip, id); + setCaptchaSolution(text, id); logger.info(`Captchas: ${ip} got captcha with text: ${text}`); res.set({ diff --git a/src/socket/APISocketServer.js b/src/socket/APISocketServer.js index d12088e..e077c6b 100644 --- a/src/socket/APISocketServer.js +++ b/src/socket/APISocketServer.js @@ -10,6 +10,7 @@ import WebSocket from 'ws'; import socketEvents from './socketEvents'; +import { dehydrateOnlineCounter } from './packets/server'; import chatProvider, { ChatProvider } from '../core/ChatProvider'; import { RegUser } from '../data/sql'; import { getIPFromRequest } from '../utils/ip'; @@ -55,7 +56,6 @@ class APISocketServer { }); }); - this.broadcast = this.broadcast.bind(this); this.broadcastOnlineCounter = this.broadcastOnlineCounter.bind(this); this.broadcastPixelBuffer = this.broadcastPixelBuffer.bind(this); this.ping = this.ping.bind(this); @@ -155,8 +155,9 @@ class APISocketServer { } } - broadcastOnlineCounter(data) { - this.broadcast(data, (client) => client.subOnline); + broadcastOnlineCounter(online) { + const buffer = dehydrateOnlineCounter(online); + this.broadcast(buffer, (client) => client.subOnline); } broadcastPixelBuffer(canvasId, chunkid, buffer) { diff --git a/src/socket/MessageBroker.js b/src/socket/MessageBroker.js index 67a7a8f..24b59f2 100644 --- a/src/socket/MessageBroker.js +++ b/src/socket/MessageBroker.js @@ -371,11 +371,10 @@ class MessageBroker extends SocketEvents { broadcastOnlineCounter(online) { this.updateShardOnlineCounter(this.thisShard, online); // send our online counter to other shards - let buffer = dehydrateOnlineCounter(online); + const buffer = dehydrateOnlineCounter(online); this.publisher.publish(this.thisShard, buffer); // send total counter to our players - buffer = dehydrateOnlineCounter(this.onlineCounter); - super.emit('onlineCounter', buffer); + super.emit('onlineCounter', this.onlineCounter); } checkHealth() { diff --git a/src/socket/SockEvents.js b/src/socket/SockEvents.js index 1eca518..8a0029e 100644 --- a/src/socket/SockEvents.js +++ b/src/socket/SockEvents.js @@ -4,7 +4,6 @@ import EventEmitter from 'events'; import { - dehydrateOnlineCounter, dehydratePixelUpdate, } from './packets/server'; @@ -255,8 +254,7 @@ class SocketEvents extends EventEmitter { */ broadcastOnlineCounter(online) { this.onlineCounter = online; - const buffer = dehydrateOnlineCounter(online); - this.emit('onlineCounter', buffer); + this.emit('onlineCounter', online); } } diff --git a/src/socket/SocketClient.js b/src/socket/SocketClient.js index f57ec27..0381777 100644 --- a/src/socket/SocketClient.js +++ b/src/socket/SocketClient.js @@ -8,11 +8,11 @@ import { hydratePixelReturn, hydrateOnlineCounter, hydrateCoolDown, + hydrateCaptchaReturn, dehydrateRegCanvas, dehydrateRegChunk, dehydrateRegMChunks, dehydrateDeRegChunk, - dehydrateCaptchaSolution, dehydratePixelUpdate, dehydratePing, } from './packets/client'; @@ -22,7 +22,20 @@ import { ONLINE_COUNTER_OP, COOLDOWN_OP, CHANGE_ME_OP, + CAPTCHA_RETURN_OP, } from './packets/op'; +import { + socketOpen, + socketClose, + receiveOnline, + receiveCoolDown, + receiveChatMessage, + addChatChannel, + removeChatChannel, +} from '../store/actions/socket'; +import { + fetchMe, +} from '../store/actions/thunks'; import { shardHost } from '../store/actions/fetch'; const chunks = []; @@ -42,6 +55,7 @@ class SocketClient extends EventEmitter { */ this.readyState = WebSocket.CLOSED; this.msgQueue = []; + this.reqQueue = []; this.checkHealth = this.checkHealth.bind(this); setInterval(this.checkHealth, 2000); @@ -122,10 +136,10 @@ class SocketClient extends EventEmitter { this.timeLastPing = now; this.timeLastSent = now; - this.emit('open', {}); + this.store.dispatch(socketOpen()); this.readyState = WebSocket.OPEN; this.send(dehydrateRegCanvas( - this.store.getState().canvas, + this.store.getState().canvas.canvasId, )); console.log(`Register ${chunks.length} chunks`); this.send(dehydrateRegMChunks(chunks)); @@ -136,7 +150,9 @@ class SocketClient extends EventEmitter { if (canvasId === null) { return; } - console.log('Notify websocket server that we changed canvas'); + console.log( + `Notify websocket server that we changed canvas to ${canvasId}`, + ); chunks.length = 0; this.send(dehydrateRegCanvas(canvasId)); } @@ -163,10 +179,28 @@ class SocketClient extends EventEmitter { } /* - sendCaptchaSolution(solution) { - const buffer = dehydrateCaptchaSolution(solution); + * send captcha solution + * @param solution text + * @return promise that resolves when response arrives + */ + sendCaptchaSolution(solution, captchaid) { + return new Promise((resolve, reject) => { + let id; + const queueObj = ['cs', (args) => { + resolve(args); + clearTimeout(id); + }]; + this.reqQueue.push(queueObj); + id = setTimeout(() => { + const pos = this.reqQueue.indexOf(queueObj); + if (~pos) this.reqQueue.splice(pos, 1); + reject(new Error('Timeout')); + }, 20000); + this.sendWhenReady( + `cs,${JSON.stringify([solution, captchaid])}`, + ); + }); } - */ /* * Send pixel request @@ -182,7 +216,9 @@ class SocketClient extends EventEmitter { } sendChatMessage(message, channelId) { - this.sendWhenReady(JSON.stringify([message, channelId])); + this.sendWhenReady( + `cm,${JSON.stringify([message, channelId])}`, + ); } onMessage({ data: message }) { @@ -201,27 +237,24 @@ class SocketClient extends EventEmitter { } onTextMessage(message) { - if (!message) return; - const data = JSON.parse(message); - - if (Array.isArray(data)) { - switch (data.length) { - case 5: { - // chat message - const [name, text, country, channelId, userId] = data; - this.emit('chatMessage', - name, text, country, Number(channelId), userId); - return; - } - case 2: { - // signal - const [signal, args] = data; - this.emit(signal, args); - break; - } - default: - // nothing - } + const comma = message.indexOf(','); + if (comma === -1) { + return; + } + const key = message.slice(0, comma); + const val = JSON.parse(message.slice(comma + 1)); + switch (key) { + case 'cm': + this.store.dispatch(receiveChatMessage(...val)); + break; + case 'ac': + this.store.dispatch(addChatChannel(val)); + break; + case 'rc': + this.store.dispatch(removeChatChannel(val)); + break; + default: + // nothing } } @@ -240,16 +273,23 @@ class SocketClient extends EventEmitter { this.emit('pixelReturn', hydratePixelReturn(data)); break; case ONLINE_COUNTER_OP: - this.emit('onlineCounter', hydrateOnlineCounter(data)); + this.store.dispatch(receiveOnline(hydrateOnlineCounter(data))); break; case COOLDOWN_OP: - this.emit('cooldownPacket', hydrateCoolDown(data)); + this.store.dispatch(receiveCoolDown(hydrateCoolDown(data))); break; case CHANGE_ME_OP: console.log('Websocket requested api/me reload'); - this.emit('changedMe'); + this.store.dispatch(fetchMe()); this.reconnect(); break; + case CAPTCHA_RETURN_OP: { + const pos = this.reqQueue.findIndex((q) => q[0] === 'cs'); + if (~pos) { + this.reqQueue.splice(pos, 1)[0][1](hydrateCaptchaReturn(data)); + } + break; + } default: console.error(`Unknown op_code ${opcode} received`); break; @@ -257,7 +297,7 @@ class SocketClient extends EventEmitter { } onClose(e) { - this.emit('close'); + this.store.dispatch(socketClose()); this.ws = null; this.readyState = WebSocket.CONNECTING; // reconnect in 1s if last connect was longer than 7s ago, else 5s diff --git a/src/socket/SocketServer.js b/src/socket/SocketServer.js index 2fbee83..78b7e41 100644 --- a/src/socket/SocketServer.js +++ b/src/socket/SocketServer.js @@ -21,19 +21,19 @@ import { hydrateDeRegChunk, hydrateRegMChunks, hydrateDeRegMChunks, - hydrateCaptchaSolution, hydratePixelUpdate, dehydrateChangeMe, dehydrateOnlineCounter, - dehydratePixelUpdate, dehydrateCoolDown, dehydratePixelReturn, + dehydrateCaptchaReturn, } from './packets/server'; import socketEvents from './socketEvents'; import chatProvider, { ChatProvider } from '../core/ChatProvider'; import authenticateClient from './authenticateClient'; import drawByOffsets from '../core/draw'; import isIPAllowed from '../core/isAllowed'; +import { checkCaptchaSolution } from '../data/redis/captcha'; const ipCounter = new Counter(); @@ -64,7 +64,6 @@ class SocketServer { constructor() { this.CHUNK_CLIENTS = new Map(); - this.broadcast = this.broadcast.bind(this); this.broadcastPixelBuffer = this.broadcastPixelBuffer.bind(this); this.reloadUser = this.reloadUser.bind(this); this.onlineCounterBroadcast = this.onlineCounterBroadcast.bind(this); @@ -93,7 +92,6 @@ class SocketServer { ws.canvasId = null; const { user } = req; ws.user = user; - ws.name = user.getName(); ws.chunkCnt = 0; const { ip } = user; @@ -101,7 +99,8 @@ class SocketServer { ws.send(dehydrateOnlineCounter(socketEvents.onlineCounter)); ws.on('error', (e) => { - logger.error(`WebSocket Client Error for ${ws.name}: ${e.message}`); + // eslint-disable-next-line max-len + logger.error(`WebSocket Client Error for ${ws.user.name}: ${e.message}`); }); ws.on('close', () => { @@ -120,7 +119,10 @@ class SocketServer { }); }); - socketEvents.on('onlineCounter', this.broadcast); + socketEvents.on('onlineCounter', (online) => { + const onlineBuffer = dehydrateOnlineCounter(online); + this.broadcast(onlineBuffer); + }); socketEvents.on('pixelUpdate', this.broadcastPixelBuffer); socketEvents.on('reloadUser', this.reloadUser); @@ -132,8 +134,10 @@ class SocketServer { id, country, ) => { + const text = `cm,${JSON.stringify( + [name, message, country, channelId, id], + )}`; this.findAllWsByUerId(userId).forEach((ws) => { - const text = JSON.stringify([name, message, country, channelId, id]); ws.send(text); }); }); @@ -145,7 +149,9 @@ class SocketServer { id, country, ) => { - const text = JSON.stringify([name, message, country, channelId, id]); + const text = `cm,${JSON.stringify( + [name, message, country, channelId, id], + )}`; const clientArray = []; this.wss.clients.forEach((ws) => { if (ws.user && chatProvider.userHasChannelAccess(ws.user, channelId)) { @@ -158,11 +164,9 @@ class SocketServer { socketEvents.on('addChatChannel', (userId, channelId, channelArray) => { this.findAllWsByUerId(userId).forEach((ws) => { ws.user.addChannel(channelId, channelArray); - const text = JSON.stringify([ - 'addch', { - [channelId]: channelArray, - }, - ]); + const text = `ac,${JSON.stringify({ + [channelId]: channelArray, + })}`; ws.send(text); }); }); @@ -170,7 +174,7 @@ class SocketServer { socketEvents.on('remChatChannel', (userId, channelId) => { this.findAllWsByUerId(userId).forEach((ws) => { ws.user.removeChannel(channelId); - const text = JSON.stringify(['remch', channelId]); + const text = `rc,${JSON.stringify(channelId)}`; ws.send(text); }); }); @@ -348,9 +352,8 @@ class SocketServer { reloadUser(name) { this.wss.clients.forEach(async (ws) => { - if (ws.name === name) { + if (ws.user.name === name) { await ws.user.reload(); - ws.name = ws.user.getName(); const buffer = dehydrateChangeMe(); ws.send(buffer); } @@ -419,64 +422,66 @@ class SocketServer { * chat messages in [message, channelId] format */ try { - let message; - let channelId; - try { - const data = JSON.parse(text); - [message, channelId] = data; - channelId = Number(channelId); - if (Number.isNaN(channelId)) { - throw new Error('NaN'); - } - } catch { - logger.warn( - `Received unparseable message from ${ws.name} on websocket: ${text}`, - ); - return; + const comma = text.indexOf(','); + if (comma === -1) { + throw new Error('No comma'); } - message = message.trim(); - - /* - * just if logged in - */ - if (ws.name && message) { - const { user } = ws; - /* - * if DM channel, make sure that other user has DM open - * (needed because we allow user to leave one-sided - * and auto-join on message) - */ - const dmUserId = chatProvider.checkIfDm(user, channelId); - if (dmUserId) { - const dmWs = this.findWsByUserId(dmUserId); - if (!dmWs - || !chatProvider.userHasChannelAccess(dmWs.user, channelId) - ) { - // TODO this is really ugly - // DMS have to be rethought - if (!user.addedDM) user.addedDM = []; - if (!user.addedDM.includes(dmUserId)) { - await ChatProvider.addUserToChannel( - dmUserId, - channelId, - [ws.name, 1, Date.now(), user.id], - ); - user.addedDM.push(dmUserId); + const key = text.slice(0, comma); + const val = JSON.parse(text.slice(comma + 1)); + const { user } = ws; + switch (key) { + case 'cm': { + // chat message + const message = val[0].trim(); + if (!user.isRegistered || !message) { + return; + } + const channelId = val[1]; + /* + * if DM channel, make sure that other user has DM open + * (needed because we allow user to leave one-sided + * and auto-join on message) + */ + const dmUserId = chatProvider.checkIfDm(user, channelId); + if (dmUserId) { + const dmWs = this.findWsByUserId(dmUserId); + if (!dmWs + || !chatProvider.userHasChannelAccess(dmWs.user, channelId) + ) { + // TODO this is really ugly + // DMS have to be rethought + if (!user.addedDM) user.addedDM = []; + if (!user.addedDM.includes(dmUserId)) { + await ChatProvider.addUserToChannel( + dmUserId, + channelId, + [user.name, 1, Date.now(), user.id], + ); + user.addedDM.push(dmUserId); + } } } + socketEvents.recvChatMessage(user, message, channelId); + break; } - - /* - * send chat message - */ - socketEvents.recvChatMessage(user, message, channelId); - } else { - logger.info('Got empty message or message from unidentified ws'); + case 'cs': { + // captcha solution + const [solution, captchaid] = val; + const ret = await checkCaptchaSolution( + solution, + user.ip, + false, + captchaid, + ); + ws.send(dehydrateCaptchaReturn(ret)); + break; + } + default: + throw new Error('Unknown key'); } - } catch (error) { - logger.error('Got invalid ws text message'); - logger.error(error.message); - logger.error(error.stack); + } catch (err) { + // eslint-disable-next-line max-len + logger.error(`Got invalid ws text message ${text}, with error: ${err.message}`); } } diff --git a/src/socket/packets/client.js b/src/socket/packets/client.js index 0a70484..8ede8f2 100644 --- a/src/socket/packets/client.js +++ b/src/socket/packets/client.js @@ -7,7 +7,6 @@ import { DEREG_CHUNK_OP, REG_MCHUNKS_OP, DEREG_MCHUNKS_OP, - CAPTCHA_SOLUTION_OP, PING_OP, PIXEL_UPDATE_OP, } from './op'; @@ -70,7 +69,6 @@ export function hydrateCoolDown(data) { * @return see ui/placePixels */ export function hydratePixelReturn(data) { - // Client (receiver) const retCode = data.getUint8(1); const wait = data.getUint32(2); const coolDownSeconds = data.getInt16(6); @@ -85,6 +83,13 @@ export function hydratePixelReturn(data) { }; } +/* + * @return code of captcha success + */ +export function hydrateCaptchaReturn(data) { + return data.getUint8(1); +} + /* * dehydrate functions return ArrayBuffer object */ @@ -152,18 +157,6 @@ export function dehydrateDeRegMChunks(chunks) { return buffer; } -/* - * @param solution string of entered captcha - */ -export function dehydrateCaptchaSolution(solution) { - const encoder = new TextEncoder(); - const view = encoder.encode(solution); - const buffer = new Uint8Array(view.byteLength + 1); - buffer[0] = CAPTCHA_SOLUTION_OP; - buffer.set(view, 1); - return buffer.buffer; -} - export function dehydratePing() { return new Uint8Array([PING_OP]).buffer; } diff --git a/src/socket/packets/op.js b/src/socket/packets/op.js index 263d5ef..c4dbfae 100644 --- a/src/socket/packets/op.js +++ b/src/socket/packets/op.js @@ -10,12 +10,12 @@ export const REG_CHUNK_OP = 0xA1; export const DEREG_CHUNK_OP = 0xA2; export const REG_MCHUNKS_OP = 0xA3; export const DEREG_MCHUNKS_OP = 0xA4; -export const CAPTCHA_SOLUTION_OP = 0xA5; export const CHANGE_ME_OP = 0xA6; export const ONLINE_COUNTER_OP = 0xA7; export const PING_OP = 0xB0; export const PIXEL_UPDATE_OP = 0xC1; -export const PIXEL_UPDATE_MB_OP = 0xC1; export const COOLDOWN_OP = 0xC2; export const PIXEL_RETURN_OP = 0xC3; export const CHUNK_UPDATE_MB_OP = 0xC4; +export const PIXEL_UPDATE_MB_OP = 0xC5; +export const CAPTCHA_RETURN_OP = 0xC6; diff --git a/src/socket/packets/server.js b/src/socket/packets/server.js index 796516d..0e69174 100644 --- a/src/socket/packets/server.js +++ b/src/socket/packets/server.js @@ -9,6 +9,7 @@ import { COOLDOWN_OP, PIXEL_RETURN_OP, CHUNK_UPDATE_MB_OP, + CAPTCHA_RETURN_OP, } from './op'; /* @@ -80,13 +81,6 @@ export function hydrateDeRegMChunks(data, cb) { } } -/* -* @return captcha solution -*/ -export function hydrateCaptchaSolution(data) { - return data.toString('utf8', 1); -} - /* * @return chunk id and array of pixel offset and color */ @@ -225,6 +219,10 @@ export function dehydratePixelReturn( return buffer; } +export function dehydrateCaptchaReturn(retCode) { + return Buffer.from([CAPTCHA_RETURN_OP, retCode]); +} + /* * @param canvasId * @param Array with chunk coordinates diff --git a/src/store/actions/index.js b/src/store/actions/index.js index 9879ae5..ce30d4a 100644 --- a/src/store/actions/index.js +++ b/src/store/actions/index.js @@ -250,15 +250,6 @@ export function receiveBigChunkFailure(center, error) { }; } -export function receiveCoolDown( - wait, -) { - return { - type: 'REC_COOLDOWN', - wait, - }; -} - export function receiveMe( me, ) { @@ -296,13 +287,6 @@ export function receiveStats( }; } -export function receiveOnline(online) { - return { - type: 'REC_ONLINE', - online, - }; -} - export function sendChatMessage( text, channel, @@ -314,27 +298,6 @@ export function sendChatMessage( }; } -export function receiveChatMessage( - name, - text, - country, - channel, - user, - isPing, - isRead, -) { - return { - type: 's/REC_CHAT_MESSAGE', - name, - text, - country, - channel, - user, - isPing, - isRead, - }; -} - /* * check socket/packets/PixelReturn.js for args */ @@ -445,13 +408,6 @@ export function initTimer() { }; } -export function addChatChannel(channel) { - return { - type: 's/ADD_CHAT_CHANNEL', - channel, - }; -} - export function blockUser(userId, userName) { return { type: 's/BLOCK_USER', @@ -482,13 +438,6 @@ export function privatize(priv) { }; } -export function removeChatChannel(cid) { - return { - type: 's/REMOVE_CHAT_CHANNEL', - cid, - }; -} - export function muteChatChannel(cid) { return { type: 's/MUTE_CHAT_CHANNEL', diff --git a/src/store/actions/socket.js b/src/store/actions/socket.js new file mode 100644 index 0000000..5a3110f --- /dev/null +++ b/src/store/actions/socket.js @@ -0,0 +1,74 @@ +/* + * actions that are fired when received by the websocket + */ +export function socketClose() { + return { + type: 'w/CLOSE', + }; +} + +export function socketOpen() { + return { + type: 'w/OPEN', + }; +} + +export function receiveChatMessage( + name, + text, + country, + channel, + user, +) { + return (dispatch, getState) => { + channel = Number(channel); + const state = getState(); + const isRead = state.windows.showWindows + // eslint-disable-next-line max-len + && state.windows.windows.find((win) => win.windowType === 'CHAT' && !win.hidden) + // eslint-disable-next-line max-len + && Object.values(state.windows.args).find((args) => args.chatChannel === channel); + + // TODO ping doesn't work since update + // const { nameRegExp } = state.user; + // const isPing = (nameRegExp && text.match(nameRegExp)); + dispatch({ + type: 's/REC_CHAT_MESSAGE', + name, + text, + country, + channel, + user, + isPing: false, + isRead: !!isRead, + }); + }; +} + +export function receiveCoolDown(wait) { + return { + type: 'REC_COOLDOWN', + wait, + }; +} + +export function receiveOnline(online) { + return { + type: 'REC_ONLINE', + online, + }; +} + +export function addChatChannel(channel) { + return { + type: 's/ADD_CHAT_CHANNEL', + channel, + }; +} + +export function removeChatChannel(cid) { + return { + type: 's/REMOVE_CHAT_CHANNEL', + cid, + }; +} diff --git a/src/store/actions/thunks.js b/src/store/actions/thunks.js index dd8a903..4c1559d 100644 --- a/src/store/actions/thunks.js +++ b/src/store/actions/thunks.js @@ -11,9 +11,7 @@ import { requestChatMessages, requestMe, } from './fetch'; - import { - addChatChannel, pAlert, receiveStats, receiveMe, @@ -21,8 +19,11 @@ import { unblockUser, blockingDm, privatize, - removeChatChannel, } from './index'; +import { + addChatChannel, + removeChatChannel, +} from './socket'; function setApiFetching(fetching) { return { diff --git a/src/store/middleware/socketClientHook.js b/src/store/middleware/socketClientHook.js index 5ff839b..4bdfb3c 100644 --- a/src/store/middleware/socketClientHook.js +++ b/src/store/middleware/socketClientHook.js @@ -48,10 +48,7 @@ export default (store) => (next) => (action) => { const ret = next(action); const state = store.getState(); const { canvasId } = state.canvas; - if (prevState.canvas.canvasId === canvasId) { - // TODO see if this is the case anywhere - console.log('Not triggering change canvas'); - } else { + if (prevState.canvas.canvasId !== canvasId) { SocketClient.setCanvas(canvasId); } return ret; diff --git a/src/store/reducers/canvas.js b/src/store/reducers/canvas.js index 1fa5a8c..b0670db 100644 --- a/src/store/reducers/canvas.js +++ b/src/store/reducers/canvas.js @@ -17,7 +17,7 @@ import { /* export type CanvasState = { - canvasId: number, + canvasId: string, canvasIdent: string, selectedColor: number, is3D: boolean,