// inbound-notifications.js — what the fediverse sent us, in the shape favela already uses.
//
// ⛔ WHY NOT "WRITE IT INTO THE PEER'S LOG", which is where this was heading.
//
// favela-peer has nowhere to put a record whose author has no feed. Every record lives in its
// author's feed; `notifications()` is a SCAN of the feeds this node holds (peer.js:640); and the
// doorbell — the answer for "somebody you hold NOTHING of" — is keyed by FEED KEY (peer.js:705),
// which a Mastodon actor does not have and cannot get. Writing an inbound Note into a feed would
// mean the bridge authoring records as though it were the remote person. That is impersonation with
// extra steps, and it is the thing SOCIAL-MODEL §8's "never re-encode a record you did not author"
// exists to forbid.
//
// ★ THE MODEL ALREADY ALLOWS THE HONEST VERSION. peer.js:623 — a notification is "a PROJECTION…
// a pure function from records to a view, derived on read, never stored, never replicated". A view
// deriving from two sources is still a projection. So the bridge offers a second source in the same
// shape, the view merges them, and favela-peer goes on not knowing what a fediverse is — which its
// own boundary test requires.
//
// ⚠ This is a projection, not storage. Nothing here is written anywhere; `InboundStore` remains the
// only durable copy, and it is already capped and already truncates bodies.
// AS2 activity type → favela notification kind (peer.js:646: reaction · repost · reply · mention ·
// follow · knock). A `Create` is a reply when it answers something and a mention otherwise, which is
// exactly the distinction `inReplyTo` was kept for (inbound-store.js:41).
const KIND = {
Like: 'reaction',
Announce: 'repost',
Follow: 'follow'
}
/**
* One stored inbound item → one notification row, or null if we cannot say what it was.
*
* ⛔ FAILS CLOSED. A remote server can send any activity type it likes; rendering an unrecognised
* one as the nearest familiar kind tells the reader something that did not happen.
*/
export function toNotification (item) {
if (!item || !item.type) return null
const kind = item.type === 'Create'
? (item.inReplyTo ? 'reply' : 'mention')
: KIND[item.type]
if (!kind) return null
return {
kind,
// ⛔ An actor URI, NOT a feed key — which is what a peer row carries here. `source` below is
// what lets a view tell them apart instead of trying to resolve one as the other.
author: item.actor || null,
at: item.published || item.receivedAt || '',
// What it acted on, always something of OURS: the post that was liked or boosted, or the post a
// reply answers. A Follow acts on you rather than on a post, so it has no target.
target: item.type === 'Create' ? (item.inReplyTo || null) : (item.type === 'Follow' ? null : item.objectId || null),
value: null,
name: item.objectId || item.id || null,
// ⛔ VERBATIM. inbound-store.js keeps remote HTML and says it must "NEVER be rendered as trusted
// markup". Sanitising here would move the guarantee somewhere the renderer cannot see it, and a
// caller would reasonably believe the string was already safe. It is not. Escape it downstream.
body: item.content || '',
// Load-bearing, not a label: a federated row is not addressable the way a native one is.
source: 'fediverse'
}
}
/** Newest first, matching the peer's ordering so a merged list needs no second sort key. */
export function toNotifications (items, { limit = 100 } = {}) {
return (items || [])
.map(toNotification)
.filter(Boolean)
.sort((a, b) => (a.at === b.at ? 0 : (a.at < b.at ? 1 : -1)))
.slice(0, limit)
}
export default { toNotification, toNotifications }