OverviewFilesCommitsIssuesDocsReleasesPeers

favela-web / lib / postgate.js

74 lines · javascript ref:
// postgate — who may QUOTE a post (app.bsky.feed.postgate).
//
// Same rkey binding as a threadgate: the record's rkey is IDENTICAL to the post's, and `post` carries the
// post's at:// uri. So it needs no index either — given a post we know the rkey.
//
// ⚠ THE POLARITY IS INVERTED RELATIVE TO A THREADGATE, and that is the whole trap. A threadgate carries
// `allow` — a list of who MAY reply, where an empty array means nobody. A postgate carries
// `embeddingRules` — a list of RESTRICTIONS, where an empty array means NO restrictions, i.e. anyone may
// quote. So:
//
//   threadgate  allow: []          -> nobody may reply
//   postgate    embeddingRules: [] -> ANYONE may quote
//
// The two look alike and mean opposite things. Writing one with the other's polarity silently produces
// the reverse of what the author asked for, and nothing errors.
//
// There is one rule today, #disableRule: quoting off entirely. `detachedEmbeddingUris` is the other half
// of the lexicon — specific existing quotes the author has pulled down — and is preserved on read so a
// future writer does not clobber it.

export const QUOTE_CHOICES = ['anyone', 'nobody']

// A UI choice -> the record, or null for 'anyone'. Like a threadgate's 'everyone', 'anyone' writes NO
// RECORD rather than one with an empty rule list: the absent record and the empty list happen to mean the
// same thing here, but writing nothing is unambiguous and leaves no artefact to misread later.
export function postgateRecord ({ postUri, choice, detachedEmbeddingUris = [], createdAt = new Date().toISOString() }) {
  if (choice !== 'nobody') return null
  return {
    $type: 'app.bsky.feed.postgate',
    post: postUri,
    createdAt,
    detachedEmbeddingUris,
    embeddingRules: [{ $type: 'app.bsky.feed.postgate#disableRule' }]
  }
}

// The inverse, for rendering. An unrecognised rule reads as 'limited' — never guessed permissive, since
// assuming an unknown restriction is harmless is how a protected post ends up quotable.
export function quoteChoiceOf (record) {
  if (!record) return 'anyone'
  const rules = record.embeddingRules
  if (!Array.isArray(rules) || rules.length === 0) return 'anyone' // empty = NO restrictions
  const types = rules.map((r) => String((r && r.$type) || ''))
  if (types.some((t) => t.endsWith('#disableRule'))) return 'nobody'
  return 'limited'
}

// May `viewerDid` quote a post by `authorDid`? The author may always quote their own — that is how a
// self-thread of quotes works, and every client behaves that way.
export function canQuote ({ viewerDid, authorDid, record } = {}) {
  if (!viewerDid) return false
  if (viewerDid === authorDid) return true
  const c = quoteChoiceOf(record)
  if (c === 'anyone') return true
  // 'nobody' and 'limited' both fail closed for OUR writes: posting a quote the author asked to prevent
  // is worse than declining to offer the button.
  return false
}

export function describeQuoteGate (choice) {
  switch (choice) {
    case 'nobody': return 'the author turned off quotes for this post'
    case 'limited': return 'quoting this post is limited'
    default: return ''
  }
}

// Quotes the author has individually detached. Preserved on any rewrite so a later gate change does not
// silently re-attach something they pulled down.
export function detachedUris (record) {
  const d = record && record.detachedEmbeddingUris
  return Array.isArray(d) ? d.filter((u) => typeof u === 'string') : []
}