fedipod 1.8.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9589,6 +9589,7 @@ __export(wire_exports, {
9589
9589
  AS_CTX: () => AS_CTX,
9590
9590
  DEFAULT_ROOT: () => DEFAULT_ROOT,
9591
9591
  FOLLOWERS_PAGE_SIZE: () => FOLLOWERS_PAGE_SIZE,
9592
+ HASHTAG_RE: () => HASHTAG_RE,
9592
9593
  MENTION_RE: () => MENTION_RE,
9593
9594
  OUTBOX_PAGE_SIZE: () => OUTBOX_PAGE_SIZE,
9594
9595
  PUBLIC: () => PUBLIC,
@@ -9615,6 +9616,7 @@ __export(wire_exports, {
9615
9616
  followersPageId: () => followersPageId,
9616
9617
  followersPaging: () => followersPaging,
9617
9618
  followsNeedApproval: () => followsNeedApproval,
9619
+ hashtagsIn: () => hashtagsIn,
9618
9620
  hostMeta: () => hostMeta,
9619
9621
  jrd: () => jrd,
9620
9622
  likeActivity: () => likeActivity,
@@ -9627,11 +9629,13 @@ __export(wire_exports, {
9627
9629
  orderedCollection: () => orderedCollection,
9628
9630
  outboxHead: () => outboxHead,
9629
9631
  outboxItemId: () => outboxItemId,
9632
+ outboxLocalItem: () => outboxLocalItem,
9630
9633
  outboxPage: () => outboxPage,
9631
9634
  outboxPageCount: () => outboxPageCount,
9632
9635
  outboxPageId: () => outboxPageId,
9633
9636
  outboxPages: () => outboxPages,
9634
9637
  outboxPaging: () => outboxPaging,
9638
+ outboxWireItem: () => outboxWireItem,
9635
9639
  pageItems: () => pageItems,
9636
9640
  profilePageHtml: () => profilePageHtml,
9637
9641
  publicHandle: () => publicHandle,
@@ -9639,6 +9643,7 @@ __export(wire_exports, {
9639
9643
  rejectActivity: () => rejectActivity,
9640
9644
  repliesId: () => repliesId,
9641
9645
  sanitizeHtml: () => sanitizeHtml,
9646
+ tagHref: () => tagHref,
9642
9647
  titledContent: () => titledContent,
9643
9648
  tombstoneDoc: () => tombstoneDoc,
9644
9649
  undoActivity: () => undoActivity,
@@ -9659,11 +9664,14 @@ function hostMeta(base) {
9659
9664
  </XRD>
9660
9665
  `;
9661
9666
  }
9662
- function jrd({ handle: handle7, host, actor, aliases = [] }) {
9667
+ function jrd({ handle: handle7, host, actor, aliases = [], page: page2 = null }) {
9663
9668
  return {
9664
9669
  subject: `acct:${handle7}@${host}`,
9665
9670
  ...aliases.length ? { aliases } : {},
9666
- links: [{ rel: "self", type: "application/activity+json", href: actor }]
9671
+ links: [
9672
+ { rel: "self", type: "application/activity+json", href: actor },
9673
+ ...page2 ? [{ rel: "http://webfinger.net/rel/profile-page", type: "text/html", href: page2 }] : []
9674
+ ]
9667
9675
  };
9668
9676
  }
9669
9677
  function followsNeedApproval(config = {}) {
@@ -9926,7 +9934,7 @@ function outboxPage(outboxId, n, items) {
9926
9934
  id: outboxPageId(outboxId, n),
9927
9935
  type: "OrderedCollectionPage",
9928
9936
  partOf: outboxId,
9929
- orderedItems: items,
9937
+ orderedItems: items.map(outboxWireItem),
9930
9938
  ...n > 1 ? { next: outboxPageId(outboxId, n - 1) } : {}
9931
9939
  };
9932
9940
  }
@@ -10046,8 +10054,27 @@ function attachmentsOf(note) {
10046
10054
  function mentionsIn(text) {
10047
10055
  return [...new Set(String(text).match(MENTION_RE) || [])].map((m) => m.slice(1));
10048
10056
  }
10049
- function contentHtml(text, mentions = []) {
10057
+ function hashtagsIn(text) {
10058
+ const seen = /* @__PURE__ */ new Map();
10059
+ for (const m of String(text).matchAll(HASHTAG_RE)) {
10060
+ const key = m[2].toLowerCase();
10061
+ if (!seen.has(key)) seen.set(key, m[2]);
10062
+ }
10063
+ return [...seen.values()];
10064
+ }
10065
+ function tagHref(urls, name) {
10066
+ return urls.profileHtml + "?tag=" + encodeURIComponent(String(name).toLowerCase());
10067
+ }
10068
+ function contentHtml(text, mentions = [], hashtags = []) {
10050
10069
  let html = String(text).replace(/[&<>]/g, (c) => HTML_ESCAPES[c]);
10070
+ if (hashtags.length) {
10071
+ const byName = new Map(hashtags.filter((h) => h?.name && h?.href).map((h) => [h.name.toLowerCase(), h.href]));
10072
+ html = html.replace(HASHTAG_RE, (all, lead, name) => {
10073
+ const href = byName.get(name.toLowerCase());
10074
+ if (!href) return all;
10075
+ return `${lead}<a href="${String(href).replace(/[&<>"]/g, (c) => HTML_ESCAPES[c] || "&quot;")}" class="mention hashtag" rel="tag">#<span>${name}</span></a>`;
10076
+ });
10077
+ }
10051
10078
  for (const m of mentions) {
10052
10079
  if (!m?.handle || !m?.actor) continue;
10053
10080
  const href = String(m.page || m.actor).replace(/[&<>"]/g, (c) => HTML_ESCAPES[c] || "&quot;");
@@ -10075,28 +10102,35 @@ function noteDoc({
10075
10102
  mentions = [],
10076
10103
  visibility = "public",
10077
10104
  summary = null,
10105
+ sensitive = false,
10078
10106
  updated = null,
10079
- container = null
10107
+ container = null,
10108
+ also = []
10080
10109
  }) {
10081
10110
  const id = (container || urls.notes) + slug;
10082
10111
  const who = mentions.map((m) => m.actor);
10083
- const addressed = addressing(urls, visibility, who);
10112
+ const extra = visibility === "direct" ? [] : also.filter((a) => a && !who.includes(a));
10113
+ const addressed = addressing(urls, visibility, [...who, ...extra]);
10114
+ const hashtags = hashtagsIn(content).map((name) => ({ name, href: tagHref(urls, name) }));
10084
10115
  const note = {
10085
10116
  "@context": AS_CTX,
10086
10117
  id,
10087
10118
  type: "Note",
10088
10119
  attributedTo: urls.actor,
10089
- content: contentHtml(content, mentions),
10120
+ content: contentHtml(content, mentions, hashtags),
10090
10121
  published,
10091
10122
  to: addressed.to,
10092
10123
  cc: addressed.cc,
10093
10124
  replies: repliesId(id)
10094
10125
  };
10095
10126
  if (summary) note.summary = summary;
10127
+ if (summary || sensitive) note.sensitive = true;
10096
10128
  if (updated) note.updated = updated;
10097
- if (mentions.length) {
10098
- note.tag = mentions.map((m) => ({ type: "Mention", href: m.actor, name: "@" + m.handle }));
10099
- }
10129
+ const tag = [
10130
+ ...mentions.map((m) => ({ type: "Mention", href: m.actor, name: "@" + m.handle })),
10131
+ ...hashtags.map((h) => ({ type: "Hashtag", href: h.href, name: "#" + h.name }))
10132
+ ];
10133
+ if (tag.length) note.tag = tag;
10100
10134
  if (inReplyTo) note.inReplyTo = inReplyTo;
10101
10135
  if (attachments?.length) {
10102
10136
  note.attachment = attachments.map((a) => ({
@@ -10274,7 +10308,7 @@ function addRemoveActivity({ urls, type, object, target, serial }) {
10274
10308
  target
10275
10309
  };
10276
10310
  }
10277
- var import_sanitize_html, AS_CTX, SEC_CTX, PUBLIC, DEFAULT_ROOT, assertionKeyId, OUTBOX_PAGE_SIZE, outboxPageId, outboxPageCount, outboxItemId, FOLLOWERS_PAGE_SIZE, followersPageId, followersPageCount, ALLOWED_TAGS, ALLOWED_ATTRS, MAX_ATTACHMENTS, MAX_ATTACHMENT_URL, attachmentUrl, HTML_ESCAPES, MENTION_RE;
10311
+ var import_sanitize_html, AS_CTX, SEC_CTX, PUBLIC, DEFAULT_ROOT, assertionKeyId, OUTBOX_PAGE_SIZE, outboxPageId, outboxPageCount, outboxItemId, outboxWireItem, outboxLocalItem, FOLLOWERS_PAGE_SIZE, followersPageId, followersPageCount, ALLOWED_TAGS, ALLOWED_ATTRS, MAX_ATTACHMENTS, MAX_ATTACHMENT_URL, attachmentUrl, HTML_ESCAPES, MENTION_RE, HASHTAG_RE;
10278
10312
  var init_wire = __esm({
10279
10313
  "lib/core/wire.mjs"() {
10280
10314
  init_urls();
@@ -10289,6 +10323,8 @@ var init_wire = __esm({
10289
10323
  outboxPageId = (outboxId, n) => `${outboxId}-${n}`;
10290
10324
  outboxPageCount = (total) => Math.max(1, Math.ceil(total / OUTBOX_PAGE_SIZE));
10291
10325
  outboxItemId = (i) => typeof i === "string" ? i : i?.id || null;
10326
+ outboxWireItem = (i) => typeof i === "string" ? createActivityId(i) : i;
10327
+ outboxLocalItem = (i) => typeof i === "string" && i.endsWith("-create") ? i.slice(0, -"-create".length) : i;
10292
10328
  FOLLOWERS_PAGE_SIZE = 20;
10293
10329
  followersPageId = (id, n) => `${id}-${n}`;
10294
10330
  followersPageCount = (total) => Math.max(1, Math.ceil(total / FOLLOWERS_PAGE_SIZE));
@@ -10329,6 +10365,7 @@ var init_wire = __esm({
10329
10365
  };
10330
10366
  HTML_ESCAPES = { "&": "&amp;", "<": "&lt;", ">": "&gt;" };
10331
10367
  MENTION_RE = /@([A-Za-z0-9_.-]+)@([A-Za-z0-9.-]+\.[A-Za-z]{2,}(?::\d+)?)/g;
10368
+ HASHTAG_RE = /(^|[^\/\w)])#([\p{L}\p{N}_]*[\p{L}_][\p{L}\p{N}_]*)/gu;
10332
10369
  }
10333
10370
  });
10334
10371
 
@@ -32650,11 +32687,12 @@ async function verifyHttpSignature(request, { documentLoader, keyCache, timeWind
32650
32687
  } catch {
32651
32688
  result = null;
32652
32689
  }
32690
+ const method = request.headers.has("signature-input") ? "rfc9421" : "draft-cavage";
32653
32691
  if (result?.verified) {
32654
32692
  const key = result.key;
32655
32693
  return {
32656
32694
  verified: true,
32657
- method: "draft-cavage",
32695
+ method,
32658
32696
  keyId: key.id?.href ?? null,
32659
32697
  actor: key.ownerId?.href ?? null,
32660
32698
  reason: null,
@@ -32675,7 +32713,7 @@ async function verifyHttpSignature(request, { documentLoader, keyCache, timeWind
32675
32713
  if (kind === "keyFetchError") {
32676
32714
  return {
32677
32715
  verified: false,
32678
- method: "draft-cavage",
32716
+ method,
32679
32717
  keyId: null,
32680
32718
  actor: null,
32681
32719
  reason: "key-unfetchable",
@@ -32684,7 +32722,7 @@ async function verifyHttpSignature(request, { documentLoader, keyCache, timeWind
32684
32722
  }
32685
32723
  return {
32686
32724
  verified: false,
32687
- method: "draft-cavage",
32725
+ method,
32688
32726
  keyId: null,
32689
32727
  actor: null,
32690
32728
  reason: "bad-signature",
@@ -34481,6 +34519,14 @@ var PodStore = class {
34481
34519
  // — not from the pod, not from the RDF. Unverified entries are dropped first
34482
34520
  // now, so a flood can only ever evict itself, and a stranger's genuine
34483
34521
  // favourite still shows up, which is what recording them at all is for.
34522
+ // What an Undo unsays: the favourite or boost it names, matched by a
34523
+ // predicate on what was recorded. Returns how many went.
34524
+ removeNotifications(pred) {
34525
+ const all = this.getNotifications();
34526
+ const kept = all.filter((n) => !pred(n));
34527
+ if (kept.length !== all.length) this.write("notifications.json", kept);
34528
+ return all.length - kept.length;
34529
+ }
34484
34530
  addNotification(n) {
34485
34531
  const all = this.getNotifications();
34486
34532
  const id = node_crypto_default.createHash("sha256").update(JSON.stringify(n)).digest("hex").slice(0, 16);
@@ -45433,8 +45479,9 @@ async function publishOutbox(publisher, outbox, { acls = false, force = false }
45433
45479
  );
45434
45480
  return wrote;
45435
45481
  }
45436
- function readPublishedOutbox(publisher) {
45437
- return readPublished(publisher.remote, publisher.urls);
45482
+ async function readPublishedOutbox(publisher) {
45483
+ const items = await readPublished(publisher.remote, publisher.urls);
45484
+ return Array.isArray(items) ? items.map(outboxLocalItem) : items;
45438
45485
  }
45439
45486
  async function publishFollowers(publisher, actors, { acls = false, force = false } = {}) {
45440
45487
  const { urls } = publisher;
@@ -55941,6 +55988,35 @@ async function mentionsFor(publisher, content, inReplyTo) {
55941
55988
  }
55942
55989
  return mentions;
55943
55990
  }
55991
+ async function replyTarget(publisher, inReplyTo, mentions = [], visibility = "public") {
55992
+ if (!inReplyTo || visibility === "direct" || !publisher.resolveActor) return null;
55993
+ const parent = publisher.store.getStatuses().find((s) => s.noteId === inReplyTo);
55994
+ let actor = parent?.actor || null;
55995
+ if (!parent) {
55996
+ const doc2 = await publisher.resolveActor(inReplyTo).catch(() => null);
55997
+ const by = doc2?.attributedTo;
55998
+ actor = typeof by === "string" ? by : (Array.isArray(by) ? by[0] : by)?.id || null;
55999
+ if (typeof actor === "object") actor = actor?.id || null;
56000
+ }
56001
+ if (!actor || actor === publisher.urls.actor) return null;
56002
+ if (mentions.some((m) => m.actor === actor)) return null;
56003
+ if (publisher.store.isBlocked?.(actor)) return null;
56004
+ const doc = await publisher.resolveActor(actor).catch(() => null);
56005
+ const inbox = doc?.endpoints?.sharedInbox || doc?.inbox || null;
56006
+ if (!inbox) publisher.log(`reply: no inbox found for ${actor} \u2014 they get it only through followers`);
56007
+ return { actor, inbox };
56008
+ }
56009
+ async function inboxesFor(publisher, actorIds = []) {
56010
+ const out = [];
56011
+ for (const id of [...new Set(actorIds.filter((a) => typeof a === "string" && a && a !== publisher.urls.actor))]) {
56012
+ if (!publisher.resolveActor) break;
56013
+ const doc = await publisher.resolveActor(id).catch(() => null);
56014
+ const inbox = doc?.endpoints?.sharedInbox || doc?.inbox;
56015
+ if (inbox) out.push(inbox);
56016
+ else publisher.log(`addressed actor ${id} has no inbox the agent can find \u2014 not delivered to`);
56017
+ }
56018
+ return out;
56019
+ }
55944
56020
  function assertDirectAddressed(content, mentions) {
55945
56021
  const wanted = [...new Set(mentionsIn(content))];
55946
56022
  const missing = wanted.filter((h) => !mentions.some((m) => m.handle === h));
@@ -55966,7 +56042,7 @@ function rowContent(obj) {
55966
56042
  const esc = (v) => String(v).replace(/[&<>"]/gu, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
55967
56043
  return `<p><a href="${esc(obj?.id || "")}">${esc(obj?.type || "object")}</a></p>`;
55968
56044
  }
55969
- async function publishNote(publisher, content, { inReplyTo, attachments, visibility = "public", spoilerText = null, slug: wanted = null } = {}) {
56045
+ async function publishNote(publisher, content, { inReplyTo, attachments, visibility = "public", spoilerText = null, sensitive = false, slug: wanted = null, also = [], deliverTo = [] } = {}) {
55970
56046
  const { urls } = publisher;
55971
56047
  const priv = visibility === "private" || visibility === "direct";
55972
56048
  if (priv) {
@@ -55977,6 +56053,7 @@ async function publishNote(publisher, content, { inReplyTo, attachments, visibil
55977
56053
  const slug = await slugFor(publisher, priv ? urls.privateNotes : urls.notes, wanted, published);
55978
56054
  const mentions = await publisher._mentionsFor(content, inReplyTo);
55979
56055
  if (visibility === "direct") assertDirectAddressed(content, mentions);
56056
+ const reply = await replyTarget(publisher, inReplyTo, mentions, visibility);
55980
56057
  const note = noteDoc({
55981
56058
  urls,
55982
56059
  slug,
@@ -55987,8 +56064,11 @@ async function publishNote(publisher, content, { inReplyTo, attachments, visibil
55987
56064
  mentions,
55988
56065
  visibility,
55989
56066
  summary: spoilerText,
55990
- container: priv ? urls.privateNotes : urls.notes
56067
+ sensitive,
56068
+ container: priv ? urls.privateNotes : urls.notes,
56069
+ also: [...also, ...reply ? [reply.actor] : []]
55991
56070
  });
56071
+ const addressedInboxes = await inboxesFor(publisher, [...also, ...deliverTo]);
55992
56072
  await write4(publisher.remote, note.id, note);
55993
56073
  await writeEmptyReplies(
55994
56074
  publisher.remote,
@@ -56007,8 +56087,9 @@ async function publishNote(publisher, content, { inReplyTo, attachments, visibil
56007
56087
  text: content,
56008
56088
  visibility,
56009
56089
  ...spoilerText ? { spoiler: spoilerText } : {},
56090
+ ...note.sensitive ? { sensitive: true } : {},
56010
56091
  ...attachments?.length ? { attachments } : {},
56011
- ...note.tag?.length ? { mentions: note.tag.map((t) => ({ href: t.href, name: t.name })) } : {}
56092
+ ...rowTags(note)
56012
56093
  });
56013
56094
  if (await publisher.store.commit?.() === false) publisher.log(`post published but its timeline row was refused: ${note.id}`);
56014
56095
  const create = createActivity(note, urls);
@@ -56016,7 +56097,9 @@ async function publishNote(publisher, content, { inReplyTo, attachments, visibil
56016
56097
  const contacts = publisher.store.getContacts();
56017
56098
  const inboxes = [...new Set([
56018
56099
  ...visibility === "direct" ? [] : contacts.followers.map((f) => f.sharedInbox || f.inbox),
56019
- ...mentions.map((m) => m.inbox)
56100
+ ...mentions.map((m) => m.inbox),
56101
+ reply?.inbox,
56102
+ ...addressedInboxes
56020
56103
  ].filter(Boolean))];
56021
56104
  await publisher.deliverer.deliverToAll(inboxes, create);
56022
56105
  publisher.log(`note published: ${note.id} \u2192 ${inboxes.length} inbox(es)`);
@@ -56035,7 +56118,7 @@ async function publishNote(publisher, content, { inReplyTo, attachments, visibil
56035
56118
  }
56036
56119
  return note;
56037
56120
  }
56038
- async function publishObject(publisher, object, { visibility = "public", slug: wanted = null } = {}) {
56121
+ async function publishObject(publisher, object, { visibility = "public", slug: wanted = null, also = [], deliverTo = [] } = {}) {
56039
56122
  const { urls } = publisher;
56040
56123
  const priv = visibility === "private" || visibility === "direct";
56041
56124
  if (priv) {
@@ -56047,11 +56130,13 @@ async function publishObject(publisher, object, { visibility = "public", slug: w
56047
56130
  const pod = publisher.config?.remotePod || urls.base;
56048
56131
  const ownId = typeof object.id === "string" && /^https?:\/\//u.test(object.id) && object.id.startsWith(pod) ? object.id : null;
56049
56132
  const name = await slugFor(publisher, container, wanted, published);
56050
- const addressed = addressing(urls, visibility);
56133
+ const addressed = addressing(urls, visibility, visibility === "direct" ? also : []);
56134
+ if (visibility !== "direct") addressed.cc = [.../* @__PURE__ */ new Set([...addressed.cc, ...also])];
56051
56135
  let doc = null;
56052
56136
  const id = ownId || container + name;
56053
56137
  if (!ownId) {
56054
- doc = { ...object, id, attributedTo: urls.actor, published: object.published || published, to: addressed.to, cc: addressed.cc };
56138
+ const { bto, bcc, ...sent } = object;
56139
+ doc = { ...sent, id, attributedTo: urls.actor, published: object.published || published, to: addressed.to, cc: addressed.cc };
56055
56140
  if (!doc["@context"]) doc["@context"] = AS_CTX;
56056
56141
  if (typeof doc.content === "string") doc.content = sanitizeHtml(doc.content);
56057
56142
  await write4(publisher.remote, id, doc);
@@ -56081,12 +56166,21 @@ async function publishObject(publisher, object, { visibility = "public", slug: w
56081
56166
  };
56082
56167
  await writeCreate(publisher.remote, create.id, create);
56083
56168
  const contacts = publisher.store.getContacts();
56084
- const inboxes = visibility === "direct" ? [] : [...new Set(contacts.followers.map((f) => f.sharedInbox || f.inbox).filter(Boolean))];
56169
+ const inboxes = [...new Set([
56170
+ ...visibility === "direct" ? [] : contacts.followers.map((f) => f.sharedInbox || f.inbox),
56171
+ ...await inboxesFor(publisher, [...also, ...deliverTo])
56172
+ ].filter(Boolean))];
56085
56173
  await publisher.deliverer.deliverToAll(inboxes, create);
56086
56174
  publisher.log(`${row.type || "object"} published: ${id} \u2192 ${inboxes.length} inbox(es)`);
56087
56175
  return { id, createId, copied: !ownId };
56088
56176
  }
56089
- async function updateNote(publisher, s, { content, spoilerText = null, attachments = null } = {}) {
56177
+ function rowTags(note) {
56178
+ const tags = [].concat(note.tag || []);
56179
+ const mentions = tags.filter((t) => t.type === "Mention").map((t) => ({ href: t.href, name: t.name }));
56180
+ const hashtags = tags.filter((t) => t.type === "Hashtag").map((t) => ({ name: String(t.name).replace(/^#/, ""), url: t.href }));
56181
+ return { ...mentions.length ? { mentions } : {}, ...hashtags.length ? { tags: hashtags } : {} };
56182
+ }
56183
+ async function updateNote(publisher, s, { content, spoilerText = null, sensitive = null, attachments = null } = {}) {
56090
56184
  const { urls } = publisher;
56091
56185
  const updated = (/* @__PURE__ */ new Date()).toISOString();
56092
56186
  const inText = new Set(mentionsIn(content));
@@ -56103,6 +56197,7 @@ async function updateNote(publisher, s, { content, spoilerText = null, attachmen
56103
56197
  const atts = attachments ?? s.attachments ?? [];
56104
56198
  const container = String(s.noteId).startsWith(urls.privateNotes) ? urls.privateNotes : urls.notes;
56105
56199
  const slug = s.slug || String(s.noteId).slice(container.length);
56200
+ const reply = await replyTarget(publisher, s.inReplyTo, mentions, s.visibility || "public");
56106
56201
  const note = noteDoc({
56107
56202
  urls,
56108
56203
  slug,
@@ -56113,8 +56208,10 @@ async function updateNote(publisher, s, { content, spoilerText = null, attachmen
56113
56208
  mentions,
56114
56209
  visibility: s.visibility || "public",
56115
56210
  summary: spoilerText,
56211
+ sensitive: sensitive ?? !!s.sensitive,
56116
56212
  updated,
56117
- container
56213
+ container,
56214
+ also: reply ? [reply.actor] : []
56118
56215
  });
56119
56216
  await write4(publisher.remote, note.id, note);
56120
56217
  await writeCreate(publisher.remote, createActivityId(note.id), createActivity(note, urls));
@@ -56123,14 +56220,17 @@ async function updateNote(publisher, s, { content, spoilerText = null, attachmen
56123
56220
  text: content,
56124
56221
  editedAt: updated,
56125
56222
  spoiler: spoilerText || void 0,
56223
+ sensitive: note.sensitive || void 0,
56126
56224
  attachments: atts.length ? atts : void 0,
56127
- mentions: note.tag?.length ? note.tag.map((t) => ({ href: t.href, name: t.name })) : void 0
56225
+ mentions: rowTags(note).mentions,
56226
+ tags: rowTags(note).tags
56128
56227
  });
56129
56228
  const update = updateActivity(note, urls);
56130
56229
  const contacts = publisher.store.getContacts();
56131
56230
  const inboxes = [...new Set([
56132
56231
  ...s.visibility === "direct" ? [] : contacts.followers.map((f) => f.sharedInbox || f.inbox),
56133
- ...mentions.map((m) => m.inbox)
56232
+ ...mentions.map((m) => m.inbox),
56233
+ reply?.inbox
56134
56234
  ].filter(Boolean))];
56135
56235
  await publisher.deliverer.deliverToAll(inboxes, update);
56136
56236
  publisher.log(`note edited: ${note.id} \u2192 ${inboxes.length} inbox(es)`);
@@ -56356,7 +56456,8 @@ async function publishQuestion(publisher, content, {
56356
56456
  expiresAt = null,
56357
56457
  inReplyTo = void 0,
56358
56458
  visibility = "public",
56359
- spoilerText = null
56459
+ spoilerText = null,
56460
+ sensitive = false
56360
56461
  } = {}) {
56361
56462
  const { urls } = publisher;
56362
56463
  const priv = visibility === "private" || visibility === "direct";
@@ -56383,6 +56484,7 @@ async function publishQuestion(publisher, content, {
56383
56484
  const slug = published.slice(0, 10) + "-" + node_crypto_default.randomBytes(4).toString("hex");
56384
56485
  const mentions = await publisher._mentionsFor(content, inReplyTo);
56385
56486
  if (visibility === "direct") assertDirectAddressed(content, mentions);
56487
+ const reply = await replyTarget(publisher, inReplyTo, mentions, visibility);
56386
56488
  const poll = {
56387
56489
  multiple: !!multiple,
56388
56490
  expiresAt: expiresAt || null,
@@ -56391,7 +56493,7 @@ async function publishQuestion(publisher, content, {
56391
56493
  votersCount: 0,
56392
56494
  // Resolved once, here: a tally rewrite must not cost a webfinger lookup
56393
56495
  // per vote for people the poll named.
56394
- mentionInboxes: [...new Set(mentions.map((m) => m.inbox).filter(Boolean))]
56496
+ mentionInboxes: [...new Set([...mentions.map((m) => m.inbox), reply?.inbox].filter(Boolean))]
56395
56497
  };
56396
56498
  const question = questionDoc({
56397
56499
  urls,
@@ -56403,6 +56505,8 @@ async function publishQuestion(publisher, content, {
56403
56505
  mentions,
56404
56506
  visibility,
56405
56507
  summary: spoilerText,
56508
+ sensitive,
56509
+ also: reply ? [reply.actor] : [],
56406
56510
  container: priv ? urls.privateNotes : urls.notes,
56407
56511
  options: poll.options,
56408
56512
  multiple: poll.multiple,
@@ -56427,8 +56531,10 @@ async function publishQuestion(publisher, content, {
56427
56531
  visibility,
56428
56532
  poll,
56429
56533
  inReplyTo,
56534
+ ...reply ? { replyActor: reply.actor } : {},
56430
56535
  ...spoilerText ? { spoiler: spoilerText } : {},
56431
- ...question.tag?.length ? { mentions: question.tag.map((t) => ({ href: t.href, name: t.name })) } : {}
56536
+ ...question.sensitive ? { sensitive: true } : {},
56537
+ ...rowTags(question)
56432
56538
  });
56433
56539
  if (await publisher.store.commit?.() === false) publisher.log(`poll published but its timeline row was refused: ${question.id}`);
56434
56540
  const create = publisher._pollActivity("Create", question, createActivityId(question.id));
@@ -56507,8 +56613,10 @@ async function republishPoll(publisher, questionId, { closing = null } = {}) {
56507
56613
  inReplyTo: s.inReplyTo,
56508
56614
  attachments: [],
56509
56615
  mentions,
56616
+ also: s.replyActor ? [s.replyActor] : [],
56510
56617
  visibility: s.visibility || "public",
56511
56618
  summary: s.spoiler || null,
56619
+ sensitive: !!s.sensitive,
56512
56620
  container,
56513
56621
  options: poll.options,
56514
56622
  multiple: !!poll.multiple,
@@ -56580,6 +56688,7 @@ var Publisher = class {
56580
56688
  log: log2 = console.log,
56581
56689
  probeFetch = null,
56582
56690
  resolveMention = null,
56691
+ resolveActor = null,
56583
56692
  clientOrigin = null
56584
56693
  }) {
56585
56694
  this.config = config;
@@ -56594,6 +56703,7 @@ var Publisher = class {
56594
56703
  if (this.urls.toPod && this.remote?.setUrlMap) this.remote.setUrlMap(this.urls.toPod);
56595
56704
  this.probeFetch = probeFetch || ((u, i) => this.remote.probe(u, i));
56596
56705
  this.resolveMention = resolveMention;
56706
+ this.resolveActor = resolveActor;
56597
56707
  this.pollTimers = /* @__PURE__ */ new Map();
56598
56708
  this.log = log2;
56599
56709
  }
@@ -56619,19 +56729,17 @@ var Publisher = class {
56619
56729
  if (gw.frontActor) return String(gw.frontActor).replace(/ap\/actor\/?$/u, "ap/outbox");
56620
56730
  return String(gw.url).replace(/ap\/inbox\/?$/u, "ap/outbox");
56621
56731
  }
56622
- async publishProfile({ force = false } = {}) {
56732
+ // Every actor document this identity publishes, from one place: the
56733
+ // profile publish and a Move both write it, and a Move that rebuilt it
56734
+ // by hand dropped the gateway inbox, the outbox door and the collections.
56735
+ actorDocFor({ priv = false, moderators = null, inbox = null, movedTo = this.config.movedTo || null } = {}) {
56623
56736
  const { urls } = this;
56624
- const host = new URL(urls.base).host;
56625
- const priv = await this.privateReady() === true;
56626
- const moderators = (this.config.moderators || []).length ? urls.moderators : null;
56627
- const gw = this.config.gateway;
56628
- const gwActive = gw && gw.url && gw.mode && gw.mode !== "off";
56629
- const actorDoc2 = actorDoc({
56737
+ return actorDoc({
56630
56738
  urls,
56631
56739
  handle: publicHandle(this.config),
56632
56740
  name: this.config.name,
56633
56741
  publicKeyPem: this.publicKeyPem,
56634
- movedTo: this.config.movedTo || null,
56742
+ movedTo,
56635
56743
  kind: this.config.kind,
56636
56744
  approveJoins: followsNeedApproval(this.config),
56637
56745
  assertionKey: this.assertionKey,
@@ -56645,7 +56753,7 @@ var Publisher = class {
56645
56753
  pendingFollowers: priv ? urls.pendingFollowers : null,
56646
56754
  pendingFollowing: priv ? urls.pendingFollowing : null,
56647
56755
  blocked: priv ? urls.blocked : null,
56648
- inbox: gwActive ? gw.url : null,
56756
+ inbox,
56649
56757
  // The agent's own outbox endpoint, where it is reachable: a client
56650
56758
  // following the actor must arrive somewhere that will take a write.
56651
56759
  // Otherwise the Gateway's outbox door, when one is attached.
@@ -56655,6 +56763,15 @@ var Publisher = class {
56655
56763
  oauthAuthorize: this.clientOrigin ? `${this.clientOrigin}oauth/authorize` : null,
56656
56764
  oauthToken: this.clientOrigin ? `${this.clientOrigin}oauth/token` : null
56657
56765
  });
56766
+ }
56767
+ async publishProfile({ force = false } = {}) {
56768
+ const { urls } = this;
56769
+ const host = new URL(urls.base).host;
56770
+ const priv = await this.privateReady() === true;
56771
+ const moderators = (this.config.moderators || []).length ? urls.moderators : null;
56772
+ const gw = this.config.gateway;
56773
+ const gwActive = gw && gw.url && gw.mode && gw.mode !== "off";
56774
+ const actorDoc2 = this.actorDocFor({ priv, moderators, inbox: gwActive ? gw.url : null });
56658
56775
  const surface = node_crypto_default.createHash("sha256").update(JSON.stringify({
56659
56776
  actor: actorDoc2,
56660
56777
  handle: this.config.handle,
@@ -56670,7 +56787,7 @@ var Publisher = class {
56670
56787
  await writeWebfinger(
56671
56788
  this.remote,
56672
56789
  urls,
56673
- jrd({ handle: this.config.handle, host, actor: urls.actor })
56790
+ jrd({ handle: this.config.handle, host, actor: urls.actor, page: urls.profileHtml })
56674
56791
  );
56675
56792
  await writeHostMeta(this.remote, urls, hostMeta(urls.base));
56676
56793
  const nodeinfoDocUrl = urls.home + "ap/nodeinfo-2.0";
@@ -56865,21 +56982,15 @@ var Publisher = class {
56865
56982
  await this.deliverer.deliverToAll(inboxes, moveActivity(urls, target, Date.parse(at)));
56866
56983
  this.config.movedTo = target;
56867
56984
  this.store.setConfig({ ...this.store.getConfig(), movedTo: target, movedAt: at });
56868
- await writeMoved(this.remote, urls, actorDoc({
56869
- urls,
56870
- handle: publicHandle(this.config),
56871
- name: this.config.name,
56872
- publicKeyPem: this.publicKeyPem,
56873
- movedTo: target,
56874
- kind: this.config.kind,
56875
- approveJoins: followsNeedApproval(this.config),
56876
- assertionKey: this.assertionKey,
56877
- summary: this.config.summary || null,
56878
- icon: this.config.icon || null,
56879
- image: this.config.image || null,
56880
- fields: this.config.fields || [],
56881
- webId: this.remote.webId || null,
56882
- aliases: this.config.aliases || []
56985
+ const gw = this.config.gateway;
56986
+ const gwActive = gw && gw.url && gw.mode && gw.mode !== "off";
56987
+ const priv = await this.privateReady() === true;
56988
+ const moderators = (this.config.moderators || []).length ? urls.moderators : null;
56989
+ await writeMoved(this.remote, urls, this.actorDocFor({
56990
+ priv,
56991
+ moderators,
56992
+ inbox: gwActive ? gw.url : null,
56993
+ movedTo: target
56883
56994
  }));
56884
56995
  await this.store.flush();
56885
56996
  this.log(`moved to ${target}: Move sent to ${inboxes.length} inbox(es), actor now advertises movedTo`);
@@ -57583,6 +57694,8 @@ async function onFollow(intake, activity, actor, { trusted = false } = {}) {
57583
57694
  intake.log(`Accept sent \u2192 ${doc.inbox}`);
57584
57695
  }
57585
57696
  async function onUndo(intake, activity, actor, { trusted = false } = {}) {
57697
+ const inner = activity.object && typeof activity.object === "object" ? activity.object : null;
57698
+ if (inner?.type === "Like" || inner?.type === "Announce") return onUndoReaction(intake, inner, actor, { trusted });
57586
57699
  if (typeof activity.object === "object" && activity.object?.type && activity.object.type !== "Follow") return;
57587
57700
  const named = typeof activity.object === "string" ? activity.object : activity.object?.id;
57588
57701
  if (!named && !trusted) return;
@@ -57670,6 +57783,47 @@ async function onAnnounce(intake, activity, actor, objectId) {
57670
57783
  }
57671
57784
  return intake.ingestNote(objectId, actor, { via: actor });
57672
57785
  }
57786
+ async function onUndoReaction(intake, inner, actor, { trusted = false } = {}) {
57787
+ const innerActor = typeof inner.actor === "string" ? inner.actor : inner.actor?.id;
57788
+ if (innerActor && innerActor !== actor) return;
57789
+ const wrapped = inner.object;
57790
+ const target = wrapped && typeof wrapped === "object" && (wrapped.type === "Create" || wrapped.type === "Update") ? wrapped.object : wrapped;
57791
+ const objectId = typeof target === "string" ? target : target?.id;
57792
+ if (!objectId) return;
57793
+ if (objectId.startsWith(intake.urls.notes)) {
57794
+ const type = inner.type === "Like" ? "favourite" : "reblog";
57795
+ const gone = intake.store.removeNotifications((n) => n.type === type && n.actor === actor && n.noteId === objectId);
57796
+ if (gone) intake.log(`${inner.type} withdrawn by ${actor} on ${objectId}`);
57797
+ return;
57798
+ }
57799
+ if (inner.type !== "Announce") return;
57800
+ const followed = trusted || intake.store.getContacts().following.some((f) => f.actor === actor && f.accepted);
57801
+ if (!followed) return;
57802
+ const s = intake.store.getStatuses().find((x) => x.noteId === objectId);
57803
+ if (!s || s.kind === "post" || s.via !== actor) return;
57804
+ await intake.forget(s);
57805
+ intake.log(`boost withdrawn by ${actor}: ${objectId} left the timeline`);
57806
+ }
57807
+ async function onBlock(intake, activity, actor, { trusted = false } = {}) {
57808
+ const objectId = typeof activity.object === "string" ? activity.object : activity.object?.id;
57809
+ if (objectId !== intake.urls.actor) return;
57810
+ if (!trusted) {
57811
+ intake.log(`Block from ${actor} arrived unverified \u2014 ignored`);
57812
+ return;
57813
+ }
57814
+ const contacts = intake.store.getContacts();
57815
+ const wasFollower = contacts.followers.some((f) => f.actor === actor);
57816
+ const wasFollowing = contacts.following.some((f) => f.actor === actor);
57817
+ if (wasFollower) dropFollower(contacts, actor, "blocked-us");
57818
+ contacts.following = contacts.following.filter((f) => f.actor !== actor);
57819
+ intake.store.setContacts(contacts);
57820
+ const reqs = intake.store.getRequests();
57821
+ const left = reqs.filter((r) => r.actor !== actor);
57822
+ if (left.length !== reqs.length) intake.store.setRequests(left);
57823
+ await intake.republish({ followers: wasFollower, following: wasFollowing, pending: true });
57824
+ const what = [wasFollower && "their follow of us", wasFollowing && "our follow of them"].filter(Boolean).join(" and ");
57825
+ intake.log(`blocked by ${actor}: ${what || "no relationship"} dropped`);
57826
+ }
57673
57827
  async function onDelete(intake, activity, actor) {
57674
57828
  const objectId = typeof activity.object === "string" ? activity.object : activity.object?.id;
57675
57829
  if (!objectId) return "Delete without object id";
@@ -57875,6 +58029,7 @@ async function ingestNote(intake, objectId, actor, { via, inline = null } = {})
57875
58029
  const followed = via || known2 || !known2 && await intake.isCoMember(author);
57876
58030
  const kind = followed ? "timeline" : "mention";
57877
58031
  const mentions = [].concat(note.tag || []).filter((t) => t?.type === "Mention" && t.href && t.name).slice(0, MAX_MENTIONS).map((t) => ({ href: httpOnly(String(t.href).slice(0, MAX_URL_CHARS)), name: String(t.name).slice(0, 256) })).filter((m) => m.href);
58032
+ const hashtags = [].concat(note.tag || []).filter((t) => t?.type === "Hashtag" && t.name).slice(0, MAX_MENTIONS).map((t) => ({ name: String(t.name).replace(/^#/, "").slice(0, 128), url: httpOnly(String(t.href || "").slice(0, MAX_URL_CHARS)) })).filter((t) => t.name);
57878
58033
  const emojis = emojisOf(note);
57879
58034
  const poll = pollOf(note);
57880
58035
  const audience = [].concat(note.to || [], note.cc || []).map(String);
@@ -57891,6 +58046,8 @@ async function ingestNote(intake, objectId, actor, { via, inline = null } = {})
57891
58046
  ...nonPublic ? { nonPublic: true } : {},
57892
58047
  // The author's content warning, shown as one: plain text only.
57893
58048
  ...note.summary ? { spoiler: String(note.summary).replace(/<[^>]*>/g, "") } : {},
58049
+ ...note.sensitive === true ? { sensitive: true } : {},
58050
+ ...hashtags.length ? { tags: hashtags } : {},
57894
58051
  ...poll ? { poll } : {},
57895
58052
  ...emojis.length ? { emojis } : {},
57896
58053
  ...mentions.length ? { mentions } : {},
@@ -63413,6 +63570,8 @@ var Intake = class {
63413
63570
  case "Add":
63414
63571
  case "Remove":
63415
63572
  return this.onAddRemove(activity, actor);
63573
+ case "Block":
63574
+ return this.onBlock(activity, actor, { trusted });
63416
63575
  default:
63417
63576
  this.log(`ignored ${activity.type} from ${actor}`);
63418
63577
  }
@@ -63506,6 +63665,9 @@ var Intake = class {
63506
63665
  onUndo(...a) {
63507
63666
  return onUndo(this, ...a);
63508
63667
  }
63668
+ onBlock(...a) {
63669
+ return onBlock(this, ...a);
63670
+ }
63509
63671
  onCreate(...a) {
63510
63672
  return onCreate(this, ...a);
63511
63673
  }
@@ -64135,7 +64297,7 @@ var C2S = class {
64135
64297
  return this.send(res, 405, { error: "deliveries go to this actor's inbox on the pod, which the actor document names; this address is the owner reading their own" });
64136
64298
  }
64137
64299
  const reader = await this.auth(req, pathname);
64138
- if (!reader.ok) return this.send(res, reader.status, { error: reader.error });
64300
+ if (!reader.ok) return this.send(res, reader.status, { error: reader.error }, reader.headers || {});
64139
64301
  return this.sendInbox(res, url);
64140
64302
  }
64141
64303
  if (req.method === "GET" || req.method === "HEAD") {
@@ -64148,7 +64310,7 @@ var C2S = class {
64148
64310
  return this.send(res, 405, { error: "POST the outbox; GET redirects to the pod" });
64149
64311
  }
64150
64312
  const who = await this.auth(req, pathname);
64151
- if (!who.ok) return this.send(res, who.status, { error: who.error });
64313
+ if (!who.ok) return this.send(res, who.status, { error: who.error }, who.headers || {});
64152
64314
  if (this.agent.viewer) {
64153
64315
  const took = await this.agent.requestTakeover?.();
64154
64316
  if (!took) return this.send(res, 503, { error: "another agent is active for this pod \u2014 takeover failed, try again" });
@@ -64202,6 +64364,13 @@ var C2S = class {
64202
64364
  return reply(422, { error: e.message || String(e) });
64203
64365
  }
64204
64366
  }
64367
+ // The actors a client addressed by id, beyond the audience words: the ones
64368
+ // in to/cc are listed and delivered to; the ones in bto/bcc are delivered
64369
+ // to and never listed (§6).
64370
+ addressedActors(activity, object) {
64371
+ const pick = (...fields) => [...new Set(fields.flatMap((f) => arr(activity[f] ?? object?.[f]).map(idOf)))].filter((a) => typeof a === "string" && /^https?:\/\//u.test(a) && a !== PUBLIC && a !== this.urls.followers && a !== this.urls.actor);
64372
+ return { also: pick("to", "cc"), deliverTo: pick("bto", "bcc") };
64373
+ }
64205
64374
  async _dispatch(activity, { slug, raw, reply }) {
64206
64375
  const agent2 = this.agent;
64207
64376
  const object = typeof activity.object === "object" && activity.object !== null ? activity.object : null;
@@ -64211,9 +64380,10 @@ var C2S = class {
64211
64380
  const makes = object?.type || "Note";
64212
64381
  if (!object) return reply(422, { error: "a Create carries the object it creates" });
64213
64382
  const visibility = this.visibilityOf(activity, object);
64383
+ const { also, deliverTo } = this.addressedActors(activity, object);
64214
64384
  if (makes !== "Note" && makes !== "Question") {
64215
64385
  const asSent = raw && typeof raw === "object" && !Array.isArray(raw) ? ACTIVITY_TYPES.has(raw.type) ? raw.object && typeof raw.object === "object" ? raw.object : null : raw : null;
64216
- const made = await agent2.publisher.publishObject(asSent || object, { visibility, slug });
64386
+ const made = await agent2.publisher.publishObject(asSent || object, { visibility, slug, also, deliverTo });
64217
64387
  return reply(201, { id: made.createId, object: made.id }, { location: made.createId });
64218
64388
  }
64219
64389
  const text = String(object.source?.content ?? object.content ?? "");
@@ -64229,7 +64399,8 @@ var C2S = class {
64229
64399
  expiresAt: object.endTime || null,
64230
64400
  inReplyTo: idOf(object.inReplyTo) || void 0,
64231
64401
  visibility,
64232
- spoilerText: object.summary || null
64402
+ spoilerText: object.summary || null,
64403
+ sensitive: object.sensitive === true
64233
64404
  });
64234
64405
  return reply(
64235
64406
  201,
@@ -64250,7 +64421,10 @@ var C2S = class {
64250
64421
  attachments,
64251
64422
  visibility,
64252
64423
  spoilerText: object.summary || null,
64253
- slug
64424
+ sensitive: object.sensitive === true,
64425
+ slug,
64426
+ also,
64427
+ deliverTo
64254
64428
  });
64255
64429
  return reply(
64256
64430
  201,
@@ -64277,6 +64451,7 @@ var C2S = class {
64277
64451
  await agent2.publisher.updateNote(s, {
64278
64452
  content: text,
64279
64453
  spoilerText: object?.summary || null,
64454
+ sensitive: object?.sensitive === void 0 ? null : object.sensitive === true,
64280
64455
  attachments
64281
64456
  });
64282
64457
  return reply(200, { ok: true, object: s.noteId });
@@ -65482,7 +65657,7 @@ function page(api, items, url, { limit = 20, max = 40, idOf: idOf2 = (s) => api.
65482
65657
  }
65483
65658
  const pageItems2 = items.slice(0, n);
65484
65659
  if (!pageItems2.length) return { items: pageItems2, headers: {} };
65485
- const base = `http://${api.host}${url.pathname}`;
65660
+ const base = url.origin + url.pathname;
65486
65661
  const q = (extra) => {
65487
65662
  const u = new URL(base);
65488
65663
  for (const [k, v] of url.searchParams) if (!["max_id", "since_id", "min_id"].includes(k)) u.searchParams.set(k, v);
@@ -65608,7 +65783,7 @@ function status(api, s, { all } = {}) {
65608
65783
  created_at: s.published || (/* @__PURE__ */ new Date()).toISOString(),
65609
65784
  in_reply_to_id: s.inReplyTo ? api.store.idFor(s.inReplyTo) : null,
65610
65785
  in_reply_to_account_id: null,
65611
- sensitive: !!s.spoiler,
65786
+ sensitive: !!(s.spoiler || s.sensitive),
65612
65787
  spoiler_text: s.spoiler || "",
65613
65788
  visibility: s.visibility || "public",
65614
65789
  language: null,
@@ -65648,7 +65823,7 @@ function status(api, s, { all } = {}) {
65648
65823
  acct: bare.includes("@") ? bare : host ? `${user}@${host}` : bare
65649
65824
  };
65650
65825
  }),
65651
- tags: [],
65826
+ tags: (s.tags || []).map((t) => ({ name: t.name, url: t.url })),
65652
65827
  emojis: (s.emojis || []).map((e) => ({
65653
65828
  shortcode: e.shortcode,
65654
65829
  url: e.url,
@@ -66580,6 +66755,7 @@ async function handle6(api, ctx) {
66580
66755
  if (ready !== true) return send(422, { error: ready });
66581
66756
  }
66582
66757
  const spoilerText = String(body.spoiler_text || "").trim() || null;
66758
+ const sensitive = body.sensitive === true || body.sensitive === "true" || body.sensitive === "1";
66583
66759
  const mediaIds = [].concat(body.media_ids || body["media_ids[]"] || []).filter(Boolean);
66584
66760
  const media = api.store.getMedia();
66585
66761
  const attachments = mediaIds.map((id) => media[id] && { id, ...media[id] }).filter(Boolean);
@@ -66610,7 +66786,8 @@ async function handle6(api, ctx) {
66610
66786
  expiresAt: new Date(Date.now() + seconds * 1e3).toISOString(),
66611
66787
  inReplyTo,
66612
66788
  visibility,
66613
- spoilerText
66789
+ spoilerText,
66790
+ sensitive
66614
66791
  });
66615
66792
  return send(200, api.status(api.store.getStatuses().find((x) => x.noteId === q.id)));
66616
66793
  } catch (e) {
@@ -66639,7 +66816,7 @@ async function handle6(api, ctx) {
66639
66816
  try {
66640
66817
  note = await api.agent.publisher.publishNote(
66641
66818
  body.status,
66642
- { inReplyTo, attachments, visibility, spoilerText }
66819
+ { inReplyTo, attachments, visibility, spoilerText, sensitive }
66643
66820
  );
66644
66821
  } catch (e) {
66645
66822
  if (e.code === "unaddressed") return send(422, { error: e.message });
@@ -66663,7 +66840,7 @@ async function handle6(api, ctx) {
66663
66840
  return send(200, [{
66664
66841
  content: s.content || "",
66665
66842
  spoiler_text: s.spoiler || "",
66666
- sensitive: !!s.spoiler,
66843
+ sensitive: !!(s.spoiler || s.sensitive),
66667
66844
  created_at: s.editedAt || s.published,
66668
66845
  account: api.account(s.actor),
66669
66846
  media_attachments: (s.attachments || []).map((a) => api.mediaJson(a)),
@@ -66688,9 +66865,10 @@ async function handle6(api, ctx) {
66688
66865
  const media = api.store.getMedia();
66689
66866
  const attachments = mediaIds.length ? mediaIds.map((id) => media[id] && { id, ...media[id] }).filter(Boolean) : null;
66690
66867
  const spoilerText = String(body.spoiler_text || "").trim() || null;
66868
+ const sensitive = body.sensitive === void 0 ? null : body.sensitive === true || body.sensitive === "true" || body.sensitive === "1";
66691
66869
  const patched = await api.agent.publisher.updateNote(
66692
66870
  s,
66693
- { content: body.status, spoilerText, attachments }
66871
+ { content: body.status, spoilerText, sensitive, attachments }
66694
66872
  );
66695
66873
  return send(200, api.status(patched || s));
66696
66874
  }
@@ -66929,7 +67107,13 @@ var MastoApi = class _MastoApi {
66929
67107
  res.end(body);
66930
67108
  return true;
66931
67109
  };
66932
- const ctx = { req, res, pathname, url, send };
67110
+ const scheme = String(this.scheme || (req.headers?.["x-forwarded-proto"] === "https" || req.socket?.encrypted ? "https" : "http")).replace(/:.*$/, "");
67111
+ let self2 = url;
67112
+ try {
67113
+ self2 = new URL(`${scheme}://${req.headers?.host || "localhost"}${this.mount || ""}${pathname}${url.search || ""}`);
67114
+ } catch {
67115
+ }
67116
+ const ctx = { req, res, pathname, url: self2, send };
66933
67117
  if (await handle(this, ctx)) return true;
66934
67118
  if (!pathname.startsWith("/api/")) return false;
66935
67119
  if (await handle2(this, ctx)) return true;
@@ -67940,6 +68124,7 @@ var hostOf = (inbox) => {
67940
68124
  }
67941
68125
  };
67942
68126
  var aboutTheHost = (e) => !e.status || e.status >= 500 || e.status === 429;
68127
+ var unsalvageable = (e) => e.status === 404 || e.status === 410;
67943
68128
  var Deliverer = class {
67944
68129
  // passive: signing-only (viewer-mode agents) — no queue drain timer, so a
67945
68130
  // read-only agent never mutates shared delivery state. startQueue() flips
@@ -67952,9 +68137,11 @@ var Deliverer = class {
67952
68137
  edPrivate = null,
67953
68138
  proofKeyId = null,
67954
68139
  log: log2 = console.log,
67955
- passive = false
68140
+ passive = false,
68141
+ onGone = null
67956
68142
  }) {
67957
68143
  this.store = store;
68144
+ this.onGone = onGone;
67958
68145
  this.keyId = keyId;
67959
68146
  this.rsaPrivate = rsaPrivate;
67960
68147
  this.actorId = actorId;
@@ -68050,6 +68237,10 @@ var Deliverer = class {
68050
68237
  await this.deliverNow(inbox, signed);
68051
68238
  this.log(`delivered ${signed.type} \u2192 ${inbox}`);
68052
68239
  } catch (e) {
68240
+ if (unsalvageable(e)) {
68241
+ await this._unsalvageable(inbox, e);
68242
+ return;
68243
+ }
68053
68244
  this.log(`delivery failed (${e.message}) \u2014 queued`);
68054
68245
  const wait = e.retryAfterMs || 6e4;
68055
68246
  if (aboutTheHost(e)) {
@@ -68059,6 +68250,26 @@ var Deliverer = class {
68059
68250
  this._enqueue({ inbox, activity: signed, attempts: 1, nextAt: Date.now() + wait });
68060
68251
  }
68061
68252
  }
68253
+ // Not retried, and on a 410 the followers who received there are dropped,
68254
+ // so the next fan-out stops asking; the followers collection is republished
68255
+ // through onGone. A 404 drops only this delivery.
68256
+ async _unsalvageable(inbox, e) {
68257
+ if (e.status !== 410) {
68258
+ this.log(`delivery dropped (${e.message}) \u2014 nothing answers there`);
68259
+ return;
68260
+ }
68261
+ const contacts = this.store.getContacts?.();
68262
+ const left = (contacts?.followers || []).filter((f) => f.inbox === inbox || f.sharedInbox === inbox);
68263
+ this.log(`${inbox} is gone (410) \u2014 delivery dropped${left.length ? `, ${left.length} follower(s) there forgotten` : ""}`);
68264
+ if (!left.length) return;
68265
+ for (const f of left) dropFollower(contacts, f.actor, "inbox-gone");
68266
+ this.store.setContacts(contacts);
68267
+ try {
68268
+ await this.onGone?.();
68269
+ } catch (err) {
68270
+ this.log(`followers republish after 410: ${err.message}`);
68271
+ }
68272
+ }
68062
68273
  // The queue has a ceiling: a fan-out into a sea of down servers must not grow
68063
68274
  // queue.json without bound. Overflow keeps the evidence as a dead letter.
68064
68275
  _enqueue(item) {
@@ -68143,6 +68354,11 @@ var Deliverer = class {
68143
68354
  changed = true;
68144
68355
  this.log(`retry ok: ${item.activity.type} \u2192 ${item.inbox}`);
68145
68356
  } catch (e) {
68357
+ if (unsalvageable(e)) {
68358
+ changed = true;
68359
+ await this._unsalvageable(item.inbox, e);
68360
+ continue;
68361
+ }
68146
68362
  item.attempts += 1;
68147
68363
  changed = true;
68148
68364
  const wait = e.retryAfterMs || ladderFor(item);
@@ -70436,7 +70652,8 @@ var BrowserAgent = class _BrowserAgent {
70436
70652
  // The relay finds the account by the front's own key for it, which for a
70437
70653
  // mail-door account is the full address, not the bare handle.
70438
70654
  handle: doorKeyOf(config.gateway?.url) || config.handle,
70439
- sessionFetch: session.fetch
70655
+ sessionFetch: session.fetch,
70656
+ onGone: () => this.publisher.publishCollections({ followers: true })
70440
70657
  });
70441
70658
  this.publisher = new Publisher({
70442
70659
  config: this.store.getConfig(),
@@ -70449,7 +70666,8 @@ var BrowserAgent = class _BrowserAgent {
70449
70666
  // Who a post names, resolved — the same lookup the DeviceAgent
70450
70667
  // gives its publisher. Without it no mention from the browser ever
70451
70668
  // resolved: a direct message went to nobody, a mention notified no one.
70452
- resolveMention: (h) => resolveHandle(this, h)
70669
+ resolveMention: (h) => resolveHandle(this, h),
70670
+ resolveActor: (u) => this.intake.fetchAP(u)
70453
70671
  });
70454
70672
  this.atproto = new BrowserAtproto({ store: this.store, actorId: this.urls.actor, log: this.log });
70455
70673
  this.publisher.atproto = this.atproto;