fedipod-server 0.18.0 → 0.19.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,
@@ -9926,7 +9931,7 @@ function outboxPage(outboxId, n, items) {
9926
9931
  id: outboxPageId(outboxId, n),
9927
9932
  type: "OrderedCollectionPage",
9928
9933
  partOf: outboxId,
9929
- orderedItems: items,
9934
+ orderedItems: items.map(outboxWireItem),
9930
9935
  ...n > 1 ? { next: outboxPageId(outboxId, n - 1) } : {}
9931
9936
  };
9932
9937
  }
@@ -10046,8 +10051,27 @@ function attachmentsOf(note) {
10046
10051
  function mentionsIn(text) {
10047
10052
  return [...new Set(String(text).match(MENTION_RE) || [])].map((m) => m.slice(1));
10048
10053
  }
10049
- function contentHtml(text, mentions = []) {
10054
+ function hashtagsIn(text) {
10055
+ const seen = /* @__PURE__ */ new Map();
10056
+ for (const m of String(text).matchAll(HASHTAG_RE)) {
10057
+ const key = m[2].toLowerCase();
10058
+ if (!seen.has(key)) seen.set(key, m[2]);
10059
+ }
10060
+ return [...seen.values()];
10061
+ }
10062
+ function tagHref(urls, name) {
10063
+ return urls.profileHtml + "?tag=" + encodeURIComponent(String(name).toLowerCase());
10064
+ }
10065
+ function contentHtml(text, mentions = [], hashtags = []) {
10050
10066
  let html = String(text).replace(/[&<>]/g, (c) => HTML_ESCAPES[c]);
10067
+ if (hashtags.length) {
10068
+ const byName = new Map(hashtags.filter((h) => h?.name && h?.href).map((h) => [h.name.toLowerCase(), h.href]));
10069
+ html = html.replace(HASHTAG_RE, (all, lead, name) => {
10070
+ const href = byName.get(name.toLowerCase());
10071
+ if (!href) return all;
10072
+ return `${lead}<a href="${String(href).replace(/[&<>"]/g, (c) => HTML_ESCAPES[c] || "&quot;")}" class="mention hashtag" rel="tag">#<span>${name}</span></a>`;
10073
+ });
10074
+ }
10051
10075
  for (const m of mentions) {
10052
10076
  if (!m?.handle || !m?.actor) continue;
10053
10077
  const href = String(m.page || m.actor).replace(/[&<>"]/g, (c) => HTML_ESCAPES[c] || "&quot;");
@@ -10075,28 +10099,35 @@ function noteDoc({
10075
10099
  mentions = [],
10076
10100
  visibility = "public",
10077
10101
  summary = null,
10102
+ sensitive = false,
10078
10103
  updated = null,
10079
- container = null
10104
+ container = null,
10105
+ also = []
10080
10106
  }) {
10081
10107
  const id = (container || urls.notes) + slug;
10082
10108
  const who = mentions.map((m) => m.actor);
10083
- const addressed = addressing(urls, visibility, who);
10109
+ const extra = visibility === "direct" ? [] : also.filter((a) => a && !who.includes(a));
10110
+ const addressed = addressing(urls, visibility, [...who, ...extra]);
10111
+ const hashtags = hashtagsIn(content).map((name) => ({ name, href: tagHref(urls, name) }));
10084
10112
  const note = {
10085
10113
  "@context": AS_CTX,
10086
10114
  id,
10087
10115
  type: "Note",
10088
10116
  attributedTo: urls.actor,
10089
- content: contentHtml(content, mentions),
10117
+ content: contentHtml(content, mentions, hashtags),
10090
10118
  published,
10091
10119
  to: addressed.to,
10092
10120
  cc: addressed.cc,
10093
10121
  replies: repliesId(id)
10094
10122
  };
10095
10123
  if (summary) note.summary = summary;
10124
+ if (summary || sensitive) note.sensitive = true;
10096
10125
  if (updated) note.updated = updated;
10097
- if (mentions.length) {
10098
- note.tag = mentions.map((m) => ({ type: "Mention", href: m.actor, name: "@" + m.handle }));
10099
- }
10126
+ const tag = [
10127
+ ...mentions.map((m) => ({ type: "Mention", href: m.actor, name: "@" + m.handle })),
10128
+ ...hashtags.map((h) => ({ type: "Hashtag", href: h.href, name: "#" + h.name }))
10129
+ ];
10130
+ if (tag.length) note.tag = tag;
10100
10131
  if (inReplyTo) note.inReplyTo = inReplyTo;
10101
10132
  if (attachments?.length) {
10102
10133
  note.attachment = attachments.map((a) => ({
@@ -10274,7 +10305,7 @@ function addRemoveActivity({ urls, type, object, target, serial }) {
10274
10305
  target
10275
10306
  };
10276
10307
  }
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;
10308
+ 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
10309
  var init_wire = __esm({
10279
10310
  "lib/core/wire.mjs"() {
10280
10311
  init_urls();
@@ -10289,6 +10320,8 @@ var init_wire = __esm({
10289
10320
  outboxPageId = (outboxId, n) => `${outboxId}-${n}`;
10290
10321
  outboxPageCount = (total) => Math.max(1, Math.ceil(total / OUTBOX_PAGE_SIZE));
10291
10322
  outboxItemId = (i) => typeof i === "string" ? i : i?.id || null;
10323
+ outboxWireItem = (i) => typeof i === "string" ? createActivityId(i) : i;
10324
+ outboxLocalItem = (i) => typeof i === "string" && i.endsWith("-create") ? i.slice(0, -"-create".length) : i;
10292
10325
  FOLLOWERS_PAGE_SIZE = 20;
10293
10326
  followersPageId = (id, n) => `${id}-${n}`;
10294
10327
  followersPageCount = (total) => Math.max(1, Math.ceil(total / FOLLOWERS_PAGE_SIZE));
@@ -10329,6 +10362,7 @@ var init_wire = __esm({
10329
10362
  };
10330
10363
  HTML_ESCAPES = { "&": "&amp;", "<": "&lt;", ">": "&gt;" };
10331
10364
  MENTION_RE = /@([A-Za-z0-9_.-]+)@([A-Za-z0-9.-]+\.[A-Za-z]{2,}(?::\d+)?)/g;
10365
+ HASHTAG_RE = /(^|[^\/\w)])#([\p{L}\p{N}_]*[\p{L}_][\p{L}\p{N}_]*)/gu;
10332
10366
  }
10333
10367
  });
10334
10368
 
@@ -32650,11 +32684,12 @@ async function verifyHttpSignature(request, { documentLoader, keyCache, timeWind
32650
32684
  } catch {
32651
32685
  result = null;
32652
32686
  }
32687
+ const method = request.headers.has("signature-input") ? "rfc9421" : "draft-cavage";
32653
32688
  if (result?.verified) {
32654
32689
  const key = result.key;
32655
32690
  return {
32656
32691
  verified: true,
32657
- method: "draft-cavage",
32692
+ method,
32658
32693
  keyId: key.id?.href ?? null,
32659
32694
  actor: key.ownerId?.href ?? null,
32660
32695
  reason: null,
@@ -32675,7 +32710,7 @@ async function verifyHttpSignature(request, { documentLoader, keyCache, timeWind
32675
32710
  if (kind === "keyFetchError") {
32676
32711
  return {
32677
32712
  verified: false,
32678
- method: "draft-cavage",
32713
+ method,
32679
32714
  keyId: null,
32680
32715
  actor: null,
32681
32716
  reason: "key-unfetchable",
@@ -32684,7 +32719,7 @@ async function verifyHttpSignature(request, { documentLoader, keyCache, timeWind
32684
32719
  }
32685
32720
  return {
32686
32721
  verified: false,
32687
- method: "draft-cavage",
32722
+ method,
32688
32723
  keyId: null,
32689
32724
  actor: null,
32690
32725
  reason: "bad-signature",
@@ -34481,6 +34516,14 @@ var PodStore = class {
34481
34516
  // — not from the pod, not from the RDF. Unverified entries are dropped first
34482
34517
  // now, so a flood can only ever evict itself, and a stranger's genuine
34483
34518
  // favourite still shows up, which is what recording them at all is for.
34519
+ // What an Undo unsays: the favourite or boost it names, matched by a
34520
+ // predicate on what was recorded. Returns how many went.
34521
+ removeNotifications(pred) {
34522
+ const all = this.getNotifications();
34523
+ const kept = all.filter((n) => !pred(n));
34524
+ if (kept.length !== all.length) this.write("notifications.json", kept);
34525
+ return all.length - kept.length;
34526
+ }
34484
34527
  addNotification(n) {
34485
34528
  const all = this.getNotifications();
34486
34529
  const id = node_crypto_default.createHash("sha256").update(JSON.stringify(n)).digest("hex").slice(0, 16);
@@ -45433,8 +45476,9 @@ async function publishOutbox(publisher, outbox, { acls = false, force = false }
45433
45476
  );
45434
45477
  return wrote;
45435
45478
  }
45436
- function readPublishedOutbox(publisher) {
45437
- return readPublished(publisher.remote, publisher.urls);
45479
+ async function readPublishedOutbox(publisher) {
45480
+ const items = await readPublished(publisher.remote, publisher.urls);
45481
+ return Array.isArray(items) ? items.map(outboxLocalItem) : items;
45438
45482
  }
45439
45483
  async function publishFollowers(publisher, actors, { acls = false, force = false } = {}) {
45440
45484
  const { urls } = publisher;
@@ -55941,6 +55985,35 @@ async function mentionsFor(publisher, content, inReplyTo) {
55941
55985
  }
55942
55986
  return mentions;
55943
55987
  }
55988
+ async function replyTarget(publisher, inReplyTo, mentions = [], visibility = "public") {
55989
+ if (!inReplyTo || visibility === "direct" || !publisher.resolveActor) return null;
55990
+ const parent = publisher.store.getStatuses().find((s) => s.noteId === inReplyTo);
55991
+ let actor = parent?.actor || null;
55992
+ if (!parent) {
55993
+ const doc2 = await publisher.resolveActor(inReplyTo).catch(() => null);
55994
+ const by = doc2?.attributedTo;
55995
+ actor = typeof by === "string" ? by : (Array.isArray(by) ? by[0] : by)?.id || null;
55996
+ if (typeof actor === "object") actor = actor?.id || null;
55997
+ }
55998
+ if (!actor || actor === publisher.urls.actor) return null;
55999
+ if (mentions.some((m) => m.actor === actor)) return null;
56000
+ if (publisher.store.isBlocked?.(actor)) return null;
56001
+ const doc = await publisher.resolveActor(actor).catch(() => null);
56002
+ const inbox = doc?.endpoints?.sharedInbox || doc?.inbox || null;
56003
+ if (!inbox) publisher.log(`reply: no inbox found for ${actor} \u2014 they get it only through followers`);
56004
+ return { actor, inbox };
56005
+ }
56006
+ async function inboxesFor(publisher, actorIds = []) {
56007
+ const out = [];
56008
+ for (const id of [...new Set(actorIds.filter((a) => typeof a === "string" && a && a !== publisher.urls.actor))]) {
56009
+ if (!publisher.resolveActor) break;
56010
+ const doc = await publisher.resolveActor(id).catch(() => null);
56011
+ const inbox = doc?.endpoints?.sharedInbox || doc?.inbox;
56012
+ if (inbox) out.push(inbox);
56013
+ else publisher.log(`addressed actor ${id} has no inbox the agent can find \u2014 not delivered to`);
56014
+ }
56015
+ return out;
56016
+ }
55944
56017
  function assertDirectAddressed(content, mentions) {
55945
56018
  const wanted = [...new Set(mentionsIn(content))];
55946
56019
  const missing = wanted.filter((h) => !mentions.some((m) => m.handle === h));
@@ -55966,7 +56039,7 @@ function rowContent(obj) {
55966
56039
  const esc = (v) => String(v).replace(/[&<>"]/gu, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
55967
56040
  return `<p><a href="${esc(obj?.id || "")}">${esc(obj?.type || "object")}</a></p>`;
55968
56041
  }
55969
- async function publishNote(publisher, content, { inReplyTo, attachments, visibility = "public", spoilerText = null, slug: wanted = null } = {}) {
56042
+ async function publishNote(publisher, content, { inReplyTo, attachments, visibility = "public", spoilerText = null, sensitive = false, slug: wanted = null, also = [], deliverTo = [] } = {}) {
55970
56043
  const { urls } = publisher;
55971
56044
  const priv = visibility === "private" || visibility === "direct";
55972
56045
  if (priv) {
@@ -55977,6 +56050,7 @@ async function publishNote(publisher, content, { inReplyTo, attachments, visibil
55977
56050
  const slug = await slugFor(publisher, priv ? urls.privateNotes : urls.notes, wanted, published);
55978
56051
  const mentions = await publisher._mentionsFor(content, inReplyTo);
55979
56052
  if (visibility === "direct") assertDirectAddressed(content, mentions);
56053
+ const reply = await replyTarget(publisher, inReplyTo, mentions, visibility);
55980
56054
  const note = noteDoc({
55981
56055
  urls,
55982
56056
  slug,
@@ -55987,8 +56061,11 @@ async function publishNote(publisher, content, { inReplyTo, attachments, visibil
55987
56061
  mentions,
55988
56062
  visibility,
55989
56063
  summary: spoilerText,
55990
- container: priv ? urls.privateNotes : urls.notes
56064
+ sensitive,
56065
+ container: priv ? urls.privateNotes : urls.notes,
56066
+ also: [...also, ...reply ? [reply.actor] : []]
55991
56067
  });
56068
+ const addressedInboxes = await inboxesFor(publisher, [...also, ...deliverTo]);
55992
56069
  await write4(publisher.remote, note.id, note);
55993
56070
  await writeEmptyReplies(
55994
56071
  publisher.remote,
@@ -56007,8 +56084,9 @@ async function publishNote(publisher, content, { inReplyTo, attachments, visibil
56007
56084
  text: content,
56008
56085
  visibility,
56009
56086
  ...spoilerText ? { spoiler: spoilerText } : {},
56087
+ ...note.sensitive ? { sensitive: true } : {},
56010
56088
  ...attachments?.length ? { attachments } : {},
56011
- ...note.tag?.length ? { mentions: note.tag.map((t) => ({ href: t.href, name: t.name })) } : {}
56089
+ ...rowTags(note)
56012
56090
  });
56013
56091
  if (await publisher.store.commit?.() === false) publisher.log(`post published but its timeline row was refused: ${note.id}`);
56014
56092
  const create = createActivity(note, urls);
@@ -56016,7 +56094,9 @@ async function publishNote(publisher, content, { inReplyTo, attachments, visibil
56016
56094
  const contacts = publisher.store.getContacts();
56017
56095
  const inboxes = [...new Set([
56018
56096
  ...visibility === "direct" ? [] : contacts.followers.map((f) => f.sharedInbox || f.inbox),
56019
- ...mentions.map((m) => m.inbox)
56097
+ ...mentions.map((m) => m.inbox),
56098
+ reply?.inbox,
56099
+ ...addressedInboxes
56020
56100
  ].filter(Boolean))];
56021
56101
  await publisher.deliverer.deliverToAll(inboxes, create);
56022
56102
  publisher.log(`note published: ${note.id} \u2192 ${inboxes.length} inbox(es)`);
@@ -56035,7 +56115,7 @@ async function publishNote(publisher, content, { inReplyTo, attachments, visibil
56035
56115
  }
56036
56116
  return note;
56037
56117
  }
56038
- async function publishObject(publisher, object, { visibility = "public", slug: wanted = null } = {}) {
56118
+ async function publishObject(publisher, object, { visibility = "public", slug: wanted = null, also = [], deliverTo = [] } = {}) {
56039
56119
  const { urls } = publisher;
56040
56120
  const priv = visibility === "private" || visibility === "direct";
56041
56121
  if (priv) {
@@ -56047,11 +56127,13 @@ async function publishObject(publisher, object, { visibility = "public", slug: w
56047
56127
  const pod = publisher.config?.remotePod || urls.base;
56048
56128
  const ownId = typeof object.id === "string" && /^https?:\/\//u.test(object.id) && object.id.startsWith(pod) ? object.id : null;
56049
56129
  const name = await slugFor(publisher, container, wanted, published);
56050
- const addressed = addressing(urls, visibility);
56130
+ const addressed = addressing(urls, visibility, visibility === "direct" ? also : []);
56131
+ if (visibility !== "direct") addressed.cc = [.../* @__PURE__ */ new Set([...addressed.cc, ...also])];
56051
56132
  let doc = null;
56052
56133
  const id = ownId || container + name;
56053
56134
  if (!ownId) {
56054
- doc = { ...object, id, attributedTo: urls.actor, published: object.published || published, to: addressed.to, cc: addressed.cc };
56135
+ const { bto, bcc, ...sent } = object;
56136
+ doc = { ...sent, id, attributedTo: urls.actor, published: object.published || published, to: addressed.to, cc: addressed.cc };
56055
56137
  if (!doc["@context"]) doc["@context"] = AS_CTX;
56056
56138
  if (typeof doc.content === "string") doc.content = sanitizeHtml(doc.content);
56057
56139
  await write4(publisher.remote, id, doc);
@@ -56081,12 +56163,21 @@ async function publishObject(publisher, object, { visibility = "public", slug: w
56081
56163
  };
56082
56164
  await writeCreate(publisher.remote, create.id, create);
56083
56165
  const contacts = publisher.store.getContacts();
56084
- const inboxes = visibility === "direct" ? [] : [...new Set(contacts.followers.map((f) => f.sharedInbox || f.inbox).filter(Boolean))];
56166
+ const inboxes = [...new Set([
56167
+ ...visibility === "direct" ? [] : contacts.followers.map((f) => f.sharedInbox || f.inbox),
56168
+ ...await inboxesFor(publisher, [...also, ...deliverTo])
56169
+ ].filter(Boolean))];
56085
56170
  await publisher.deliverer.deliverToAll(inboxes, create);
56086
56171
  publisher.log(`${row.type || "object"} published: ${id} \u2192 ${inboxes.length} inbox(es)`);
56087
56172
  return { id, createId, copied: !ownId };
56088
56173
  }
56089
- async function updateNote(publisher, s, { content, spoilerText = null, attachments = null } = {}) {
56174
+ function rowTags(note) {
56175
+ const tags = [].concat(note.tag || []);
56176
+ const mentions = tags.filter((t) => t.type === "Mention").map((t) => ({ href: t.href, name: t.name }));
56177
+ const hashtags = tags.filter((t) => t.type === "Hashtag").map((t) => ({ name: String(t.name).replace(/^#/, ""), url: t.href }));
56178
+ return { ...mentions.length ? { mentions } : {}, ...hashtags.length ? { tags: hashtags } : {} };
56179
+ }
56180
+ async function updateNote(publisher, s, { content, spoilerText = null, sensitive = null, attachments = null } = {}) {
56090
56181
  const { urls } = publisher;
56091
56182
  const updated = (/* @__PURE__ */ new Date()).toISOString();
56092
56183
  const inText = new Set(mentionsIn(content));
@@ -56103,6 +56194,7 @@ async function updateNote(publisher, s, { content, spoilerText = null, attachmen
56103
56194
  const atts = attachments ?? s.attachments ?? [];
56104
56195
  const container = String(s.noteId).startsWith(urls.privateNotes) ? urls.privateNotes : urls.notes;
56105
56196
  const slug = s.slug || String(s.noteId).slice(container.length);
56197
+ const reply = await replyTarget(publisher, s.inReplyTo, mentions, s.visibility || "public");
56106
56198
  const note = noteDoc({
56107
56199
  urls,
56108
56200
  slug,
@@ -56113,8 +56205,10 @@ async function updateNote(publisher, s, { content, spoilerText = null, attachmen
56113
56205
  mentions,
56114
56206
  visibility: s.visibility || "public",
56115
56207
  summary: spoilerText,
56208
+ sensitive: sensitive ?? !!s.sensitive,
56116
56209
  updated,
56117
- container
56210
+ container,
56211
+ also: reply ? [reply.actor] : []
56118
56212
  });
56119
56213
  await write4(publisher.remote, note.id, note);
56120
56214
  await writeCreate(publisher.remote, createActivityId(note.id), createActivity(note, urls));
@@ -56123,14 +56217,17 @@ async function updateNote(publisher, s, { content, spoilerText = null, attachmen
56123
56217
  text: content,
56124
56218
  editedAt: updated,
56125
56219
  spoiler: spoilerText || void 0,
56220
+ sensitive: note.sensitive || void 0,
56126
56221
  attachments: atts.length ? atts : void 0,
56127
- mentions: note.tag?.length ? note.tag.map((t) => ({ href: t.href, name: t.name })) : void 0
56222
+ mentions: rowTags(note).mentions,
56223
+ tags: rowTags(note).tags
56128
56224
  });
56129
56225
  const update = updateActivity(note, urls);
56130
56226
  const contacts = publisher.store.getContacts();
56131
56227
  const inboxes = [...new Set([
56132
56228
  ...s.visibility === "direct" ? [] : contacts.followers.map((f) => f.sharedInbox || f.inbox),
56133
- ...mentions.map((m) => m.inbox)
56229
+ ...mentions.map((m) => m.inbox),
56230
+ reply?.inbox
56134
56231
  ].filter(Boolean))];
56135
56232
  await publisher.deliverer.deliverToAll(inboxes, update);
56136
56233
  publisher.log(`note edited: ${note.id} \u2192 ${inboxes.length} inbox(es)`);
@@ -56356,7 +56453,8 @@ async function publishQuestion(publisher, content, {
56356
56453
  expiresAt = null,
56357
56454
  inReplyTo = void 0,
56358
56455
  visibility = "public",
56359
- spoilerText = null
56456
+ spoilerText = null,
56457
+ sensitive = false
56360
56458
  } = {}) {
56361
56459
  const { urls } = publisher;
56362
56460
  const priv = visibility === "private" || visibility === "direct";
@@ -56383,6 +56481,7 @@ async function publishQuestion(publisher, content, {
56383
56481
  const slug = published.slice(0, 10) + "-" + node_crypto_default.randomBytes(4).toString("hex");
56384
56482
  const mentions = await publisher._mentionsFor(content, inReplyTo);
56385
56483
  if (visibility === "direct") assertDirectAddressed(content, mentions);
56484
+ const reply = await replyTarget(publisher, inReplyTo, mentions, visibility);
56386
56485
  const poll = {
56387
56486
  multiple: !!multiple,
56388
56487
  expiresAt: expiresAt || null,
@@ -56391,7 +56490,7 @@ async function publishQuestion(publisher, content, {
56391
56490
  votersCount: 0,
56392
56491
  // Resolved once, here: a tally rewrite must not cost a webfinger lookup
56393
56492
  // per vote for people the poll named.
56394
- mentionInboxes: [...new Set(mentions.map((m) => m.inbox).filter(Boolean))]
56493
+ mentionInboxes: [...new Set([...mentions.map((m) => m.inbox), reply?.inbox].filter(Boolean))]
56395
56494
  };
56396
56495
  const question = questionDoc({
56397
56496
  urls,
@@ -56403,6 +56502,8 @@ async function publishQuestion(publisher, content, {
56403
56502
  mentions,
56404
56503
  visibility,
56405
56504
  summary: spoilerText,
56505
+ sensitive,
56506
+ also: reply ? [reply.actor] : [],
56406
56507
  container: priv ? urls.privateNotes : urls.notes,
56407
56508
  options: poll.options,
56408
56509
  multiple: poll.multiple,
@@ -56427,8 +56528,10 @@ async function publishQuestion(publisher, content, {
56427
56528
  visibility,
56428
56529
  poll,
56429
56530
  inReplyTo,
56531
+ ...reply ? { replyActor: reply.actor } : {},
56430
56532
  ...spoilerText ? { spoiler: spoilerText } : {},
56431
- ...question.tag?.length ? { mentions: question.tag.map((t) => ({ href: t.href, name: t.name })) } : {}
56533
+ ...question.sensitive ? { sensitive: true } : {},
56534
+ ...rowTags(question)
56432
56535
  });
56433
56536
  if (await publisher.store.commit?.() === false) publisher.log(`poll published but its timeline row was refused: ${question.id}`);
56434
56537
  const create = publisher._pollActivity("Create", question, createActivityId(question.id));
@@ -56507,8 +56610,10 @@ async function republishPoll(publisher, questionId, { closing = null } = {}) {
56507
56610
  inReplyTo: s.inReplyTo,
56508
56611
  attachments: [],
56509
56612
  mentions,
56613
+ also: s.replyActor ? [s.replyActor] : [],
56510
56614
  visibility: s.visibility || "public",
56511
56615
  summary: s.spoiler || null,
56616
+ sensitive: !!s.sensitive,
56512
56617
  container,
56513
56618
  options: poll.options,
56514
56619
  multiple: !!poll.multiple,
@@ -56580,6 +56685,7 @@ var Publisher = class {
56580
56685
  log: log2 = console.log,
56581
56686
  probeFetch = null,
56582
56687
  resolveMention = null,
56688
+ resolveActor = null,
56583
56689
  clientOrigin = null
56584
56690
  }) {
56585
56691
  this.config = config;
@@ -56594,6 +56700,7 @@ var Publisher = class {
56594
56700
  if (this.urls.toPod && this.remote?.setUrlMap) this.remote.setUrlMap(this.urls.toPod);
56595
56701
  this.probeFetch = probeFetch || ((u, i) => this.remote.probe(u, i));
56596
56702
  this.resolveMention = resolveMention;
56703
+ this.resolveActor = resolveActor;
56597
56704
  this.pollTimers = /* @__PURE__ */ new Map();
56598
56705
  this.log = log2;
56599
56706
  }
@@ -56619,19 +56726,17 @@ var Publisher = class {
56619
56726
  if (gw.frontActor) return String(gw.frontActor).replace(/ap\/actor\/?$/u, "ap/outbox");
56620
56727
  return String(gw.url).replace(/ap\/inbox\/?$/u, "ap/outbox");
56621
56728
  }
56622
- async publishProfile({ force = false } = {}) {
56729
+ // Every actor document this identity publishes, from one place: the
56730
+ // profile publish and a Move both write it, and a Move that rebuilt it
56731
+ // by hand dropped the gateway inbox, the outbox door and the collections.
56732
+ actorDocFor({ priv = false, moderators = null, inbox = null, movedTo = this.config.movedTo || null } = {}) {
56623
56733
  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({
56734
+ return actorDoc({
56630
56735
  urls,
56631
56736
  handle: publicHandle(this.config),
56632
56737
  name: this.config.name,
56633
56738
  publicKeyPem: this.publicKeyPem,
56634
- movedTo: this.config.movedTo || null,
56739
+ movedTo,
56635
56740
  kind: this.config.kind,
56636
56741
  approveJoins: followsNeedApproval(this.config),
56637
56742
  assertionKey: this.assertionKey,
@@ -56645,7 +56750,7 @@ var Publisher = class {
56645
56750
  pendingFollowers: priv ? urls.pendingFollowers : null,
56646
56751
  pendingFollowing: priv ? urls.pendingFollowing : null,
56647
56752
  blocked: priv ? urls.blocked : null,
56648
- inbox: gwActive ? gw.url : null,
56753
+ inbox,
56649
56754
  // The agent's own outbox endpoint, where it is reachable: a client
56650
56755
  // following the actor must arrive somewhere that will take a write.
56651
56756
  // Otherwise the Gateway's outbox door, when one is attached.
@@ -56655,6 +56760,15 @@ var Publisher = class {
56655
56760
  oauthAuthorize: this.clientOrigin ? `${this.clientOrigin}oauth/authorize` : null,
56656
56761
  oauthToken: this.clientOrigin ? `${this.clientOrigin}oauth/token` : null
56657
56762
  });
56763
+ }
56764
+ async publishProfile({ force = false } = {}) {
56765
+ const { urls } = this;
56766
+ const host = new URL(urls.base).host;
56767
+ const priv = await this.privateReady() === true;
56768
+ const moderators = (this.config.moderators || []).length ? urls.moderators : null;
56769
+ const gw = this.config.gateway;
56770
+ const gwActive = gw && gw.url && gw.mode && gw.mode !== "off";
56771
+ const actorDoc2 = this.actorDocFor({ priv, moderators, inbox: gwActive ? gw.url : null });
56658
56772
  const surface = node_crypto_default.createHash("sha256").update(JSON.stringify({
56659
56773
  actor: actorDoc2,
56660
56774
  handle: this.config.handle,
@@ -56865,21 +56979,15 @@ var Publisher = class {
56865
56979
  await this.deliverer.deliverToAll(inboxes, moveActivity(urls, target, Date.parse(at)));
56866
56980
  this.config.movedTo = target;
56867
56981
  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 || []
56982
+ const gw = this.config.gateway;
56983
+ const gwActive = gw && gw.url && gw.mode && gw.mode !== "off";
56984
+ const priv = await this.privateReady() === true;
56985
+ const moderators = (this.config.moderators || []).length ? urls.moderators : null;
56986
+ await writeMoved(this.remote, urls, this.actorDocFor({
56987
+ priv,
56988
+ moderators,
56989
+ inbox: gwActive ? gw.url : null,
56990
+ movedTo: target
56883
56991
  }));
56884
56992
  await this.store.flush();
56885
56993
  this.log(`moved to ${target}: Move sent to ${inboxes.length} inbox(es), actor now advertises movedTo`);
@@ -57583,6 +57691,8 @@ async function onFollow(intake, activity, actor, { trusted = false } = {}) {
57583
57691
  intake.log(`Accept sent \u2192 ${doc.inbox}`);
57584
57692
  }
57585
57693
  async function onUndo(intake, activity, actor, { trusted = false } = {}) {
57694
+ const inner = activity.object && typeof activity.object === "object" ? activity.object : null;
57695
+ if (inner?.type === "Like" || inner?.type === "Announce") return onUndoReaction(intake, inner, actor, { trusted });
57586
57696
  if (typeof activity.object === "object" && activity.object?.type && activity.object.type !== "Follow") return;
57587
57697
  const named = typeof activity.object === "string" ? activity.object : activity.object?.id;
57588
57698
  if (!named && !trusted) return;
@@ -57670,6 +57780,47 @@ async function onAnnounce(intake, activity, actor, objectId) {
57670
57780
  }
57671
57781
  return intake.ingestNote(objectId, actor, { via: actor });
57672
57782
  }
57783
+ async function onUndoReaction(intake, inner, actor, { trusted = false } = {}) {
57784
+ const innerActor = typeof inner.actor === "string" ? inner.actor : inner.actor?.id;
57785
+ if (innerActor && innerActor !== actor) return;
57786
+ const wrapped = inner.object;
57787
+ const target = wrapped && typeof wrapped === "object" && (wrapped.type === "Create" || wrapped.type === "Update") ? wrapped.object : wrapped;
57788
+ const objectId = typeof target === "string" ? target : target?.id;
57789
+ if (!objectId) return;
57790
+ if (objectId.startsWith(intake.urls.notes)) {
57791
+ const type = inner.type === "Like" ? "favourite" : "reblog";
57792
+ const gone = intake.store.removeNotifications((n) => n.type === type && n.actor === actor && n.noteId === objectId);
57793
+ if (gone) intake.log(`${inner.type} withdrawn by ${actor} on ${objectId}`);
57794
+ return;
57795
+ }
57796
+ if (inner.type !== "Announce") return;
57797
+ const followed = trusted || intake.store.getContacts().following.some((f) => f.actor === actor && f.accepted);
57798
+ if (!followed) return;
57799
+ const s = intake.store.getStatuses().find((x) => x.noteId === objectId);
57800
+ if (!s || s.kind === "post" || s.via !== actor) return;
57801
+ await intake.forget(s);
57802
+ intake.log(`boost withdrawn by ${actor}: ${objectId} left the timeline`);
57803
+ }
57804
+ async function onBlock(intake, activity, actor, { trusted = false } = {}) {
57805
+ const objectId = typeof activity.object === "string" ? activity.object : activity.object?.id;
57806
+ if (objectId !== intake.urls.actor) return;
57807
+ if (!trusted) {
57808
+ intake.log(`Block from ${actor} arrived unverified \u2014 ignored`);
57809
+ return;
57810
+ }
57811
+ const contacts = intake.store.getContacts();
57812
+ const wasFollower = contacts.followers.some((f) => f.actor === actor);
57813
+ const wasFollowing = contacts.following.some((f) => f.actor === actor);
57814
+ if (wasFollower) dropFollower(contacts, actor, "blocked-us");
57815
+ contacts.following = contacts.following.filter((f) => f.actor !== actor);
57816
+ intake.store.setContacts(contacts);
57817
+ const reqs = intake.store.getRequests();
57818
+ const left = reqs.filter((r) => r.actor !== actor);
57819
+ if (left.length !== reqs.length) intake.store.setRequests(left);
57820
+ await intake.republish({ followers: wasFollower, following: wasFollowing, pending: true });
57821
+ const what = [wasFollower && "their follow of us", wasFollowing && "our follow of them"].filter(Boolean).join(" and ");
57822
+ intake.log(`blocked by ${actor}: ${what || "no relationship"} dropped`);
57823
+ }
57673
57824
  async function onDelete(intake, activity, actor) {
57674
57825
  const objectId = typeof activity.object === "string" ? activity.object : activity.object?.id;
57675
57826
  if (!objectId) return "Delete without object id";
@@ -57875,6 +58026,7 @@ async function ingestNote(intake, objectId, actor, { via, inline = null } = {})
57875
58026
  const followed = via || known2 || !known2 && await intake.isCoMember(author);
57876
58027
  const kind = followed ? "timeline" : "mention";
57877
58028
  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);
58029
+ 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
58030
  const emojis = emojisOf(note);
57879
58031
  const poll = pollOf(note);
57880
58032
  const audience = [].concat(note.to || [], note.cc || []).map(String);
@@ -57891,6 +58043,8 @@ async function ingestNote(intake, objectId, actor, { via, inline = null } = {})
57891
58043
  ...nonPublic ? { nonPublic: true } : {},
57892
58044
  // The author's content warning, shown as one: plain text only.
57893
58045
  ...note.summary ? { spoiler: String(note.summary).replace(/<[^>]*>/g, "") } : {},
58046
+ ...note.sensitive === true ? { sensitive: true } : {},
58047
+ ...hashtags.length ? { tags: hashtags } : {},
57894
58048
  ...poll ? { poll } : {},
57895
58049
  ...emojis.length ? { emojis } : {},
57896
58050
  ...mentions.length ? { mentions } : {},
@@ -63413,6 +63567,8 @@ var Intake = class {
63413
63567
  case "Add":
63414
63568
  case "Remove":
63415
63569
  return this.onAddRemove(activity, actor);
63570
+ case "Block":
63571
+ return this.onBlock(activity, actor, { trusted });
63416
63572
  default:
63417
63573
  this.log(`ignored ${activity.type} from ${actor}`);
63418
63574
  }
@@ -63506,6 +63662,9 @@ var Intake = class {
63506
63662
  onUndo(...a) {
63507
63663
  return onUndo(this, ...a);
63508
63664
  }
63665
+ onBlock(...a) {
63666
+ return onBlock(this, ...a);
63667
+ }
63509
63668
  onCreate(...a) {
63510
63669
  return onCreate(this, ...a);
63511
63670
  }
@@ -64135,7 +64294,7 @@ var C2S = class {
64135
64294
  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
64295
  }
64137
64296
  const reader = await this.auth(req, pathname);
64138
- if (!reader.ok) return this.send(res, reader.status, { error: reader.error });
64297
+ if (!reader.ok) return this.send(res, reader.status, { error: reader.error }, reader.headers || {});
64139
64298
  return this.sendInbox(res, url);
64140
64299
  }
64141
64300
  if (req.method === "GET" || req.method === "HEAD") {
@@ -64148,7 +64307,7 @@ var C2S = class {
64148
64307
  return this.send(res, 405, { error: "POST the outbox; GET redirects to the pod" });
64149
64308
  }
64150
64309
  const who = await this.auth(req, pathname);
64151
- if (!who.ok) return this.send(res, who.status, { error: who.error });
64310
+ if (!who.ok) return this.send(res, who.status, { error: who.error }, who.headers || {});
64152
64311
  if (this.agent.viewer) {
64153
64312
  const took = await this.agent.requestTakeover?.();
64154
64313
  if (!took) return this.send(res, 503, { error: "another agent is active for this pod \u2014 takeover failed, try again" });
@@ -64202,6 +64361,13 @@ var C2S = class {
64202
64361
  return reply(422, { error: e.message || String(e) });
64203
64362
  }
64204
64363
  }
64364
+ // The actors a client addressed by id, beyond the audience words: the ones
64365
+ // in to/cc are listed and delivered to; the ones in bto/bcc are delivered
64366
+ // to and never listed (§6).
64367
+ addressedActors(activity, object) {
64368
+ 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);
64369
+ return { also: pick("to", "cc"), deliverTo: pick("bto", "bcc") };
64370
+ }
64205
64371
  async _dispatch(activity, { slug, raw, reply }) {
64206
64372
  const agent2 = this.agent;
64207
64373
  const object = typeof activity.object === "object" && activity.object !== null ? activity.object : null;
@@ -64211,9 +64377,10 @@ var C2S = class {
64211
64377
  const makes = object?.type || "Note";
64212
64378
  if (!object) return reply(422, { error: "a Create carries the object it creates" });
64213
64379
  const visibility = this.visibilityOf(activity, object);
64380
+ const { also, deliverTo } = this.addressedActors(activity, object);
64214
64381
  if (makes !== "Note" && makes !== "Question") {
64215
64382
  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 });
64383
+ const made = await agent2.publisher.publishObject(asSent || object, { visibility, slug, also, deliverTo });
64217
64384
  return reply(201, { id: made.createId, object: made.id }, { location: made.createId });
64218
64385
  }
64219
64386
  const text = String(object.source?.content ?? object.content ?? "");
@@ -64229,7 +64396,8 @@ var C2S = class {
64229
64396
  expiresAt: object.endTime || null,
64230
64397
  inReplyTo: idOf(object.inReplyTo) || void 0,
64231
64398
  visibility,
64232
- spoilerText: object.summary || null
64399
+ spoilerText: object.summary || null,
64400
+ sensitive: object.sensitive === true
64233
64401
  });
64234
64402
  return reply(
64235
64403
  201,
@@ -64250,7 +64418,10 @@ var C2S = class {
64250
64418
  attachments,
64251
64419
  visibility,
64252
64420
  spoilerText: object.summary || null,
64253
- slug
64421
+ sensitive: object.sensitive === true,
64422
+ slug,
64423
+ also,
64424
+ deliverTo
64254
64425
  });
64255
64426
  return reply(
64256
64427
  201,
@@ -64277,6 +64448,7 @@ var C2S = class {
64277
64448
  await agent2.publisher.updateNote(s, {
64278
64449
  content: text,
64279
64450
  spoilerText: object?.summary || null,
64451
+ sensitive: object?.sensitive === void 0 ? null : object.sensitive === true,
64280
64452
  attachments
64281
64453
  });
64282
64454
  return reply(200, { ok: true, object: s.noteId });
@@ -65482,7 +65654,7 @@ function page(api, items, url, { limit = 20, max = 40, idOf: idOf2 = (s) => api.
65482
65654
  }
65483
65655
  const pageItems2 = items.slice(0, n);
65484
65656
  if (!pageItems2.length) return { items: pageItems2, headers: {} };
65485
- const base = `http://${api.host}${url.pathname}`;
65657
+ const base = url.origin + url.pathname;
65486
65658
  const q = (extra) => {
65487
65659
  const u = new URL(base);
65488
65660
  for (const [k, v] of url.searchParams) if (!["max_id", "since_id", "min_id"].includes(k)) u.searchParams.set(k, v);
@@ -65608,7 +65780,7 @@ function status(api, s, { all } = {}) {
65608
65780
  created_at: s.published || (/* @__PURE__ */ new Date()).toISOString(),
65609
65781
  in_reply_to_id: s.inReplyTo ? api.store.idFor(s.inReplyTo) : null,
65610
65782
  in_reply_to_account_id: null,
65611
- sensitive: !!s.spoiler,
65783
+ sensitive: !!(s.spoiler || s.sensitive),
65612
65784
  spoiler_text: s.spoiler || "",
65613
65785
  visibility: s.visibility || "public",
65614
65786
  language: null,
@@ -65648,7 +65820,7 @@ function status(api, s, { all } = {}) {
65648
65820
  acct: bare.includes("@") ? bare : host ? `${user}@${host}` : bare
65649
65821
  };
65650
65822
  }),
65651
- tags: [],
65823
+ tags: (s.tags || []).map((t) => ({ name: t.name, url: t.url })),
65652
65824
  emojis: (s.emojis || []).map((e) => ({
65653
65825
  shortcode: e.shortcode,
65654
65826
  url: e.url,
@@ -66580,6 +66752,7 @@ async function handle6(api, ctx) {
66580
66752
  if (ready !== true) return send(422, { error: ready });
66581
66753
  }
66582
66754
  const spoilerText = String(body.spoiler_text || "").trim() || null;
66755
+ const sensitive = body.sensitive === true || body.sensitive === "true" || body.sensitive === "1";
66583
66756
  const mediaIds = [].concat(body.media_ids || body["media_ids[]"] || []).filter(Boolean);
66584
66757
  const media = api.store.getMedia();
66585
66758
  const attachments = mediaIds.map((id) => media[id] && { id, ...media[id] }).filter(Boolean);
@@ -66610,7 +66783,8 @@ async function handle6(api, ctx) {
66610
66783
  expiresAt: new Date(Date.now() + seconds * 1e3).toISOString(),
66611
66784
  inReplyTo,
66612
66785
  visibility,
66613
- spoilerText
66786
+ spoilerText,
66787
+ sensitive
66614
66788
  });
66615
66789
  return send(200, api.status(api.store.getStatuses().find((x) => x.noteId === q.id)));
66616
66790
  } catch (e) {
@@ -66639,7 +66813,7 @@ async function handle6(api, ctx) {
66639
66813
  try {
66640
66814
  note = await api.agent.publisher.publishNote(
66641
66815
  body.status,
66642
- { inReplyTo, attachments, visibility, spoilerText }
66816
+ { inReplyTo, attachments, visibility, spoilerText, sensitive }
66643
66817
  );
66644
66818
  } catch (e) {
66645
66819
  if (e.code === "unaddressed") return send(422, { error: e.message });
@@ -66663,7 +66837,7 @@ async function handle6(api, ctx) {
66663
66837
  return send(200, [{
66664
66838
  content: s.content || "",
66665
66839
  spoiler_text: s.spoiler || "",
66666
- sensitive: !!s.spoiler,
66840
+ sensitive: !!(s.spoiler || s.sensitive),
66667
66841
  created_at: s.editedAt || s.published,
66668
66842
  account: api.account(s.actor),
66669
66843
  media_attachments: (s.attachments || []).map((a) => api.mediaJson(a)),
@@ -66688,9 +66862,10 @@ async function handle6(api, ctx) {
66688
66862
  const media = api.store.getMedia();
66689
66863
  const attachments = mediaIds.length ? mediaIds.map((id) => media[id] && { id, ...media[id] }).filter(Boolean) : null;
66690
66864
  const spoilerText = String(body.spoiler_text || "").trim() || null;
66865
+ const sensitive = body.sensitive === void 0 ? null : body.sensitive === true || body.sensitive === "true" || body.sensitive === "1";
66691
66866
  const patched = await api.agent.publisher.updateNote(
66692
66867
  s,
66693
- { content: body.status, spoilerText, attachments }
66868
+ { content: body.status, spoilerText, sensitive, attachments }
66694
66869
  );
66695
66870
  return send(200, api.status(patched || s));
66696
66871
  }
@@ -66929,7 +67104,13 @@ var MastoApi = class _MastoApi {
66929
67104
  res.end(body);
66930
67105
  return true;
66931
67106
  };
66932
- const ctx = { req, res, pathname, url, send };
67107
+ const scheme = String(this.scheme || (req.headers?.["x-forwarded-proto"] === "https" || req.socket?.encrypted ? "https" : "http")).replace(/:.*$/, "");
67108
+ let self2 = url;
67109
+ try {
67110
+ self2 = new URL(`${scheme}://${req.headers?.host || "localhost"}${this.mount || ""}${pathname}${url.search || ""}`);
67111
+ } catch {
67112
+ }
67113
+ const ctx = { req, res, pathname, url: self2, send };
66933
67114
  if (await handle(this, ctx)) return true;
66934
67115
  if (!pathname.startsWith("/api/")) return false;
66935
67116
  if (await handle2(this, ctx)) return true;
@@ -67940,6 +68121,7 @@ var hostOf = (inbox) => {
67940
68121
  }
67941
68122
  };
67942
68123
  var aboutTheHost = (e) => !e.status || e.status >= 500 || e.status === 429;
68124
+ var unsalvageable = (e) => e.status === 404 || e.status === 410;
67943
68125
  var Deliverer = class {
67944
68126
  // passive: signing-only (viewer-mode agents) — no queue drain timer, so a
67945
68127
  // read-only agent never mutates shared delivery state. startQueue() flips
@@ -67952,9 +68134,11 @@ var Deliverer = class {
67952
68134
  edPrivate = null,
67953
68135
  proofKeyId = null,
67954
68136
  log: log2 = console.log,
67955
- passive = false
68137
+ passive = false,
68138
+ onGone = null
67956
68139
  }) {
67957
68140
  this.store = store;
68141
+ this.onGone = onGone;
67958
68142
  this.keyId = keyId;
67959
68143
  this.rsaPrivate = rsaPrivate;
67960
68144
  this.actorId = actorId;
@@ -68050,6 +68234,10 @@ var Deliverer = class {
68050
68234
  await this.deliverNow(inbox, signed);
68051
68235
  this.log(`delivered ${signed.type} \u2192 ${inbox}`);
68052
68236
  } catch (e) {
68237
+ if (unsalvageable(e)) {
68238
+ await this._unsalvageable(inbox, e);
68239
+ return;
68240
+ }
68053
68241
  this.log(`delivery failed (${e.message}) \u2014 queued`);
68054
68242
  const wait = e.retryAfterMs || 6e4;
68055
68243
  if (aboutTheHost(e)) {
@@ -68059,6 +68247,26 @@ var Deliverer = class {
68059
68247
  this._enqueue({ inbox, activity: signed, attempts: 1, nextAt: Date.now() + wait });
68060
68248
  }
68061
68249
  }
68250
+ // Not retried, and on a 410 the followers who received there are dropped,
68251
+ // so the next fan-out stops asking; the followers collection is republished
68252
+ // through onGone. A 404 drops only this delivery.
68253
+ async _unsalvageable(inbox, e) {
68254
+ if (e.status !== 410) {
68255
+ this.log(`delivery dropped (${e.message}) \u2014 nothing answers there`);
68256
+ return;
68257
+ }
68258
+ const contacts = this.store.getContacts?.();
68259
+ const left = (contacts?.followers || []).filter((f) => f.inbox === inbox || f.sharedInbox === inbox);
68260
+ this.log(`${inbox} is gone (410) \u2014 delivery dropped${left.length ? `, ${left.length} follower(s) there forgotten` : ""}`);
68261
+ if (!left.length) return;
68262
+ for (const f of left) dropFollower(contacts, f.actor, "inbox-gone");
68263
+ this.store.setContacts(contacts);
68264
+ try {
68265
+ await this.onGone?.();
68266
+ } catch (err) {
68267
+ this.log(`followers republish after 410: ${err.message}`);
68268
+ }
68269
+ }
68062
68270
  // The queue has a ceiling: a fan-out into a sea of down servers must not grow
68063
68271
  // queue.json without bound. Overflow keeps the evidence as a dead letter.
68064
68272
  _enqueue(item) {
@@ -68143,6 +68351,11 @@ var Deliverer = class {
68143
68351
  changed = true;
68144
68352
  this.log(`retry ok: ${item.activity.type} \u2192 ${item.inbox}`);
68145
68353
  } catch (e) {
68354
+ if (unsalvageable(e)) {
68355
+ changed = true;
68356
+ await this._unsalvageable(item.inbox, e);
68357
+ continue;
68358
+ }
68146
68359
  item.attempts += 1;
68147
68360
  changed = true;
68148
68361
  const wait = e.retryAfterMs || ladderFor(item);
@@ -70436,7 +70649,8 @@ var BrowserAgent = class _BrowserAgent {
70436
70649
  // The relay finds the account by the front's own key for it, which for a
70437
70650
  // mail-door account is the full address, not the bare handle.
70438
70651
  handle: doorKeyOf(config.gateway?.url) || config.handle,
70439
- sessionFetch: session.fetch
70652
+ sessionFetch: session.fetch,
70653
+ onGone: () => this.publisher.publishCollections({ followers: true })
70440
70654
  });
70441
70655
  this.publisher = new Publisher({
70442
70656
  config: this.store.getConfig(),
@@ -70449,7 +70663,8 @@ var BrowserAgent = class _BrowserAgent {
70449
70663
  // Who a post names, resolved — the same lookup the DeviceAgent
70450
70664
  // gives its publisher. Without it no mention from the browser ever
70451
70665
  // resolved: a direct message went to nobody, a mention notified no one.
70452
- resolveMention: (h) => resolveHandle(this, h)
70666
+ resolveMention: (h) => resolveHandle(this, h),
70667
+ resolveActor: (u) => this.intake.fetchAP(u)
70453
70668
  });
70454
70669
  this.atproto = new BrowserAtproto({ store: this.store, actorId: this.urls.actor, log: this.log });
70455
70670
  this.publisher.atproto = this.atproto;