'use strict' const crypto = require('hypercore-crypto') const b4a = require('b4a') function checksumSettingsSnapshot (obj) { const json = JSON.stringify(obj || {}) return b4a.toString(crypto.hash(b4a.from(json)), 'hex').slice(0, 16) } /** * Prune compose drafts for deleted or inaccessible channels (P409-18). */ function pruneStaleComposeDrafts (composeDrafts, { channelIds, canPostByChannel = {} } = {}) { const allowed = new Set(channelIds || []) const out = { ...(composeDrafts || {}) } const removed = [] for (const chId of Object.keys(out)) { if (!allowed.has(chId)) { removed.push(chId) delete out[chId] continue } if (canPostByChannel[chId] === false) { removed.push(chId) delete out[chId] } } return { composeDrafts: out, removed } } /** * Cross-reference messages, reactions, pins before renderer (P409-17). */ function auditMessageReactionCrossRefs ({ messages = [], messageById = {}, reactions = {}, pins = [] } = {}) { const historyIds = new Set(messages.map((m) => m.id)) const issues = [] for (const messageId of Object.keys(reactions)) { if (!messageById[messageId] && !historyIds.has(messageId)) { issues.push({ kind: 'reaction-orphan', messageId }) } } for (const pin of pins) { const mid = pin.messageId if (!mid) continue if (!pin.message && !messageById[mid] && !historyIds.has(mid)) { issues.push({ kind: 'pin-missing-body', messageId: mid }) } } return { ok: issues.length === 0, issueCount: issues.length, issues: issues.slice(0, 32) } } /** * Safe pin row when message body missing (P409-19). */ function pinWithMessageFallback (pin, messageById = {}) { if (!pin?.messageId) return pin if (pin.message) return pin const embedded = messageById[pin.messageId] if (!embedded) { return { ...pin, message: { id: pin.messageId, content: '(message unavailable)', displayContent: '(message unavailable)', authorId: pin.authorId || null, missing: true } } } return { ...pin, message: embedded } } module.exports = { checksumSettingsSnapshot, pruneStaleComposeDrafts, auditMessageReactionCrossRefs, pinWithMessageFallback }