fedipod 1.8.0 → 1.11.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 = {}) {
@@ -9789,9 +9797,30 @@ function actorDoc({
9789
9797
  publicKey: { id: urls.actor + "#main-key", owner: urls.actor, publicKeyPem }
9790
9798
  };
9791
9799
  }
9792
- function profilePageHtml({ name, address, summary = null, icon = null, kind = "person" }) {
9800
+ function profilePageHtml({
9801
+ name,
9802
+ address,
9803
+ summary = null,
9804
+ icon = null,
9805
+ image = null,
9806
+ fields = [],
9807
+ joined = null,
9808
+ pinned = [],
9809
+ kind = "person"
9810
+ }) {
9793
9811
  const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ ...HTML_ESCAPES, '"': "&quot;" })[c]);
9794
9812
  const what = kind === "group" ? "a group" : "an account";
9813
+ const month = (iso) => {
9814
+ const d = new Date(iso);
9815
+ return Number.isNaN(d.getTime()) ? null : d.toLocaleDateString("en", { month: "long", year: "numeric", timeZone: "UTC" });
9816
+ };
9817
+ const day = (iso) => {
9818
+ const d = new Date(iso);
9819
+ return Number.isNaN(d.getTime()) ? "" : d.toISOString().slice(0, 10);
9820
+ };
9821
+ const fieldRows = fields.filter((f) => f?.name && f?.value !== void 0 && f?.value !== null && String(f.value) !== "");
9822
+ const joinedText = joined ? month(joined) : null;
9823
+ const pins = pinned.filter((x) => x && typeof x.content === "string");
9795
9824
  return `<!doctype html>
9796
9825
  <html lang="en">
9797
9826
  <head>
@@ -9801,14 +9830,23 @@ function profilePageHtml({ name, address, summary = null, icon = null, kind = "p
9801
9830
  <style>
9802
9831
  :root { color-scheme: light dark; }
9803
9832
  body { font: 112.5%/1.5 system-ui, sans-serif; max-width: 34rem; margin: 3rem auto; padding: 0 1rem; }
9804
- img.avatar { width: 6rem; height: 6rem; border-radius: 1rem; object-fit: cover; }
9833
+ img.header { width: 100%; max-height: 12rem; object-fit: cover; border-radius: 1rem; }
9834
+ img.avatar { width: 6rem; height: 6rem; border-radius: 1rem; object-fit: cover; margin-top: 1rem; }
9805
9835
  h1 { margin: .5rem 0 0; }
9806
9836
  .address { font-size: 1.1rem; user-select: all; }
9837
+ dl.fields { display: grid; grid-template-columns: max-content 1fr; gap: .4rem 1rem; margin: 1.5rem 0; }
9838
+ dl.fields dt { font-weight: 600; }
9839
+ dl.fields dd { margin: 0; overflow-wrap: anywhere; }
9840
+ .joined { margin: 1rem 0; }
9841
+ section.pinned h2 { font-size: 1.1rem; margin: 2rem 0 .5rem; }
9842
+ article.pin { border: 1px solid #8884; border-radius: 1rem; padding: 1rem 1.2rem; margin: 1rem 0; }
9843
+ article.pin p:last-child { margin-bottom: 0; }
9844
+ article.pin .when { font-size: 1rem; }
9807
9845
  form { margin-top: 2rem; }
9808
9846
  label { display: block; margin-bottom: .3rem; }
9809
9847
  input { font: inherit; padding: .5rem; width: 14rem; max-width: 100%; }
9810
9848
  button { font: inherit; padding: .5rem 1rem; }
9811
- .hint { color: #777; font-size: .9rem; }
9849
+ .hint { color: #666; font-size: 1rem; }
9812
9850
  .err { color: #b00020; }
9813
9851
  .err:empty { display: none; }
9814
9852
  @media (prefers-color-scheme: dark) {
@@ -9819,10 +9857,22 @@ button { font: inherit; padding: .5rem 1rem; }
9819
9857
  </head>
9820
9858
  <body>
9821
9859
  <main>
9860
+ ${image ? `<img class="header" src="${esc(image)}" alt="">` : ""}
9822
9861
  ${icon ? `<img class="avatar" src="${esc(icon)}" alt="">` : ""}
9823
9862
  <h1>${esc(name)}</h1>
9824
9863
  <p class="address">${esc(address)}</p>
9825
9864
  ${summary ? `<div>${summary}</div>` : ""}
9865
+ ${fieldRows.length ? `<dl class="fields">
9866
+ ${fieldRows.map((f) => `<dt>${esc(f.name)}</dt><dd>${esc(f.value)}</dd>`).join("\n")}
9867
+ </dl>` : ""}
9868
+ ${joinedText ? `<p class="joined">Joined ${esc(joinedText)}</p>` : ""}
9869
+ ${pins.length ? `<section class="pinned">
9870
+ <h2>Pinned</h2>
9871
+ ${pins.map((x) => `<article class="pin">
9872
+ ${x.content}
9873
+ <p class="when">${x.url ? `<a href="${esc(x.url)}">${esc(day(x.published))}</a>` : esc(day(x.published))}</p>
9874
+ </article>`).join("\n")}
9875
+ </section>` : ""}
9826
9876
  <p>This is ${what} on the Fediverse. To follow it, paste the address above
9827
9877
  into the search box of Mastodon or any Fediverse app \u2014 or use the form.</p>
9828
9878
  <form id="follow">
@@ -9926,7 +9976,7 @@ function outboxPage(outboxId, n, items) {
9926
9976
  id: outboxPageId(outboxId, n),
9927
9977
  type: "OrderedCollectionPage",
9928
9978
  partOf: outboxId,
9929
- orderedItems: items,
9979
+ orderedItems: items.map(outboxWireItem),
9930
9980
  ...n > 1 ? { next: outboxPageId(outboxId, n - 1) } : {}
9931
9981
  };
9932
9982
  }
@@ -10046,8 +10096,27 @@ function attachmentsOf(note) {
10046
10096
  function mentionsIn(text) {
10047
10097
  return [...new Set(String(text).match(MENTION_RE) || [])].map((m) => m.slice(1));
10048
10098
  }
10049
- function contentHtml(text, mentions = []) {
10099
+ function hashtagsIn(text) {
10100
+ const seen = /* @__PURE__ */ new Map();
10101
+ for (const m of String(text).matchAll(HASHTAG_RE)) {
10102
+ const key = m[2].toLowerCase();
10103
+ if (!seen.has(key)) seen.set(key, m[2]);
10104
+ }
10105
+ return [...seen.values()];
10106
+ }
10107
+ function tagHref(urls, name) {
10108
+ return urls.profileHtml + "?tag=" + encodeURIComponent(String(name).toLowerCase());
10109
+ }
10110
+ function contentHtml(text, mentions = [], hashtags = []) {
10050
10111
  let html = String(text).replace(/[&<>]/g, (c) => HTML_ESCAPES[c]);
10112
+ if (hashtags.length) {
10113
+ const byName = new Map(hashtags.filter((h) => h?.name && h?.href).map((h) => [h.name.toLowerCase(), h.href]));
10114
+ html = html.replace(HASHTAG_RE, (all, lead, name) => {
10115
+ const href = byName.get(name.toLowerCase());
10116
+ if (!href) return all;
10117
+ return `${lead}<a href="${String(href).replace(/[&<>"]/g, (c) => HTML_ESCAPES[c] || "&quot;")}" class="mention hashtag" rel="tag">#<span>${name}</span></a>`;
10118
+ });
10119
+ }
10051
10120
  for (const m of mentions) {
10052
10121
  if (!m?.handle || !m?.actor) continue;
10053
10122
  const href = String(m.page || m.actor).replace(/[&<>"]/g, (c) => HTML_ESCAPES[c] || "&quot;");
@@ -10075,28 +10144,35 @@ function noteDoc({
10075
10144
  mentions = [],
10076
10145
  visibility = "public",
10077
10146
  summary = null,
10147
+ sensitive = false,
10078
10148
  updated = null,
10079
- container = null
10149
+ container = null,
10150
+ also = []
10080
10151
  }) {
10081
10152
  const id = (container || urls.notes) + slug;
10082
10153
  const who = mentions.map((m) => m.actor);
10083
- const addressed = addressing(urls, visibility, who);
10154
+ const extra = visibility === "direct" ? [] : also.filter((a) => a && !who.includes(a));
10155
+ const addressed = addressing(urls, visibility, [...who, ...extra]);
10156
+ const hashtags = hashtagsIn(content).map((name) => ({ name, href: tagHref(urls, name) }));
10084
10157
  const note = {
10085
10158
  "@context": AS_CTX,
10086
10159
  id,
10087
10160
  type: "Note",
10088
10161
  attributedTo: urls.actor,
10089
- content: contentHtml(content, mentions),
10162
+ content: contentHtml(content, mentions, hashtags),
10090
10163
  published,
10091
10164
  to: addressed.to,
10092
10165
  cc: addressed.cc,
10093
10166
  replies: repliesId(id)
10094
10167
  };
10095
10168
  if (summary) note.summary = summary;
10169
+ if (summary || sensitive) note.sensitive = true;
10096
10170
  if (updated) note.updated = updated;
10097
- if (mentions.length) {
10098
- note.tag = mentions.map((m) => ({ type: "Mention", href: m.actor, name: "@" + m.handle }));
10099
- }
10171
+ const tag = [
10172
+ ...mentions.map((m) => ({ type: "Mention", href: m.actor, name: "@" + m.handle })),
10173
+ ...hashtags.map((h) => ({ type: "Hashtag", href: h.href, name: "#" + h.name }))
10174
+ ];
10175
+ if (tag.length) note.tag = tag;
10100
10176
  if (inReplyTo) note.inReplyTo = inReplyTo;
10101
10177
  if (attachments?.length) {
10102
10178
  note.attachment = attachments.map((a) => ({
@@ -10274,7 +10350,7 @@ function addRemoveActivity({ urls, type, object, target, serial }) {
10274
10350
  target
10275
10351
  };
10276
10352
  }
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;
10353
+ 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
10354
  var init_wire = __esm({
10279
10355
  "lib/core/wire.mjs"() {
10280
10356
  init_urls();
@@ -10289,6 +10365,8 @@ var init_wire = __esm({
10289
10365
  outboxPageId = (outboxId, n) => `${outboxId}-${n}`;
10290
10366
  outboxPageCount = (total) => Math.max(1, Math.ceil(total / OUTBOX_PAGE_SIZE));
10291
10367
  outboxItemId = (i) => typeof i === "string" ? i : i?.id || null;
10368
+ outboxWireItem = (i) => typeof i === "string" ? createActivityId(i) : i;
10369
+ outboxLocalItem = (i) => typeof i === "string" && i.endsWith("-create") ? i.slice(0, -"-create".length) : i;
10292
10370
  FOLLOWERS_PAGE_SIZE = 20;
10293
10371
  followersPageId = (id, n) => `${id}-${n}`;
10294
10372
  followersPageCount = (total) => Math.max(1, Math.ceil(total / FOLLOWERS_PAGE_SIZE));
@@ -10329,6 +10407,7 @@ var init_wire = __esm({
10329
10407
  };
10330
10408
  HTML_ESCAPES = { "&": "&amp;", "<": "&lt;", ">": "&gt;" };
10331
10409
  MENTION_RE = /@([A-Za-z0-9_.-]+)@([A-Za-z0-9.-]+\.[A-Za-z]{2,}(?::\d+)?)/g;
10410
+ HASHTAG_RE = /(^|[^\/\w)])#([\p{L}\p{N}_]*[\p{L}_][\p{L}\p{N}_]*)/gu;
10332
10411
  }
10333
10412
  });
10334
10413
 
@@ -32650,11 +32729,12 @@ async function verifyHttpSignature(request, { documentLoader, keyCache, timeWind
32650
32729
  } catch {
32651
32730
  result = null;
32652
32731
  }
32732
+ const method = request.headers.has("signature-input") ? "rfc9421" : "draft-cavage";
32653
32733
  if (result?.verified) {
32654
32734
  const key = result.key;
32655
32735
  return {
32656
32736
  verified: true,
32657
- method: "draft-cavage",
32737
+ method,
32658
32738
  keyId: key.id?.href ?? null,
32659
32739
  actor: key.ownerId?.href ?? null,
32660
32740
  reason: null,
@@ -32675,7 +32755,7 @@ async function verifyHttpSignature(request, { documentLoader, keyCache, timeWind
32675
32755
  if (kind === "keyFetchError") {
32676
32756
  return {
32677
32757
  verified: false,
32678
- method: "draft-cavage",
32758
+ method,
32679
32759
  keyId: null,
32680
32760
  actor: null,
32681
32761
  reason: "key-unfetchable",
@@ -32684,7 +32764,7 @@ async function verifyHttpSignature(request, { documentLoader, keyCache, timeWind
32684
32764
  }
32685
32765
  return {
32686
32766
  verified: false,
32687
- method: "draft-cavage",
32767
+ method,
32688
32768
  keyId: null,
32689
32769
  actor: null,
32690
32770
  reason: "bad-signature",
@@ -34481,6 +34561,14 @@ var PodStore = class {
34481
34561
  // — not from the pod, not from the RDF. Unverified entries are dropped first
34482
34562
  // now, so a flood can only ever evict itself, and a stranger's genuine
34483
34563
  // favourite still shows up, which is what recording them at all is for.
34564
+ // What an Undo unsays: the favourite or boost it names, matched by a
34565
+ // predicate on what was recorded. Returns how many went.
34566
+ removeNotifications(pred) {
34567
+ const all = this.getNotifications();
34568
+ const kept = all.filter((n) => !pred(n));
34569
+ if (kept.length !== all.length) this.write("notifications.json", kept);
34570
+ return all.length - kept.length;
34571
+ }
34484
34572
  addNotification(n) {
34485
34573
  const all = this.getNotifications();
34486
34574
  const id = node_crypto_default.createHash("sha256").update(JSON.stringify(n)).digest("hex").slice(0, 16);
@@ -45433,8 +45521,9 @@ async function publishOutbox(publisher, outbox, { acls = false, force = false }
45433
45521
  );
45434
45522
  return wrote;
45435
45523
  }
45436
- function readPublishedOutbox(publisher) {
45437
- return readPublished(publisher.remote, publisher.urls);
45524
+ async function readPublishedOutbox(publisher) {
45525
+ const items = await readPublished(publisher.remote, publisher.urls);
45526
+ return Array.isArray(items) ? items.map(outboxLocalItem) : items;
45438
45527
  }
45439
45528
  async function publishFollowers(publisher, actors, { acls = false, force = false } = {}) {
45440
45529
  const { urls } = publisher;
@@ -55941,6 +56030,35 @@ async function mentionsFor(publisher, content, inReplyTo) {
55941
56030
  }
55942
56031
  return mentions;
55943
56032
  }
56033
+ async function replyTarget(publisher, inReplyTo, mentions = [], visibility = "public") {
56034
+ if (!inReplyTo || visibility === "direct" || !publisher.resolveActor) return null;
56035
+ const parent = publisher.store.getStatuses().find((s) => s.noteId === inReplyTo);
56036
+ let actor = parent?.actor || null;
56037
+ if (!parent) {
56038
+ const doc2 = await publisher.resolveActor(inReplyTo).catch(() => null);
56039
+ const by = doc2?.attributedTo;
56040
+ actor = typeof by === "string" ? by : (Array.isArray(by) ? by[0] : by)?.id || null;
56041
+ if (typeof actor === "object") actor = actor?.id || null;
56042
+ }
56043
+ if (!actor || actor === publisher.urls.actor) return null;
56044
+ if (mentions.some((m) => m.actor === actor)) return null;
56045
+ if (publisher.store.isBlocked?.(actor)) return null;
56046
+ const doc = await publisher.resolveActor(actor).catch(() => null);
56047
+ const inbox = doc?.endpoints?.sharedInbox || doc?.inbox || null;
56048
+ if (!inbox) publisher.log(`reply: no inbox found for ${actor} \u2014 they get it only through followers`);
56049
+ return { actor, inbox };
56050
+ }
56051
+ async function inboxesFor(publisher, actorIds = []) {
56052
+ const out = [];
56053
+ for (const id of [...new Set(actorIds.filter((a) => typeof a === "string" && a && a !== publisher.urls.actor))]) {
56054
+ if (!publisher.resolveActor) break;
56055
+ const doc = await publisher.resolveActor(id).catch(() => null);
56056
+ const inbox = doc?.endpoints?.sharedInbox || doc?.inbox;
56057
+ if (inbox) out.push(inbox);
56058
+ else publisher.log(`addressed actor ${id} has no inbox the agent can find \u2014 not delivered to`);
56059
+ }
56060
+ return out;
56061
+ }
55944
56062
  function assertDirectAddressed(content, mentions) {
55945
56063
  const wanted = [...new Set(mentionsIn(content))];
55946
56064
  const missing = wanted.filter((h) => !mentions.some((m) => m.handle === h));
@@ -55966,7 +56084,7 @@ function rowContent(obj) {
55966
56084
  const esc = (v) => String(v).replace(/[&<>"]/gu, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
55967
56085
  return `<p><a href="${esc(obj?.id || "")}">${esc(obj?.type || "object")}</a></p>`;
55968
56086
  }
55969
- async function publishNote(publisher, content, { inReplyTo, attachments, visibility = "public", spoilerText = null, slug: wanted = null } = {}) {
56087
+ async function publishNote(publisher, content, { inReplyTo, attachments, visibility = "public", spoilerText = null, sensitive = false, slug: wanted = null, also = [], deliverTo = [] } = {}) {
55970
56088
  const { urls } = publisher;
55971
56089
  const priv = visibility === "private" || visibility === "direct";
55972
56090
  if (priv) {
@@ -55977,6 +56095,7 @@ async function publishNote(publisher, content, { inReplyTo, attachments, visibil
55977
56095
  const slug = await slugFor(publisher, priv ? urls.privateNotes : urls.notes, wanted, published);
55978
56096
  const mentions = await publisher._mentionsFor(content, inReplyTo);
55979
56097
  if (visibility === "direct") assertDirectAddressed(content, mentions);
56098
+ const reply = await replyTarget(publisher, inReplyTo, mentions, visibility);
55980
56099
  const note = noteDoc({
55981
56100
  urls,
55982
56101
  slug,
@@ -55987,8 +56106,11 @@ async function publishNote(publisher, content, { inReplyTo, attachments, visibil
55987
56106
  mentions,
55988
56107
  visibility,
55989
56108
  summary: spoilerText,
55990
- container: priv ? urls.privateNotes : urls.notes
56109
+ sensitive,
56110
+ container: priv ? urls.privateNotes : urls.notes,
56111
+ also: [...also, ...reply ? [reply.actor] : []]
55991
56112
  });
56113
+ const addressedInboxes = await inboxesFor(publisher, [...also, ...deliverTo]);
55992
56114
  await write4(publisher.remote, note.id, note);
55993
56115
  await writeEmptyReplies(
55994
56116
  publisher.remote,
@@ -56007,8 +56129,9 @@ async function publishNote(publisher, content, { inReplyTo, attachments, visibil
56007
56129
  text: content,
56008
56130
  visibility,
56009
56131
  ...spoilerText ? { spoiler: spoilerText } : {},
56132
+ ...note.sensitive ? { sensitive: true } : {},
56010
56133
  ...attachments?.length ? { attachments } : {},
56011
- ...note.tag?.length ? { mentions: note.tag.map((t) => ({ href: t.href, name: t.name })) } : {}
56134
+ ...rowTags(note)
56012
56135
  });
56013
56136
  if (await publisher.store.commit?.() === false) publisher.log(`post published but its timeline row was refused: ${note.id}`);
56014
56137
  const create = createActivity(note, urls);
@@ -56016,7 +56139,9 @@ async function publishNote(publisher, content, { inReplyTo, attachments, visibil
56016
56139
  const contacts = publisher.store.getContacts();
56017
56140
  const inboxes = [...new Set([
56018
56141
  ...visibility === "direct" ? [] : contacts.followers.map((f) => f.sharedInbox || f.inbox),
56019
- ...mentions.map((m) => m.inbox)
56142
+ ...mentions.map((m) => m.inbox),
56143
+ reply?.inbox,
56144
+ ...addressedInboxes
56020
56145
  ].filter(Boolean))];
56021
56146
  await publisher.deliverer.deliverToAll(inboxes, create);
56022
56147
  publisher.log(`note published: ${note.id} \u2192 ${inboxes.length} inbox(es)`);
@@ -56035,7 +56160,7 @@ async function publishNote(publisher, content, { inReplyTo, attachments, visibil
56035
56160
  }
56036
56161
  return note;
56037
56162
  }
56038
- async function publishObject(publisher, object, { visibility = "public", slug: wanted = null } = {}) {
56163
+ async function publishObject(publisher, object, { visibility = "public", slug: wanted = null, also = [], deliverTo = [] } = {}) {
56039
56164
  const { urls } = publisher;
56040
56165
  const priv = visibility === "private" || visibility === "direct";
56041
56166
  if (priv) {
@@ -56047,11 +56172,13 @@ async function publishObject(publisher, object, { visibility = "public", slug: w
56047
56172
  const pod = publisher.config?.remotePod || urls.base;
56048
56173
  const ownId = typeof object.id === "string" && /^https?:\/\//u.test(object.id) && object.id.startsWith(pod) ? object.id : null;
56049
56174
  const name = await slugFor(publisher, container, wanted, published);
56050
- const addressed = addressing(urls, visibility);
56175
+ const addressed = addressing(urls, visibility, visibility === "direct" ? also : []);
56176
+ if (visibility !== "direct") addressed.cc = [.../* @__PURE__ */ new Set([...addressed.cc, ...also])];
56051
56177
  let doc = null;
56052
56178
  const id = ownId || container + name;
56053
56179
  if (!ownId) {
56054
- doc = { ...object, id, attributedTo: urls.actor, published: object.published || published, to: addressed.to, cc: addressed.cc };
56180
+ const { bto, bcc, ...sent } = object;
56181
+ doc = { ...sent, id, attributedTo: urls.actor, published: object.published || published, to: addressed.to, cc: addressed.cc };
56055
56182
  if (!doc["@context"]) doc["@context"] = AS_CTX;
56056
56183
  if (typeof doc.content === "string") doc.content = sanitizeHtml(doc.content);
56057
56184
  await write4(publisher.remote, id, doc);
@@ -56081,12 +56208,21 @@ async function publishObject(publisher, object, { visibility = "public", slug: w
56081
56208
  };
56082
56209
  await writeCreate(publisher.remote, create.id, create);
56083
56210
  const contacts = publisher.store.getContacts();
56084
- const inboxes = visibility === "direct" ? [] : [...new Set(contacts.followers.map((f) => f.sharedInbox || f.inbox).filter(Boolean))];
56211
+ const inboxes = [...new Set([
56212
+ ...visibility === "direct" ? [] : contacts.followers.map((f) => f.sharedInbox || f.inbox),
56213
+ ...await inboxesFor(publisher, [...also, ...deliverTo])
56214
+ ].filter(Boolean))];
56085
56215
  await publisher.deliverer.deliverToAll(inboxes, create);
56086
56216
  publisher.log(`${row.type || "object"} published: ${id} \u2192 ${inboxes.length} inbox(es)`);
56087
56217
  return { id, createId, copied: !ownId };
56088
56218
  }
56089
- async function updateNote(publisher, s, { content, spoilerText = null, attachments = null } = {}) {
56219
+ function rowTags(note) {
56220
+ const tags = [].concat(note.tag || []);
56221
+ const mentions = tags.filter((t) => t.type === "Mention").map((t) => ({ href: t.href, name: t.name }));
56222
+ const hashtags = tags.filter((t) => t.type === "Hashtag").map((t) => ({ name: String(t.name).replace(/^#/, ""), url: t.href }));
56223
+ return { ...mentions.length ? { mentions } : {}, ...hashtags.length ? { tags: hashtags } : {} };
56224
+ }
56225
+ async function updateNote(publisher, s, { content, spoilerText = null, sensitive = null, attachments = null } = {}) {
56090
56226
  const { urls } = publisher;
56091
56227
  const updated = (/* @__PURE__ */ new Date()).toISOString();
56092
56228
  const inText = new Set(mentionsIn(content));
@@ -56103,6 +56239,7 @@ async function updateNote(publisher, s, { content, spoilerText = null, attachmen
56103
56239
  const atts = attachments ?? s.attachments ?? [];
56104
56240
  const container = String(s.noteId).startsWith(urls.privateNotes) ? urls.privateNotes : urls.notes;
56105
56241
  const slug = s.slug || String(s.noteId).slice(container.length);
56242
+ const reply = await replyTarget(publisher, s.inReplyTo, mentions, s.visibility || "public");
56106
56243
  const note = noteDoc({
56107
56244
  urls,
56108
56245
  slug,
@@ -56113,8 +56250,10 @@ async function updateNote(publisher, s, { content, spoilerText = null, attachmen
56113
56250
  mentions,
56114
56251
  visibility: s.visibility || "public",
56115
56252
  summary: spoilerText,
56253
+ sensitive: sensitive ?? !!s.sensitive,
56116
56254
  updated,
56117
- container
56255
+ container,
56256
+ also: reply ? [reply.actor] : []
56118
56257
  });
56119
56258
  await write4(publisher.remote, note.id, note);
56120
56259
  await writeCreate(publisher.remote, createActivityId(note.id), createActivity(note, urls));
@@ -56123,14 +56262,17 @@ async function updateNote(publisher, s, { content, spoilerText = null, attachmen
56123
56262
  text: content,
56124
56263
  editedAt: updated,
56125
56264
  spoiler: spoilerText || void 0,
56265
+ sensitive: note.sensitive || void 0,
56126
56266
  attachments: atts.length ? atts : void 0,
56127
- mentions: note.tag?.length ? note.tag.map((t) => ({ href: t.href, name: t.name })) : void 0
56267
+ mentions: rowTags(note).mentions,
56268
+ tags: rowTags(note).tags
56128
56269
  });
56129
56270
  const update = updateActivity(note, urls);
56130
56271
  const contacts = publisher.store.getContacts();
56131
56272
  const inboxes = [...new Set([
56132
56273
  ...s.visibility === "direct" ? [] : contacts.followers.map((f) => f.sharedInbox || f.inbox),
56133
- ...mentions.map((m) => m.inbox)
56274
+ ...mentions.map((m) => m.inbox),
56275
+ reply?.inbox
56134
56276
  ].filter(Boolean))];
56135
56277
  await publisher.deliverer.deliverToAll(inboxes, update);
56136
56278
  publisher.log(`note edited: ${note.id} \u2192 ${inboxes.length} inbox(es)`);
@@ -56356,7 +56498,8 @@ async function publishQuestion(publisher, content, {
56356
56498
  expiresAt = null,
56357
56499
  inReplyTo = void 0,
56358
56500
  visibility = "public",
56359
- spoilerText = null
56501
+ spoilerText = null,
56502
+ sensitive = false
56360
56503
  } = {}) {
56361
56504
  const { urls } = publisher;
56362
56505
  const priv = visibility === "private" || visibility === "direct";
@@ -56383,6 +56526,7 @@ async function publishQuestion(publisher, content, {
56383
56526
  const slug = published.slice(0, 10) + "-" + node_crypto_default.randomBytes(4).toString("hex");
56384
56527
  const mentions = await publisher._mentionsFor(content, inReplyTo);
56385
56528
  if (visibility === "direct") assertDirectAddressed(content, mentions);
56529
+ const reply = await replyTarget(publisher, inReplyTo, mentions, visibility);
56386
56530
  const poll = {
56387
56531
  multiple: !!multiple,
56388
56532
  expiresAt: expiresAt || null,
@@ -56391,7 +56535,7 @@ async function publishQuestion(publisher, content, {
56391
56535
  votersCount: 0,
56392
56536
  // Resolved once, here: a tally rewrite must not cost a webfinger lookup
56393
56537
  // per vote for people the poll named.
56394
- mentionInboxes: [...new Set(mentions.map((m) => m.inbox).filter(Boolean))]
56538
+ mentionInboxes: [...new Set([...mentions.map((m) => m.inbox), reply?.inbox].filter(Boolean))]
56395
56539
  };
56396
56540
  const question = questionDoc({
56397
56541
  urls,
@@ -56403,6 +56547,8 @@ async function publishQuestion(publisher, content, {
56403
56547
  mentions,
56404
56548
  visibility,
56405
56549
  summary: spoilerText,
56550
+ sensitive,
56551
+ also: reply ? [reply.actor] : [],
56406
56552
  container: priv ? urls.privateNotes : urls.notes,
56407
56553
  options: poll.options,
56408
56554
  multiple: poll.multiple,
@@ -56427,8 +56573,10 @@ async function publishQuestion(publisher, content, {
56427
56573
  visibility,
56428
56574
  poll,
56429
56575
  inReplyTo,
56576
+ ...reply ? { replyActor: reply.actor } : {},
56430
56577
  ...spoilerText ? { spoiler: spoilerText } : {},
56431
- ...question.tag?.length ? { mentions: question.tag.map((t) => ({ href: t.href, name: t.name })) } : {}
56578
+ ...question.sensitive ? { sensitive: true } : {},
56579
+ ...rowTags(question)
56432
56580
  });
56433
56581
  if (await publisher.store.commit?.() === false) publisher.log(`poll published but its timeline row was refused: ${question.id}`);
56434
56582
  const create = publisher._pollActivity("Create", question, createActivityId(question.id));
@@ -56507,8 +56655,10 @@ async function republishPoll(publisher, questionId, { closing = null } = {}) {
56507
56655
  inReplyTo: s.inReplyTo,
56508
56656
  attachments: [],
56509
56657
  mentions,
56658
+ also: s.replyActor ? [s.replyActor] : [],
56510
56659
  visibility: s.visibility || "public",
56511
56660
  summary: s.spoiler || null,
56661
+ sensitive: !!s.sensitive,
56512
56662
  container,
56513
56663
  options: poll.options,
56514
56664
  multiple: !!poll.multiple,
@@ -56580,6 +56730,7 @@ var Publisher = class {
56580
56730
  log: log2 = console.log,
56581
56731
  probeFetch = null,
56582
56732
  resolveMention = null,
56733
+ resolveActor = null,
56583
56734
  clientOrigin = null
56584
56735
  }) {
56585
56736
  this.config = config;
@@ -56594,6 +56745,7 @@ var Publisher = class {
56594
56745
  if (this.urls.toPod && this.remote?.setUrlMap) this.remote.setUrlMap(this.urls.toPod);
56595
56746
  this.probeFetch = probeFetch || ((u, i) => this.remote.probe(u, i));
56596
56747
  this.resolveMention = resolveMention;
56748
+ this.resolveActor = resolveActor;
56597
56749
  this.pollTimers = /* @__PURE__ */ new Map();
56598
56750
  this.log = log2;
56599
56751
  }
@@ -56619,19 +56771,17 @@ var Publisher = class {
56619
56771
  if (gw.frontActor) return String(gw.frontActor).replace(/ap\/actor\/?$/u, "ap/outbox");
56620
56772
  return String(gw.url).replace(/ap\/inbox\/?$/u, "ap/outbox");
56621
56773
  }
56622
- async publishProfile({ force = false } = {}) {
56774
+ // Every actor document this identity publishes, from one place: the
56775
+ // profile publish and a Move both write it, and a Move that rebuilt it
56776
+ // by hand dropped the gateway inbox, the outbox door and the collections.
56777
+ actorDocFor({ priv = false, moderators = null, inbox = null, movedTo = this.config.movedTo || null } = {}) {
56623
56778
  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({
56779
+ return actorDoc({
56630
56780
  urls,
56631
56781
  handle: publicHandle(this.config),
56632
56782
  name: this.config.name,
56633
56783
  publicKeyPem: this.publicKeyPem,
56634
- movedTo: this.config.movedTo || null,
56784
+ movedTo,
56635
56785
  kind: this.config.kind,
56636
56786
  approveJoins: followsNeedApproval(this.config),
56637
56787
  assertionKey: this.assertionKey,
@@ -56645,7 +56795,7 @@ var Publisher = class {
56645
56795
  pendingFollowers: priv ? urls.pendingFollowers : null,
56646
56796
  pendingFollowing: priv ? urls.pendingFollowing : null,
56647
56797
  blocked: priv ? urls.blocked : null,
56648
- inbox: gwActive ? gw.url : null,
56798
+ inbox,
56649
56799
  // The agent's own outbox endpoint, where it is reachable: a client
56650
56800
  // following the actor must arrive somewhere that will take a write.
56651
56801
  // Otherwise the Gateway's outbox door, when one is attached.
@@ -56655,6 +56805,49 @@ var Publisher = class {
56655
56805
  oauthAuthorize: this.clientOrigin ? `${this.clientOrigin}oauth/authorize` : null,
56656
56806
  oauthToken: this.clientOrigin ? `${this.clientOrigin}oauth/token` : null
56657
56807
  });
56808
+ }
56809
+ // The human half: a page a browser can open and follow from. The actor
56810
+ // document is for servers; this is the address you hand to a person. It
56811
+ // shows who the account is, its fields, when it joined and what it pinned;
56812
+ // the counts and the posts live in the collections. Written only when its
56813
+ // content changed.
56814
+ async publishProfilePage({ force = false } = {}) {
56815
+ const { urls } = this;
56816
+ const host = new URL(urls.base).host;
56817
+ if (!this.config.createdAt) {
56818
+ const oldest = this.store.getStatuses().filter((s) => s.kind === "post" && s.published).map((s) => s.published).sort()[0];
56819
+ this.config.createdAt = oldest || (/* @__PURE__ */ new Date()).toISOString();
56820
+ this.store.setConfig?.({ ...this.store.getConfig?.(), createdAt: this.config.createdAt });
56821
+ }
56822
+ const pinned = this.store.getStatuses().filter((s) => s.kind === "post" && s.pinned && s.visibility !== "private" && s.visibility !== "direct").sort((a, b) => String(b.published).localeCompare(String(a.published))).slice(0, 5).map((s) => ({ content: s.content, published: s.published, url: s.noteId }));
56823
+ const frontActor = this.config.gateway?.frontActor;
56824
+ const address = frontActor ? `@${publicHandle(this.config)}@${new URL(frontActor).host}` : webfingerHost(urls.base) ? `@${this.config.handle}@${host}` : urls.actor;
56825
+ const html = profilePageHtml({
56826
+ name: this.config.name || this.config.handle,
56827
+ address,
56828
+ summary: this.config.summary ? contentHtml(this.config.summary) : null,
56829
+ icon: this.config.icon || null,
56830
+ image: this.config.image || null,
56831
+ fields: this.config.fields || [],
56832
+ joined: this.config.createdAt,
56833
+ pinned,
56834
+ kind: this.config.kind
56835
+ });
56836
+ const digest = node_crypto_default.createHash("sha256").update(html).digest("hex").slice(0, 32);
56837
+ const seen = this.store.read("published.json", {});
56838
+ if (!force && seen.pageDigest === digest) return false;
56839
+ await writeProfilePage(this.remote, urls, html);
56840
+ this.store.write("published.json", { ...this.store.read("published.json", {}), pageDigest: digest });
56841
+ return true;
56842
+ }
56843
+ async publishProfile({ force = false } = {}) {
56844
+ const { urls } = this;
56845
+ const host = new URL(urls.base).host;
56846
+ const priv = await this.privateReady() === true;
56847
+ const moderators = (this.config.moderators || []).length ? urls.moderators : null;
56848
+ const gw = this.config.gateway;
56849
+ const gwActive = gw && gw.url && gw.mode && gw.mode !== "off";
56850
+ const actorDoc2 = this.actorDocFor({ priv, moderators, inbox: gwActive ? gw.url : null });
56658
56851
  const surface = node_crypto_default.createHash("sha256").update(JSON.stringify({
56659
56852
  actor: actorDoc2,
56660
56853
  handle: this.config.handle,
@@ -56663,6 +56856,7 @@ var Publisher = class {
56663
56856
  version: AGENT_VERSION,
56664
56857
  moderators: this.config.moderators || []
56665
56858
  })).digest("hex").slice(0, 32);
56859
+ await this.publishProfilePage({ force });
56666
56860
  if (!force && this.store.read("published.json", {}).surfaceDigest === surface) {
56667
56861
  this.log("profile unchanged \u2014 nothing republished");
56668
56862
  return { unreachable: [], updated: 0, skipped: true };
@@ -56670,7 +56864,7 @@ var Publisher = class {
56670
56864
  await writeWebfinger(
56671
56865
  this.remote,
56672
56866
  urls,
56673
- jrd({ handle: this.config.handle, host, actor: urls.actor })
56867
+ jrd({ handle: this.config.handle, host, actor: urls.actor, page: urls.profileHtml })
56674
56868
  );
56675
56869
  await writeHostMeta(this.remote, urls, hostMeta(urls.base));
56676
56870
  const nodeinfoDocUrl = urls.home + "ap/nodeinfo-2.0";
@@ -56689,13 +56883,6 @@ var Publisher = class {
56689
56883
  );
56690
56884
  }
56691
56885
  if (gwActive) await this.publishGatewayPolicy();
56692
- await writeProfilePage(this.remote, urls, profilePageHtml({
56693
- name: this.config.name || this.config.handle,
56694
- address: webfingerHost(urls.base) ? `@${this.config.handle}@${host}` : urls.actor,
56695
- summary: this.config.summary ? contentHtml(this.config.summary) : null,
56696
- icon: this.config.icon || null,
56697
- kind: this.config.kind
56698
- }));
56699
56886
  try {
56700
56887
  const wrote = await linkInWebIdProfile(this.remote, {
56701
56888
  actorUrl: urls.actor,
@@ -56865,21 +57052,15 @@ var Publisher = class {
56865
57052
  await this.deliverer.deliverToAll(inboxes, moveActivity(urls, target, Date.parse(at)));
56866
57053
  this.config.movedTo = target;
56867
57054
  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 || []
57055
+ const gw = this.config.gateway;
57056
+ const gwActive = gw && gw.url && gw.mode && gw.mode !== "off";
57057
+ const priv = await this.privateReady() === true;
57058
+ const moderators = (this.config.moderators || []).length ? urls.moderators : null;
57059
+ await writeMoved(this.remote, urls, this.actorDocFor({
57060
+ priv,
57061
+ moderators,
57062
+ inbox: gwActive ? gw.url : null,
57063
+ movedTo: target
56883
57064
  }));
56884
57065
  await this.store.flush();
56885
57066
  this.log(`moved to ${target}: Move sent to ${inboxes.length} inbox(es), actor now advertises movedTo`);
@@ -57583,6 +57764,8 @@ async function onFollow(intake, activity, actor, { trusted = false } = {}) {
57583
57764
  intake.log(`Accept sent \u2192 ${doc.inbox}`);
57584
57765
  }
57585
57766
  async function onUndo(intake, activity, actor, { trusted = false } = {}) {
57767
+ const inner = activity.object && typeof activity.object === "object" ? activity.object : null;
57768
+ if (inner?.type === "Like" || inner?.type === "Announce") return onUndoReaction(intake, inner, actor, { trusted });
57586
57769
  if (typeof activity.object === "object" && activity.object?.type && activity.object.type !== "Follow") return;
57587
57770
  const named = typeof activity.object === "string" ? activity.object : activity.object?.id;
57588
57771
  if (!named && !trusted) return;
@@ -57670,6 +57853,47 @@ async function onAnnounce(intake, activity, actor, objectId) {
57670
57853
  }
57671
57854
  return intake.ingestNote(objectId, actor, { via: actor });
57672
57855
  }
57856
+ async function onUndoReaction(intake, inner, actor, { trusted = false } = {}) {
57857
+ const innerActor = typeof inner.actor === "string" ? inner.actor : inner.actor?.id;
57858
+ if (innerActor && innerActor !== actor) return;
57859
+ const wrapped = inner.object;
57860
+ const target = wrapped && typeof wrapped === "object" && (wrapped.type === "Create" || wrapped.type === "Update") ? wrapped.object : wrapped;
57861
+ const objectId = typeof target === "string" ? target : target?.id;
57862
+ if (!objectId) return;
57863
+ if (objectId.startsWith(intake.urls.notes)) {
57864
+ const type = inner.type === "Like" ? "favourite" : "reblog";
57865
+ const gone = intake.store.removeNotifications((n) => n.type === type && n.actor === actor && n.noteId === objectId);
57866
+ if (gone) intake.log(`${inner.type} withdrawn by ${actor} on ${objectId}`);
57867
+ return;
57868
+ }
57869
+ if (inner.type !== "Announce") return;
57870
+ const followed = trusted || intake.store.getContacts().following.some((f) => f.actor === actor && f.accepted);
57871
+ if (!followed) return;
57872
+ const s = intake.store.getStatuses().find((x) => x.noteId === objectId);
57873
+ if (!s || s.kind === "post" || s.via !== actor) return;
57874
+ await intake.forget(s);
57875
+ intake.log(`boost withdrawn by ${actor}: ${objectId} left the timeline`);
57876
+ }
57877
+ async function onBlock(intake, activity, actor, { trusted = false } = {}) {
57878
+ const objectId = typeof activity.object === "string" ? activity.object : activity.object?.id;
57879
+ if (objectId !== intake.urls.actor) return;
57880
+ if (!trusted) {
57881
+ intake.log(`Block from ${actor} arrived unverified \u2014 ignored`);
57882
+ return;
57883
+ }
57884
+ const contacts = intake.store.getContacts();
57885
+ const wasFollower = contacts.followers.some((f) => f.actor === actor);
57886
+ const wasFollowing = contacts.following.some((f) => f.actor === actor);
57887
+ if (wasFollower) dropFollower(contacts, actor, "blocked-us");
57888
+ contacts.following = contacts.following.filter((f) => f.actor !== actor);
57889
+ intake.store.setContacts(contacts);
57890
+ const reqs = intake.store.getRequests();
57891
+ const left = reqs.filter((r) => r.actor !== actor);
57892
+ if (left.length !== reqs.length) intake.store.setRequests(left);
57893
+ await intake.republish({ followers: wasFollower, following: wasFollowing, pending: true });
57894
+ const what = [wasFollower && "their follow of us", wasFollowing && "our follow of them"].filter(Boolean).join(" and ");
57895
+ intake.log(`blocked by ${actor}: ${what || "no relationship"} dropped`);
57896
+ }
57673
57897
  async function onDelete(intake, activity, actor) {
57674
57898
  const objectId = typeof activity.object === "string" ? activity.object : activity.object?.id;
57675
57899
  if (!objectId) return "Delete without object id";
@@ -57875,6 +58099,7 @@ async function ingestNote(intake, objectId, actor, { via, inline = null } = {})
57875
58099
  const followed = via || known2 || !known2 && await intake.isCoMember(author);
57876
58100
  const kind = followed ? "timeline" : "mention";
57877
58101
  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);
58102
+ 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
58103
  const emojis = emojisOf(note);
57879
58104
  const poll = pollOf(note);
57880
58105
  const audience = [].concat(note.to || [], note.cc || []).map(String);
@@ -57891,6 +58116,8 @@ async function ingestNote(intake, objectId, actor, { via, inline = null } = {})
57891
58116
  ...nonPublic ? { nonPublic: true } : {},
57892
58117
  // The author's content warning, shown as one: plain text only.
57893
58118
  ...note.summary ? { spoiler: String(note.summary).replace(/<[^>]*>/g, "") } : {},
58119
+ ...note.sensitive === true ? { sensitive: true } : {},
58120
+ ...hashtags.length ? { tags: hashtags } : {},
57894
58121
  ...poll ? { poll } : {},
57895
58122
  ...emojis.length ? { emojis } : {},
57896
58123
  ...mentions.length ? { mentions } : {},
@@ -63413,6 +63640,8 @@ var Intake = class {
63413
63640
  case "Add":
63414
63641
  case "Remove":
63415
63642
  return this.onAddRemove(activity, actor);
63643
+ case "Block":
63644
+ return this.onBlock(activity, actor, { trusted });
63416
63645
  default:
63417
63646
  this.log(`ignored ${activity.type} from ${actor}`);
63418
63647
  }
@@ -63506,6 +63735,9 @@ var Intake = class {
63506
63735
  onUndo(...a) {
63507
63736
  return onUndo(this, ...a);
63508
63737
  }
63738
+ onBlock(...a) {
63739
+ return onBlock(this, ...a);
63740
+ }
63509
63741
  onCreate(...a) {
63510
63742
  return onCreate(this, ...a);
63511
63743
  }
@@ -63986,6 +64218,9 @@ async function pinStatus(agent2, s, pinned) {
63986
64218
  if (s.actor !== agent2.publisher.urls.actor) throw new Error("not your status");
63987
64219
  const updated = agent2.store.updateStatus(s.noteId, { pinned: !!pinned });
63988
64220
  await agent2.publisher.publishFeatured().catch((e) => agent2.log?.(`featured: ${e.message}`));
64221
+ if (agent2.publisher.publishProfilePage) {
64222
+ await agent2.publisher.publishProfilePage().catch((e) => agent2.log?.(`profile page: ${e.message}`));
64223
+ }
63989
64224
  agent2.publisher.publishProfile?.().catch(() => {
63990
64225
  });
63991
64226
  return updated || s;
@@ -64135,7 +64370,7 @@ var C2S = class {
64135
64370
  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
64371
  }
64137
64372
  const reader = await this.auth(req, pathname);
64138
- if (!reader.ok) return this.send(res, reader.status, { error: reader.error });
64373
+ if (!reader.ok) return this.send(res, reader.status, { error: reader.error }, reader.headers || {});
64139
64374
  return this.sendInbox(res, url);
64140
64375
  }
64141
64376
  if (req.method === "GET" || req.method === "HEAD") {
@@ -64148,7 +64383,7 @@ var C2S = class {
64148
64383
  return this.send(res, 405, { error: "POST the outbox; GET redirects to the pod" });
64149
64384
  }
64150
64385
  const who = await this.auth(req, pathname);
64151
- if (!who.ok) return this.send(res, who.status, { error: who.error });
64386
+ if (!who.ok) return this.send(res, who.status, { error: who.error }, who.headers || {});
64152
64387
  if (this.agent.viewer) {
64153
64388
  const took = await this.agent.requestTakeover?.();
64154
64389
  if (!took) return this.send(res, 503, { error: "another agent is active for this pod \u2014 takeover failed, try again" });
@@ -64202,6 +64437,13 @@ var C2S = class {
64202
64437
  return reply(422, { error: e.message || String(e) });
64203
64438
  }
64204
64439
  }
64440
+ // The actors a client addressed by id, beyond the audience words: the ones
64441
+ // in to/cc are listed and delivered to; the ones in bto/bcc are delivered
64442
+ // to and never listed (§6).
64443
+ addressedActors(activity, object) {
64444
+ 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);
64445
+ return { also: pick("to", "cc"), deliverTo: pick("bto", "bcc") };
64446
+ }
64205
64447
  async _dispatch(activity, { slug, raw, reply }) {
64206
64448
  const agent2 = this.agent;
64207
64449
  const object = typeof activity.object === "object" && activity.object !== null ? activity.object : null;
@@ -64211,9 +64453,10 @@ var C2S = class {
64211
64453
  const makes = object?.type || "Note";
64212
64454
  if (!object) return reply(422, { error: "a Create carries the object it creates" });
64213
64455
  const visibility = this.visibilityOf(activity, object);
64456
+ const { also, deliverTo } = this.addressedActors(activity, object);
64214
64457
  if (makes !== "Note" && makes !== "Question") {
64215
64458
  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 });
64459
+ const made = await agent2.publisher.publishObject(asSent || object, { visibility, slug, also, deliverTo });
64217
64460
  return reply(201, { id: made.createId, object: made.id }, { location: made.createId });
64218
64461
  }
64219
64462
  const text = String(object.source?.content ?? object.content ?? "");
@@ -64229,7 +64472,8 @@ var C2S = class {
64229
64472
  expiresAt: object.endTime || null,
64230
64473
  inReplyTo: idOf(object.inReplyTo) || void 0,
64231
64474
  visibility,
64232
- spoilerText: object.summary || null
64475
+ spoilerText: object.summary || null,
64476
+ sensitive: object.sensitive === true
64233
64477
  });
64234
64478
  return reply(
64235
64479
  201,
@@ -64250,7 +64494,10 @@ var C2S = class {
64250
64494
  attachments,
64251
64495
  visibility,
64252
64496
  spoilerText: object.summary || null,
64253
- slug
64497
+ sensitive: object.sensitive === true,
64498
+ slug,
64499
+ also,
64500
+ deliverTo
64254
64501
  });
64255
64502
  return reply(
64256
64503
  201,
@@ -64277,6 +64524,7 @@ var C2S = class {
64277
64524
  await agent2.publisher.updateNote(s, {
64278
64525
  content: text,
64279
64526
  spoilerText: object?.summary || null,
64527
+ sensitive: object?.sensitive === void 0 ? null : object.sensitive === true,
64280
64528
  attachments
64281
64529
  });
64282
64530
  return reply(200, { ok: true, object: s.noteId });
@@ -65441,7 +65689,7 @@ function account(api, actorUrl, { selfAcct } = {}) {
65441
65689
  bot: false,
65442
65690
  discoverable: true,
65443
65691
  group: self2 ? api.store.getConfig()?.kind === "group" : cached.type === "Group",
65444
- created_at: "2026-01-01T00:00:00.000Z",
65692
+ created_at: self2 && api.store.getConfig()?.createdAt || "2026-01-01T00:00:00.000Z",
65445
65693
  note: (self2 ? api.store.getConfig()?.summary : cached.summary) || "",
65446
65694
  url: actorUrl,
65447
65695
  uri: actorUrl,
@@ -65482,7 +65730,7 @@ function page(api, items, url, { limit = 20, max = 40, idOf: idOf2 = (s) => api.
65482
65730
  }
65483
65731
  const pageItems2 = items.slice(0, n);
65484
65732
  if (!pageItems2.length) return { items: pageItems2, headers: {} };
65485
- const base = `http://${api.host}${url.pathname}`;
65733
+ const base = url.origin + url.pathname;
65486
65734
  const q = (extra) => {
65487
65735
  const u = new URL(base);
65488
65736
  for (const [k, v] of url.searchParams) if (!["max_id", "since_id", "min_id"].includes(k)) u.searchParams.set(k, v);
@@ -65608,7 +65856,7 @@ function status(api, s, { all } = {}) {
65608
65856
  created_at: s.published || (/* @__PURE__ */ new Date()).toISOString(),
65609
65857
  in_reply_to_id: s.inReplyTo ? api.store.idFor(s.inReplyTo) : null,
65610
65858
  in_reply_to_account_id: null,
65611
- sensitive: !!s.spoiler,
65859
+ sensitive: !!(s.spoiler || s.sensitive),
65612
65860
  spoiler_text: s.spoiler || "",
65613
65861
  visibility: s.visibility || "public",
65614
65862
  language: null,
@@ -65648,7 +65896,7 @@ function status(api, s, { all } = {}) {
65648
65896
  acct: bare.includes("@") ? bare : host ? `${user}@${host}` : bare
65649
65897
  };
65650
65898
  }),
65651
- tags: [],
65899
+ tags: (s.tags || []).map((t) => ({ name: t.name, url: t.url })),
65652
65900
  emojis: (s.emojis || []).map((e) => ({
65653
65901
  shortcode: e.shortcode,
65654
65902
  url: e.url,
@@ -66580,6 +66828,7 @@ async function handle6(api, ctx) {
66580
66828
  if (ready !== true) return send(422, { error: ready });
66581
66829
  }
66582
66830
  const spoilerText = String(body.spoiler_text || "").trim() || null;
66831
+ const sensitive = body.sensitive === true || body.sensitive === "true" || body.sensitive === "1";
66583
66832
  const mediaIds = [].concat(body.media_ids || body["media_ids[]"] || []).filter(Boolean);
66584
66833
  const media = api.store.getMedia();
66585
66834
  const attachments = mediaIds.map((id) => media[id] && { id, ...media[id] }).filter(Boolean);
@@ -66610,7 +66859,8 @@ async function handle6(api, ctx) {
66610
66859
  expiresAt: new Date(Date.now() + seconds * 1e3).toISOString(),
66611
66860
  inReplyTo,
66612
66861
  visibility,
66613
- spoilerText
66862
+ spoilerText,
66863
+ sensitive
66614
66864
  });
66615
66865
  return send(200, api.status(api.store.getStatuses().find((x) => x.noteId === q.id)));
66616
66866
  } catch (e) {
@@ -66639,7 +66889,7 @@ async function handle6(api, ctx) {
66639
66889
  try {
66640
66890
  note = await api.agent.publisher.publishNote(
66641
66891
  body.status,
66642
- { inReplyTo, attachments, visibility, spoilerText }
66892
+ { inReplyTo, attachments, visibility, spoilerText, sensitive }
66643
66893
  );
66644
66894
  } catch (e) {
66645
66895
  if (e.code === "unaddressed") return send(422, { error: e.message });
@@ -66663,7 +66913,7 @@ async function handle6(api, ctx) {
66663
66913
  return send(200, [{
66664
66914
  content: s.content || "",
66665
66915
  spoiler_text: s.spoiler || "",
66666
- sensitive: !!s.spoiler,
66916
+ sensitive: !!(s.spoiler || s.sensitive),
66667
66917
  created_at: s.editedAt || s.published,
66668
66918
  account: api.account(s.actor),
66669
66919
  media_attachments: (s.attachments || []).map((a) => api.mediaJson(a)),
@@ -66688,9 +66938,10 @@ async function handle6(api, ctx) {
66688
66938
  const media = api.store.getMedia();
66689
66939
  const attachments = mediaIds.length ? mediaIds.map((id) => media[id] && { id, ...media[id] }).filter(Boolean) : null;
66690
66940
  const spoilerText = String(body.spoiler_text || "").trim() || null;
66941
+ const sensitive = body.sensitive === void 0 ? null : body.sensitive === true || body.sensitive === "true" || body.sensitive === "1";
66691
66942
  const patched = await api.agent.publisher.updateNote(
66692
66943
  s,
66693
- { content: body.status, spoilerText, attachments }
66944
+ { content: body.status, spoilerText, sensitive, attachments }
66694
66945
  );
66695
66946
  return send(200, api.status(patched || s));
66696
66947
  }
@@ -66929,7 +67180,13 @@ var MastoApi = class _MastoApi {
66929
67180
  res.end(body);
66930
67181
  return true;
66931
67182
  };
66932
- const ctx = { req, res, pathname, url, send };
67183
+ const scheme = String(this.scheme || (req.headers?.["x-forwarded-proto"] === "https" || req.socket?.encrypted ? "https" : "http")).replace(/:.*$/, "");
67184
+ let self2 = url;
67185
+ try {
67186
+ self2 = new URL(`${scheme}://${req.headers?.host || "localhost"}${this.mount || ""}${pathname}${url.search || ""}`);
67187
+ } catch {
67188
+ }
67189
+ const ctx = { req, res, pathname, url: self2, send };
66933
67190
  if (await handle(this, ctx)) return true;
66934
67191
  if (!pathname.startsWith("/api/")) return false;
66935
67192
  if (await handle2(this, ctx)) return true;
@@ -67940,6 +68197,7 @@ var hostOf = (inbox) => {
67940
68197
  }
67941
68198
  };
67942
68199
  var aboutTheHost = (e) => !e.status || e.status >= 500 || e.status === 429;
68200
+ var unsalvageable = (e) => e.status === 404 || e.status === 410;
67943
68201
  var Deliverer = class {
67944
68202
  // passive: signing-only (viewer-mode agents) — no queue drain timer, so a
67945
68203
  // read-only agent never mutates shared delivery state. startQueue() flips
@@ -67952,9 +68210,11 @@ var Deliverer = class {
67952
68210
  edPrivate = null,
67953
68211
  proofKeyId = null,
67954
68212
  log: log2 = console.log,
67955
- passive = false
68213
+ passive = false,
68214
+ onGone = null
67956
68215
  }) {
67957
68216
  this.store = store;
68217
+ this.onGone = onGone;
67958
68218
  this.keyId = keyId;
67959
68219
  this.rsaPrivate = rsaPrivate;
67960
68220
  this.actorId = actorId;
@@ -68050,6 +68310,10 @@ var Deliverer = class {
68050
68310
  await this.deliverNow(inbox, signed);
68051
68311
  this.log(`delivered ${signed.type} \u2192 ${inbox}`);
68052
68312
  } catch (e) {
68313
+ if (unsalvageable(e)) {
68314
+ await this._unsalvageable(inbox, e);
68315
+ return;
68316
+ }
68053
68317
  this.log(`delivery failed (${e.message}) \u2014 queued`);
68054
68318
  const wait = e.retryAfterMs || 6e4;
68055
68319
  if (aboutTheHost(e)) {
@@ -68059,6 +68323,26 @@ var Deliverer = class {
68059
68323
  this._enqueue({ inbox, activity: signed, attempts: 1, nextAt: Date.now() + wait });
68060
68324
  }
68061
68325
  }
68326
+ // Not retried, and on a 410 the followers who received there are dropped,
68327
+ // so the next fan-out stops asking; the followers collection is republished
68328
+ // through onGone. A 404 drops only this delivery.
68329
+ async _unsalvageable(inbox, e) {
68330
+ if (e.status !== 410) {
68331
+ this.log(`delivery dropped (${e.message}) \u2014 nothing answers there`);
68332
+ return;
68333
+ }
68334
+ const contacts = this.store.getContacts?.();
68335
+ const left = (contacts?.followers || []).filter((f) => f.inbox === inbox || f.sharedInbox === inbox);
68336
+ this.log(`${inbox} is gone (410) \u2014 delivery dropped${left.length ? `, ${left.length} follower(s) there forgotten` : ""}`);
68337
+ if (!left.length) return;
68338
+ for (const f of left) dropFollower(contacts, f.actor, "inbox-gone");
68339
+ this.store.setContacts(contacts);
68340
+ try {
68341
+ await this.onGone?.();
68342
+ } catch (err) {
68343
+ this.log(`followers republish after 410: ${err.message}`);
68344
+ }
68345
+ }
68062
68346
  // The queue has a ceiling: a fan-out into a sea of down servers must not grow
68063
68347
  // queue.json without bound. Overflow keeps the evidence as a dead letter.
68064
68348
  _enqueue(item) {
@@ -68143,6 +68427,11 @@ var Deliverer = class {
68143
68427
  changed = true;
68144
68428
  this.log(`retry ok: ${item.activity.type} \u2192 ${item.inbox}`);
68145
68429
  } catch (e) {
68430
+ if (unsalvageable(e)) {
68431
+ changed = true;
68432
+ await this._unsalvageable(item.inbox, e);
68433
+ continue;
68434
+ }
68146
68435
  item.attempts += 1;
68147
68436
  changed = true;
68148
68437
  const wait = e.retryAfterMs || ladderFor(item);
@@ -70325,6 +70614,7 @@ var BrowserAgent = class _BrowserAgent {
70325
70614
  try {
70326
70615
  await this.store.load({ force: true }).catch((e) => this.log(`re-reading state: ${e.message}`));
70327
70616
  await this.publisher.healStatuses().catch((e) => this.log(`healing the timeline index: ${e.message}`));
70617
+ await this.publisher.publishProfilePage().catch((e) => this.log(`profile page: ${e.message}`));
70328
70618
  this.deliverer?.startQueue?.();
70329
70619
  await this.publisher.publishProfile();
70330
70620
  await this.store.flush?.();
@@ -70436,7 +70726,8 @@ var BrowserAgent = class _BrowserAgent {
70436
70726
  // The relay finds the account by the front's own key for it, which for a
70437
70727
  // mail-door account is the full address, not the bare handle.
70438
70728
  handle: doorKeyOf(config.gateway?.url) || config.handle,
70439
- sessionFetch: session.fetch
70729
+ sessionFetch: session.fetch,
70730
+ onGone: () => this.publisher.publishCollections({ followers: true })
70440
70731
  });
70441
70732
  this.publisher = new Publisher({
70442
70733
  config: this.store.getConfig(),
@@ -70449,7 +70740,8 @@ var BrowserAgent = class _BrowserAgent {
70449
70740
  // Who a post names, resolved — the same lookup the DeviceAgent
70450
70741
  // gives its publisher. Without it no mention from the browser ever
70451
70742
  // resolved: a direct message went to nobody, a mention notified no one.
70452
- resolveMention: (h) => resolveHandle(this, h)
70743
+ resolveMention: (h) => resolveHandle(this, h),
70744
+ resolveActor: (u) => this.intake.fetchAP(u)
70453
70745
  });
70454
70746
  this.atproto = new BrowserAtproto({ store: this.store, actorId: this.urls.actor, log: this.log });
70455
70747
  this.publisher.atproto = this.atproto;