feat: limiting, with message impl
This commit is contained in:
parent
c23570c25d
commit
0ca11a0ec4
|
@ -45,6 +45,7 @@
|
|||
"passport-local": "^1.0.0",
|
||||
"passport-reddit": "0.2.4",
|
||||
"prop-types": "^15",
|
||||
"rate-limiter-flexible": "2.1.9",
|
||||
"react": "16.8.6",
|
||||
"react-dom": "16.8.6",
|
||||
"react-hot-loader": "^4.12.21",
|
||||
|
|
|
@ -9,7 +9,15 @@ import {useSelector} from 'react-redux';
|
|||
import {usernameSelector} from '~/common/state/usernames';
|
||||
|
||||
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 dtf = new Intl.DateTimeFormat(undefined, {
|
||||
hour: '2-digit',
|
||||
|
@ -26,12 +34,12 @@ export default function ChatMessage(props) {
|
|||
);
|
||||
return (
|
||||
<div
|
||||
className={classnames('chat--message', {short: isShort})}
|
||||
className={classnames('chat--message', {rejected, short: isShort, system: -1 === owner})}
|
||||
>
|
||||
{
|
||||
!isShort && (
|
||||
<header>
|
||||
<div className="chat--messageOwner">{username}</div>
|
||||
<div className="chat--messageOwner">{-1 === owner ? '! System !' : username}</div>
|
||||
{$messageTime}
|
||||
</header>
|
||||
)
|
||||
|
@ -50,6 +58,7 @@ ChatMessage.propTypes = {
|
|||
isShort: PropTypes.bool.isRequired,
|
||||
message: PropTypes.shape({
|
||||
message: PropTypes.string,
|
||||
rejected: PropTypes.bool,
|
||||
timestamp: PropTypes.number,
|
||||
owner: PropTypes.number,
|
||||
}).isRequired,
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
@import '~/client/scss/colors.scss';
|
||||
|
||||
.chat--message {
|
||||
margin-top: 0.75rem;
|
||||
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 * {
|
||||
|
@ -43,6 +51,9 @@ header {
|
|||
font-family: Caladea, 'Times New Roman', Times, serif;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.25rem;
|
||||
.system & {
|
||||
color: lighten($color-active, 30%);
|
||||
}
|
||||
}
|
||||
|
||||
.chat--messageTime {
|
||||
|
@ -57,6 +68,10 @@ header {
|
|||
|
||||
.chat--messageText {
|
||||
font-size: 0.9rem;
|
||||
.rejected & {
|
||||
color: red;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
|
||||
.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 AddFriend from '~/common/packets/add-friend.packet';
|
||||
import Block from '~/common/packets/block.packet';
|
||||
|
@ -13,6 +15,7 @@ import {
|
|||
join,
|
||||
joined,
|
||||
leave,
|
||||
rejectMessage,
|
||||
submitJoin,
|
||||
submitLeave,
|
||||
submitMessage,
|
||||
|
@ -68,7 +71,7 @@ const effects = {
|
|||
const state = getState();
|
||||
const userId = idSelector(state);
|
||||
const hasFriendship = !!payload.addeeId;
|
||||
socket.send(new AddFriend({nameOrStatus: payload.nameOrStatus}), ({error, id}) => {
|
||||
socket.send(new AddFriend({nameOrStatus: payload.nameOrStatus}), (error, id) => {
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
@ -88,7 +91,7 @@ const effects = {
|
|||
},
|
||||
[submitJoin]: ({dispatch}, {payload}) => {
|
||||
const {channel} = payload;
|
||||
socket.send(new Join(payload), ({messages, users}) => {
|
||||
socket.send(new Join(payload), (error, {messages, users}) => {
|
||||
dispatch(join({channel, messages, users}));
|
||||
});
|
||||
},
|
||||
|
@ -98,7 +101,27 @@ const effects = {
|
|||
},
|
||||
[submitMessage]: ({dispatch}, {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}));
|
||||
});
|
||||
},
|
||||
|
|
|
@ -70,6 +70,9 @@ const slice = createSlice({
|
|||
const {users} = channels[channel];
|
||||
users.splice(users.indexOf(id), 1);
|
||||
},
|
||||
rejectMessage: ({messages}, {payload: uuid}) => {
|
||||
messages[uuid].rejected = true;
|
||||
},
|
||||
removeMessage: (state, {payload: {channel, uuid}}) => {
|
||||
delete state.messages[uuid];
|
||||
const {messages} = state.channels[channel];
|
||||
|
@ -89,6 +92,7 @@ export const {
|
|||
joined,
|
||||
leave,
|
||||
left,
|
||||
rejectMessage,
|
||||
removeMessage,
|
||||
submitJoin,
|
||||
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,
|
||||
}));
|
||||
});
|
||||
fn({id: addeeId});
|
||||
fn(addeeId);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@ import {allModels} from '~/server/models/registrar';
|
|||
|
||||
export default {
|
||||
Packet: Message,
|
||||
limiter: {points: 10, duration: 15},
|
||||
validator: () => true,
|
||||
responder: async (packet, socket, fn) => {
|
||||
const {req} = socket;
|
||||
|
|
|
@ -6,6 +6,7 @@ import {joinChannel, parseChannel} from '~/common/channel';
|
|||
|
||||
import {channelsToHydrate} from '~/server/entry';
|
||||
|
||||
import createLimiter from './limiter';
|
||||
import * as PacketHandlers from './packet';
|
||||
import {userJoin} from './packet/join';
|
||||
import {userLeave} from './packet/leave';
|
||||
|
@ -20,8 +21,14 @@ const packetHandlers = new Map();
|
|||
|
||||
export function createSocketServer(httpServer) {
|
||||
Object.keys(PacketHandlers).forEach((key) => {
|
||||
const {Packet, responder, validator} = PacketHandlers[key];
|
||||
packetHandlers.set(Packet, {responder, validator});
|
||||
const {
|
||||
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});
|
||||
socketServer.io.use(socketSession(session()));
|
||||
|
@ -56,8 +63,23 @@ export function createSocketServer(httpServer) {
|
|||
});
|
||||
socketServer.on('connect', (socket) => {
|
||||
socket.on('packet', async (packet, fn) => {
|
||||
const {responder} = packetHandlers.get(packet.constructor);
|
||||
responder(packet, socket, fn);
|
||||
const {limiter, responder} = packetHandlers.get(packet.constructor);
|
||||
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 () => {
|
||||
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"
|
||||
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:
|
||||
version "2.4.0"
|
||||
resolved "https://npm.i12e.cha0s.io/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"
|
||||
|
|
Loading…
Reference in New Issue
Block a user