From 85fcaedc6a02f71df78cf568975686e81de88ecd Mon Sep 17 00:00:00 2001 From: legop3 Date: Fri, 27 Mar 2026 16:15:08 -0400 Subject: [PATCH] roerd --- pi/roverd/roverd | Bin 10480535 -> 10480535 bytes server/src/services/assignmentService.js | 4 + server/src/services/discordBotService.js | 68 +++++- .../privateRoverAccessRequestService.js | 230 +++++++++++++++++- server/src/services/roverManager.js | 13 +- 5 files changed, 311 insertions(+), 4 deletions(-) diff --git a/pi/roverd/roverd b/pi/roverd/roverd index 6935b9bec741e3b639bba94dbecd8d76add823c1..dfc24cbfc81a692c75a8e12ec4a013467984e32f 100755 GIT binary patch delta 907 zcmbWx$x~7R0Dy5yDzk-3T5K`eZSy@JPv3)912Gj45KJi}b_@a+l%mDTlFDp1+Pin^ z#!Tb?&@E;e}^)Gbz9lx2cSpK6e;$uoTVYmD2skANa4Q8AUSBu=+m9+}3 z{+?D_P7ZXNf)3x9WhjyI2LxB#s(9t3za#EcJ$W@9$n|v!BYAJq>6TPW?{I%KGdk|h z=3N@CMx)WA3hhpx=KH6go<#M)><`0l;n&v><(bhh(?V)6)FKO$6_G$e zVjuS701l!N0z@=Hf)O&B(Ed?N$4PXc6J4;w z0VlfQf*T(6zzZLGaSDCthaabL1_L;Y0L~$Z5W7$X=(7GoGk4i|9=d0fU7T*Wm^;5u&LCMIzUw=snR?%*!&VH)=_g9mts gM|g}Uc#3D3#d8$#0x$6juki+N@vdahzt5Hb0{RDJ*8l(j delta 907 zcmbWx$yX8q0Dy5yw6cW?S}ZfN%@&rKVTKtii^K>`b`xnK_DE7Qk+Mb265ID^?^N$~ z>&C_Zp`(|M-MD(MuYaM#@A$p~e?Y(OAkBj9a{_ zFyu)`qQm_bX`nYGj@SY@w=?1xjCpfDH8i0P#Y@p{TcPM5FBV25e?HPV77k`y9yOuU z>vTF3>frJ9=~{o)D^^c~s(hbp{x(;27{7K*+Y0%l;;_xs4CR_(36`P`deox<%di|P z(1?{-h1FPtf9+bV!+IED!Uk-_CTvC%wqProu?;QQ4l{P36+5vDyRip*(T084j{`Ue z3#_og4gn%0I3PoT6Dl;c;}8zx2s&^S$Kb+obi!S&8M { handleVerificationReaction(reaction, user).catch((err) => { logger.warn('Error handling verification reaction', err.message); }); + handlePrivateAccessReaction(reaction, user).catch((err) => { + logger.warn('Error handling private access reaction', err.message); + }); }); client.once('ready', () => { diff --git a/server/src/services/privateRoverAccessRequestService.js b/server/src/services/privateRoverAccessRequestService.js index f4635371..c99abfbf 100644 --- a/server/src/services/privateRoverAccessRequestService.js +++ b/server/src/services/privateRoverAccessRequestService.js @@ -10,10 +10,14 @@ const { getSocketIp, normalizeIp } = require('../helpers/ipResolver'); const requestEvents = new EventEmitter(); const REQUEST_COOLDOWN_MS = 15 * 1000; +const DM_APPROVE_EMOJI = '✅'; +const DM_DENY_EMOJI = '❌'; const pendingRequests = new Map(); // requestId -> request const pendingByRequesterRover = new Map(); // `${requesterKey}:${roverId}` -> requestId const lastRequestAtByRequester = new Map(); // requesterKey -> ts +const dmMessages = new Map(); // messageId -> { requestId, adminDiscordId, createdAt } +const grants = new Map(); // `${requesterKey}:${roverId}` -> { requesterKey, roverId, grantedAt, grantedBy, requestId } function normalizeRoverId(value) { return String(value || '').trim(); @@ -25,6 +29,14 @@ function buildRequesterKey(socket) { return `socket:${socket?.id || 'unknown'}`; } +function normalizeRequesterKey(value) { + return String(value || '').trim().toLowerCase(); +} + +function buildGrantKey(requesterKey, roverId) { + return `${normalizeRequesterKey(requesterKey)}:${normalizeRoverId(roverId)}`; +} + function isClosedPrivateRoverRecord(record) { return Boolean(record?.private?.enabled && !record?.privateOpen); } @@ -60,9 +72,21 @@ function listPendingForRequester(socket) { } function getStateForSocket(socket) { + const requesterKey = buildRequesterKey(socket); + const grantedRovers = []; + for (const grant of grants.values()) { + if (grant.requesterKey !== requesterKey) continue; + grantedRovers.push({ + roverId: grant.roverId, + grantedAt: grant.grantedAt, + requestId: grant.requestId || null, + }); + } + grantedRovers.sort((a, b) => b.grantedAt - a.grantedAt); return { requestableRovers: listClosedPrivateRovers(), pendingRequests: listPendingForRequester(socket), + grantedRovers, }; } @@ -83,6 +107,188 @@ function clearPendingForRover(roverId, reason = 'resolved') { } } +function getGrantForRequester(requesterKey, roverId) { + if (!requesterKey || !roverId) return null; + return grants.get(buildGrantKey(requesterKey, roverId)) || null; +} + +function hasClosedPrivateAccessForSocket(socket, roverId) { + if (!socket || !roverId) return false; + const requesterKey = buildRequesterKey(socket); + return Boolean(getGrantForRequester(requesterKey, roverId)); +} + +function listGrantedRoversForRequester(requesterKey) { + const key = normalizeRequesterKey(requesterKey); + if (!key) return []; + const roverIds = []; + for (const grant of grants.values()) { + if (grant.requesterKey !== key) continue; + roverIds.push(grant.roverId); + } + return Array.from(new Set(roverIds)); +} + +function applySocketGrantCache(socket) { + if (!socket) return; + socket.data = socket.data || {}; + const requesterKey = buildRequesterKey(socket); + socket.data.privateClosedAccessRovers = listGrantedRoversForRequester(requesterKey); +} + +function findPendingRequestById(requestId) { + const id = String(requestId || '').trim(); + if (!id) return null; + const request = pendingRequests.get(id); + if (!request || request.status !== 'pending') return null; + return request; +} + +function attachDmMessage(requestId, messageId, adminDiscordId = null) { + const rid = String(requestId || '').trim(); + const mid = String(messageId || '').trim(); + if (!rid || !mid) return; + dmMessages.set(mid, { + requestId: rid, + adminDiscordId: adminDiscordId ? String(adminDiscordId) : null, + createdAt: Date.now(), + }); +} + +function getRequestByMessageId(messageId) { + const mid = String(messageId || '').trim(); + if (!mid) return null; + const map = dmMessages.get(mid); + if (!map) return null; + const request = pendingRequests.get(map.requestId) || null; + if (!request) return null; + return { request, map }; +} + +function getSocketByRequesterKey(requesterKey) { + if (!requesterKey) return null; + if (requesterKey.startsWith('socket:')) { + const socketId = requesterKey.slice('socket:'.length); + return io.sockets.sockets.get(socketId) || null; + } + return null; +} + +function tryAssignClosedPrivateRover(socket, roverId) { + if (!socket || !roverId) return false; + const record = roverManager.rovers.get(String(roverId)); + if (!record) return false; + const access = roverManager.canRequestControl(roverId, socket, { allowUser: true }); + if (!access.ok) { + throw new Error(access.reason || 'Control denied'); + } + const previousJoined = roverManager.getRoversForSocket(socket.id); + roverManager.requestControl(roverId, socket, { allowUser: true }); + previousJoined.forEach((rid) => { + if (rid !== roverId) { + roverManager.releaseControl(rid, socket); + } + }); + roverManager.managerEvents.emit('switch', { socketId: socket.id, roverId: String(roverId) }); + try { + const assignmentService = require('./assignmentService'); + assignmentService.moveAssignment(socket, String(roverId), { releasePrevious: false }); + } catch (err) { + logger.warn('Failed to move assignment after private access grant', { socketId: socket.id, error: err.message }); + } + socket.emit('controlGranted', { roverId: String(roverId) }); + return true; +} + +function approveRequest(requestId, actorDiscordId = null) { + const request = findPendingRequestById(requestId); + if (!request) { + throw new Error('Request not found or already resolved.'); + } + request.status = 'approved'; + request.resolvedAt = Date.now(); + request.resolvedBy = actorDiscordId ? String(actorDiscordId) : null; + pendingByRequesterRover.delete(`${request.requesterKey}:${request.roverId}`); + + const grantKey = buildGrantKey(request.requesterKey, request.roverId); + grants.set(grantKey, { + requesterKey: request.requesterKey, + roverId: request.roverId, + requestId: request.id, + grantedAt: Date.now(), + grantedBy: request.resolvedBy, + }); + + let assignedSocketId = null; + const socket = getSocketByRequesterKey(request.requesterKey); + if (socket) { + applySocketGrantCache(socket); + try { + if (tryAssignClosedPrivateRover(socket, request.roverId)) { + assignedSocketId = socket.id; + } + } catch (err) { + logger.warn('Private rover access approved but assignment failed', { + requestId: request.id, + roverId: request.roverId, + socketId: socket.id, + error: err.message, + }); + } + } + + publishEvent({ + source: 'privateRoverAccessRequest', + type: 'privateRoverAccess.resolved', + payload: { + requestId: request.id, + decision: 'approved', + roverId: request.roverId, + requesterKey: request.requesterKey, + resolvedBy: request.resolvedBy, + resolvedAt: request.resolvedAt, + assignedSocketId, + }, + }); + requestEvents.emit('change', { + reason: 'approved', + requestId: request.id, + roverId: request.roverId, + socketId: assignedSocketId, + }); + return { request, assignedSocketId }; +} + +function denyRequest(requestId, actorDiscordId = null) { + const request = findPendingRequestById(requestId); + if (!request) { + throw new Error('Request not found or already resolved.'); + } + request.status = 'denied'; + request.resolvedAt = Date.now(); + request.resolvedBy = actorDiscordId ? String(actorDiscordId) : null; + pendingByRequesterRover.delete(`${request.requesterKey}:${request.roverId}`); + + publishEvent({ + source: 'privateRoverAccessRequest', + type: 'privateRoverAccess.resolved', + payload: { + requestId: request.id, + decision: 'denied', + roverId: request.roverId, + requesterKey: request.requesterKey, + resolvedBy: request.resolvedBy, + resolvedAt: request.resolvedAt, + }, + }); + requestEvents.emit('change', { + reason: 'denied', + requestId: request.id, + roverId: request.roverId, + }); + return { request }; +} + function createRequest(socket, roverIdRaw) { if (!socket?.id) { throw new Error('Socket required'); @@ -167,6 +373,11 @@ roverManager.managerEvents.on('private', ({ roverId, open } = {}) => { if (!roverId) return; if (open) { clearPendingForRover(roverId, 'opened'); + for (const [key, grant] of grants.entries()) { + if (String(grant.roverId) === String(roverId)) { + grants.delete(key); + } + } } requestEvents.emit('change', { reason: 'private_state', @@ -179,6 +390,11 @@ roverManager.managerEvents.on('rover', ({ roverId, action } = {}) => { if (!roverId) return; if (action === 'removed') { clearPendingForRover(roverId, 'rover_removed'); + for (const [key, grant] of grants.entries()) { + if (String(grant.roverId) === String(roverId)) { + grants.delete(key); + } + } } requestEvents.emit('change', { reason: 'rover', @@ -188,6 +404,8 @@ roverManager.managerEvents.on('rover', ({ roverId, action } = {}) => { }); io.on('connection', (socket) => { + applySocketGrantCache(socket); + function handleRequest({ roverId } = {}, cb = () => {}) { try { const { request, isNew } = createRequest(socket, roverId); @@ -204,11 +422,21 @@ io.on('connection', (socket) => { socket.on('privateRover:requestAccess', handleRequest); socket.on('session:privateRover:requestAccess', handleRequest); + socket.on('session:identify', () => { + applySocketGrantCache(socket); + requestEvents.emit('change', { reason: 'identify', socketId: socket.id }); + }); }); module.exports = { + DM_APPROVE_EMOJI, + DM_DENY_EMOJI, requestEvents, getStateForSocket, createRequest, + hasClosedPrivateAccessForSocket, + attachDmMessage, + getRequestByMessageId, + approveRequest, + denyRequest, }; - diff --git a/server/src/services/roverManager.js b/server/src/services/roverManager.js index fe86bdd0..20529b07 100644 --- a/server/src/services/roverManager.js +++ b/server/src/services/roverManager.js @@ -150,11 +150,19 @@ function shouldApplyPrivateSensorSafety(record) { return !isLockdownAdmin(activeSocket); } +function socketHasClosedPrivateAccess(socket, roverId) { + const list = Array.isArray(socket?.data?.privateClosedAccessRovers) + ? socket.data.privateClosedAccessRovers + : []; + return list.some((id) => String(id) === String(roverId)); +} + function isRoverVisibleToSocket(record, socket) { if (!record) return false; if (!isPrivateRecord(record)) return true; if (isPrivateOpen(record)) return true; - return isLockdownAdmin(socket); + if (isLockdownAdmin(socket)) return true; + return socketHasClosedPrivateAccess(socket, record.id); } function getControlDenialReason(record, socket, options = {}) { @@ -183,6 +191,9 @@ function getControlDenialReason(record, socket, options = {}) { } if (!isPrivateOpen(record)) { if (!isLockdownAdmin(socket)) { + if (socketHasClosedPrivateAccess(socket, record.id)) { + return null; + } return 'Private rover is closed'; } return null;