'use strict'; /* Reader settings per login session (docs/architecture.md, "Reader settings per session"). * * The user's own reader settings (`user.settings`) are the "latest" set. A session that never changed a * reader setting follows the latest set; the first change copies the latest set into the session and * locks it there, and also makes it the new latest set. Using a locked session makes its set the latest * again, so a new login starts from the last active session. With `readerPerSession: false` there is * one set per user, as before; locked sets are kept and return when the switch is turned on again. */ const crypto = require('crypto'); const READER_KEYS = ['startMode', 'lastMode', 'adapt', 'skipExtras', 'skipCovers', 'flowPageGap', 'pageTone', 'zoom']; const perSession = settings => settings.readerPerSession !== false; const publicId = key => crypto.createHash('sha256').update('reader-session:' + key).digest('hex').slice(0, 16); function pick(settings) { const out = {}; for (const k of READER_KEYS) if (settings[k] !== undefined) out[k] = k === 'zoom' ? { ...settings[k] } : settings[k]; return out; } // `base` is the user's merged settings (defaults + user.settings); the session's locked set wins over it. function effective(base, session) { if (!perSession(base) || !session?.reader) return base; return { ...base, ...pick(session.reader), zoom: { ...session.reader.zoom } }; } // Splits a validated settings patch: reader keys go to the session (locking it) and to the latest set. function applyPatch({ user, base, session, key, patch }) { const s = (user.settings = user.settings || {}); const reader = pick(patch), rest = Object.fromEntries(Object.entries(patch).filter(([k]) => !READER_KEYS.includes(k))); Object.assign(s, rest); if (!Object.keys(reader).length) return; if (perSession({ ...base, ...rest }) && session) { session.reader = { ...pick(effective(base, session)), ...reader }; Object.assign(s, pick(session.reader)); user.readerFrom = publicId(key); } else { Object.assign(s, reader); user.readerFrom = null; } } // A request from a locked session makes its set the latest one. Returns true when something changed. function noteActive(user, base, session, key) { if (!perSession(base) || !session?.reader) return false; const id = publicId(key); if (user.readerFrom === id) return false; user.settings = { ...(user.settings || {}), ...pick(session.reader) }; user.readerFrom = id; return true; } function deviceLabel(agent) { const a = String(agent || ''); if (!a) return 'Unknown device'; const os = /iPhone/.test(a) ? 'iPhone' : /iPad/.test(a) ? 'iPad' : /Android/.test(a) ? 'Android' : /Windows/.test(a) ? 'Windows' : /Mac OS X|Macintosh/.test(a) ? 'Mac' : /CrOS/.test(a) ? 'ChromeOS' : /Linux/.test(a) ? 'Linux' : ''; const browser = /Edg\//.test(a) ? 'Edge' : /OPR\//.test(a) ? 'Opera' : /Firefox\//.test(a) ? 'Firefox' : /Chrome\//.test(a) ? 'Chrome' : /Safari\//.test(a) ? 'Safari' : ''; return [browser, os].filter(Boolean).join(' on ') || a.slice(0, 60); } // What Settings shows: every login session of the user with its state and effective reader set. function overview(user, base, sessions, currentKey) { const list = Object.entries(sessions).filter(([, s]) => s.u === user.name).map(([key, s]) => ({ id: publicId(key), device: deviceLabel(s.agent), created: s.t, seen: s.seen || s.t, current: key === currentKey, locked: !!s.reader, reader: pick(effective(base, s)), })).sort((a, b) => b.current - a.current || b.seen - a.seen); return { enabled: perSession(base), current: currentKey ? publicId(currentKey) : null, latestFrom: user.readerFrom || null, latest: pick(base), sessions: list }; } // Settings actions on one of the user's sessions: 'follow' drops its lock, 'latest' makes its set the latest. function act(user, base, sessions, id, action) { const entry = Object.entries(sessions).find(([key, s]) => s.u === user.name && publicId(key) === id); if (!entry) return false; const [key, session] = entry; if (action === 'follow') { delete session.reader; if (user.readerFrom === id) user.readerFrom = null; return true; } if (action === 'latest') { user.settings = { ...(user.settings || {}), ...pick(effective(base, session)) }; user.readerFrom = session.reader ? publicId(key) : null; return true; } return false; } module.exports = { READER_KEYS, publicId, pick, effective, applyPatch, noteActive, deviceLabel, overview, act };