mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 17:40:46 -04:00
better banning, new deter mute, replay discord fixes.
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
// Scope: Owns message validation pipeline and typed outbound message construction.
|
||||
const logger = require('../../globals/logger').child('chatService');
|
||||
const { getRole } = require('../roleService');
|
||||
const { isDeterred, isMuted } = require('../verificationService');
|
||||
const { withinRateLimit } = require('./state');
|
||||
const { hasProfanity, isKeymash, normalizeUserText } = require('./contentFilters');
|
||||
const { buildMessage, buildTypingPayload, resolveRoverId, isPrivateClosedRoverId, buildRoverCtxSnapshot } = require('./contextBuilders');
|
||||
@@ -17,6 +18,12 @@ function createHandlers({ sendSystemMessage }) {
|
||||
const normalized = normalizeUserText(text);
|
||||
const clean = normalized.trim();
|
||||
if (!clean) return cb({ error: 'Message required' });
|
||||
/*
|
||||
Mute is narrower than deterrence: the socket may continue driving and
|
||||
using ordinary features, but its message must stop before broadcast,
|
||||
command parsing, TTS, or any other chat-derived side effect occurs.
|
||||
*/
|
||||
if (isMuted(socket)) return cb({ error: 'Muted' });
|
||||
if (!withinRateLimit(socket.id)) return cb({ error: 'Slow down' });
|
||||
// This service no longer enforces a character-count ceiling for chat text.
|
||||
// The chat layer only rejects empty, rate-limited, or moderated content so
|
||||
@@ -37,23 +44,40 @@ function createHandlers({ sendSystemMessage }) {
|
||||
});
|
||||
|
||||
logger.info('Chat message', { socket: socket.id, roverId: message.roverId });
|
||||
playTypingNote(roverId, TYPING_SEND_NOTE, socket?.id);
|
||||
const deterred = isDeterred(socket);
|
||||
/*
|
||||
Deterred users retain text chat, but chat must not become an indirect
|
||||
hardware-control path. Suppress the rover typing note and TTS while still
|
||||
constructing and broadcasting the same visible message as everyone else.
|
||||
*/
|
||||
if (!deterred) {
|
||||
playTypingNote(roverId, TYPING_SEND_NOTE, socket?.id);
|
||||
}
|
||||
|
||||
if (isPrivateClosedRoverId(message.roverId)) {
|
||||
// Private-closed chat does not broadcast the text, so TTS is the only
|
||||
// delivery path. Use the same Google speech default as normal chat when
|
||||
// the sender did not provide explicit TTS settings.
|
||||
const forcedTts = ttsOptions || { speak: true, engine: 'chromegtts' };
|
||||
maybeSpeak(socket, message, forcedTts);
|
||||
if (!deterred) {
|
||||
maybeSpeak(socket, message, forcedTts);
|
||||
}
|
||||
cb({ success: true, privateOnly: true });
|
||||
return;
|
||||
}
|
||||
|
||||
broadcastMessage(message);
|
||||
maybeSendAccessNotice(message, sendSystemMessage);
|
||||
maybeSpeak(socket, message, ttsOptions);
|
||||
if (!deterred) {
|
||||
maybeSpeak(socket, message, ttsOptions);
|
||||
}
|
||||
|
||||
const command = isTextCommand(clean);
|
||||
/*
|
||||
Command-shaped text from a deterred user remains ordinary visible chat.
|
||||
Reporting command=false prevents the client from implying that the server
|
||||
accepted an action, and the command router is never invoked.
|
||||
*/
|
||||
const command = !deterred && isTextCommand(clean);
|
||||
// Chat delivery is complete once validation, broadcast, and local side
|
||||
// effects above have succeeded. A command may wait on Home Assistant,
|
||||
// hardware, replay preparation, or an external transport, so tying the
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// Scope: Bridges socket events to chat handlers and publishes chat updates to connected clients.
|
||||
const io = require('../../globals/io');
|
||||
const { subscribe } = require('../eventBus');
|
||||
const { isDeterred, isMuted } = require('../verificationService');
|
||||
const { typingBySocket } = require('./state');
|
||||
const { buildTypingPayload, resolveRoverId, isPrivateClosedRoverId } = require('./contextBuilders');
|
||||
const { broadcastTyping } = require('./broadcast');
|
||||
@@ -13,11 +14,29 @@ function registerChatSocketHooks({ history, handleIncoming }) {
|
||||
socket.emit('chat:init', history);
|
||||
socket.on('chat:send', (payload = {}, cb = () => {}) => handleIncoming(payload, socket, cb));
|
||||
socket.on('chat:typing', (payload = {}) => {
|
||||
/*
|
||||
A muted typing packet must not leak presence or produce rover notes.
|
||||
Clearing any prior state also removes a typing indicator that began
|
||||
immediately before an administrator applied the mute.
|
||||
*/
|
||||
if (isMuted(socket)) {
|
||||
const wasTyping = typingBySocket.delete(socket.id);
|
||||
const roverId = resolveRoverId(socket?.id);
|
||||
if (wasTyping && !isPrivateClosedRoverId(roverId)) {
|
||||
broadcastTyping(buildTypingPayload(socket, { roverId, fromDiscord: false, isTyping: false }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
const isTyping = Boolean(payload?.isTyping);
|
||||
const wasTyping = typingBySocket.get(socket.id);
|
||||
if (isTyping) {
|
||||
typingBySocket.set(socket.id, true);
|
||||
if (!wasTyping) {
|
||||
/*
|
||||
The typing indicator is part of chat and remains available to a
|
||||
deterred user. The rover note is a physical side effect, however, so
|
||||
text-only deterrence suppresses that note without changing presence.
|
||||
*/
|
||||
if (!wasTyping && !isDeterred(socket)) {
|
||||
const roverId = resolveRoverId(socket?.id);
|
||||
playTypingNote(roverId, TYPING_START_NOTE, socket?.id);
|
||||
}
|
||||
|
||||
@@ -17,8 +17,11 @@ const {
|
||||
listVerifiedUsers,
|
||||
removeVerifiedUser,
|
||||
listDeterredUsers,
|
||||
listMutedUsers,
|
||||
deterUser,
|
||||
undeterUser,
|
||||
muteUser,
|
||||
unmuteUser,
|
||||
} = require('../verificationService');
|
||||
const { publishEvent } = require('../eventBus');
|
||||
const assignmentService = require('../assignmentService');
|
||||
@@ -176,8 +179,11 @@ async function runChatTextCommand({ text, socket, sendSystemMessage }) {
|
||||
listVerifiedUsers,
|
||||
removeVerifiedUser,
|
||||
listDeterredUsers,
|
||||
listMutedUsers,
|
||||
deterUser,
|
||||
undeterUser,
|
||||
muteUser,
|
||||
unmuteUser,
|
||||
sanitizeMentions,
|
||||
sendToChannel: null,
|
||||
isAdminUser: (id) => String(id) === String(socket.id) && isAdmin(socket),
|
||||
|
||||
Reference in New Issue
Block a user