add websocket messages or chat joining and leaving

This commit is contained in:
HF 2020-11-27 23:48:59 +01:00
parent 8f24a34a1d
commit 46ba5188b5
19 changed files with 403 additions and 142 deletions

View File

@ -369,7 +369,6 @@ export function move([dx, dy]: Cell): ThunkAction {
}
export function moveDirection([vx, vy]: Cell): ThunkAction {
// TODO check direction is unitary vector
return (dispatch, getState) => {
const { viewscale } = getState().canvas;
@ -768,7 +767,7 @@ export function setChatChannel(cid: number): Action {
};
}
export function addChatChannel(channel: Array): Action {
export function addChatChannel(channel: Object): Action {
return {
type: 'ADD_CHAT_CHANNEL',
channel,

View File

@ -70,7 +70,7 @@ export type Action =
}
| { type: 'RECEIVE_CHAT_HISTORY', cid: number, history: Array }
| { type: 'SET_CHAT_CHANNEL', cid: number }
| { type: 'ADD_CHAT_CHANNEL', channel: Array }
| { type: 'ADD_CHAT_CHANNEL', channel: Object }
| { type: 'REMOVE_CHAT_CHANNEL', cid: number }
| { type: 'SET_CHAT_FETCHING', fetching: boolean }
| { type: 'SET_CHAT_INPUT_MSG', message: string }

View File

@ -17,6 +17,8 @@ import {
receiveCoolDown,
receiveChatMessage,
receivePixelReturn,
addChatChannel,
removeChatChannel,
setMobile,
tryPlacePixel,
} from './actions';
@ -31,7 +33,6 @@ import ProtocolClient from './socket/ProtocolClient';
function init() {
initRenderer(store, false);
let nameRegExp = null;
ProtocolClient.on('pixelUpdate', ({
i, j, offset, color,
}) => {
@ -49,9 +50,6 @@ function init() {
ProtocolClient.on('onlineCounter', ({ online }) => {
store.dispatch(receiveOnline(online));
});
ProtocolClient.on('setWsName', (name) => {
nameRegExp = new RegExp(`(^|\\s+)(@${name})(\\s+|$)`, 'g');
});
ProtocolClient.on('chatMessage', (
name,
text,
@ -59,6 +57,8 @@ function init() {
channelId,
userId,
) => {
const state = store.getState();
const { nameRegExp } = state.user;
const isPing = (nameRegExp && text.match(nameRegExp));
store.dispatch(receiveChatMessage(
name,
@ -72,6 +72,12 @@ function init() {
ProtocolClient.on('changedMe', () => {
store.dispatch(fetchMe());
});
ProtocolClient.on('remch', (cid) => {
store.dispatch(removeChatChannel(cid));
});
ProtocolClient.on('addch', (channel) => {
store.dispatch(addChatChannel(channel));
});
window.addEventListener('hashchange', () => {
store.dispatch(urlChange());

View File

@ -3,8 +3,9 @@ import logger from './logger';
import redis from '../data/redis';
import User from '../data/models/User';
import webSockets from '../socket/websockets';
import { Channel, RegUser } from '../data/models';
import { Channel, RegUser, UserChannel } from '../data/models';
import ChatMessageBuffer from './ChatMessageBuffer';
import { cheapDetector } from './isProxy';
import { CHAT_CHANNELS, EVENT_USER_NAME, INFO_USER_NAME } from './constants';
@ -100,6 +101,34 @@ export class ChatProvider {
this.eventUserId = eventUser[0].id;
}
static async addUserToChannel(
userId,
channelId,
channelArray,
notify = true,
) {
/*
* since UserId and ChannelId are primary keys,
* this will throw if already exists
*/
const relation = await UserChannel.create({
UserId: userId,
ChannelId: channelId,
}, {
raw: true,
});
console.log('HEREEEEE HHEEERRREEE');
console.log(relation);
webSockets.broadcastAddChatChannel(
userId,
channelId,
channelArray,
notify,
);
}
userHasChannelAccess(user, cid, write = false) {
if (this.defaultChannels[cid]) {
if (!write || user.regUser) {
@ -111,18 +140,110 @@ export class ChatProvider {
return false;
}
checkIfDm(user, cid) {
if (this.defaultChannels[cid]) {
return null;
}
const channelArray = user.channels[cid];
if (channelArray && channelArray.length === 4) {
return user.channels[cid][4];
}
return null;
}
getHistory(cid, limit = 30) {
return this.chatMessageBuffer.getMessages(cid, limit);
}
adminCommands(message: string, channelId: number) {
// admin commands
const cmdArr = message.split(' ');
const cmd = cmdArr[0].substr(1);
const args = cmdArr.slice(1);
switch (cmd) {
case 'mute': {
const timeMin = Number(args.slice(-1));
if (Number.isNaN(timeMin)) {
return this.mute(args.join(' '), channelId);
}
return this.mute(
args.slice(0, -1).join(' '),
channelId,
timeMin,
);
}
case 'unmute':
return this.unmute(args.join(' '), channelId);
case 'mutec': {
if (args[0]) {
const cc = args[0].toLowerCase();
this.mutedCountries.push(cc);
this.broadcastChatMessage(
'info',
`Country ${cc} has been muted`,
channelId,
this.infoUserId,
);
return null;
}
return 'No country defined for mutec';
}
case 'unmutec': {
if (args[0]) {
const cc = args[0].toLowerCase();
if (!this.mutedCountries.includes(cc)) {
return `Country ${cc} is not muted`;
}
this.mutedCountries = this.mutedCountries.filter((c) => c !== cc);
this.broadcastChatMessage(
'info',
`Country ${cc} has been unmuted`,
channelId,
this.infoUserId,
);
return null;
}
if (this.mutedCountries.length) {
this.broadcastChatMessage(
'info',
`Countries ${this.mutedCountries} have been unmuted`,
channelId,
this.infoUserId,
);
this.mutedCountries = [];
return null;
}
return 'No country is currently muted';
}
default:
return `Couln't parse command ${cmd}`;
}
}
async sendMessage(user, message, channelId: number = 0) {
const { id } = user;
const name = user.getName();
if (!user.isAdmin() && await cheapDetector(user.ip)) {
logger.info(
`${name} / ${user.ip} tried to send chat message with proxy`,
);
return 'You can not send chat messages with proxy';
}
if (!name || !id) {
// eslint-disable-next-line max-len
return 'Couldn\'t send your message, pls log out and back in again.';
}
if (user.isAdmin() && message.charAt(0) === '/') {
return this.adminCommands(message, channelId);
}
if (!this.userHasChannelAccess(user, channelId)) {
return 'You don\'t have access to this channel';
}
@ -181,49 +302,6 @@ export class ChatProvider {
return 'You can\'t send a message this long :(';
}
if (user.isAdmin() && message.charAt(0) === '/') {
// admin commands
const cmdArr = message.split(' ');
const cmd = cmdArr[0].substr(1);
const args = cmdArr.slice(1);
if (cmd === 'mute') {
const timeMin = Number(args.slice(-1));
if (Number.isNaN(timeMin)) {
return this.mute(args.join(' '), channelId);
}
return this.mute(
args.slice(0, -1).join(' '),
channelId,
timeMin,
);
} if (cmd === 'unmute') {
return this.unmute(args.join(' '), channelId);
} if (cmd === 'mutec' && args[0]) {
const cc = args[0].toLowerCase();
this.mutedCountries.push(cc);
this.broadcastChatMessage(
'info',
`Country ${cc} has been muted`,
channelId,
this.infoUserId,
);
return null;
} if (cmd === 'unmutec' && args[0]) {
const cc = args[0].toLowerCase();
if (!this.mutedCountries.includes(cc)) {
return `Country ${cc} is not muted`;
}
this.mutedCountries = this.mutedCountries.filter((c) => c !== cc);
this.broadcastChatMessage(
'info',
`Country ${cc} has been unmuted`,
channelId,
this.infoUserId,
);
return null;
}
}
if (message.match(this.cyrillic) && channelId === this.enChannelId) {
return 'Please use int channel';
}
@ -232,6 +310,21 @@ export class ChatProvider {
return 'Your country is temporary muted from chat';
}
if (user.last_message && user.last_message === message) {
user.message_repeat += 1;
if (user.message_repeat >= 4) {
this.mute(name, channelId, 60);
user.message_repeat = 0;
return 'Stop flooding.';
}
} else {
user.message_repeat = 0;
user.last_message = message;
}
logger.info(
`Received chat message ${message} from ${name} / ${user.ip}`,
);
this.broadcastChatMessage(
name,
message,
@ -270,16 +363,6 @@ export class ChatProvider {
);
}
automute(name, channelId) {
this.mute(name, channelId, 60);
this.broadcastChatMessage(
'info',
`${name} has been muted for spam for 60min`,
channelId,
this.infoUserId,
);
}
static async checkIfMuted(user) {
const key = `mute:${user.id}`;
const ttl: number = await redis.ttlAsync(key);
@ -296,14 +379,12 @@ export class ChatProvider {
if (timeMin) {
const ttl = timeMin * 60;
await redis.setAsync(key, '', 'EX', ttl);
if (timeMin !== 600 && timeMin !== 60) {
this.broadcastChatMessage(
'info',
`${name} has been muted for ${timeMin}min`,
channelId,
this.infoUserId,
);
}
this.broadcastChatMessage(
'info',
`${name} has been muted for ${timeMin}min`,
channelId,
this.infoUserId,
);
} else {
await redis.setAsync(key, '');
this.broadcastChatMessage(

View File

@ -260,3 +260,13 @@ export function setBrightness(hex, dark: boolean = false) {
}
return `#${r.toString(16)}${g.toString(16)}${b.toString(16)}`;
}
/*
* create RegExp to search for ping in chat messages
* @param name name
* @return regular expression to search for name in message
*/
export function createNameRegExp(name: string) {
if (!name) return null;
return new RegExp(`(^|\\s+)(@${name})(\\s+|$)`, 'g');
}

View File

@ -64,17 +64,28 @@ class User {
dmu1,
dmu2,
} = reguser.channel[i];
// in DMs the name is the name of the other user
let { name } = reguser.channel[i];
if (type === 1) {
name = (dmu1.id === this.id) ? dmu2.name : dmu1.name;
}
this.channelIds.push(id);
this.channels[id] = [
name,
type,
lastTs,
];
if (type === 1) {
/* in DMs:
* the name is the name of the other user
* id also gets grabbed
*/
const name = (dmu1.id === this.id) ? dmu2.name : dmu1.name;
const dmu = (dmu1.id === this.id) ? dmu2.id : dmu1.id;
this.channels[id] = [
name,
type,
lastTs,
dmu,
];
} else {
const { name } = reguser.channel[i];
this.channels[id] = [
name,
type,
lastTs,
];
}
}
}
if (reguser.blocked) {

View File

@ -22,7 +22,6 @@ export default function audio(
case 'TOGGLE_MUTE':
return {
...state,
// TODO error prone
mute: !state.mute,
};

View File

@ -17,6 +17,7 @@ export type ChatState = {
* name,
* type,
* lastTs,
* dmUserId,
* ],
* ...
* }
@ -50,12 +51,29 @@ export default function chat(
case 'BLOCK_USER': {
const { userId, userName } = action;
const blocked = [
...state.blocked,
[userId, userName],
];
/*
* remove DM channel if exists
*/
const channels = { ...state.channels };
const chanKeys = Object.keys(channels);
for (let i = 0; i < chanKeys; i += 1) {
const cid = chanKeys[i];
if (channels[cid][1] === 1 && channels[cid][3] === userId) {
delete channels[cid];
return {
...state,
channels,
blocked,
};
}
}
return {
...state,
blocked: [
...state.blocked,
[userId, userName],
],
blocked,
};
}

View File

@ -117,7 +117,7 @@ export default function gui(
chatRead: {
...state.chatRead,
cid: Date.now(),
}
},
};
}
@ -148,7 +148,7 @@ export default function gui(
case 'RECEIVE_ME': {
const { channels } = action;
const cids = Object.keys(channels);
const chatRead = {...state.chatRead};
const chatRead = { ...state.chatRead };
for (let i = 0; i < cids.length; i += 1) {
const cid = cids[i];
chatRead[cid] = 0;

View File

@ -2,6 +2,9 @@
import type { Action } from '../actions/types';
import { createNameRegExp } from '../core/utils';
export type UserState = {
name: string,
@ -32,6 +35,8 @@ export type UserState = {
notification: string,
// 1: Admin, 0: ordinary user
userlvl: number,
// regExp for detecting ping
nameRegExp: RegExp,
};
const initialState: UserState = {
@ -51,6 +56,7 @@ const initialState: UserState = {
isOnMobile: false,
notification: null,
userlvl: 0,
nameRegExp: null,
};
export default function user(
@ -145,6 +151,7 @@ export default function user(
blockDm,
userlvl,
} = action;
const nameRegExp = createNameRegExp(name);
const messages = (action.messages) ? action.messages : [];
return {
...state,
@ -158,6 +165,7 @@ export default function user(
minecraftname,
blockDm,
userlvl,
nameRegExp,
};
}
@ -172,9 +180,11 @@ export default function user(
case 'SET_NAME': {
const { name } = action;
const nameRegExp = createNameRegExp(name);
return {
...state,
name,
nameRegExp,
};
}

View File

@ -8,6 +8,7 @@
import type { Request, Response } from 'express';
import logger from '../../core/logger';
import webSockets from '../../socket/websockets';
import { RegUser, UserBlock, Channel } from '../../data/models';
async function block(req: Request, res: Response) {
@ -108,10 +109,10 @@ async function block(req: Request, res: Response) {
if (channel) {
const channelId = channel.id;
channel.destroy();
webSockets.broadcastRemoveChatChannel(user.id, channelId, false);
webSockets.broadcastRemoveChatChannel(userId, channelId, true);
}
// TODO notify websocket
if (ret) {
res.json({
status: 'ok',

View File

@ -8,6 +8,7 @@
import type { Request, Response } from 'express';
import logger from '../../core/logger';
import webSockets from '../../socket/websockets';
async function blockdm(req: Request, res: Response) {
const { block } = req.body;
@ -36,7 +37,20 @@ async function blockdm(req: Request, res: Response) {
blockDm: block,
});
// TODO notify websocket
/*
* remove all dm channels
*/
const channels = user.regUser.channel;
for (let i = 0; i < channels.length; i += 1) {
const channel = channels[i];
if (channel.type === 1) {
const channelId = channel.id;
channel.destroy();
const { dmu1id, dmu2id } = channel;
webSockets.broadcastRemoveChatChannel(dmu1id, channelId, true);
webSockets.broadcastRemoveChatChannel(dmu2id, channelId, true);
}
}
res.json({
status: 'ok',

View File

@ -8,6 +8,7 @@
import type { Request, Response } from 'express';
import logger from '../../core/logger';
import webSockets from '../../socket/websockets';
async function leaveChan(req: Request, res: Response) {
const channelId = parseInt(req.body.channelId, 10);
@ -61,9 +62,11 @@ async function leaveChan(req: Request, res: Response) {
logger.info(
`Removing user ${user.getName()} from channel ${channel.name || channelId}`,
);
user.regUser.removeChannel(channel);
// TODO: inform websocket to remove channelId from user
webSockets.broadcastRemoveChatChannel(user.id, channelId, false);
res.json({
status: 'ok',
});

View File

@ -8,7 +8,8 @@
import type { Request, Response } from 'express';
import logger from '../../core/logger';
import { Channel, UserChannel, RegUser } from '../../data/models';
import { ChatProvider } from '../../core/ChatProvider';
import { Channel, RegUser } from '../../data/models';
import { isUserBlockedBy } from '../../data/models/UserBlock';
async function startDm(req: Request, res: Response) {
@ -107,30 +108,18 @@ async function startDm(req: Request, res: Response) {
const ChannelId = channel[0].id;
const promises = [
UserChannel.findOrCreate({
where: {
UserId: dmu1id,
ChannelId,
},
raw: true,
}),
UserChannel.findOrCreate({
where: {
UserId: dmu2id,
ChannelId,
},
raw: true,
}),
ChatProvider.addUserToChannel(user.id, ChannelId, false),
ChatProvider.addUserToChannel(userId, ChannelId, true),
];
await Promise.all(promises);
// TODO: inform websocket to add channelId to user
res.json({
channel: {
[ChannelId]: [
userName,
1,
Date.now(),
userId,
],
},
});

View File

@ -169,15 +169,25 @@ class ProtocolClient extends EventEmitter {
const data = JSON.parse(message);
if (Array.isArray(data)) {
if (data.length === 5) {
// Ordinary array: Chat message
const [name, text, country, channelId, userId] = data;
this.emit('chatMessage', name, text, country, channelId, userId);
switch (data.length) {
case 5: {
// chat message
const [name, text, country, channelId, userId] = data;
this.emit('chatMessage', name, text, country, channelId, userId);
return;
}
case 2: {
// signal
const [signal, args] = data;
this.emit(signal, args);
}
default:
// nothing
}
} else {
// string = name
this.name = data;
this.emit('setWsName', data);
}
}

View File

@ -19,7 +19,7 @@ import DeRegisterMultipleChunks from './packets/DeRegisterMultipleChunks';
import ChangedMe from './packets/ChangedMe';
import OnlineCounter from './packets/OnlineCounter';
import chatProvider from '../core/ChatProvider';
import chatProvider, { ChatProvider } from '../core/ChatProvider';
import authenticateClient from './verifyClient';
import WebSocketEvents from './WebSocketEvents';
import webSockets from './websockets';
@ -179,6 +179,52 @@ class SocketServer extends WebSocketEvents {
});
}
findWsByUserId(userId) {
const { clients } = this.wss;
for (let i = 0; i < clients.length; i += 1) {
const ws = clients[i];
if (ws.user.id === userId && ws.readyState === WebSocket.OPEN) {
return ws;
}
}
return null;
}
broadcastAddChatChannel(
userId: number,
channelId: number,
channelArray: Array,
notify: boolean,
) {
const ws = this.findWsByUserId(userId);
if (ws) {
ws.user.channels[channelId] = channelArray;
const text = JSON.stringify([
'addch', {
[channelId]: channelArray,
},
]);
if (notify) {
ws.send(text);
}
}
}
broadcastRemoveChatChannel(
userId: number,
channelId: number,
notify: boolean,
) {
const ws = this.findWsByUserId(userId);
if (ws) {
delete ws.user.channels[channelId];
const text = JSON.stringify('remch', channelId);
if (notify) {
ws.send(text);
}
}
}
broadcastPixelBuffer(canvasId: number, chunkid, data: Buffer) {
const frame = WebSocket.Sender.frame(data, {
readOnly: true,
@ -245,6 +291,10 @@ class SocketServer extends WebSocketEvents {
}
static async onTextMessage(text, ws) {
/*
* all client -> server text messages are
* chat messages in [message, channelId] format
*/
try {
let message;
let channelId;
@ -263,6 +313,9 @@ class SocketServer extends WebSocketEvents {
}
message = message.trim();
/*
* just if logged in
*/
if (ws.name && message) {
const { user } = ws;
const waitLeft = ws.rateLimiter.tick();
@ -276,44 +329,38 @@ class SocketServer extends WebSocketEvents {
]));
return;
}
// check proxy
if (!user.isAdmin() && await cheapDetector(user.ip)) {
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;
/*
* 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) {
const { user: dmUser } = dmWs;
if (!dmUser || !dmUser.userHasChannelAccess(channelId)) {
ChatProvider.addUserToChannel(
dmUserId,
channelId,
[ws.name, 1, Date.now(), user.id],
);
}
}
}
//
/*
* send chat message
*/
const errorMsg = await chatProvider.sendMessage(
user,
message,
channelId,
);
if (!errorMsg) {
// automute on repeated message spam
if (ws.last_message && ws.last_message === message) {
ws.message_repeat += 1;
if (ws.message_repeat >= 4) {
logger.info(`User ${ws.name} got automuted`);
chatProvider.automute(ws.name, channelId);
ws.message_repeat = 0;
}
} else {
ws.message_repeat = 0;
ws.last_message = message;
}
} else {
if (errorMsg) {
ws.send(JSON.stringify(['info', errorMsg, 'il', channelId]));
}
logger.info(
`Received chat message ${message} from ${ws.name} / ${ws.user.ip}`,
);
} else {
logger.info('Got empty message or message from unidentified ws');
}

View File

@ -24,6 +24,21 @@ class WebSocketEvents {
broadcastMinecraftLink(name: string, minecraftid: string, accepted: boolean) {
}
broadcastAddChatChannel(
userId: number,
channelId: number,
channelArray: Array,
notify: boolean,
) {
}
broadcastRemoveChatChannel(
userId: number,
channelId: number,
notify: boolean,
) {
}
notifyChangedMe(name: string) {
}

View File

@ -89,6 +89,50 @@ class WebSockets {
);
}
/*
* broadcast Assigning chat channel to user
* @param userId numerical id of user
* @param channelId numerical id of chat channel
* @param channelArray array with channel info [name, type, lastTs]
* @param notify if user should get notified over websocket
* (i.e. false if the user already gets it via api response)
*/
broadcastAddChatChannel(
userId: number,
channelId: number,
channelArray: Array,
notify: boolean = true,
) {
this.listeners.forEach(
(listener) => listener.broadcastAddChatChannel(
userId,
channelArray,
notify,
),
);
}
/*
* broadcast Removing chat channel from user
* @param userId numerical id of user
* @param channelId numerical id of chat channel
* @param notify if user should get notified over websocket
* (i.e. false if the user already gets it via api response)
*/
broadcastRemoveChatChannel(
userId: number,
channelId: number,
notify: boolean = true,
) {
this.listeners.forEach(
(listener) => listener.broadcastRemoveChatChannel(
userId,
channelId,
notify,
),
);
}
/*
* broadcast minecraft linking to API
* @param name pixelplanetname

View File

@ -18,6 +18,10 @@ export default (store) => (next) => (action) => {
break;
}
/*
* TODO
* make LOGIN / LOGOUT Actions instead of comparing name changes
*/
case 'RECEIVE_ME': {
const { name } = action;
ProtocolClient.setName(name);