driver removal information finaly!

This commit is contained in:
legop3
2026-07-05 22:45:38 -04:00
parent df22ac6d81
commit b526beb712
18 changed files with 502 additions and 168 deletions
@@ -12,6 +12,7 @@ function formatHelp() {
'`rs bridge mode <global|private>` — change chat bridge mode',
'`rs bridge off` — disable chat bridge for this server',
'`rs lights <status|lock|unlock>` — show or change room light lock state',
'`rs kick <user> [reason]` — remove a user from their current rover; use `user | reason` for multi-word names',
'`rs lock <rover>` — lock a rover; rover names can be fuzzy',
'`rs unlock <rover>` — unlock a rover; rover names can be fuzzy',
'`rs mode <open|turns|admin|lockdown>` — change server mode',
@@ -13,6 +13,7 @@ const { createDeterCommand } = require('./deter');
const { createBridgeCommand } = require('./bridge');
const { createTimeStatusCommand } = require('./timeStatus');
const { createLightsCommand } = require('./lights');
const { createKickCommand } = require('./kick');
function createCommandHandlers(deps) {
const {
@@ -35,6 +36,7 @@ function createCommandHandlers(deps) {
const handleBridgeCommand = createBridgeCommand(deps);
const handleTimeStatusCommand = createTimeStatusCommand(deps);
const handleLightsCommand = createLightsCommand(deps);
const handleKickCommand = createKickCommand(deps);
async function handleCommand(message) {
if (message.author.bot) return;
@@ -58,7 +60,7 @@ function createCommandHandlers(deps) {
// lockdown mode narrows them from normal admins to lockdown admins. Room
// light locking belongs here because it can force the physical room lights
// on and disables ordinary Home Assistant room controls for everyone else.
const moderationActions = new Set(['lock', 'unlock', 'mode', 'goal', 'reason', 'verify', 'deter', 'lights']);
const moderationActions = new Set(['lock', 'unlock', 'mode', 'goal', 'reason', 'verify', 'deter', 'lights', 'kick']);
if (!isAdmin && action !== '' && action !== 'status' && action !== 'help' && action !== 'replay' && action !== 'bridge' && action !== 'goal' && action !== 'reason' && action !== 'verify' && action !== 'deter') {
await message.reply({ content: 'Only admins can run that command.', allowedMentions: { parse: [], repliedUser: false } });
@@ -82,6 +84,8 @@ function createCommandHandlers(deps) {
return handleBridgeCommand(message, tokens);
case 'lights':
return handleLightsCommand(message, tokens);
case 'kick':
return handleKickCommand(message, rest);
case 'lock':
return handleLockCommand(message, rest, true);
case 'unlock':
@@ -0,0 +1,136 @@
// Discord Kick Command
// Purpose: Removes a connected user from their current rover without applying any persistent moderation state.
// Scope: Resolves an online driver, sends them a UI-visible reason, and releases their current rover assignment.
const Fuse = require('fuse.js');
const DEFAULT_KICK_REASON = 'Removed from rover by admin.';
function normalizeText(value) {
return String(value || '').trim();
}
function normalizeSearchText(value) {
return normalizeText(value).toLowerCase().replace(/\s+/g, ' ');
}
function splitSelectorAndReason(rawText) {
const text = normalizeText(rawText);
if (!text) return { selector: '', reason: '' };
const pipeIndex = text.indexOf('|');
if (pipeIndex >= 0) {
/*
A pipe delimiter is the escape hatch for multi-word nicknames. Without a
delimiter the command intentionally treats the first token as the selector
so quick admin commands stay short: `rs kick bob being reckless`.
*/
return {
selector: normalizeText(text.slice(0, pipeIndex)),
reason: normalizeText(text.slice(pipeIndex + 1)),
};
}
const parts = text.split(/\s+/);
return {
selector: normalizeText(parts.shift()),
reason: normalizeText(parts.join(' ')),
};
}
function buildKickCandidates({ io, roverManager, assignmentService, getNickname }) {
return Array.from(io.sockets.sockets.values())
.map((socket) => {
const socketId = normalizeText(socket?.id);
const assignedRoverId = assignmentService?.getAssignedRover?.(socketId) || null;
const primaryRoverId = roverManager.getPrimaryRoverForSocket(socketId);
const roverId = assignedRoverId || primaryRoverId || null;
if (!socketId || !roverId) return null;
const nickname = normalizeText(getNickname(socket));
const username = normalizeText(socket?.data?.user?.username);
return {
socket,
socketId,
roverId,
nickname,
username,
label: nickname || username || socketId.slice(0, 6),
searchSocketId: normalizeSearchText(socketId),
searchShortSocketId: normalizeSearchText(socketId.slice(0, 6)),
searchNickname: normalizeSearchText(nickname),
searchUsername: normalizeSearchText(username),
};
})
.filter(Boolean);
}
function resolveKickTarget(selector, candidates) {
const query = normalizeSearchText(selector);
if (!query) return { error: 'Specify a user to kick. Example: `rs kick nickname reason`' };
const exact = candidates.filter((entry) => (
entry.searchSocketId === query ||
entry.searchShortSocketId === query ||
entry.searchNickname === query ||
entry.searchUsername === query
));
if (exact.length === 1) return { target: exact[0] };
if (exact.length > 1) {
return { error: `User matched multiple drivers: ${exact.map((entry) => entry.label).join(', ')}.` };
}
const fuse = new Fuse(candidates, {
includeScore: true,
threshold: 0.38,
ignoreLocation: true,
keys: [
{ name: 'nickname', weight: 0.7 },
{ name: 'username', weight: 0.2 },
{ name: 'socketId', weight: 0.1 },
],
});
const results = fuse.search(selector);
if (!results.length) return { error: 'User not found among current rover drivers.' };
const first = results[0];
const second = results[1];
if (second && Math.abs(Number(second.score || 0) - Number(first.score || 0)) < 0.08) {
return {
error: `User matched multiple drivers: ${results.slice(0, 5).map((entry) => entry.item.label).join(', ')}.`,
};
}
return { target: first.item };
}
function createKickCommand({ io, roverManager, getNickname, sanitizeMentions }) {
return async function handleKickCommand(message, rawText) {
const { selector, reason } = splitSelectorAndReason(rawText);
const assignmentService = require('../../assignmentService');
const candidates = buildKickCandidates({
io,
roverManager,
assignmentService,
getNickname,
});
const resolved = resolveKickTarget(selector, candidates);
if (resolved.error) {
await message.reply({ content: sanitizeMentions(resolved.error), allowedMentions: { parse: [], repliedUser: false } });
return;
}
const target = resolved.target;
const removalReason = reason || DEFAULT_KICK_REASON;
/*
The command deliberately calls the notice-aware release helper instead of
roverManager.releaseControl. That keeps admin kicks aligned with automated
removals and gives the driver a stable explanation in the video panel.
*/
assignmentService.forceReleaseWithNotice(target.roverId, target.socketId, {
title: 'Removed by admin',
message: removalReason,
reasonCode: 'admin-kick',
actor: message.author?.id || null,
});
await message.reply({
content: sanitizeMentions(`Removed ${target.label} from ${target.roverId}: ${removalReason}`),
allowedMentions: { parse: [], repliedUser: false },
});
};
}
module.exports = {
createKickCommand,
};