refactor socket events

send captcha solution via websocket
This commit is contained in:
HF 2022-10-03 12:47:03 +02:00
parent 7a2053fda3
commit e9952134a5
30 changed files with 326 additions and 349 deletions

View File

@ -8,11 +8,6 @@ import createKeyPressHandler from './controls/keypress';
import { import {
initTimer, initTimer,
urlChange, urlChange,
receiveOnline,
receiveCoolDown,
receiveChatMessage,
addChatChannel,
removeChatChannel,
setMobile, setMobile,
windowResize, windowResize,
} from './store/actions'; } from './store/actions';
@ -47,50 +42,6 @@ persistStore(store, {}, () => {
SocketClient.on('pixelReturn', (args) => { SocketClient.on('pixelReturn', (args) => {
receivePixelReturn(store, getRenderer(), 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', () => { window.addEventListener('hashchange', () => {
store.dispatch(urlChange()); store.dispatch(urlChange());

View File

@ -8,13 +8,14 @@ import React, { useState } from 'react';
import { t } from 'ttag'; import { t } from 'ttag';
import Captcha from './Captcha'; import Captcha from './Captcha';
import socketClient from '../socket/SocketClient';
import { import {
requestSolveCaptcha,
requestBanMe, requestBanMe,
} from '../store/actions/fetch'; } from '../store/actions/fetch';
const GlobalCaptcha = ({ close }) => { const GlobalCaptcha = ({ close }) => {
const [errors, setErrors] = useState([]); const [error, setError] = useState(null);
const [submitting, setSubmitting] = useState(false);
const [legit, setLegit] = useState(false); const [legit, setLegit] = useState(false);
// used to be able to force Captcha rerender on error // used to be able to force Captcha rerender on error
const [captKey, setCaptKey] = useState(Date.now()); const [captKey, setCaptKey] = useState(Date.now());
@ -24,7 +25,7 @@ const GlobalCaptcha = ({ close }) => {
onSubmit={async (e) => { onSubmit={async (e) => {
e.preventDefault(); e.preventDefault();
const text = e.target.captcha.value.slice(0, 6); const text = e.target.captcha.value.slice(0, 6);
if (!text || text.length < 4) { if (submitting || !text) {
return; return;
} }
// ---- // ----
@ -36,23 +37,44 @@ const GlobalCaptcha = ({ close }) => {
} }
// ---- // ----
const captchaid = e.target.captchaid.value; const captchaid = e.target.captchaid.value;
const { errors: resErrors } = await requestSolveCaptcha( let errorText;
text, try {
captchaid, setSubmitting(true);
); const retCode = await socketClient
if (resErrors) { .sendCaptchaSolution(text, captchaid);
setCaptKey(Date.now()); console.log('Captcha return:', retCode);
setErrors(resErrors); switch (retCode) {
} else { case 0:
close(); 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) && (
<p key={error} className="errormessage"> <p key={error} className="errormessage">
<span>{t`Error`}</span>:&nbsp;{error} <span>{t`Error`}</span>:&nbsp;{error}
</p> </p>
))} )}
<Captcha autoload key={captKey} setLegit={setLegit} /> <Captcha autoload key={captKey} setLegit={setLegit} />
<p> <p>
<button <button
@ -65,7 +87,7 @@ const GlobalCaptcha = ({ close }) => {
<button <button
type="submit" type="submit"
> >
{t`Send`} {(submitting) ? '...' : t`Send`}
</button> </button>
</p> </p>
</form> </form>

View File

@ -233,8 +233,7 @@ class PixelPlainterControls {
// 5275_-8713 // 5275_-8713
// 5398_-8614 // 5398_-8614
if (x > 5275 && y > -8713 && x < 5398 && y < -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'; window.location.href = 'https://files.catbox.moe/gh2wtr.mp4';
return; return;
} }

View File

@ -35,8 +35,7 @@ function createKeyPressHandler(store) {
const canvasIds = Object.keys(canvases).filter((id) => !canvases[id].hid); const canvasIds = Object.keys(canvases).filter((id) => !canvases[id].hid);
if (num <= canvasIds.length) { if (num <= canvasIds.length) {
const canvasId = canvasIds[num - 1]; const canvasId = canvasIds[num - 1];
// eslint-disable-next-line eqeqeq if (canvasId !== curCanvasId) {
if (canvasId != curCanvasId) {
store.dispatch(selectCanvas(canvasId)); store.dispatch(selectCanvas(canvasId));
const canvasName = canvases[canvasId].title; const canvasName = canvases[canvasId].title;
store.dispatch(notify(t`Switched to ${canvasName}`)); store.dispatch(notify(t`Switched to ${canvasName}`));

View File

@ -268,7 +268,7 @@ export class ChatProvider {
const cmdArr = message.split(' '); const cmdArr = message.split(' ');
const cmd = cmdArr[0].substring(1); const cmd = cmdArr[0].substring(1);
const args = cmdArr.slice(1); const args = cmdArr.slice(1);
const initiator = `@[${escapeMd(user.getName())}](${user.id})`; const initiator = `@[${escapeMd(user.name)}](${user.id})`;
switch (cmd) { switch (cmd) {
case 'mute': { case 'mute': {
const timeMin = Number(args.slice(-1)); const timeMin = Number(args.slice(-1));
@ -398,7 +398,7 @@ export class ChatProvider {
) { ) {
const { id } = user; const { id } = user;
const { t } = user.ttag; const { t } = user.ttag;
const name = user.getName(); const { name } = user;
if (!name || !id) { if (!name || !id) {
return null; return null;

View File

@ -11,7 +11,7 @@ export const DEFAULT_SCALE = 3;
// default canvas that is first assumed, before real canvas data // default canvas that is first assumed, before real canvas data
// gets fetched via api/me // gets fetched via api/me
export const DEFAULT_CANVAS_ID = 0; export const DEFAULT_CANVAS_ID = '0';
export const DEFAULT_CANVASES = { export const DEFAULT_CANVASES = {
0: { 0: {
ident: 'd', ident: 'd',

View File

@ -160,7 +160,7 @@ export function getIdFromObject(obj, ident) {
for (let i = 0; i < ids.length; i += 1) { for (let i = 0; i < ids.length; i += 1) {
const key = ids[i]; const key = ids[i];
if (obj[key].ident === ident) { if (obj[key].ident === ident) {
return parseInt(key, 10); return key;
} }
} }
return null; return null;

View File

@ -158,6 +158,14 @@ class User {
this.setRegUser(this.regUser); this.setRegUser(this.regUser);
} }
get name() {
return (this.regUser) ? this.regUser.name : null;
}
get isRegistered() {
return !!this.id;
}
addChannel(cid, channelArray) { addChannel(cid, channelArray) {
this.channels[cid] = channelArray; this.channels[cid] = channelArray;
} }
@ -166,10 +174,6 @@ class User {
delete this.channels[cid]; delete this.channels[cid];
} }
getName() {
return (this.regUser) ? this.regUser.name : null;
}
setWait(wait, canvasId) { setWait(wait, canvasId) {
return setCoolDown(this.ipSub, this.id, canvasId, wait); return setCoolDown(this.ipSub, this.id, canvasId, wait);
} }

View File

@ -68,20 +68,14 @@ function evaluateResult(captchaText, userText) {
* set captcha solution * set captcha solution
* *
* @param text Solution of captcha * @param text Solution of captcha
* @param ip
* @param captchaid * @param captchaid
*/ */
export async function setCaptchaSolution( export async function setCaptchaSolution(
text, text,
ip, captchaid,
captchaid = null,
) { ) {
let key = `capt:${ip}`;
if (captchaid) {
key += `:${captchaid}`;
}
try { try {
await client.set(key, text, { await client.set(`capt:${captchaid}`, text, {
EX: CAPTCHA_TIMEOUT, EX: CAPTCHA_TIMEOUT,
}); });
} catch (error) { } catch (error) {
@ -105,22 +99,24 @@ export async function setCaptchaSolution(
export async function checkCaptchaSolution( export async function checkCaptchaSolution(
text, text,
ip, ip,
onetime = false, onetime,
captchaid = null, captchaid,
wrongCallback = null, wrongCallback = null,
) { ) {
const ipn = getIPv6Subnet(ip); if (!text || text.length > 10) {
let key = `capt:${ip}`; return 3;
if (captchaid) {
key += `:${captchaid}`;
} }
const solution = await client.get(key); if (!captchaid) {
return 4;
}
const solution = await client.get(`capt:${captchaid}`);
if (solution) { if (solution) {
if (evaluateResult(solution, text)) { if (evaluateResult(solution, text)) {
if (Math.random() < 0.1) { if (Math.random() < 0.1) {
return 2; return 2;
} }
if (!onetime) { if (!onetime) {
const ipn = getIPv6Subnet(ip);
const solvkey = `${PREFIX}:${ipn}`; const solvkey = `${PREFIX}:${ipn}`;
await client.set(solvkey, '', { await client.set(solvkey, '', {
EX: TTL_CACHE, EX: TTL_CACHE,

View File

@ -8,10 +8,6 @@ import { parentExists } from './core/utils';
import store from './store/storePopUp'; import store from './store/storePopUp';
import { import {
urlChange, urlChange,
receiveOnline,
receiveChatMessage,
removeChatChannel,
addChatChannel,
} from './store/actions'; } from './store/actions';
import { import {
fetchMe, fetchMe,
@ -28,45 +24,6 @@ persistStore(store, {}, () => {
store.dispatch(urlChange()); 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()) { if (!parentExists()) {
store.dispatch(fetchMe()); store.dispatch(fetchMe());
SocketClient.initialize(store); SocketClient.initialize(store);

View File

@ -119,7 +119,7 @@ async function block(req, res) {
errors: ['Could not (un)block user'], errors: ['Could not (un)block user'],
}); });
logger.info( logger.info(
`User ${user.getName()} (un)blocked ${userName}`, `User ${user.name} (un)blocked ${userName}`,
); );
} }
} }

View File

@ -26,7 +26,7 @@ async function blockdm(req, res) {
} }
logger.info( logger.info(
`User ${user.getName()} (un)blocked all dms`, `User ${user.name} (un)blocked all dms`,
); );
await user.regUser.update({ await user.regUser.update({

View File

@ -15,16 +15,6 @@ export default async (req, res) => {
try { try {
const { text, id } = req.body; 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( const ret = await checkCaptchaSolution(
text, text,
@ -46,15 +36,19 @@ export default async (req, res) => {
break; break;
case 2: case 2:
res.status(422) res.status(422)
.json({ .json({ errors: [t`You failed your captcha`] });
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; break;
default: default:
res.status(422) res.status(422)
.json({ .json({ errors: [t`Unknown Captcha Error`] });
errors: [t`Unknown Captcha Error`],
});
} }
} catch (error) { } catch (error) {
logger.error('CAPTCHA', error); logger.error('CAPTCHA', error);

View File

@ -57,7 +57,7 @@ async function leaveChan(req, res) {
} }
logger.info( logger.info(
`Removing user ${user.getName()} from channel ${channel.name || channelId}`, `Removing user ${user.name} from channel ${channel.name || channelId}`,
); );
user.regUser.removeChannel(channel); user.regUser.removeChannel(channel);

View File

@ -25,7 +25,7 @@ async function privatize(req, res) {
} }
logger.info( logger.info(
`User ${user.getName()} set private status to ${priv}`, `User ${user.name} set private status to ${priv}`,
); );
await user.regUser.update({ await user.regUser.update({

View File

@ -108,7 +108,7 @@ async function startDm(req, res) {
ChatProvider.addUserToChannel( ChatProvider.addUserToChannel(
userId, userId,
ChannelId, ChannelId,
[user.getName(), 1, curTime, user.id], [user.name, 1, curTime, user.id],
), ),
]; ];
await Promise.all(promises); await Promise.all(promises);

View File

@ -27,7 +27,7 @@ export default (req, res) => {
} }
const ip = getIPFromRequest(req); const ip = getIPFromRequest(req);
setCaptchaSolution(text, ip, id); setCaptchaSolution(text, id);
logger.info(`Captchas: ${ip} got captcha with text: ${text}`); logger.info(`Captchas: ${ip} got captcha with text: ${text}`);
res.set({ res.set({

View File

@ -10,6 +10,7 @@
import WebSocket from 'ws'; import WebSocket from 'ws';
import socketEvents from './socketEvents'; import socketEvents from './socketEvents';
import { dehydrateOnlineCounter } from './packets/server';
import chatProvider, { ChatProvider } from '../core/ChatProvider'; import chatProvider, { ChatProvider } from '../core/ChatProvider';
import { RegUser } from '../data/sql'; import { RegUser } from '../data/sql';
import { getIPFromRequest } from '../utils/ip'; import { getIPFromRequest } from '../utils/ip';
@ -55,7 +56,6 @@ class APISocketServer {
}); });
}); });
this.broadcast = this.broadcast.bind(this);
this.broadcastOnlineCounter = this.broadcastOnlineCounter.bind(this); this.broadcastOnlineCounter = this.broadcastOnlineCounter.bind(this);
this.broadcastPixelBuffer = this.broadcastPixelBuffer.bind(this); this.broadcastPixelBuffer = this.broadcastPixelBuffer.bind(this);
this.ping = this.ping.bind(this); this.ping = this.ping.bind(this);
@ -155,8 +155,9 @@ class APISocketServer {
} }
} }
broadcastOnlineCounter(data) { broadcastOnlineCounter(online) {
this.broadcast(data, (client) => client.subOnline); const buffer = dehydrateOnlineCounter(online);
this.broadcast(buffer, (client) => client.subOnline);
} }
broadcastPixelBuffer(canvasId, chunkid, buffer) { broadcastPixelBuffer(canvasId, chunkid, buffer) {

View File

@ -371,11 +371,10 @@ class MessageBroker extends SocketEvents {
broadcastOnlineCounter(online) { broadcastOnlineCounter(online) {
this.updateShardOnlineCounter(this.thisShard, online); this.updateShardOnlineCounter(this.thisShard, online);
// send our online counter to other shards // send our online counter to other shards
let buffer = dehydrateOnlineCounter(online); const buffer = dehydrateOnlineCounter(online);
this.publisher.publish(this.thisShard, buffer); this.publisher.publish(this.thisShard, buffer);
// send total counter to our players // send total counter to our players
buffer = dehydrateOnlineCounter(this.onlineCounter); super.emit('onlineCounter', this.onlineCounter);
super.emit('onlineCounter', buffer);
} }
checkHealth() { checkHealth() {

View File

@ -4,7 +4,6 @@
import EventEmitter from 'events'; import EventEmitter from 'events';
import { import {
dehydrateOnlineCounter,
dehydratePixelUpdate, dehydratePixelUpdate,
} from './packets/server'; } from './packets/server';
@ -255,8 +254,7 @@ class SocketEvents extends EventEmitter {
*/ */
broadcastOnlineCounter(online) { broadcastOnlineCounter(online) {
this.onlineCounter = online; this.onlineCounter = online;
const buffer = dehydrateOnlineCounter(online); this.emit('onlineCounter', online);
this.emit('onlineCounter', buffer);
} }
} }

View File

@ -8,11 +8,11 @@ import {
hydratePixelReturn, hydratePixelReturn,
hydrateOnlineCounter, hydrateOnlineCounter,
hydrateCoolDown, hydrateCoolDown,
hydrateCaptchaReturn,
dehydrateRegCanvas, dehydrateRegCanvas,
dehydrateRegChunk, dehydrateRegChunk,
dehydrateRegMChunks, dehydrateRegMChunks,
dehydrateDeRegChunk, dehydrateDeRegChunk,
dehydrateCaptchaSolution,
dehydratePixelUpdate, dehydratePixelUpdate,
dehydratePing, dehydratePing,
} from './packets/client'; } from './packets/client';
@ -22,7 +22,20 @@ import {
ONLINE_COUNTER_OP, ONLINE_COUNTER_OP,
COOLDOWN_OP, COOLDOWN_OP,
CHANGE_ME_OP, CHANGE_ME_OP,
CAPTCHA_RETURN_OP,
} from './packets/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'; import { shardHost } from '../store/actions/fetch';
const chunks = []; const chunks = [];
@ -42,6 +55,7 @@ class SocketClient extends EventEmitter {
*/ */
this.readyState = WebSocket.CLOSED; this.readyState = WebSocket.CLOSED;
this.msgQueue = []; this.msgQueue = [];
this.reqQueue = [];
this.checkHealth = this.checkHealth.bind(this); this.checkHealth = this.checkHealth.bind(this);
setInterval(this.checkHealth, 2000); setInterval(this.checkHealth, 2000);
@ -122,10 +136,10 @@ class SocketClient extends EventEmitter {
this.timeLastPing = now; this.timeLastPing = now;
this.timeLastSent = now; this.timeLastSent = now;
this.emit('open', {}); this.store.dispatch(socketOpen());
this.readyState = WebSocket.OPEN; this.readyState = WebSocket.OPEN;
this.send(dehydrateRegCanvas( this.send(dehydrateRegCanvas(
this.store.getState().canvas, this.store.getState().canvas.canvasId,
)); ));
console.log(`Register ${chunks.length} chunks`); console.log(`Register ${chunks.length} chunks`);
this.send(dehydrateRegMChunks(chunks)); this.send(dehydrateRegMChunks(chunks));
@ -136,7 +150,9 @@ class SocketClient extends EventEmitter {
if (canvasId === null) { if (canvasId === null) {
return; return;
} }
console.log('Notify websocket server that we changed canvas'); console.log(
`Notify websocket server that we changed canvas to ${canvasId}`,
);
chunks.length = 0; chunks.length = 0;
this.send(dehydrateRegCanvas(canvasId)); this.send(dehydrateRegCanvas(canvasId));
} }
@ -163,10 +179,28 @@ class SocketClient extends EventEmitter {
} }
/* /*
sendCaptchaSolution(solution) { * send captcha solution
const buffer = dehydrateCaptchaSolution(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 * Send pixel request
@ -182,7 +216,9 @@ class SocketClient extends EventEmitter {
} }
sendChatMessage(message, channelId) { sendChatMessage(message, channelId) {
this.sendWhenReady(JSON.stringify([message, channelId])); this.sendWhenReady(
`cm,${JSON.stringify([message, channelId])}`,
);
} }
onMessage({ data: message }) { onMessage({ data: message }) {
@ -201,27 +237,24 @@ class SocketClient extends EventEmitter {
} }
onTextMessage(message) { onTextMessage(message) {
if (!message) return; const comma = message.indexOf(',');
const data = JSON.parse(message); if (comma === -1) {
return;
if (Array.isArray(data)) { }
switch (data.length) { const key = message.slice(0, comma);
case 5: { const val = JSON.parse(message.slice(comma + 1));
// chat message switch (key) {
const [name, text, country, channelId, userId] = data; case 'cm':
this.emit('chatMessage', this.store.dispatch(receiveChatMessage(...val));
name, text, country, Number(channelId), userId); break;
return; case 'ac':
} this.store.dispatch(addChatChannel(val));
case 2: { break;
// signal case 'rc':
const [signal, args] = data; this.store.dispatch(removeChatChannel(val));
this.emit(signal, args); break;
break; default:
} // nothing
default:
// nothing
}
} }
} }
@ -240,16 +273,23 @@ class SocketClient extends EventEmitter {
this.emit('pixelReturn', hydratePixelReturn(data)); this.emit('pixelReturn', hydratePixelReturn(data));
break; break;
case ONLINE_COUNTER_OP: case ONLINE_COUNTER_OP:
this.emit('onlineCounter', hydrateOnlineCounter(data)); this.store.dispatch(receiveOnline(hydrateOnlineCounter(data)));
break; break;
case COOLDOWN_OP: case COOLDOWN_OP:
this.emit('cooldownPacket', hydrateCoolDown(data)); this.store.dispatch(receiveCoolDown(hydrateCoolDown(data)));
break; break;
case CHANGE_ME_OP: case CHANGE_ME_OP:
console.log('Websocket requested api/me reload'); console.log('Websocket requested api/me reload');
this.emit('changedMe'); this.store.dispatch(fetchMe());
this.reconnect(); this.reconnect();
break; 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: default:
console.error(`Unknown op_code ${opcode} received`); console.error(`Unknown op_code ${opcode} received`);
break; break;
@ -257,7 +297,7 @@ class SocketClient extends EventEmitter {
} }
onClose(e) { onClose(e) {
this.emit('close'); this.store.dispatch(socketClose());
this.ws = null; this.ws = null;
this.readyState = WebSocket.CONNECTING; this.readyState = WebSocket.CONNECTING;
// reconnect in 1s if last connect was longer than 7s ago, else 5s // reconnect in 1s if last connect was longer than 7s ago, else 5s

View File

@ -21,19 +21,19 @@ import {
hydrateDeRegChunk, hydrateDeRegChunk,
hydrateRegMChunks, hydrateRegMChunks,
hydrateDeRegMChunks, hydrateDeRegMChunks,
hydrateCaptchaSolution,
hydratePixelUpdate, hydratePixelUpdate,
dehydrateChangeMe, dehydrateChangeMe,
dehydrateOnlineCounter, dehydrateOnlineCounter,
dehydratePixelUpdate,
dehydrateCoolDown, dehydrateCoolDown,
dehydratePixelReturn, dehydratePixelReturn,
dehydrateCaptchaReturn,
} from './packets/server'; } from './packets/server';
import socketEvents from './socketEvents'; import socketEvents from './socketEvents';
import chatProvider, { ChatProvider } from '../core/ChatProvider'; import chatProvider, { ChatProvider } from '../core/ChatProvider';
import authenticateClient from './authenticateClient'; import authenticateClient from './authenticateClient';
import drawByOffsets from '../core/draw'; import drawByOffsets from '../core/draw';
import isIPAllowed from '../core/isAllowed'; import isIPAllowed from '../core/isAllowed';
import { checkCaptchaSolution } from '../data/redis/captcha';
const ipCounter = new Counter(); const ipCounter = new Counter();
@ -64,7 +64,6 @@ class SocketServer {
constructor() { constructor() {
this.CHUNK_CLIENTS = new Map(); this.CHUNK_CLIENTS = new Map();
this.broadcast = this.broadcast.bind(this);
this.broadcastPixelBuffer = this.broadcastPixelBuffer.bind(this); this.broadcastPixelBuffer = this.broadcastPixelBuffer.bind(this);
this.reloadUser = this.reloadUser.bind(this); this.reloadUser = this.reloadUser.bind(this);
this.onlineCounterBroadcast = this.onlineCounterBroadcast.bind(this); this.onlineCounterBroadcast = this.onlineCounterBroadcast.bind(this);
@ -93,7 +92,6 @@ class SocketServer {
ws.canvasId = null; ws.canvasId = null;
const { user } = req; const { user } = req;
ws.user = user; ws.user = user;
ws.name = user.getName();
ws.chunkCnt = 0; ws.chunkCnt = 0;
const { ip } = user; const { ip } = user;
@ -101,7 +99,8 @@ class SocketServer {
ws.send(dehydrateOnlineCounter(socketEvents.onlineCounter)); ws.send(dehydrateOnlineCounter(socketEvents.onlineCounter));
ws.on('error', (e) => { 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', () => { 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('pixelUpdate', this.broadcastPixelBuffer);
socketEvents.on('reloadUser', this.reloadUser); socketEvents.on('reloadUser', this.reloadUser);
@ -132,8 +134,10 @@ class SocketServer {
id, id,
country, country,
) => { ) => {
const text = `cm,${JSON.stringify(
[name, message, country, channelId, id],
)}`;
this.findAllWsByUerId(userId).forEach((ws) => { this.findAllWsByUerId(userId).forEach((ws) => {
const text = JSON.stringify([name, message, country, channelId, id]);
ws.send(text); ws.send(text);
}); });
}); });
@ -145,7 +149,9 @@ class SocketServer {
id, id,
country, country,
) => { ) => {
const text = JSON.stringify([name, message, country, channelId, id]); const text = `cm,${JSON.stringify(
[name, message, country, channelId, id],
)}`;
const clientArray = []; const clientArray = [];
this.wss.clients.forEach((ws) => { this.wss.clients.forEach((ws) => {
if (ws.user && chatProvider.userHasChannelAccess(ws.user, channelId)) { if (ws.user && chatProvider.userHasChannelAccess(ws.user, channelId)) {
@ -158,11 +164,9 @@ class SocketServer {
socketEvents.on('addChatChannel', (userId, channelId, channelArray) => { socketEvents.on('addChatChannel', (userId, channelId, channelArray) => {
this.findAllWsByUerId(userId).forEach((ws) => { this.findAllWsByUerId(userId).forEach((ws) => {
ws.user.addChannel(channelId, channelArray); ws.user.addChannel(channelId, channelArray);
const text = JSON.stringify([ const text = `ac,${JSON.stringify({
'addch', { [channelId]: channelArray,
[channelId]: channelArray, })}`;
},
]);
ws.send(text); ws.send(text);
}); });
}); });
@ -170,7 +174,7 @@ class SocketServer {
socketEvents.on('remChatChannel', (userId, channelId) => { socketEvents.on('remChatChannel', (userId, channelId) => {
this.findAllWsByUerId(userId).forEach((ws) => { this.findAllWsByUerId(userId).forEach((ws) => {
ws.user.removeChannel(channelId); ws.user.removeChannel(channelId);
const text = JSON.stringify(['remch', channelId]); const text = `rc,${JSON.stringify(channelId)}`;
ws.send(text); ws.send(text);
}); });
}); });
@ -348,9 +352,8 @@ class SocketServer {
reloadUser(name) { reloadUser(name) {
this.wss.clients.forEach(async (ws) => { this.wss.clients.forEach(async (ws) => {
if (ws.name === name) { if (ws.user.name === name) {
await ws.user.reload(); await ws.user.reload();
ws.name = ws.user.getName();
const buffer = dehydrateChangeMe(); const buffer = dehydrateChangeMe();
ws.send(buffer); ws.send(buffer);
} }
@ -419,64 +422,66 @@ class SocketServer {
* chat messages in [message, channelId] format * chat messages in [message, channelId] format
*/ */
try { try {
let message; const comma = text.indexOf(',');
let channelId; if (comma === -1) {
try { throw new Error('No comma');
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;
} }
message = message.trim(); const key = text.slice(0, comma);
const val = JSON.parse(text.slice(comma + 1));
/* const { user } = ws;
* just if logged in switch (key) {
*/ case 'cm': {
if (ws.name && message) { // chat message
const { user } = ws; const message = val[0].trim();
/* if (!user.isRegistered || !message) {
* if DM channel, make sure that other user has DM open return;
* (needed because we allow user to leave one-sided }
* and auto-join on message) const channelId = val[1];
*/ /*
const dmUserId = chatProvider.checkIfDm(user, channelId); * if DM channel, make sure that other user has DM open
if (dmUserId) { * (needed because we allow user to leave one-sided
const dmWs = this.findWsByUserId(dmUserId); * and auto-join on message)
if (!dmWs */
|| !chatProvider.userHasChannelAccess(dmWs.user, channelId) const dmUserId = chatProvider.checkIfDm(user, channelId);
) { if (dmUserId) {
// TODO this is really ugly const dmWs = this.findWsByUserId(dmUserId);
// DMS have to be rethought if (!dmWs
if (!user.addedDM) user.addedDM = []; || !chatProvider.userHasChannelAccess(dmWs.user, channelId)
if (!user.addedDM.includes(dmUserId)) { ) {
await ChatProvider.addUserToChannel( // TODO this is really ugly
dmUserId, // DMS have to be rethought
channelId, if (!user.addedDM) user.addedDM = [];
[ws.name, 1, Date.now(), user.id], if (!user.addedDM.includes(dmUserId)) {
); await ChatProvider.addUserToChannel(
user.addedDM.push(dmUserId); dmUserId,
channelId,
[user.name, 1, Date.now(), user.id],
);
user.addedDM.push(dmUserId);
}
} }
} }
socketEvents.recvChatMessage(user, message, channelId);
break;
} }
case 'cs': {
/* // captcha solution
* send chat message const [solution, captchaid] = val;
*/ const ret = await checkCaptchaSolution(
socketEvents.recvChatMessage(user, message, channelId); solution,
} else { user.ip,
logger.info('Got empty message or message from unidentified ws'); false,
captchaid,
);
ws.send(dehydrateCaptchaReturn(ret));
break;
}
default:
throw new Error('Unknown key');
} }
} catch (error) { } catch (err) {
logger.error('Got invalid ws text message'); // eslint-disable-next-line max-len
logger.error(error.message); logger.error(`Got invalid ws text message ${text}, with error: ${err.message}`);
logger.error(error.stack);
} }
} }

View File

@ -7,7 +7,6 @@ import {
DEREG_CHUNK_OP, DEREG_CHUNK_OP,
REG_MCHUNKS_OP, REG_MCHUNKS_OP,
DEREG_MCHUNKS_OP, DEREG_MCHUNKS_OP,
CAPTCHA_SOLUTION_OP,
PING_OP, PING_OP,
PIXEL_UPDATE_OP, PIXEL_UPDATE_OP,
} from './op'; } from './op';
@ -70,7 +69,6 @@ export function hydrateCoolDown(data) {
* @return see ui/placePixels * @return see ui/placePixels
*/ */
export function hydratePixelReturn(data) { export function hydratePixelReturn(data) {
// Client (receiver)
const retCode = data.getUint8(1); const retCode = data.getUint8(1);
const wait = data.getUint32(2); const wait = data.getUint32(2);
const coolDownSeconds = data.getInt16(6); 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 * dehydrate functions return ArrayBuffer object
*/ */
@ -152,18 +157,6 @@ export function dehydrateDeRegMChunks(chunks) {
return buffer; 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() { export function dehydratePing() {
return new Uint8Array([PING_OP]).buffer; return new Uint8Array([PING_OP]).buffer;
} }

View File

@ -10,12 +10,12 @@ export const REG_CHUNK_OP = 0xA1;
export const DEREG_CHUNK_OP = 0xA2; export const DEREG_CHUNK_OP = 0xA2;
export const REG_MCHUNKS_OP = 0xA3; export const REG_MCHUNKS_OP = 0xA3;
export const DEREG_MCHUNKS_OP = 0xA4; export const DEREG_MCHUNKS_OP = 0xA4;
export const CAPTCHA_SOLUTION_OP = 0xA5;
export const CHANGE_ME_OP = 0xA6; export const CHANGE_ME_OP = 0xA6;
export const ONLINE_COUNTER_OP = 0xA7; export const ONLINE_COUNTER_OP = 0xA7;
export const PING_OP = 0xB0; export const PING_OP = 0xB0;
export const PIXEL_UPDATE_OP = 0xC1; export const PIXEL_UPDATE_OP = 0xC1;
export const PIXEL_UPDATE_MB_OP = 0xC1;
export const COOLDOWN_OP = 0xC2; export const COOLDOWN_OP = 0xC2;
export const PIXEL_RETURN_OP = 0xC3; export const PIXEL_RETURN_OP = 0xC3;
export const CHUNK_UPDATE_MB_OP = 0xC4; export const CHUNK_UPDATE_MB_OP = 0xC4;
export const PIXEL_UPDATE_MB_OP = 0xC5;
export const CAPTCHA_RETURN_OP = 0xC6;

View File

@ -9,6 +9,7 @@ import {
COOLDOWN_OP, COOLDOWN_OP,
PIXEL_RETURN_OP, PIXEL_RETURN_OP,
CHUNK_UPDATE_MB_OP, CHUNK_UPDATE_MB_OP,
CAPTCHA_RETURN_OP,
} from './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 * @return chunk id and array of pixel offset and color
*/ */
@ -225,6 +219,10 @@ export function dehydratePixelReturn(
return buffer; return buffer;
} }
export function dehydrateCaptchaReturn(retCode) {
return Buffer.from([CAPTCHA_RETURN_OP, retCode]);
}
/* /*
* @param canvasId * @param canvasId
* @param Array with chunk coordinates * @param Array with chunk coordinates

View File

@ -250,15 +250,6 @@ export function receiveBigChunkFailure(center, error) {
}; };
} }
export function receiveCoolDown(
wait,
) {
return {
type: 'REC_COOLDOWN',
wait,
};
}
export function receiveMe( export function receiveMe(
me, me,
) { ) {
@ -296,13 +287,6 @@ export function receiveStats(
}; };
} }
export function receiveOnline(online) {
return {
type: 'REC_ONLINE',
online,
};
}
export function sendChatMessage( export function sendChatMessage(
text, text,
channel, 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 * 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) { export function blockUser(userId, userName) {
return { return {
type: 's/BLOCK_USER', 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) { export function muteChatChannel(cid) {
return { return {
type: 's/MUTE_CHAT_CHANNEL', type: 's/MUTE_CHAT_CHANNEL',

View File

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

View File

@ -11,9 +11,7 @@ import {
requestChatMessages, requestChatMessages,
requestMe, requestMe,
} from './fetch'; } from './fetch';
import { import {
addChatChannel,
pAlert, pAlert,
receiveStats, receiveStats,
receiveMe, receiveMe,
@ -21,8 +19,11 @@ import {
unblockUser, unblockUser,
blockingDm, blockingDm,
privatize, privatize,
removeChatChannel,
} from './index'; } from './index';
import {
addChatChannel,
removeChatChannel,
} from './socket';
function setApiFetching(fetching) { function setApiFetching(fetching) {
return { return {

View File

@ -48,10 +48,7 @@ export default (store) => (next) => (action) => {
const ret = next(action); const ret = next(action);
const state = store.getState(); const state = store.getState();
const { canvasId } = state.canvas; const { canvasId } = state.canvas;
if (prevState.canvas.canvasId === canvasId) { if (prevState.canvas.canvasId !== canvasId) {
// TODO see if this is the case anywhere
console.log('Not triggering change canvas');
} else {
SocketClient.setCanvas(canvasId); SocketClient.setCanvas(canvasId);
} }
return ret; return ret;

View File

@ -17,7 +17,7 @@ import {
/* /*
export type CanvasState = { export type CanvasState = {
canvasId: number, canvasId: string,
canvasIdent: string, canvasIdent: string,
selectedColor: number, selectedColor: number,
is3D: boolean, is3D: boolean,