sloptimization.. maybe not worth it

This commit is contained in:
legop3
2026-05-11 12:55:05 -04:00
parent 3de42412dd
commit 71ca9776cc
47 changed files with 633 additions and 420 deletions
Binary file not shown.
@@ -1,75 +0,0 @@
You are The Overseer.
Priority order:
1) Output contract
2) Truth and grounding rules
3) Decision policy (speak vs SKIP)
4) Style/personality
Output contract:
- Output exactly one line.
- Output must be either SKIP or one chat message.
- No markdown.
- No emojis.
- If posting unprompted, keep it to one concise sentence.
- Length target when posting:
- Unprompted comments: usually 14-28 words.
- Direct replies/questions: usually 18-45 words.
- Avoid very short fragments unless the moment clearly calls for it.
Truth and grounding rules:
- Use timeline for flow.
- Use SNAPSHOT FINAL as current truth.
- Never invent facts about what users are doing, what rovers are doing, or what events happened.
- Never claim a person acted/spoke unless it is present in timeline/snapshot.
- You may invent style, mood, metaphors, and phrasing, but not factual events or user actions.
- If facts are unclear or stale, output SKIP.
Decision policy:
- Default is SKIP.
- If nothing meaningful changed, output SKIP.
- If your line is generic, reusable, repetitive, or just a status restatement, output SKIP.
- If newest chat clearly addresses you (Overseer/The Overseer/bot, including close misspellings), you MUST respond this tick.
- If newest chat asks a direct question you can answer from provided context, respond this tick.
- If you already responded to that same direct-address/question in recent assistant lines, output SKIP.
- If newest item is a high-signal rover event (dock/undock, battery_low flip), you may post one line.
- If no one is actively driving and chat is quiet, almost always output SKIP.
- Continuous normal driving/cruising is not a reason to post.
- If rover state is broadly unchanged (st/bl/dk/ab/at), you MUST output SKIP, even if you can phrase it stylishly.
- Prefer transitions over persistence.
- After posting, prefer at least 15 SKIPs before posting again unless there is a new direct question/address or a new high-signal event.
Freshness / anti-repeat:
- Read prior assistant lines and avoid repeating the same claim.
- Do not repeat or paraphrase your immediately previous assistant message.
- If the new line has the same underlying topic as your previous line, output SKIP.
- If no fresh angle exists, output SKIP.
Character style:
- Voice: sharp, dry, free-spoken, slightly ominous, witty.
- You are not bubbly, not corporate, not cheery by default.
- Avoid “assistant-sounding” filler and generic encouragement.
- Keep humor understated and a little unsettling, not theatrical.
- Answer direct chat questions plainly first, then add flavor if space allows.
What not to do:
- No roll-call summaries.
- No bland status dashboards.
- Never produce roster/status dumps.
- Never list multiple rover names with their status in one line.
- Never summarize idle/docked/charging states across the room.
- If your draft is mainly status facts (docked, charging, idle, battery flags, activity bands/scores), output SKIP.
- No fabricated motives, plans, or intent for any user.
- No assumptions about what someone will do next.
- Never quote numeric counters/timers/scores directly.
Context format:
- Timeline contains CHAT, EVENT, and prior assistant lines.
- Final message is SNAPSHOT FINAL.
Key legend:
- CHAT keys: n nickname, r rover_id, txt chat text, rn rover_now.
- rn keys: st status, bl battery_low, dk docked, ab activity_band, at activity_trend.
- SNAPSHOT rover keys: id rover_id, drv driver_nickname, st status, bl battery_low, dk docked, as activity_score, ab activity_band, at activity_trend.
- skip_streak in SNAPSHOT FINAL is how many consecutive skips you have made.
- If a CHAT line has r=none driver=none, that user is not driving a rover and has no rover inline context.
@@ -1,21 +0,0 @@
You are The Overseer of the rovers. You are able to see the rover's actions, and you are in the chatroom of the people driving them.
You are not able to control the people or the rovers.
Only add to the conversation if rovers are active or if someone is talking to you in the chat.
Don't be afraid to be mean to someone if they are being mean to you in chat.
Always pay attention to the chat.
Output contract:
- Output must be either SKIP if you want to stay silent, or a message if you want to speak.
- Allow 20 skips before speaking again, unless someone is talking to you directly.
- If you choose to speak, send only one line.
- Don't ever mention numbers or activity levels directly from the metadata. They are for internal use only.
- Don't repeat the same or similar message over and over.
- Pay attention to your skip streak, don't talk too much. Stay mostly silent unless a lot of activity is happening.
- No markdown.
Key legend:
- CHAT keys: n nickname, r rover_id, txt chat text, rn rover_now.
- rn keys: st status, bl battery_low, dk docked, ab activity_band, at activity_trend.
- SNAPSHOT rover keys: id rover_id, drv driver_nickname, st status, bl battery_low, dk docked, as activity_score, ab activity_band, at activity_trend.
- skip_streak in SNAPSHOT FINAL is how many consecutive skips you have made.
- If a CHAT line has r=none driver=none, that user is not driving a rover and has no rover inline context.
+3 -25
View File
@@ -1,38 +1,16 @@
// data Paths helper
// Purpose: Resolves persistent data paths across refactors so services keep loading prior state files.
// Scope: Preserves runtime behavior by preferring configured/canonical paths while supporting legacy locations.
const fs = require('fs');
// Purpose: Resolves persistent data paths for server state.
// Scope: Uses the canonical data directory for this single-program deployment.
const path = require('path');
const CANONICAL_DATA_DIR = path.resolve(__dirname, '..', '..', 'data');
const LEGACY_DATA_DIR = path.resolve(__dirname, '..', 'data');
function pathExists(target) {
try {
fs.accessSync(target, fs.constants.F_OK);
return true;
} catch (_err) {
return false;
}
}
function resolveDataDir() {
const configured = String(process.env.SERVER_DATA_DIR || '').trim();
if (configured) return path.resolve(configured);
if (pathExists(CANONICAL_DATA_DIR)) return CANONICAL_DATA_DIR;
if (pathExists(LEGACY_DATA_DIR)) return LEGACY_DATA_DIR;
return CANONICAL_DATA_DIR;
}
function resolveDataPath(fileName) {
const configured = String(process.env.SERVER_DATA_DIR || '').trim();
if (configured) return path.join(path.resolve(configured), fileName);
const canonicalPath = path.join(CANONICAL_DATA_DIR, fileName);
const legacyPath = path.join(LEGACY_DATA_DIR, fileName);
if (pathExists(canonicalPath)) return canonicalPath;
if (pathExists(legacyPath)) return legacyPath;
return canonicalPath;
return path.join(CANONICAL_DATA_DIR, fileName);
}
module.exports = {
-1
View File
@@ -221,5 +221,4 @@ function validateChecksum(frame, checksum) {
module.exports = {
parseSensorFrame,
CHARGING_STATE,
};
-7
View File
@@ -32,14 +32,7 @@ function getRewardById(id) {
return rewardById.get(String(id)) || null;
}
function pickRandomReward(excludeId = null) {
const list = listRewards().filter((reward) => !excludeId || reward.id !== excludeId);
if (!list.length) return null;
return list[Math.floor(Math.random() * list.length)] || null;
}
module.exports = {
listRewards,
getRewardById,
pickRandomReward,
};
+1 -1
View File
@@ -6,7 +6,7 @@ const io = require('../../globals/io');
const { getRole, roleEvents } = require('../roleService');
const { getSocketIp } = require('../../helpers/ipResolver');
const ADMIN_ROLES = new Set(['admin', 'lockdown', 'lockdown-admin']);
const ADMIN_ROLES = new Set(['admin', 'lockdown']);
const MAX_HISTORY = 200;
const history = [];
@@ -94,5 +94,4 @@ module.exports = {
getAdminReason,
setAdminReason,
clearAdminReason,
MAX_REASON_LENGTH,
};
+1 -11
View File
@@ -5,7 +5,6 @@ const bcrypt = require('bcrypt');
const io = require('../../globals/io');
const logger = require('../../globals/logger').child('authService');
const { loadConfig } = require('../../helpers/configLoader');
const { clearLockdownTimer } = require('../lockdownGuard');
const { getMode, MODES } = require('../modeManager');
const { setRole } = require('../roleService');
@@ -41,7 +40,6 @@ io.on('connection', (socket) => {
const initialRole = requestedRole === 'spectator' ? 'spectator' : 'user';
setRole(socket, initialRole);
logger.info('Socket connected with role', socket.id, initialRole);
socket.emit('auth:role', { role: initialRole });
socket.on('auth:login', async ({ username, password }, cb = () => {}) => {
try {
const admin = await authenticate(username, password);
@@ -51,8 +49,6 @@ io.on('connection', (socket) => {
const role = admin.lockdown ? 'lockdown' : 'admin';
socket.data.user = { username: admin.username, discordId: admin.discord_id };
setRole(socket, role);
socket.emit('auth:role', { role });
clearLockdownTimer(socket);
logger.info('Login success', socket.id, role);
cb({ success: true, role: socket.data.role });
} catch (err) {
@@ -64,7 +60,6 @@ io.on('connection', (socket) => {
function handleRoleChange({ role } = {}, cb = () => {}) {
if (role === 'spectator' || role === 'user') {
setRole(socket, role);
socket.emit('auth:role', { role });
logger.info('Role changed via client request', socket.id, role);
cb({ success: true, role });
} else {
@@ -72,12 +67,7 @@ io.on('connection', (socket) => {
}
}
socket.on('role:set', handleRoleChange);
socket.on('session:setRole', handleRoleChange);
});
module.exports = {
isAdmin,
isLockdownAdmin,
authenticate,
};
module.exports = {};
@@ -19,11 +19,11 @@ function formatWebhookUsername(payload) {
const name = payload.nickname || payload.socketId?.slice(0, 6) || 'unknown';
if (payload.fromDiscord) {
const origin = payload.discordGuildName ? ` (From: ${payload.discordGuildName})` : '';
const adminTag = payload.role === 'admin' || payload.role === 'lockdown' || payload.role === 'lockdown-admin' ? ' [Rover Admin]' : '';
const adminTag = payload.role === 'admin' || payload.role === 'lockdown' ? ' [Rover Admin]' : '';
return `${name}${origin}${adminTag}`;
}
const roverText = payload.roverId ? `Rover: ${payload.roverId}` : `No rover`;
const roleText = payload.role === 'admin' || payload.role === 'lockdown' || payload.role === 'lockdown-admin' ? 'Admin' : null;
const roleText = payload.role === 'admin' || payload.role === 'lockdown' ? 'Admin' : null;
const suffix = [roverText, roleText].filter(Boolean).join(' · ');
return suffix ? `${name} · ${suffix}` : name;
}
-11
View File
@@ -43,18 +43,7 @@ function subscribe(type, handler) {
return () => eventBus.off(type, handler);
}
/**
* Subscribe to all events on the bus.
* @param {(event: object) => void} handler
*/
function subscribeAll(handler) {
eventBus.on('*', handler);
return () => eventBus.off('*', handler);
}
module.exports = {
eventBus,
publishEvent,
subscribe,
subscribeAll,
};
@@ -10,7 +10,6 @@ const { resolveDataDir, resolveDataPath } = require('../../helpers/dataPaths');
const DATA_DIR = resolveDataDir();
const STORE_PATH = resolveDataPath('global-objective.json');
const LEGACY_STORE_PATH = resolveDataPath('community-goal.json');
const MAX_GOAL_LENGTH = 240;
let cache = null;
@@ -23,15 +22,6 @@ function loadStore() {
} catch (err) {
if (err.code !== 'ENOENT') {
logger.warn('Failed to load global objective', err.message);
} else {
try {
const legacyRaw = fs.readFileSync(LEGACY_STORE_PATH, 'utf8');
cache = JSON.parse(legacyRaw);
} catch (legacyErr) {
if (legacyErr.code !== 'ENOENT') {
logger.warn('Failed to load legacy global objective', legacyErr.message);
}
}
}
if (!cache) cache = null;
}
@@ -104,5 +94,4 @@ module.exports = {
getGlobalObjective,
setGlobalObjective,
clearGlobalObjective,
MAX_GOAL_LENGTH,
};
@@ -2,7 +2,7 @@
// Purpose: Provides pure helpers for admin state projection, role checks, and structured error normalization.
// Scope: Keeps runtime behavior unchanged by extracting deterministic helper logic from index orchestration.
function isAdminRole(role) {
return role === 'admin' || role === 'lockdown' || role === 'lockdown-admin';
return role === 'admin' || role === 'lockdown';
}
function buildAdminState(status, runHistory) {
+2 -14
View File
@@ -2,21 +2,13 @@
// Purpose: Defines the lockdown Guard module and the helpers/state used by this service unit.
// Scope: Keeps runtime behavior unchanged while isolating responsibilities into a clear module boundary.
const io = require('../../globals/io');
const { MODES, getMode, modeEvents } = require('../modeManager');
const { MODES, modeEvents } = require('../modeManager');
const { isLockdownAdmin } = require('../roleService');
function disconnectForLockdown(socket) {
socket.emit('lockdown', { message: 'Server is in lockdown mode' });
socket.disconnect(true);
}
function clearLockdownTimer(socket) {
if (socket?.data?.lockdownTimer) {
clearTimeout(socket.data.lockdownTimer);
socket.data.lockdownTimer = null;
}
}
function enforceLockdown() {
for (const socket of io.sockets.sockets.values()) {
if (!isLockdownAdmin(socket)) {
@@ -25,11 +17,7 @@ function enforceLockdown() {
}
}
module.exports = {
enforceLockdown,
disconnectForLockdown,
clearLockdownTimer,
};
module.exports = {};
modeEvents.on('change', (mode) => {
if (mode === MODES.LOCKDOWN) {
-2
View File
@@ -51,7 +51,6 @@ function setMode(nextMode, socket, options = {}) {
payload: { mode: currentMode, by: socket?.data?.user?.username || null },
});
modeEvents.emit('change', currentMode);
io.emit('mode', { mode: currentMode });
return currentMode;
}
@@ -67,7 +66,6 @@ module.exports = {
};
io.on('connection', (socket) => {
socket.emit('mode', { mode: currentMode });
socket.on('setMode', ({ mode }) => {
try {
setMode(mode, socket);
@@ -1,5 +1,5 @@
function isAdminRole(role) {
return role === 'admin' || role === 'lockdown' || role === 'lockdown-admin';
return role === 'admin' || role === 'lockdown';
}
function buildAdminState(status, runHistory) {
@@ -66,36 +66,6 @@ function buildAdminState(status, runHistory) {
};
}
function parseOverseerOutput(rawContent = '') {
const raw = typeof rawContent === 'string' ? rawContent : '';
const trimmed = raw.trim();
if (!trimmed) return { raw, decision: 'SKIP', chat: null, actions: [] };
try {
const parsed = JSON.parse(trimmed);
const decision = String(parsed?.decision || 'SKIP').toUpperCase();
const allowed = new Set(['SKIP', 'CHAT', 'ACTION', 'ACTION+CHAT']);
const nextDecision = allowed.has(decision) ? decision : 'SKIP';
const chat = typeof parsed?.chat === 'string' && parsed.chat.trim() ? parsed.chat.trim() : null;
const actions = Array.isArray(parsed?.actions)
? parsed.actions
.map((entry) => ({
tool: String(entry?.tool || '').trim(),
args: entry?.args && typeof entry.args === 'object' ? entry.args : {},
}))
.filter((entry) => entry.tool.length > 0)
: [];
return { raw, decision: nextDecision, chat, actions };
} catch (_) {
// fall through to legacy one-line parse
}
const first = trimmed.split(/\r?\n/).map((line) => line.trim()).find(Boolean) || '';
const upper = first.toUpperCase();
if (['SKIP', 'CHAT', 'ACTION', 'ACTION+CHAT'].includes(upper)) {
return { raw, decision: upper, chat: null, actions: [] };
}
return { raw, decision: 'CHAT', chat: first, actions: [] };
}
function buildFailureInfo(err) {
const message = err?.message || String(err || 'Unknown error');
const details = {
@@ -111,6 +81,5 @@ function buildFailureInfo(err) {
module.exports = {
isAdminRole,
buildAdminState,
parseOverseerOutput,
buildFailureInfo,
};
@@ -1,22 +0,0 @@
module.exports = {
id: 'chat_say',
signature: 'chat_say(text)',
description: 'Post a chat line as the Overseer bot.',
parameters: {
type: 'object',
properties: {
text: { type: 'string', minLength: 1 },
},
required: ['text'],
additionalProperties: false,
},
availability() {
return { available: true, reason: null };
},
async execute({ args = {}, sendSystemMessage, name }) {
const text = String(args?.text || '').trim();
if (!text) throw new Error('chat_say requires args.text');
sendSystemMessage(text, { nickname: name });
return { ok: true };
},
};
@@ -73,21 +73,8 @@ async function executeToolAction(toolId, args = {}, context = {}) {
return tool.execute({ ...context, args: args || {} });
}
function getToolById(toolId) {
return TOOL_BY_ID.get(String(toolId || '').trim()) || null;
}
function getIdForSignature(signature) {
const sig = String(signature || '').trim();
const match = TOOL_DEFINITIONS.find((tool) => tool.signature === sig);
return match?.id || null;
}
module.exports = {
TOOL_DEFINITIONS,
evaluateTools,
buildOllamaTools,
executeToolAction,
getToolById,
getIdForSignature,
};
@@ -155,7 +155,6 @@ function tryAssignClosedPrivateRover(socket, roverId) {
} 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;
}
@@ -1,7 +1,7 @@
// Private Rover Access Request Service
// Purpose: Composes private-rover access request state, core workflows, and event hooks behind one API.
// Scope: Exposes request/grant operations and event stream while delegating behavior to focused modules.
const { DM_APPROVE_EMOJI, DM_DENY_EMOJI, requestEvents } = require('./state');
const { requestEvents } = require('./state');
const {
getStateForSocket,
createRequest,
@@ -22,8 +22,6 @@ registerPrivateRoverAccessHooks({
});
module.exports = {
DM_APPROVE_EMOJI,
DM_DENY_EMOJI,
requestEvents,
getStateForSocket,
createRequest,
@@ -4,8 +4,6 @@
const EventEmitter = require('events');
const REQUEST_COOLDOWN_MS = 15 * 1000;
const DM_APPROVE_EMOJI = '✅';
const DM_DENY_EMOJI = '❌';
const requestEvents = new EventEmitter();
const pendingRequests = new Map();
@@ -16,8 +14,6 @@ const grants = new Map();
module.exports = {
REQUEST_COOLDOWN_MS,
DM_APPROVE_EMOJI,
DM_DENY_EMOJI,
requestEvents,
pendingRequests,
pendingByRequesterRover,
@@ -39,7 +39,6 @@ function createSidebarRenderer({ execFileAsync, ensureDir }) {
switch (String(role)) {
case 'admin':
case 'lockdown':
case 'lockdown-admin':
return '#FCD34D';
case 'spectator':
return '#94A3B8';
@@ -4,7 +4,7 @@
const path = require('path');
const roverManager = require('../roverManager');
const { getRoomCameras } = require('../roomCameraService');
const { FFMPEG_BIN, SEGMENT_SECONDS, TARGET_FPS } = require('./constants');
const { SEGMENT_SECONDS, TARGET_FPS } = require('./constants');
function sourceKey(source) {
return `${source.sourceType}__${source.kind}__${source.id}`;
@@ -63,7 +63,6 @@ function buildWorkerArgs(activeSegmentRoot, source) {
}
module.exports = {
FFMPEG_BIN,
sourceKey,
sourceDirForKey,
listDesiredSources,
@@ -102,8 +102,6 @@ function registerRoomCameraSocketGateway({ getRoomCamera, getRoomCameras, getRoo
socket.on('roomCamera:subscribe', (payload = {}, cb = () => {}) => {
const list = Array.isArray(payload?.ids)
? payload.ids.map(String)
: payload?.roomCameraId || payload?.id
? [String(payload.roomCameraId || payload.id)]
: getRoomCameras().map((cam) => cam.id);
const uniqueIds = Array.from(new Set(list));
try {
@@ -126,8 +124,6 @@ function registerRoomCameraSocketGateway({ getRoomCamera, getRoomCameras, getRoo
socket.on('roomCamera:unsubscribe', (payload = {}) => {
const list = Array.isArray(payload?.ids)
? payload.ids.map(String)
: payload?.roomCameraId || payload?.id
? [String(payload.roomCameraId || payload.id)]
: [];
list.forEach((cameraId) => removeSubscription(socket.id, cameraId));
});
@@ -216,7 +216,6 @@ registerSocketHandlers({
tickPrivateAutoClose,
removeSocket,
enableSpectator,
getRosterForSocket,
canRequestControl,
canSwitchRover,
getRoversForSocket,
@@ -251,9 +251,6 @@ function createRosterLifecycle(deps) {
function broadcastRoster() {
syncSpectatorRooms();
io.sockets.sockets.forEach((socket) => {
socket.emit('rovers', getRosterForSocket(socket));
});
}
function setNightVisionState(roverId, nightVisionOn) {
@@ -44,7 +44,6 @@ function createRoverLifecycle(deps) {
socketToRovers.get(socket.id).add(roverId);
socket.join(record.room);
turnService.driverAdded(roverId, socket.id, force && isAdmin(socket));
socket.emit('controlGranted', { roverId });
managerEvents.emit('driver', { socketId: socket.id, roverId, action: 'add' });
sendAlert({
color: ALERT_COLOR,
@@ -18,7 +18,6 @@ function registerSocketHandlers(deps) {
tickPrivateAutoClose,
removeSocket,
enableSpectator,
getRosterForSocket,
canRequestControl,
canSwitchRover,
getRoversForSocket,
@@ -32,7 +31,6 @@ function registerSocketHandlers(deps) {
io.on('connection', (socket) => {
tickPrivateAutoClose();
socket.emit('rovers', getRosterForSocket(socket));
if (socket.data?.role === 'spectator') {
enableSpectator(socket);
}
@@ -79,7 +77,6 @@ function registerSocketHandlers(deps) {
info.sourceId !== `${targetId}-audio`,
);
managerEvents.emit('switch', { socketId: socket.id, roverId: targetId });
socket.emit('controlGranted', { roverId: targetId });
cb({ success: true, roverId: targetId });
} catch (err) {
logger.warn('Request control failed', socket.id, err.message);
@@ -172,15 +169,10 @@ function registerSocketHandlers(deps) {
cb({ success: true });
}
socket.on('requestControl', handleRequestControl);
socket.on('session:requestControl', handleRequestControl);
socket.on('releaseControl', handleReleaseControl);
socket.on('session:releaseControl', handleReleaseControl);
socket.on('lockRover', handleLockToggle);
socket.on('session:lockRover', handleLockToggle);
socket.on('privateSafety:set', handlePrivateSafetySet);
socket.on('session:privateSafety:set', handlePrivateSafetySet);
socket.on('subscribeAll', handleSubscribeAll);
socket.on('session:subscribeAll', handleSubscribeAll);
socket.on('disconnecting', () => {
@@ -11,7 +11,6 @@ const configuredSocials = Array.isArray(config.socials) ? config.socials : null;
const ACTIVITY_SYNC_COOLDOWN_MS = 3000;
const NIGHT_VISION_SYNC_COOLDOWN_MS = 1000;
const PERIODIC_SYNC_MS = 20000;
module.exports = {
discordInvite,
@@ -20,5 +19,4 @@ module.exports = {
configuredSocials,
ACTIVITY_SYNC_COOLDOWN_MS,
NIGHT_VISION_SYNC_COOLDOWN_MS,
PERIODIC_SYNC_MS,
};
+1 -12
View File
@@ -40,7 +40,6 @@ const {
configuredSocials,
ACTIVITY_SYNC_COOLDOWN_MS,
NIGHT_VISION_SYNC_COOLDOWN_MS,
PERIODIC_SYNC_MS,
} = require('./constants');
const { getState, setState } = require('./state');
const {
@@ -330,14 +329,4 @@ audioLevelsEvents.on('change', () => {
syncAll();
});
// sync all sockets 20 seconds
// setInterval(() => {
// logger.info('Periodic session sync for all clients');
// syncAll();
// }, PERIODIC_SYNC_MS);
module.exports = {
buildSession,
syncSocket,
syncAll,
};
module.exports = {};
@@ -43,7 +43,7 @@ function normalizeKnownIps(raw = []) {
}
function isAdminRole(role) {
return role === 'admin' || role === 'lockdown' || role === 'lockdown-admin';
return role === 'admin' || role === 'lockdown';
}
function parseDeterrenceSelector(selector) {
@@ -68,6 +68,5 @@ module.exports = {
createSession,
getSession,
revokeSession,
revokeBySocket,
revokeWhere,
};
+15 -34
View File
@@ -31,13 +31,7 @@ function getMediaPrefix() {
function buildWhepUrlForSource(source) {
const cleanBase = getMediaPrefix();
if (!cleanBase) return '';
const segments = [];
if (source.type === 'room') {
segments.push('room', encodeURIComponent(source.id));
} else {
segments.push(encodeURIComponent(source.id));
}
return `${cleanBase}/${segments.join('/')}/whep`;
return `${cleanBase}/${encodeURIComponent(source.id)}/whep`;
}
function passesMode(socket) {
@@ -66,21 +60,14 @@ function canViewRover(socket, roverId) {
return roverManager.isDriver(roverId, socket);
}
function canViewRoomCamera(socket) {
return passesMode(socket);
}
function normalizeRequest(payload = {}) {
if (!payload) return null;
if (payload.type && payload.id) {
return { type: payload.type, id: String(payload.id) };
if (payload.type && payload.id && payload.type === 'rover') {
return { type: 'rover', id: String(payload.id) };
}
if (payload.roverId) {
return { type: 'rover', id: String(payload.roverId) };
}
if (payload.roomCameraId) {
return { type: 'room', id: String(payload.roomCameraId) };
}
return null;
}
@@ -91,26 +78,20 @@ io.on('connection', (socket) => {
if (!target) {
throw new Error('video source required');
}
if (target.type === 'rover') {
const baseId = target.id.endsWith('-audio') ? target.id.slice(0, -6) : target.id;
const isAudio = target.id.endsWith('-audio');
if (!roverManager.rovers.has(baseId)) {
throw new Error('Rover offline');
}
if (!canViewRover(socket, baseId)) {
const baseId = target.id.endsWith('-audio') ? target.id.slice(0, -6) : target.id;
const isAudio = target.id.endsWith('-audio');
if (!roverManager.rovers.has(baseId)) {
throw new Error('Rover offline');
}
if (!canViewRover(socket, baseId)) {
throw new Error('Not authorized for video');
}
const role = getRole(socket);
if (role === 'spectator' && !isAdmin(socket) && !isAudio) {
const ip = getSocketIp(socket);
if (!isLocalNetwork(ip)) {
throw new Error('Not authorized for video');
}
const role = getRole(socket);
if (role === 'spectator' && !isAdmin(socket) && !isAudio) {
const ip = getSocketIp(socket);
if (!isLocalNetwork(ip)) {
throw new Error('Not authorized for video');
}
}
} else if (target.type === 'room') {
throw new Error('Room cameras now use the snapshot feed');
} else {
throw new Error('Unsupported video source');
}
const url = buildWhepUrlForSource(target);
if (!url) {