diff --git a/src/firehose/handlers.ts b/src/firehose/handlers.ts index 709ec6a..26da928 100644 --- a/src/firehose/handlers.ts +++ b/src/firehose/handlers.ts @@ -14,6 +14,7 @@ import { deleteWikiByAtUri, getMemberRole, getNoteByAtUri, + getNoteBySlug, getWikiByAtUri, isDidBanned, setWikiTheme, @@ -228,27 +229,35 @@ export function handleCommitEvent(evt: FirehoseCommit): void { if (typeof record !== "object" || record === null) return; const r = record as Rec; + const malformed = (): void => + logDrop(atUri, "record is missing required fields"); + switch (evt.collection) { case COLLECTIONS.wiki: - if (isWikiRecord(r) && !wikiIsInvalid(atUri, r)) + if (!isWikiRecord(r)) malformed(); + else if (!wikiIsInvalid(atUri, r)) handleWiki(evt.did, evt.rkey, atUri, r); break; case COLLECTIONS.note: - if (isNoteRecord(r) && !noteIsInvalid(atUri, r)) - handleNote(evt.did, atUri, r); + if (!isNoteRecord(r)) malformed(); + else if (!noteIsInvalid(atUri, r)) handleNote(evt.did, atUri, r); break; case COLLECTIONS.noteRevision: - if (isRevisionRecord(r) && !revisionIsInvalid(atUri, r)) - handleRevision(evt.did, atUri, r); + if (!isRevisionRecord(r)) malformed(); + else if (!revisionIsInvalid(atUri, r)) handleRevision(evt.did, atUri, r); break; case COLLECTIONS.membership: - if (isMembershipRecord(r) && !membershipIsInvalid(atUri, r)) + if (!isMembershipRecord(r)) malformed(); + else if (!membershipIsInvalid(atUri, r)) handleMembership(evt.did, atUri, r); break; case COLLECTIONS.memberRequest: - if (isMemberRequestRecord(r)) handleMemberRequest(evt.did, atUri, r); + if (!isMemberRequestRecord(r)) malformed(); + else handleMemberRequest(evt.did, atUri, r); break; case COLLECTIONS.communityBookmark: + // Deliberately unlogged: wantedCollections gives us every bookmark on + // the network for this NSID, and almost none of them point at a wiki. if (isCommunityBookmarkRecord(r)) handleCommunityBookmark(evt.did, atUri, r); break; @@ -296,7 +305,9 @@ function handleWiki( record.description ?? "", ); - // Known theme name = enforce that preset; anything else falls back to reader mode. + // Per the lexicon, a theme present means "render with this", absent means "let + // the viewer choose" — so absence is reader mode. The stored theme is reset + // too, because nothing reads it outside enforce mode (see theme/apply.ts). const theme = record.theme; if (typeof theme === "string" && theme in themes) { setWikiTheme(did, rkey, "enforce", theme); @@ -309,15 +320,32 @@ function handleWiki( function handleNote(did: string, atUri: string, record: NoteRecord): void { const wiki = getWikiByAtUri(record.wikiRef); - if (!wiki) return; - if (!canDidEditWiki(wiki, did)) return; + if (!wiki) { + logDrop(atUri, `note.wikiRef unknown: ${record.wikiRef}`); + return; + } + if (!canDidEditWiki(wiki, did)) { + logDrop(atUri, `${did} may not edit ${wiki.at_uri}`); + return; + } // Store the NFC form: the same slug written in NFD is a different SQLite key, // so an un-normalized write becomes a note that exists but never resolves. + const slug = normalizeSlug(record.slug); + + // upsertNote keys on (wiki_at_uri, slug) and updates only the title, so two + // authors picking the same slug would silently merge: the second one's title + // on the first one's record. First writer wins, and the loser is reported. + const existing = getNoteBySlug(wiki.at_uri, slug); + if (existing && existing.at_uri !== atUri) { + logDrop(atUri, `note.slug "${slug}" already taken by ${existing.at_uri}`); + return; + } + upsertNote( wiki.at_uri, wiki.slug, - normalizeSlug(record.slug), + slug, record.title, did, atUri, @@ -333,10 +361,19 @@ function handleRevision( record: RevisionRecord, ): void { const note = getNoteByAtUri(record.noteRef); - if (!note) return; + if (!note) { + logDrop(atUri, `revision.noteRef unknown: ${record.noteRef}`); + return; + } const wiki = getWikiByAtUri(note.wiki_at_uri); - if (!wiki) return; - if (!canDidEditWiki(wiki, did)) return; + if (!wiki) { + logDrop(atUri, `note's wiki unknown: ${note.wiki_at_uri}`); + return; + } + if (!canDidEditWiki(wiki, did)) { + logDrop(atUri, `${did} may not edit ${wiki.at_uri}`); + return; + } applyRevisionFromFirehose( note.at_uri, @@ -359,9 +396,15 @@ function handleMembership( record: MembershipRecord, ): void { const wiki = getWikiByAtUri(record.wikiRef); - if (!wiki) return; + if (!wiki) { + logDrop(atUri, `membership.wikiRef unknown: ${record.wikiRef}`); + return; + } - if (wiki.did !== did) return; + if (wiki.did !== did) { + logDrop(atUri, `only ${wiki.did} may grant membership to its wiki`); + return; + } upsertMembership( wiki.at_uri, @@ -379,7 +422,10 @@ function handleMemberRequest( record: MemberRequestRecord, ): void { const wiki = getWikiByAtUri(record.wikiRef); - if (!wiki) return; + if (!wiki) { + logDrop(atUri, `memberRequest.wikiRef unknown: ${record.wikiRef}`); + return; + } upsertRequest(wiki.at_uri, wiki.slug, did, atUri, record.createdAt); }