openvibers/openvibe.chat Commits

Global chat refuses the built-in slur list; channel rooms keep each streamer's choice (#128)

OP OpenVibers committed 22 days ago · 083a2b96bd · 2 files, +30 −1

Changes

ddiff --git a/server/chat/chat-server.js b/server/chat/chat-server.js
iindex d67f8f3..f459c6d 100644
---- a/server/chat/chat-server.js
++++ b/server/chat/chat-server.js
@@@ -61,6 +61,9 @@ const SUB_ONLY_ANON = 'This chat is in sub-only mode: sign in and subscribe to c
// A person's own lines: what someone who blocked them on the network no longer gets (network-blocks.js).
const LINE_FRAMES = new Set(['chat', 'tts', 'tts-audio']);
const DEFAULT_SLUR_NUDGE = 'This streamer enabled Anti-Slur Nudge for this chat. Free speech is still alive, but this lane is closed today. Try a different word and keep it funny.';
++// Global chat is OpenVibe's own room, read on every site: the built-in slur list always applies there. Stream and
++// channel chats keep each streamer's own choice (the Anti-Slur Nudge settings).
++const GLOBAL_SLUR_REFUSED = 'That message was not sent: global chat does not allow slurs. Try different words.';
// Built-in slur categories and normalization are defined in moderation-utils.js
// (single source of truth — browser-side chat.js mirrors the same patterns).
const {
@@@ -1099,7 +1102,13 @@ class ChatServer {
* the anti-slur filter. Tells the sender and returns true when the line is refused.
*/
async _chatRulesBlock(ws, client, text, modStreamId) {
-- if (!modStreamId) return false;
++ if (!modStreamId) {
++ // A channel's room with no live stream is still the streamer's; only global chat has OpenVibe's rule.
++ if (client.streamId || client.channelUserId || permissions.can(client.user, 'staff.moderation.bypass')) return false;
++ if (!this._containsCoreSlur(text, [])) return false;
++ this.sendTo(ws, { type: 'system', message: GLOBAL_SLUR_REFUSED });
++ return true;
++ }
const chatSettings = await this._getChannelChatSettings(modStreamId);
const isStaff = permissions.can(client.user, 'staff.moderation.bypass');
const canModerateThisStream = await permissions.canModerateStream(client.user, modStreamId);
ddiff --git a/test/security.test.js b/test/security.test.js
iindex 91d1984..8d9057d 100644
---- a/test/security.test.js
++++ b/test/security.test.js
@@@ -111,6 +111,26 @@ t('/me and /tts obey followers-only and the slur filter', async () => {
for (const w of [viewer, bobWs, bob2]) w.close();
});
++t('global chat refuses the built-in slurs, /me included; an offline channel room keeps its streamer\'s choice', async () => {
++ const slur = ['k', 'y', 'k', 'e'].join(''); // one of the built-in antisemitic patterns, spelled out at run time
++ const reader = await joined({ ip: '198.51.100.40', token: streamer.token }, {});
++ const bobG = await joined({ ip: '198.51.100.41', token: bob.token }, {});
++ bobG.sendJson({ type: 'chat', message: `you ${slur}` });
++ await bobG.next((m) => m.type === 'system' && /global chat does not allow slurs/.test(m.message));
++ await h.sleep(1100);
++ bobG.sendJson({ type: 'chat', message: `/me says ${slur}` });
++ await bobG.next((m) => m.type === 'system' && /global chat does not allow slurs/.test(m.message));
++ assert.ok(await reader.none((m) => new RegExp(slur).test(m.message || ''), 300), 'a slur reached global chat');
++ await h.sleep(1100);
++ bobG.sendJson({ type: 'chat', message: 'a perfectly ordinary line' });
++ await reader.next((m) => m.type === 'chat' && m.message === 'a perfectly ordinary line');
++ // An offline channel's room is the streamer's own: no global rule there (their Anti-Slur Nudge is off).
++ const own = await joined({ ip: '198.51.100.42', token: bob.token }, { channelUserId: other.id });
++ own.sendJson({ type: 'chat', message: `quoting ${slur} in a channel room` });
++ assert.ok(await own.none((m) => m.type === 'system' && /global chat/.test(m.message || ''), 300), 'the global rule reached a channel room');
++ for (const w of [reader, bobG, own]) w.close();
++});
++
t('joining the channel room without the stream id does not skip slow mode or IP approval', async () => {
const own = await joined({ ip: '198.51.100.30', token: streamer.token, stream: streamX }, { streamId: streamX });
own.sendJson({ type: 'chat', message: '/slow 30' });