diff --git a/app/src/react/components/chat/messages/message/moderation/index.jsx b/app/src/react/components/chat/messages/message/moderation/index.jsx index 62e6943..d400e60 100644 --- a/app/src/react/components/chat/messages/message/moderation/index.jsx +++ b/app/src/react/components/chat/messages/message/moderation/index.jsx @@ -2,10 +2,11 @@ import './index.scss'; import {useSocket} from '@latus/socket/client'; import {channelIsAnonymous, renderChannel} from '@reddichat/core/client'; +import {ADMIN, MOD, submitMessageDistinction} from '@reddichat/chat/client'; import {idSelector, isAdminSelector, isModOfSelector} from '@reddichat/user/client'; import PropTypes from 'prop-types'; import React from 'react'; -import {useSelector} from 'react-redux'; +import {useDispatch, useSelector} from 'react-redux'; import Actions from 'components/actions'; @@ -17,6 +18,7 @@ export default function Moderation(props) { uuid, } = props; const channel = useChannel(); + const dispatch = useDispatch(); const id = useSelector(idSelector); const isAnonymous = channelIsAnonymous(channel); const isAdmin = useSelector(isAdminSelector); @@ -24,13 +26,13 @@ export default function Moderation(props) { const socket = useSocket(); const actions = []; if (isAdmin && (id === owner || 0 === owner)) { - actions.push(['👑', 'Admin distinction', () => { - socket.send(['MessageAdminDistinction', uuid]); + actions.push(['👑', 'Toggle admin distinction', () => { + dispatch(submitMessageDistinction({uuid, distinction: ADMIN})); }]); } if (isMod && id === owner) { - actions.push(['🎩', 'Moderator distinction', () => { - socket.send(['MessageModeratorDistinction', uuid]); + actions.push(['🎩', 'Toggle moderator distinction', () => { + dispatch(submitMessageDistinction({uuid, distinction: MOD})); }]); } if (isAdmin) { diff --git a/packages/chat/src/client/effects.js b/packages/chat/src/client/effects.js index a9b68d6..8efe8a2 100644 --- a/packages/chat/src/client/effects.js +++ b/packages/chat/src/client/effects.js @@ -19,6 +19,8 @@ import { submitJoin, submitLeave, submitMessage, + submitMessageDistinction, + toggleMessageDistinction, } from './state'; export default ({config: {'%socket': socket}}) => { @@ -101,5 +103,9 @@ export default ({config: {'%socket': socket}}) => { reject(Math.round(Math.max(0, error.msBeforeNext) / 1000) || 1); } }, + [submitMessageDistinction]: async ({dispatch}, {payload}) => { + dispatch(toggleMessageDistinction(payload)); + await socket.send(['MessageDistinction', payload]); + }, }); }; diff --git a/packages/chat/src/client/index.js b/packages/chat/src/client/index.js index 8714828..6b0a5a3 100644 --- a/packages/chat/src/client/index.js +++ b/packages/chat/src/client/index.js @@ -3,6 +3,7 @@ import Join from '../packets/join'; import Leave from '../packets/leave'; import Message from '../packets/message'; import MessageSiteBan from '../packets/message-site-ban'; +import MessageDistinction from '../packets/message-distinction'; import chat from './state'; @@ -17,6 +18,7 @@ export default { Leave: Leave(latus), Message: Message(latus), MessageSiteBan: MessageSiteBan(latus), + MessageDistinction: MessageDistinction(latus), }), // eslint-disable-next-line global-require '@reddichat/state/effects': (latus) => require('./effects').default(latus), diff --git a/packages/chat/src/client/state.js b/packages/chat/src/client/state.js index 27be382..c917df1 100644 --- a/packages/chat/src/client/state.js +++ b/packages/chat/src/client/state.js @@ -120,6 +120,15 @@ const slice = createSlice({ submitJoin: () => {}, submitLeave: () => {}, submitMessage: () => {}, + submitMessageDistinction: () => {}, + toggleMessageDistinction: ({messages}, {payload: {distinction, uuid}}) => { + const message = messages[uuid]; + /* eslint-disable no-bitwise */ + message.distinction = message.distinction & distinction + ? message.distinction & ~distinction + : message.distinction | distinction; + /* eslint-enable no-bitwise */ + }, }, /* eslint-enable no-param-reassign */ }); @@ -138,6 +147,8 @@ export const { submitJoin, submitLeave, submitMessage, + submitMessageDistinction, + toggleMessageDistinction, } = slice.actions; slice.reducer.subscription = slice.reducer; diff --git a/packages/chat/src/index.js b/packages/chat/src/index.js index 0afa907..a5c5061 100644 --- a/packages/chat/src/index.js +++ b/packages/chat/src/index.js @@ -13,6 +13,7 @@ import Activity from './packets/activity.server'; import Join from './packets/join.server'; import Leave from './packets/leave.server'; import Message from './packets/message.server'; +import MessageDistinction from './packets/message-distinction.server'; import MessageSiteBan from './packets/message-site-ban.server'; import ensureCanonical from './ensure-canonical'; @@ -66,6 +67,7 @@ export default { Join: Join(latus), Leave: Leave(latus), Message: Message(latus), + MessageDistinction: MessageDistinction(latus), MessageSiteBan: MessageSiteBan(latus), }), '@latus/socket/connect': async (socket, latus) => { diff --git a/packages/chat/src/message-channel.js b/packages/chat/src/message-channel.js new file mode 100644 index 0000000..4461050 --- /dev/null +++ b/packages/chat/src/message-channel.js @@ -0,0 +1,9 @@ +import {createClient, keys} from '@latus/redis'; +import {parseChannel} from '@reddichat/core'; + +const messageChannel = async (latus, uuid) => { + const key = (await keys(createClient(latus), `*:messages:${uuid}`)).pop(); + return parseChannel(key.split(':')[0]); +}; + +export default messageChannel; diff --git a/packages/chat/src/packets/message-distinction.js b/packages/chat/src/packets/message-distinction.js new file mode 100644 index 0000000..ed0ec70 --- /dev/null +++ b/packages/chat/src/packets/message-distinction.js @@ -0,0 +1,22 @@ +import {Packet, ValidationError} from '@latus/socket/packets'; +import {validate} from 'uuid'; + +export default () => class MessageDistinction extends Packet { + + static get data() { + return { + distinction: 'uint8', + uuid: 'string', + }; + } + + static async validate({data: {uuid}}, {req: {user}}) { + if (!validate(uuid)) { + throw new ValidationError({code: 400, reason: 'malformed'}); + } + if (!user) { + throw new ValidationError({code: 403, reason: 'unauthorized'}); + } + } + +}; diff --git a/packages/chat/src/packets/message-distinction.server.js b/packages/chat/src/packets/message-distinction.server.js new file mode 100644 index 0000000..0526891 --- /dev/null +++ b/packages/chat/src/packets/message-distinction.server.js @@ -0,0 +1,46 @@ +import {ValidationError} from '@latus/socket/packets'; +import {renderChannel} from '@reddichat/core'; + +import {ADMIN, MOD} from '../distinction'; +import messageChannel from '../message-channel'; +import replaceMessage from '../replace-message'; +import MessageDistinction from './message-distinction'; + +export default (latus) => class MessageDistinctionServer extends MessageDistinction() { + + static async respond({data: {distinction, uuid}}, socket) { + const {req} = socket; + const message = await replaceMessage( + req, + uuid, + (msg) => ({ + ...msg, + /* eslint-disable no-bitwise */ + distinction: msg.distinction & distinction + ? msg.distinction & ~distinction + : msg.distinction | distinction, + /* eslint-enable no-bitwise */ + }), + ); + const channel = await messageChannel(latus, uuid); + socket + .to(renderChannel(channel)) + .send(['MessageDistinction', {uuid, distinction: message.distinction}]); + } + + static async validate(packet, socket) { + super.validate(packet, socket); + const {data: {distinction, uuid}} = packet; + const {req} = socket; + const {user} = req; + // eslint-disable-next-line no-bitwise + if ((distinction & ADMIN) && !user.isAdmin) { + throw new ValidationError({code: 400, reason: 'unauthorized'}); + } + // eslint-disable-next-line no-bitwise + if ((distinction & MOD) && !user.isModOf(await messageChannel(latus, uuid))) { + throw new ValidationError({code: 400, reason: 'unauthorized'}); + } + } + +}; diff --git a/packages/chat/src/replace-message.js b/packages/chat/src/replace-message.js new file mode 100644 index 0000000..0f2fa28 --- /dev/null +++ b/packages/chat/src/replace-message.js @@ -0,0 +1,16 @@ +import {promisify} from 'util'; + +import {keys} from '@latus/redis'; + +const replaceMessage = async (req, uuid, fn) => { + const {pubClient} = req.adapter; + const get = promisify(pubClient.get.bind(pubClient)); + const key = (await keys(pubClient, `*:messages:${uuid}`)).pop(); + const message = fn(JSON.parse(await get(key))); + return new Promise((resolve, reject) => pubClient + .multi() + .set(key, JSON.stringify(message)) + .exec((error) => (error ? reject(error) : resolve(message)))); +}; + +export default replaceMessage;