roverquesting

This commit is contained in:
legop3
2026-03-27 16:08:04 -04:00
parent 998933f4c2
commit c220eb4022
14 changed files with 508 additions and 131 deletions
+44 -1
View File
@@ -40,7 +40,6 @@ const {
listVerifiedUsers,
removeVerifiedUser,
} = require('./verificationService');
const config = loadConfig();
const discordConfig = config.discord || {};
const enabled = Boolean(discordConfig.token);
@@ -1504,6 +1503,49 @@ async function sendVerificationRequestDms(event) {
);
}
async function sendPrivateRoverAccessRequestDms(event) {
const payload = event?.payload || {};
const requestId = payload.id;
if (!requestId) return;
const adminIdsToNotify = Array.from(lockdownAdminIds);
if (!adminIdsToNotify.length) {
logger.warn('No lockdown admins configured for private rover access request DM', { requestId });
return;
}
const requester = payload.requester || {};
const createdAt = payload.createdAt ? new Date(payload.createdAt).toLocaleString() : 'unknown';
const content = [
'**Private Rover Access Request**',
`Request ID: \`${requestId}\``,
`Rover: ${sanitizeMentions(payload.roverName || payload.roverId || 'unknown')} (\`${payload.roverId || 'unknown'}\`)`,
`Requester: ${sanitizeMentions(requester.nickname || requester.socketId || 'unknown')}`,
`Role: \`${requester.role || 'unknown'}\``,
`Verified: \`${requester.isVerified ? 'yes' : 'no'}\``,
`Identity key: \`${requester.cookieUserId || 'unknown'}\``,
`IP: \`${requester.ip || 'unknown'}\``,
`Created: ${createdAt}`,
'',
'Open the rover manually in the admin UI if approved.',
].join('\n');
await Promise.all(
adminIdsToNotify.map(async (adminId) => {
try {
const user = await client.users.fetch(String(adminId));
if (!user) return;
const dm = await user.createDM();
await dm.send({ content, allowedMentions: { parse: [] } });
} catch (err) {
logger.warn('Failed to DM lockdown admin for private rover access request', {
requestId,
adminId,
error: err.message,
});
}
}),
);
}
async function handleVerificationReaction(reaction, user) {
if (!reaction || !user || user.bot) return;
const emoji = reaction.emoji?.name;
@@ -1659,6 +1701,7 @@ client.once('ready', () => {
subscribe('*', handleBusEvent);
subscribe('verification.requested', sendVerificationRequestDms);
subscribe('privateRoverAccess.requested', sendPrivateRoverAccessRequestDms);
subscribe('chat:message', handleChatBridgeOutbound);
subscribe('chat:typing', handleChatTypingOutbound);
@@ -0,0 +1,214 @@
const crypto = require('crypto');
const EventEmitter = require('events');
const io = require('../globals/io');
const logger = require('../globals/logger').child('privateRoverAccessRequest');
const { publishEvent } = require('./eventBus');
const roverManager = require('./roverManager');
const { getNickname } = require('./nicknameService');
const { getRole, isLockdownAdmin } = require('./roleService');
const { getSocketIp, normalizeIp } = require('../helpers/ipResolver');
const requestEvents = new EventEmitter();
const REQUEST_COOLDOWN_MS = 15 * 1000;
const pendingRequests = new Map(); // requestId -> request
const pendingByRequesterRover = new Map(); // `${requesterKey}:${roverId}` -> requestId
const lastRequestAtByRequester = new Map(); // requesterKey -> ts
function normalizeRoverId(value) {
return String(value || '').trim();
}
function buildRequesterKey(socket) {
const cookieUserId = String(socket?.data?.cookieUserId || '').trim().toLowerCase();
if (cookieUserId) return `cookie:${cookieUserId}`;
return `socket:${socket?.id || 'unknown'}`;
}
function isClosedPrivateRoverRecord(record) {
return Boolean(record?.private?.enabled && !record?.privateOpen);
}
function listClosedPrivateRovers() {
const records = Array.from(roverManager.rovers.values())
.filter((record) => isClosedPrivateRoverRecord(record))
.map((record) => ({
id: String(record.id),
name: record.meta?.name || record.id,
color: record.meta?.color || null,
}))
.sort((a, b) => String(a.name).localeCompare(String(b.name)));
return records;
}
function listPendingForRequester(socket) {
const requesterKey = buildRequesterKey(socket);
const pending = [];
for (const request of pendingRequests.values()) {
if (request.requesterKey !== requesterKey) continue;
if (request.status !== 'pending') continue;
pending.push({
id: request.id,
roverId: request.roverId,
roverName: request.roverName,
createdAt: request.createdAt,
status: request.status,
});
}
pending.sort((a, b) => b.createdAt - a.createdAt);
return pending;
}
function getStateForSocket(socket) {
return {
requestableRovers: listClosedPrivateRovers(),
pendingRequests: listPendingForRequester(socket),
};
}
function clearPendingRequest(request, reason = 'resolved') {
if (!request || request.status !== 'pending') return;
request.status = reason;
request.resolvedAt = Date.now();
pendingByRequesterRover.delete(`${request.requesterKey}:${request.roverId}`);
requestEvents.emit('change', { reason, requestId: request.id, roverId: request.roverId });
}
function clearPendingForRover(roverId, reason = 'resolved') {
const target = normalizeRoverId(roverId);
for (const request of pendingRequests.values()) {
if (request.status !== 'pending') continue;
if (String(request.roverId) !== target) continue;
clearPendingRequest(request, reason);
}
}
function createRequest(socket, roverIdRaw) {
if (!socket?.id) {
throw new Error('Socket required');
}
const roverId = normalizeRoverId(roverIdRaw);
if (!roverId) {
throw new Error('roverId required');
}
const record = roverManager.rovers.get(roverId);
if (!record) {
throw new Error('Unknown rover');
}
if (!record?.private?.enabled) {
throw new Error('Rover is not private');
}
if (record.privateOpen) {
throw new Error('Private rover is already open');
}
if (isLockdownAdmin(socket)) {
throw new Error('Lockdown admins can open private rovers directly');
}
const requesterKey = buildRequesterKey(socket);
const dedupeKey = `${requesterKey}:${roverId}`;
const existingId = pendingByRequesterRover.get(dedupeKey);
if (existingId) {
const existing = pendingRequests.get(existingId);
if (existing && existing.status === 'pending') {
return { request: existing, isNew: false };
}
}
const now = Date.now();
const lastAt = Number(lastRequestAtByRequester.get(requesterKey) || 0);
if (lastAt && now - lastAt < REQUEST_COOLDOWN_MS) {
const remaining = Math.ceil((REQUEST_COOLDOWN_MS - (now - lastAt)) / 1000);
throw new Error(`Please wait ${remaining}s before sending another request`);
}
const request = {
id: `prr_${crypto.randomBytes(8).toString('hex')}`,
status: 'pending',
createdAt: now,
resolvedAt: null,
requesterKey,
roverId,
roverName: record.meta?.name || record.id,
requester: {
socketId: socket.id,
nickname: getNickname(socket) || null,
role: getRole(socket),
isVerified: Boolean(socket?.data?.isVerified),
cookieUserId: String(socket?.data?.cookieUserId || '').trim().toLowerCase() || null,
ip: normalizeIp(getSocketIp(socket)) || null,
},
};
pendingRequests.set(request.id, request);
pendingByRequesterRover.set(dedupeKey, request.id);
lastRequestAtByRequester.set(requesterKey, now);
publishEvent({
source: 'privateRoverAccessRequest',
type: 'privateRoverAccess.requested',
payload: request,
});
requestEvents.emit('change', {
reason: 'created',
requestId: request.id,
roverId: request.roverId,
socketId: socket.id,
});
logger.info('Private rover access requested', {
requestId: request.id,
roverId: request.roverId,
socketId: socket.id,
});
return { request, isNew: true };
}
roverManager.managerEvents.on('private', ({ roverId, open } = {}) => {
if (!roverId) return;
if (open) {
clearPendingForRover(roverId, 'opened');
}
requestEvents.emit('change', {
reason: 'private_state',
roverId: String(roverId),
open: Boolean(open),
});
});
roverManager.managerEvents.on('rover', ({ roverId, action } = {}) => {
if (!roverId) return;
if (action === 'removed') {
clearPendingForRover(roverId, 'rover_removed');
}
requestEvents.emit('change', {
reason: 'rover',
roverId: String(roverId),
action: action || null,
});
});
io.on('connection', (socket) => {
function handleRequest({ roverId } = {}, cb = () => {}) {
try {
const { request, isNew } = createRequest(socket, roverId);
cb({
success: true,
requestId: request.id,
status: request.status,
existing: !isNew,
});
} catch (err) {
cb({ error: err.message });
}
}
socket.on('privateRover:requestAccess', handleRequest);
socket.on('session:privateRover:requestAccess', handleRequest);
});
module.exports = {
requestEvents,
getStateForSocket,
createRequest,
};
+12 -2
View File
@@ -135,11 +135,21 @@ function getPrivateSafety(record) {
}
function shouldApplyPrivateSafety(record, socket) {
if (!isPrivateRecord(record) || !isPrivateOpen(record)) return false;
if (!isPrivateRecord(record)) return false;
if (isLockdownAdmin(socket)) return false;
return true;
}
function shouldApplyPrivateSensorSafety(record) {
if (!isPrivateRecord(record)) return false;
const activeDrivers = turnService.getActiveDrivers();
const activeDriverId = activeDrivers?.[record.id];
if (!activeDriverId) return false;
const activeSocket = io.sockets.sockets.get(activeDriverId);
if (!activeSocket) return true;
return !isLockdownAdmin(activeSocket);
}
function isRoverVisibleToSocket(record, socket) {
if (!record) return false;
if (!isPrivateRecord(record)) return true;
@@ -639,7 +649,7 @@ function evaluatePrivateSafety(record, sensors) {
const currentBump = bump;
const currentCliff = cliff;
if (!isPrivateRecord(record) || !isPrivateOpen(record)) {
if (!shouldApplyPrivateSensorSafety(record)) {
state.blockedUntil = 0;
state.lastOvercurrent = currentOver;
state.lastBump = currentBump;
+10
View File
@@ -14,6 +14,10 @@ const {
getIdentitySummary,
verificationEvents,
} = require('./verificationService');
const {
getStateForSocket: getPrivateRoverAccessStateForSocket,
requestEvents: privateRoverAccessRequestEvents,
} = require('./privateRoverAccessRequestService');
const { getReplayState, replayEvents } = require('./replayService');
const { getReplaySources } = require('./replaySourceService');
const { getHealthSnapshot } = require('./healthService');
@@ -128,6 +132,7 @@ function buildSession(socket) {
},
identity: getIdentitySummary(socket),
verification: getVerificationStateForSocket(socket),
privateRoverAccess: getPrivateRoverAccessStateForSocket(socket),
isVerified: Boolean(socket?.data?.isVerified),
audioForward: getAudioForwardState(),
audioLevels: getAudioLevels(),
@@ -214,6 +219,11 @@ managerEvents.on('privateSafety', ({ roverId }) => {
syncAll();
});
privateRoverAccessRequestEvents.on('change', (event = {}) => {
logger.info('Private rover access request state changed', event.reason || 'unknown');
syncAll();
});
managerEvents.on('driver', ({ socketId }) => {
if (!socketId) return;
const socket = io.sockets.sockets.get(socketId);