add chat channels

This commit is contained in:
HF 2020-04-29 17:55:44 +02:00
parent 5478faebd5
commit 8877a7658a
15 changed files with 198 additions and 70 deletions

View File

@ -189,12 +189,14 @@ export function receiveChatMessage(
name: string, name: string,
text: string, text: string,
country: string, country: string,
channel: number,
): Action { ): Action {
return { return {
type: 'RECEIVE_CHAT_MESSAGE', type: 'RECEIVE_CHAT_MESSAGE',
name, name,
text, text,
country, country,
channel,
}; };
} }
@ -620,6 +622,13 @@ export function showChatModal(): Action {
return showModal('CHAT'); return showModal('CHAT');
} }
export function setChatChannel(channelId: number): Action {
return {
type: 'SET_CHAT_CHANNEL',
channelId,
};
}
export function hideModal(): Action { export function hideModal(): Action {
return { return {
type: 'HIDE_MODAL', type: 'HIDE_MODAL',

View File

@ -56,8 +56,11 @@ export type Action =
| { type: 'RECEIVE_CHAT_MESSAGE', | { type: 'RECEIVE_CHAT_MESSAGE',
name: string, name: string,
text: string, text: string,
country: string } country: string,
channel: number,
}
| { type: 'RECEIVE_CHAT_HISTORY', data: Array } | { type: 'RECEIVE_CHAT_HISTORY', data: Array }
| { type: 'SET_CHAT_CHANNEL', channelId: number }
| { type: 'RECEIVE_ME', | { type: 'RECEIVE_ME',
name: string, name: string,
waitSeconds: number, waitSeconds: number,

View File

@ -43,8 +43,8 @@ function init() {
ProtocolClient.on('onlineCounter', ({ online }) => { ProtocolClient.on('onlineCounter', ({ online }) => {
store.dispatch(receiveOnline(online)); store.dispatch(receiveOnline(online));
}); });
ProtocolClient.on('chatMessage', (name, text, country) => { ProtocolClient.on('chatMessage', (name, text, country, channelId) => {
store.dispatch(receiveChatMessage(name, text, country)); store.dispatch(receiveChatMessage(name, text, country, channelId));
}); });
ProtocolClient.on('chatHistory', (data) => { ProtocolClient.on('chatHistory', (data) => {
store.dispatch(receiveChatHistory(data)); store.dispatch(receiveChatHistory(data));

View File

@ -12,13 +12,7 @@ import ChatInput from './ChatInput';
import { colorFromText, splitCoordsInString } from '../core/utils'; import { colorFromText, splitCoordsInString } from '../core/utils';
function onError() { const Chat = ({ chatMessages, chatChannel }) => {
this.onerror = null;
this.src = './cf/xx.gif';
}
const Chat = ({ chatMessages }) => {
const listRef = useRef(); const listRef = useRef();
const { stayScrolled } = useStayScrolled(listRef, { const { stayScrolled } = useStayScrolled(listRef, {
initialScroll: Infinity, initialScroll: Infinity,
@ -32,7 +26,7 @@ const Chat = ({ chatMessages }) => {
<div style={{ height: '100%' }}> <div style={{ height: '100%' }}>
<ul className="chatarea" ref={listRef}> <ul className="chatarea" ref={listRef}>
{ {
chatMessages.map((message) => ( chatMessages[chatChannel].map((message) => (
<p className="chatmsg"> <p className="chatmsg">
{(message[0] === 'info') {(message[0] === 'info')
? <span style={{ color: '#cc0000' }}>{message[1]}</span> ? <span style={{ color: '#cc0000' }}>{message[1]}</span>
@ -42,7 +36,10 @@ const Chat = ({ chatMessages }) => {
alt="" alt=""
title={`${message[2]}`} title={`${message[2]}`}
src={`${window.assetserver}/cf/${message[2]}.gif`} src={`${window.assetserver}/cf/${message[2]}.gif`}
onError={onError} onError={(e) => {
e.target.onerror = null;
e.target.src = './cf/xx.gif';
}}
/> />
&nbsp; &nbsp;
<span <span
@ -75,7 +72,8 @@ const Chat = ({ chatMessages }) => {
function mapStateToProps(state: State) { function mapStateToProps(state: State) {
const { chatMessages } = state.user; const { chatMessages } = state.user;
return { chatMessages }; const { chatChannel } = state.gui;
return { chatMessages, chatChannel };
} }
export default connect(mapStateToProps)(Chat); export default connect(mapStateToProps)(Chat);

View File

@ -10,7 +10,8 @@ import { connect } from 'react-redux';
import type { State } from '../reducers'; import type { State } from '../reducers';
import ProtocolClient from '../socket/ProtocolClient'; import ProtocolClient from '../socket/ProtocolClient';
import { showUserAreaModal } from '../actions'; import { showUserAreaModal, setChatChannel } from '../actions';
import { CHAT_CHANNELS } from '../core/constants';
class ChatInput extends React.Component { class ChatInput extends React.Component {
constructor() { constructor() {
@ -22,44 +23,70 @@ class ChatInput extends React.Component {
this.handleSubmit = this.handleSubmit.bind(this); this.handleSubmit = this.handleSubmit.bind(this);
} }
handleSubmit(e) { handleSubmit(e, channelId) {
e.preventDefault(); e.preventDefault();
const { message } = this.state; const { message } = this.state;
if (!message) return; if (!message) return;
// send message via websocket // send message via websocket
ProtocolClient.sendMessage(message); ProtocolClient.sendChatMessage(message, channelId);
this.setState({ this.setState({
message: '', message: '',
}); });
} }
render() { render() {
if (this.props.name) { const {
name, chatChannel, open, setChannel,
} = this.props;
const {
message,
} = this.state;
const selectedChannel = CHAT_CHANNELS[chatChannel];
if (name) {
return ( return (
<div className="chatinput"> <div className="chatinput">
<form onSubmit={this.handleSubmit}> <form onSubmit={(e) => { this.handleSubmit(e, chatChannel); }}>
<input <input
style={{ maxWidth: '80%', width: '240px' }} style={{ maxWidth: '80%', width: '240px' }}
value={this.state.message} value={message}
onChange={(evt) => this.setState({ message: evt.target.value })} onChange={(evt) => this.setState({ message: evt.target.value })}
type="text" type="text"
placeholder="Chat here" placeholder="Chat here"
/> />
<button id="chatmsginput" type="submit">Send</button> <button id="chatmsginput" type="submit">Send</button>
</form> </form>
<select
onChange={(evt) => setChannel(evt.target.selectedIndex)}
>
{
CHAT_CHANNELS.map((ch) => (
<option selected={ch === selectedChannel}>ch</option>
))
}
</select>
</div> </div>
); );
} }
return ( return (
<div className="modallink" onClick={this.props.open} style={{ textAlign: 'center', fontSize: 13 }}>You must be logged in to chat</div> <div
className="modallink"
onClick={open}
style={{ textAlign: 'center', fontSize: 13 }}
role="button"
tabIndex={0}
>
You must be logged in to chat
</div>
); );
} }
} }
function mapStateToProps(state: State) { function mapStateToProps(state: State) {
const { name } = state.user; const { name } = state.user;
return { name }; const { chatChannel } = state.gui;
return { name, chatChannel };
} }
function mapDispatchToProps(dispatch) { function mapDispatchToProps(dispatch) {
@ -67,6 +94,9 @@ function mapDispatchToProps(dispatch) {
open() { open() {
dispatch(showUserAreaModal()); dispatch(showUserAreaModal());
}, },
setChannel(channelId) {
dispatch(setChatChannel(channelId));
},
}; };
} }

View File

@ -24,7 +24,7 @@ class ChatProvider {
}, },
{ {
regexp: /FUCK/gi, regexp: /FUCK/gi,
matches: 2, matches: 3,
}, },
{ {
regexp: /FACK/gi, regexp: /FACK/gi,
@ -40,14 +40,14 @@ class ChatProvider {
this.mutedCountries = []; this.mutedCountries = [];
} }
addMessage(name, message, country) { addMessage(name, message, country, channelId = 0) {
if (this.history.length > 20) { if (this.history.length > 20) {
this.history.shift(); this.history.shift();
} }
this.history.push([name, message, country]); this.history.push([name, message, country, channelId]);
} }
async sendMessage(user, message) { async sendMessage(user, message, channelId: number = 0) {
const name = (user.regUser) ? user.regUser.name : null; const name = (user.regUser) ? user.regUser.name : null;
const country = (name.endsWith('berg') || name.endsWith('stein')) const country = (name.endsWith('berg') || name.endsWith('stein'))
? 'il' ? 'il'
@ -72,7 +72,7 @@ class ChatProvider {
const filter = this.filters[i]; const filter = this.filters[i];
const count = (message.match(filter.regexp) || []).length; const count = (message.match(filter.regexp) || []).length;
if (count >= filter.matches) { if (count >= filter.matches) {
ChatProvider.mute(name, 30); ChatProvider.mute(name, channelId, 30);
return 'Ow no! Spam protection decided to mute you'; return 'Ow no! Spam protection decided to mute you';
} }
} }
@ -99,17 +99,22 @@ class ChatProvider {
if (cmd === 'mute') { if (cmd === 'mute') {
const timeMin = Number(args.slice(-1)); const timeMin = Number(args.slice(-1));
if (Number.isNaN(timeMin)) { if (Number.isNaN(timeMin)) {
return ChatProvider.mute(args.join(' ')); return ChatProvider.mute(args.join(' '), channelId);
} }
return ChatProvider.mute(args.slice(0, -1).join(' '), timeMin); return ChatProvider.mute(
args.slice(0, -1).join(' '),
channelId,
timeMin,
);
} if (cmd === 'unmute') { } if (cmd === 'unmute') {
return ChatProvider.unmute(args.join(' ')); return ChatProvider.unmute(args.join(' '), channelId);
} if (cmd === 'mutec' && args[0]) { } if (cmd === 'mutec' && args[0]) {
const cc = args[0].toLowerCase(); const cc = args[0].toLowerCase();
this.mutedCountries.push(cc); this.mutedCountries.push(cc);
webSockets.broadcastChatMessage( webSockets.broadcastChatMessage(
'info', 'info',
`Country ${cc} has been muted`, `Country ${cc} has been muted`,
channelId,
); );
return null; return null;
} if (cmd === 'unmutec' && args[0]) { } if (cmd === 'unmutec' && args[0]) {
@ -121,6 +126,7 @@ class ChatProvider {
webSockets.broadcastChatMessage( webSockets.broadcastChatMessage(
'info', 'info',
`Country ${cc} has been unmuted`, `Country ${cc} has been unmuted`,
channelId,
); );
return null; return null;
} }
@ -141,8 +147,8 @@ class ChatProvider {
} }
return `You are muted for another ${muted} seconds`; return `You are muted for another ${muted} seconds`;
} }
this.addMessage(name, message, country); this.addMessage(name, message, country, channelId);
webSockets.broadcastChatMessage(name, message, country); webSockets.broadcastChatMessage(name, message, country, channelId);
return null; return null;
} }
@ -150,10 +156,11 @@ class ChatProvider {
name, name,
message, message,
country: string = 'xx', country: string = 'xx',
channelId: number = 0,
sendapi: boolean = true, sendapi: boolean = true,
) { ) {
this.addMessage(name, message, country); this.addMessage(name, message, country, channelId);
webSockets.broadcastChatMessage(name, message, country, sendapi); webSockets.broadcastChatMessage(name, message, country, channelId, sendapi);
} }
/* /*
@ -161,8 +168,8 @@ class ChatProvider {
* singleton * singleton
*/ */
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
automute(name) { automute(name, channelId = 0) {
ChatProvider.mute(name, 600); ChatProvider.mute(name, channelId, 600);
} }
static async checkIfMuted(user) { static async checkIfMuted(user) {
@ -171,7 +178,7 @@ class ChatProvider {
return ttl; return ttl;
} }
static async mute(name, timeMin = null) { static async mute(name, channelId = 0, timeMin = null) {
const id = await User.name2Id(name); const id = await User.name2Id(name);
if (!id) { if (!id) {
return `Couldn't find user ${name}`; return `Couldn't find user ${name}`;
@ -180,18 +187,24 @@ class ChatProvider {
if (timeMin) { if (timeMin) {
const ttl = timeMin * 60; const ttl = timeMin * 60;
await redis.setAsync(key, '', 'EX', ttl); await redis.setAsync(key, '', 'EX', ttl);
webSockets.broadcastChatMessage('info', webSockets.broadcastChatMessage(
`${name} has been muted for ${timeMin}min`); 'info',
`${name} has been muted for ${timeMin}min`,
channelId,
);
} else { } else {
await redis.setAsync(key, ''); await redis.setAsync(key, '');
webSockets.broadcastChatMessage('info', webSockets.broadcastChatMessage(
`${name} has been muted forever`); 'info',
`${name} has been muted forever`,
channelId,
);
} }
logger.info(`Muted user ${id}`); logger.info(`Muted user ${id}`);
return null; return null;
} }
static async unmute(name) { static async unmute(name, channelId = 0) {
const id = await User.name2Id(name); const id = await User.name2Id(name);
if (!id) { if (!id) {
return `Couldn't find user ${name}`; return `Couldn't find user ${name}`;
@ -201,8 +214,11 @@ class ChatProvider {
if (delKeys !== 1) { if (delKeys !== 1) {
return `User ${name} is not muted`; return `User ${name} is not muted`;
} }
webSockets.broadcastChatMessage('info', webSockets.broadcastChatMessage(
`${name} has been unmuted`); 'info',
`${name} has been unmuted`,
channelId,
);
logger.info(`Unmuted user ${id}`); logger.info(`Unmuted user ${id}`);
return null; return null;
} }

View File

@ -88,3 +88,6 @@ export const MINUTE = 60 * SECOND;
export const HOUR = 60 * MINUTE; export const HOUR = 60 * MINUTE;
export const DAY = 24 * HOUR; export const DAY = 24 * HOUR;
export const MONTH = 30 * DAY; export const MONTH = 30 * DAY;
// available Chat Channels
export const CHAT_CHANNELS = ['en', 'int'];

View File

@ -17,6 +17,7 @@ export type GUIState = {
compactPalette: boolean, compactPalette: boolean,
paletteOpen: boolean, paletteOpen: boolean,
menuOpen: boolean, menuOpen: boolean,
chatChannel: number,
}; };
const initialState: GUIState = { const initialState: GUIState = {
@ -31,6 +32,7 @@ const initialState: GUIState = {
compactPalette: false, compactPalette: false,
paletteOpen: true, paletteOpen: true,
menuOpen: false, menuOpen: false,
chatChannel: 0,
}; };
@ -95,6 +97,13 @@ export default function gui(
}; };
} }
case 'SET_CHAT_CHANNEL': {
return {
...state,
chatChannel: action.channelId,
};
}
case 'SELECT_COLOR': { case 'SELECT_COLOR': {
const { const {
compactPalette, compactPalette,

View File

@ -44,7 +44,10 @@ const initialState: UserState = {
mailreg: false, mailreg: false,
totalRanking: {}, totalRanking: {},
totalDailyRanking: {}, totalDailyRanking: {},
chatMessages: [['info', 'Welcome to the PixelPlanet Chat', 'il']], chatMessages: [
['info', 'Welcome to the PixelPlanet Chat', 'il'],
['info', 'Welcome to the PixelPlanet Chat', 'il'],
],
minecraftname: null, minecraftname: null,
isOnMobile: false, isOnMobile: false,
notification: null, notification: null,
@ -119,14 +122,25 @@ export default function user(
} }
case 'RECEIVE_CHAT_MESSAGE': { case 'RECEIVE_CHAT_MESSAGE': {
const { name, text, country } = action; const {
name, text, country, channel,
} = action;
let { chatMessages } = state; let { chatMessages } = state;
if (chatMessages.length > 50) { let channelMessages = chatMessages[channel];
chatMessages = chatMessages.slice(-50); if (channelMessages.length > 50) {
channelMessages = channelMessages.slice(-50);
} }
channelMessages = channelMessages.concat([
[name, text, country, channel],
]);
chatMessages = Object.assign(
[],
chatMessages,
{ channel: channelMessages },
);
return { return {
...state, ...state,
chatMessages: chatMessages.concat([[name, text, country]]), chatMessages,
}; };
} }

View File

@ -30,7 +30,7 @@ async function verifyClient(info, done) {
const ip = await getIPFromRequest(req); const ip = await getIPFromRequest(req);
if (!headers.authorization if (!headers.authorization
|| headers.authorization != `Bearer ${APISOCKET_KEY}`) { || headers.authorization !== `Bearer ${APISOCKET_KEY}`) {
logger.warn(`API ws request from ${ip} authenticated`); logger.warn(`API ws request from ${ip} authenticated`);
return done(false); return done(false);
} }
@ -82,8 +82,15 @@ class APISocketServer extends WebSocketEvents {
setInterval(this.ping, 45 * 1000); setInterval(this.ping, 45 * 1000);
} }
broadcastChatMessage(name, msg, country, sendapi, ws = null) { broadcastChatMessage(
if (!sendapi) return; name,
msg,
country,
channelId,
sendapi,
ws = null,
) {
if (!sendapi || channelId !== 0) return;
const sendmsg = JSON.stringify(['msg', name, msg]); const sendmsg = JSON.stringify(['msg', name, msg]);
this.wss.clients.forEach((client) => { this.wss.clients.forEach((client) => {
@ -238,14 +245,14 @@ class APISocketServer extends WebSocketEvents {
const chatname = (user.id) const chatname = (user.id)
? `[MC] ${user.regUser.name}` ? `[MC] ${user.regUser.name}`
: `[MC] ${minecraftname}`; : `[MC] ${minecraftname}`;
chatProvider.broadcastChatMessage(chatname, msg, 'xx', false); chatProvider.broadcastChatMessage(chatname, msg, 'xx', 0, false);
this.broadcastChatMessage(chatname, msg, 'xx', true, ws); this.broadcastChatMessage(chatname, msg, 'xx', 0, true, ws);
return; return;
} }
if (command == 'chat') { if (command == 'chat') {
const [name, msg] = packet; const [name, msg] = packet;
chatProvider.broadcastChatMessage(name, msg, 'xx', false); chatProvider.broadcastChatMessage(name, msg, 'xx', 0, false);
this.broadcastChatMessage(name, msg, 'xx', true, ws); this.broadcastChatMessage(name, msg, 'xx', 0, true, ws);
return; return;
} }
if (command == 'linkacc') { if (command == 'linkacc') {

View File

@ -132,8 +132,10 @@ class ProtocolClient extends EventEmitter {
if (this.isConnected) this.ws.send(buffer); if (this.isConnected) this.ws.send(buffer);
} }
sendMessage(message) { sendChatMessage(message, channelId) {
if (this.isConnected) this.ws.send(message); if (this.isConnected) {
this.ws.send(JSON.stringify([message, channelId]));
}
} }
onMessage({ data: message }) { onMessage({ data: message }) {
@ -160,10 +162,10 @@ class ProtocolClient extends EventEmitter {
this.emit('chatHistory', data); this.emit('chatHistory', data);
return; return;
} }
if (data.length === 3) { if (data.length === 4) {
// Ordinary array: Chat message // Ordinary array: Chat message
const [name, text, country] = data; const [name, text, country, channelId] = data;
this.emit('chatMessage', name, text, country); this.emit('chatMessage', name, text, country, channelId);
} }
} else { } else {
// string = name // string = name

View File

@ -145,8 +145,13 @@ class SocketServer extends WebSocketEvents {
this.broadcast(buffer); this.broadcast(buffer);
} }
broadcastChatMessage(name: string, message: string, country: string) { broadcastChatMessage(
const text = JSON.stringify([name, message, country]); name: string,
message: string,
country: string,
channelId: number = 0,
) {
const text = JSON.stringify([name, message, country, channelId]);
this.wss.clients.forEach((ws) => { this.wss.clients.forEach((ws) => {
if (ws.readyState === WebSocket.OPEN) { if (ws.readyState === WebSocket.OPEN) {
ws.send(text); ws.send(text);
@ -217,15 +222,40 @@ class SocketServer extends WebSocketEvents {
webSockets.broadcastOnlineCounter(online); webSockets.broadcastOnlineCounter(online);
} }
static async onTextMessage(message, ws) { static async onTextMessage(text, ws) {
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;
}
if (ws.name && message) { if (ws.name && message) {
const waitLeft = ws.rateLimiter.tick(); const waitLeft = ws.rateLimiter.tick();
if (waitLeft) { if (waitLeft) {
// eslint-disable-next-line max-len ws.send(JSON.stringify([
ws.send(JSON.stringify(['info', `You are sending messages too fast, you have to wait ${Math.floor(waitLeft / 1000)}s :(`, 'il'])); 'info',
// eslint-disable-next-line max-len
`You are sending messages too fast, you have to wait ${Math.floor(waitLeft / 1000)}s :(`,
'il',
channelId,
]));
return; return;
} }
const errorMsg = await chatProvider.sendMessage(ws.user, message); const errorMsg = await chatProvider.sendMessage(
ws.user,
message,
channelId,
);
if (errorMsg) { if (errorMsg) {
ws.send(JSON.stringify(['info', errorMsg, 'il'])); ws.send(JSON.stringify(['info', errorMsg, 'il']));
} }
@ -233,7 +263,7 @@ class SocketServer extends WebSocketEvents {
ws.message_repeat += 1; ws.message_repeat += 1;
if (ws.message_repeat >= 3) { if (ws.message_repeat >= 3) {
logger.info(`User ${ws.name} got automuted`); logger.info(`User ${ws.name} got automuted`);
chatProvider.automute(ws.name); chatProvider.automute(ws.name, channelId);
ws.message_repeat = 0; ws.message_repeat = 0;
} }
} else { } else {

View File

@ -14,7 +14,7 @@ class WebSocketEvents {
broadcastPixelBuffer(canvasId: number, chunkid: number, buffer: Buffer) { broadcastPixelBuffer(canvasId: number, chunkid: number, buffer: Buffer) {
} }
broadcastChatMessage(name: string, message: string) { broadcastChatMessage(name: string, message: string, channelId: number) {
} }
broadcastMinecraftLink(name: string, minecraftid: string, accepted: boolean) { broadcastMinecraftLink(name: string, minecraftid: string, accepted: boolean) {

View File

@ -5,7 +5,6 @@
* *
*/ */
import logger from '../core/logger';
import OnlineCounter from './packets/OnlineCounter'; import OnlineCounter from './packets/OnlineCounter';
import PixelUpdate from './packets/PixelUpdate'; import PixelUpdate from './packets/PixelUpdate';
@ -64,14 +63,16 @@ class WebSockets {
name: string, name: string,
message: string, message: string,
country: string, country: string,
channelId: number = 0,
sendapi: boolean = true, sendapi: boolean = true,
) { ) {
country == country || 'xx'; country = country || 'xx';
this.listeners.forEach( this.listeners.forEach(
(listener) => listener.broadcastChatMessage( (listener) => listener.broadcastChatMessage(
name, name,
message, message,
country, country,
channelId,
sendapi, sendapi,
), ),
); );

View File

@ -173,6 +173,12 @@ export default (store) => (next) => (action) => {
case 'RECEIVE_CHAT_MESSAGE': { case 'RECEIVE_CHAT_MESSAGE': {
if (!chatNotify) break; if (!chatNotify) break;
const { chatChannel } = state.gui;
if (action.channel !== chatChannel) {
break;
}
const oscillatorNode = context.createOscillator(); const oscillatorNode = context.createOscillator();
const gainNode = context.createGain(); const gainNode = context.createGain();