const fs = require('fs'); const path = require('path'); const pull = require('../server/node_modules/pull-stream'); const STEPS = ['profile', 'ux', 'workflow', 'theme', 'community', 'greeting']; const MARKED_STEPS = ['profile', 'ux', 'workflow', 'theme', 'community']; const FLAG = 'oasis-first-contact'; const PENDING = 'welcome=pending'; const DISMISSED = 'welcome=dismissed'; const DONE = 'welcome=done'; const ssbPathOf = (given) => { if (given) return given; try { return require('../server/ssb_config').path; } catch (_) { return null; } }; const flagPath = (dir) => { try { const state = require('../configs/state-manager'); if (!dir || path.resolve(dir) === state.ssbDir()) return state.statePath(FLAG); } catch (_) {} return path.join(dir, FLAG); }; function readFlag(dir) { if (!dir) return { exists: false, id: '', markers: new Set() }; let raw = ''; try { raw = fs.readFileSync(flagPath(dir), 'utf8'); } catch (_) { return { exists: false, id: '', markers: new Set() }; } const lines = raw.split('\n').map(l => l.trim()).filter(Boolean); const isMarker = (l) => /^(welcome|step)=/.test(l); const markers = new Set(lines.filter(isMarker)); return { exists: true, id: lines.find(l => !isMarker(l)) || '', markers }; } function appendMarker(dir, marker, createIfMissing) { if (!dir) return false; const flag = readFlag(dir); if (flag.markers.has(marker)) return true; if (!flag.exists && !createIfMissing) return false; try { fs.mkdirSync(dir, { recursive: true }); fs.appendFileSync(flagPath(dir), `${marker}\n`); return true; } catch (_) { return false; } } const isPending = (dir) => { if (!dir) return false; const flag = readFlag(dir); if (!flag.exists) return true; if (flag.markers.has(DISMISSED) || flag.markers.has(DONE)) return false; return flag.markers.has(PENDING); }; const bannerVisible = (ssbPath) => isPending(ssbPathOf(ssbPath)); const DEFAULT_LANGUAGE = 'en'; const browserLanguage = (header, supported) => { const list = Array.isArray(supported) ? supported : []; const wanted = String(header || '').slice(0, 1024).split(',').map((part, i) => { const [tag, ...params] = part.trim().split(';'); const qParam = params.map(p => p.trim()).find(p => /^q=/i.test(p)); const q = qParam ? Number(qParam.slice(2)) : 1; return { tag: String(tag || '').trim().toLowerCase().replace(/_/g, '-'), q: Number.isFinite(q) ? q : 0, i }; }).filter(w => w.tag && w.tag !== '*' && w.q > 0).sort((a, b) => b.q - a.q || a.i - b.i); for (const w of wanted) { if (list.includes(w.tag)) return w.tag; const base = w.tag.split('-')[0]; if (list.includes(base)) return base; } return ''; }; const firstRunLanguage = ({ fresh, isPublic, configured, cookie, header, supported } = {}) => { const list = Array.isArray(supported) ? supported : []; if (!fresh || isPublic) return { settled: true, language: '' }; if (list.includes(configured) && configured !== DEFAULT_LANGUAGE) return { settled: true, language: '' }; const picked = (list.includes(cookie) ? cookie : '') || browserLanguage(header, list); if (!picked && !String(header || '').trim()) return { settled: false, language: '' }; return { settled: true, language: picked }; }; module.exports = ({ cooler, ssbPath } = {}) => { const dir = () => ssbPathOf(ssbPath); const ownMessages = async () => { const ssb = await cooler.open(); const mine = ssb.id; const all = await new Promise((resolve, reject) => pull(ssb.createUserStream({ id: mine, reverse: true, limit: 2000 }), pull.collect((err, msgs) => err ? reject(err) : resolve(msgs || []))) ); return { mine, messages: all.filter(m => m && m.value && m.value.author === mine) }; }; const hasProfile = (mine, messages) => messages.some(m => { const c = m.value.content; if (!c || typeof c !== 'object' || c.type !== 'about' || c.about !== mine) return false; return String(c.name || '').trim().length > 0 || String(c.image || '').trim().length > 0; }); const hasGreeted = (messages) => messages.some(m => { const c = m.value.content; return c && typeof c === 'object' && c.type === 'feed' && String(c.text || '').trim().length > 0; }); const hasFederated = (mine, messages) => { try { const gossip = JSON.parse(fs.readFileSync(path.join(dir(), 'gossip.json'), 'utf8')); if (Array.isArray(gossip) && gossip.length > 0) return true; } catch (_) {} return messages.some(m => { const c = m.value.content; return c && typeof c === 'object' && c.type === 'contact' && c.following === true && c.contact && c.contact !== mine; }); }; return { STEPS, flagPath() { return flagPath(dir()); }, firstContactSeen(feedId) { const flag = readFlag(dir()); return flag.exists && (!feedId || flag.id === String(feedId)); }, begin(feedId) { const target = dir(); if (!target) return false; const flag = readFlag(target); const closed = flag.markers.has(DISMISSED) || flag.markers.has(DONE); const markers = closed ? [...flag.markers] : [...new Set([PENDING, ...flag.markers])]; try { fs.mkdirSync(target, { recursive: true }); fs.writeFileSync(flagPath(target), `${String(feedId || '')}\n${new Date().toISOString()}\n${markers.join('\n')}\n`); return true; } catch (_) { return false; } }, adopt(feedId) { const target = dir(); if (!target || !feedId) return false; try { fs.mkdirSync(target, { recursive: true }); fs.writeFileSync(flagPath(target), `${String(feedId)}\n${new Date().toISOString()}\n${DONE}\n`); return true; } catch (_) { return false; } }, markStep(step) { if (!MARKED_STEPS.includes(step)) return false; if (!isPending(dir())) return true; return appendMarker(dir(), `step=${step}`); }, dismiss() { return appendMarker(dir(), DISMISSED, true); }, isVisible() { return isPending(dir()); }, async status(available) { const target = dir(); const flag = readFlag(target); const usable = STEPS.filter(s => !available || available[s] !== false); let mine = ''; let messages = []; try { ({ mine, messages } = await ownMessages()); } catch (_) { mine = ''; messages = []; } const steps = { profile: hasProfile(mine, messages) || flag.markers.has('step=profile'), ux: flag.markers.has('step=ux'), workflow: flag.markers.has('step=workflow'), theme: flag.markers.has('step=theme'), community: hasFederated(mine, messages) || flag.markers.has('step=community'), greeting: hasGreeted(messages) }; const profile = { id: mine, name: '', description: '', image: '' }; for (const m of messages) { const c = m.value.content; if (!c || typeof c !== 'object' || c.type !== 'about' || c.about !== mine) continue; if (!profile.name && String(c.name || '').trim()) profile.name = String(c.name).trim(); if (!profile.description && String(c.description || '').trim()) profile.description = String(c.description).trim(); if (!profile.image && String(c.image || '').trim()) profile.image = String(c.image).trim(); if (profile.name && profile.description && profile.image) break; } const done = usable.filter(s => steps[s]).length; const complete = done === usable.length; if (complete && isPending(target)) appendMarker(target, DONE); return { id: flag.id, pending: isPending(target), dismissed: flag.markers.has(DISMISSED), steps, profile, usable, done, total: usable.length, complete }; } }; }; module.exports.bannerVisible = bannerVisible; module.exports.STEPS = STEPS; module.exports.browserLanguage = browserLanguage; module.exports.firstRunLanguage = firstRunLanguage;