feat: limiting, with message impl
This commit is contained in:
parent
c23570c25d
commit
0ca11a0ec4
|
@ -45,6 +45,7 @@
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"passport-reddit": "0.2.4",
|
"passport-reddit": "0.2.4",
|
||||||
"prop-types": "^15",
|
"prop-types": "^15",
|
||||||
|
"rate-limiter-flexible": "2.1.9",
|
||||||
"react": "16.8.6",
|
"react": "16.8.6",
|
||||||
"react-dom": "16.8.6",
|
"react-dom": "16.8.6",
|
||||||
"react-hot-loader": "^4.12.21",
|
"react-hot-loader": "^4.12.21",
|
||||||
|
|
|
@ -9,7 +9,15 @@ import {useSelector} from 'react-redux';
|
||||||
import {usernameSelector} from '~/common/state/usernames';
|
import {usernameSelector} from '~/common/state/usernames';
|
||||||
|
|
||||||
export default function ChatMessage(props) {
|
export default function ChatMessage(props) {
|
||||||
const {message: {owner, message, timestamp}, isShort} = props;
|
const {
|
||||||
|
message: {
|
||||||
|
owner,
|
||||||
|
message,
|
||||||
|
timestamp,
|
||||||
|
rejected,
|
||||||
|
},
|
||||||
|
isShort,
|
||||||
|
} = props;
|
||||||
const username = useSelector((state) => usernameSelector(state, owner));
|
const username = useSelector((state) => usernameSelector(state, owner));
|
||||||
const dtf = new Intl.DateTimeFormat(undefined, {
|
const dtf = new Intl.DateTimeFormat(undefined, {
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
|
@ -26,12 +34,12 @@ export default function ChatMessage(props) {
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classnames('chat--message', {short: isShort})}
|
className={classnames('chat--message', {rejected, short: isShort, system: -1 === owner})}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
!isShort && (
|
!isShort && (
|
||||||
<header>
|
<header>
|
||||||
<div className="chat--messageOwner">{username}</div>
|
<div className="chat--messageOwner">{-1 === owner ? '! System !' : username}</div>
|
||||||
{$messageTime}
|
{$messageTime}
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
|
@ -50,6 +58,7 @@ ChatMessage.propTypes = {
|
||||||
isShort: PropTypes.bool.isRequired,
|
isShort: PropTypes.bool.isRequired,
|
||||||
message: PropTypes.shape({
|
message: PropTypes.shape({
|
||||||
message: PropTypes.string,
|
message: PropTypes.string,
|
||||||
|
rejected: PropTypes.bool,
|
||||||
timestamp: PropTypes.number,
|
timestamp: PropTypes.number,
|
||||||
owner: PropTypes.number,
|
owner: PropTypes.number,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
|
|
|
@ -1,6 +1,14 @@
|
||||||
|
@import '~/client/scss/colors.scss';
|
||||||
|
|
||||||
.chat--message {
|
.chat--message {
|
||||||
margin-top: 0.75rem;
|
margin-top: 0.75rem;
|
||||||
padding: 0.25rem 1rem;
|
padding: 0.25rem 1rem;
|
||||||
|
&.rejected {
|
||||||
|
background-color: rgba(200, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
&.system {
|
||||||
|
background-color: rgba(200, 0, 0, 0.2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat--message, .chat--message * {
|
.chat--message, .chat--message * {
|
||||||
|
@ -43,6 +51,9 @@ header {
|
||||||
font-family: Caladea, 'Times New Roman', Times, serif;
|
font-family: Caladea, 'Times New Roman', Times, serif;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.25rem;
|
||||||
|
.system & {
|
||||||
|
color: lighten($color-active, 30%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat--messageTime {
|
.chat--messageTime {
|
||||||
|
@ -57,6 +68,10 @@ header {
|
||||||
|
|
||||||
.chat--messageText {
|
.chat--messageText {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
.rejected & {
|
||||||
|
color: red;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat--messageMarkdown * {
|
.chat--messageMarkdown * {
|
||||||
|
|
29
src/client/store/effects.js
vendored
29
src/client/store/effects.js
vendored
|
@ -1,3 +1,5 @@
|
||||||
|
import {v4 as uuidv4} from 'uuid';
|
||||||
|
|
||||||
import AddFavorite from '../../common/packets/add-favorite.packet';
|
import AddFavorite from '../../common/packets/add-favorite.packet';
|
||||||
import AddFriend from '~/common/packets/add-friend.packet';
|
import AddFriend from '~/common/packets/add-friend.packet';
|
||||||
import Block from '~/common/packets/block.packet';
|
import Block from '~/common/packets/block.packet';
|
||||||
|
@ -13,6 +15,7 @@ import {
|
||||||
join,
|
join,
|
||||||
joined,
|
joined,
|
||||||
leave,
|
leave,
|
||||||
|
rejectMessage,
|
||||||
submitJoin,
|
submitJoin,
|
||||||
submitLeave,
|
submitLeave,
|
||||||
submitMessage,
|
submitMessage,
|
||||||
|
@ -68,7 +71,7 @@ const effects = {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const userId = idSelector(state);
|
const userId = idSelector(state);
|
||||||
const hasFriendship = !!payload.addeeId;
|
const hasFriendship = !!payload.addeeId;
|
||||||
socket.send(new AddFriend({nameOrStatus: payload.nameOrStatus}), ({error, id}) => {
|
socket.send(new AddFriend({nameOrStatus: payload.nameOrStatus}), (error, id) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -88,7 +91,7 @@ const effects = {
|
||||||
},
|
},
|
||||||
[submitJoin]: ({dispatch}, {payload}) => {
|
[submitJoin]: ({dispatch}, {payload}) => {
|
||||||
const {channel} = payload;
|
const {channel} = payload;
|
||||||
socket.send(new Join(payload), ({messages, users}) => {
|
socket.send(new Join(payload), (error, {messages, users}) => {
|
||||||
dispatch(join({channel, messages, users}));
|
dispatch(join({channel, messages, users}));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -98,7 +101,27 @@ const effects = {
|
||||||
},
|
},
|
||||||
[submitMessage]: ({dispatch}, {payload}) => {
|
[submitMessage]: ({dispatch}, {payload}) => {
|
||||||
dispatch(addMessage(payload));
|
dispatch(addMessage(payload));
|
||||||
socket.send(new Message(payload), ([timestamp, current]) => {
|
socket.send(new Message(payload), (error, result) => {
|
||||||
|
if (error) {
|
||||||
|
switch (error.code) {
|
||||||
|
case 429: {
|
||||||
|
dispatch(rejectMessage(payload.uuid));
|
||||||
|
dispatch(addMessage({
|
||||||
|
...payload,
|
||||||
|
message: [
|
||||||
|
'You are sending too many messages.',
|
||||||
|
`Try again in ${error.ttr} second${1 === error.ttr ? '' : 's'}.`,
|
||||||
|
].join(' '),
|
||||||
|
owner: -1,
|
||||||
|
uuid: uuidv4(),
|
||||||
|
}));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [timestamp, current] = result;
|
||||||
dispatch(confirmMessage({current, previous: payload.uuid, timestamp}));
|
dispatch(confirmMessage({current, previous: payload.uuid, timestamp}));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -70,6 +70,9 @@ const slice = createSlice({
|
||||||
const {users} = channels[channel];
|
const {users} = channels[channel];
|
||||||
users.splice(users.indexOf(id), 1);
|
users.splice(users.indexOf(id), 1);
|
||||||
},
|
},
|
||||||
|
rejectMessage: ({messages}, {payload: uuid}) => {
|
||||||
|
messages[uuid].rejected = true;
|
||||||
|
},
|
||||||
removeMessage: (state, {payload: {channel, uuid}}) => {
|
removeMessage: (state, {payload: {channel, uuid}}) => {
|
||||||
delete state.messages[uuid];
|
delete state.messages[uuid];
|
||||||
const {messages} = state.channels[channel];
|
const {messages} = state.channels[channel];
|
||||||
|
@ -89,6 +92,7 @@ export const {
|
||||||
joined,
|
joined,
|
||||||
leave,
|
leave,
|
||||||
left,
|
left,
|
||||||
|
rejectMessage,
|
||||||
removeMessage,
|
removeMessage,
|
||||||
submitJoin,
|
submitJoin,
|
||||||
submitLeave,
|
submitLeave,
|
||||||
|
|
7
src/server/limiter.js
Normal file
7
src/server/limiter.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import {RateLimiterRedis} from 'rate-limiter-flexible';
|
||||||
|
|
||||||
|
import createRedisClient from './redis';
|
||||||
|
|
||||||
|
const storeClient = createRedisClient();
|
||||||
|
|
||||||
|
export default (options) => new RateLimiterRedis({...options, storeClient});
|
|
@ -33,6 +33,6 @@ export default {
|
||||||
nameOrStatus: friendship.status,
|
nameOrStatus: friendship.status,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
fn({id: addeeId});
|
fn(addeeId);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {allModels} from '~/server/models/registrar';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Packet: Message,
|
Packet: Message,
|
||||||
|
limiter: {points: 10, duration: 15},
|
||||||
validator: () => true,
|
validator: () => true,
|
||||||
responder: async (packet, socket, fn) => {
|
responder: async (packet, socket, fn) => {
|
||||||
const {req} = socket;
|
const {req} = socket;
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {joinChannel, parseChannel} from '~/common/channel';
|
||||||
|
|
||||||
import {channelsToHydrate} from '~/server/entry';
|
import {channelsToHydrate} from '~/server/entry';
|
||||||
|
|
||||||
|
import createLimiter from './limiter';
|
||||||
import * as PacketHandlers from './packet';
|
import * as PacketHandlers from './packet';
|
||||||
import {userJoin} from './packet/join';
|
import {userJoin} from './packet/join';
|
||||||
import {userLeave} from './packet/leave';
|
import {userLeave} from './packet/leave';
|
||||||
|
@ -20,8 +21,14 @@ const packetHandlers = new Map();
|
||||||
|
|
||||||
export function createSocketServer(httpServer) {
|
export function createSocketServer(httpServer) {
|
||||||
Object.keys(PacketHandlers).forEach((key) => {
|
Object.keys(PacketHandlers).forEach((key) => {
|
||||||
const {Packet, responder, validator} = PacketHandlers[key];
|
const {
|
||||||
packetHandlers.set(Packet, {responder, validator});
|
Packet,
|
||||||
|
limiter: limiterConfig,
|
||||||
|
responder,
|
||||||
|
validator,
|
||||||
|
} = PacketHandlers[key];
|
||||||
|
const limiter = limiterConfig && createLimiter({keyPrefix: Packet.name, ...limiterConfig});
|
||||||
|
packetHandlers.set(Packet, {limiter, responder, validator});
|
||||||
});
|
});
|
||||||
const socketServer = new SocketServer(httpServer, {adapter});
|
const socketServer = new SocketServer(httpServer, {adapter});
|
||||||
socketServer.io.use(socketSession(session()));
|
socketServer.io.use(socketSession(session()));
|
||||||
|
@ -56,8 +63,23 @@ export function createSocketServer(httpServer) {
|
||||||
});
|
});
|
||||||
socketServer.on('connect', (socket) => {
|
socketServer.on('connect', (socket) => {
|
||||||
socket.on('packet', async (packet, fn) => {
|
socket.on('packet', async (packet, fn) => {
|
||||||
const {responder} = packetHandlers.get(packet.constructor);
|
const {limiter, responder} = packetHandlers.get(packet.constructor);
|
||||||
responder(packet, socket, fn);
|
if (limiter) {
|
||||||
|
try {
|
||||||
|
await limiter.consume(socket.id);
|
||||||
|
responder(packet, socket, (...args) => fn(undefined, ...args));
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
fn({code: 500});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
fn({code: 429, ttr: Math.round(error.msBeforeNext / 1000) || 1});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
responder(packet, socket, (...args) => fn(undefined, ...args));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
socket.on('disconnecting', async () => {
|
socket.on('disconnecting', async () => {
|
||||||
Object.keys(socket.socket.rooms).forEach((room) => {
|
Object.keys(socket.socket.rooms).forEach((room) => {
|
||||||
|
|
|
@ -7561,6 +7561,11 @@ range-parser@^1.2.1, range-parser@~1.2.1:
|
||||||
resolved "https://npm.i12e.cha0s.io/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
|
resolved "https://npm.i12e.cha0s.io/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
|
||||||
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
|
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
|
||||||
|
|
||||||
|
rate-limiter-flexible@2.1.9:
|
||||||
|
version "2.1.9"
|
||||||
|
resolved "https://npm.i12e.cha0s.io/rate-limiter-flexible/-/rate-limiter-flexible-2.1.9.tgz#d137ffc874b8ea47d2baafe13a79474da52f8b6f"
|
||||||
|
integrity sha512-ueIXEHLZZqDBetuzyMbtSQ1Gh6Y5rw8ULoNuGA7L3xZ6njPIc2oM0ZlmsY9rS8rPU4yvdw7lk6MLbWU7WTNfnQ==
|
||||||
|
|
||||||
raw-body@2.4.0:
|
raw-body@2.4.0:
|
||||||
version "2.4.0"
|
version "2.4.0"
|
||||||
resolved "https://npm.i12e.cha0s.io/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"
|
resolved "https://npm.i12e.cha0s.io/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user