fedipod 1.7.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +14 -3
  2. package/gateway.md +16 -0
  3. package/groups.md +1 -0
  4. package/lib/client/c2s.mjs +112 -64
  5. package/lib/client/masto/index.mjs +9 -1
  6. package/lib/client/masto/render.mjs +5 -3
  7. package/lib/client/masto/statuses.mjs +7 -4
  8. package/lib/client/oidc-auth.mjs +10 -3
  9. package/lib/core/as2.mjs +1 -1
  10. package/lib/core/contexts/anno.json +126 -0
  11. package/lib/core/contexts/index.mjs +4 -0
  12. package/lib/core/contexts/map.json +2 -1
  13. package/lib/core/deliver.mjs +24 -1
  14. package/lib/core/intake/activities.mjs +54 -0
  15. package/lib/core/intake/index.mjs +34 -5
  16. package/lib/core/intake/notes.mjs +7 -0
  17. package/lib/core/publisher/collections.mjs +7 -4
  18. package/lib/core/publisher/index.mjs +49 -27
  19. package/lib/core/publisher/notes.mjs +154 -7
  20. package/lib/core/publisher/questions.mjs +11 -7
  21. package/lib/core/publisher/restore.mjs +27 -2
  22. package/lib/core/social.mjs +1 -0
  23. package/lib/core/store.mjs +13 -0
  24. package/lib/core/wire.mjs +71 -20
  25. package/lib/device/admin/static.mjs +2 -2
  26. package/lib/device/admin/surface.mjs +11 -2
  27. package/lib/gateway/front-core.mjs +76 -6
  28. package/lib/gateway/gateway-core.mjs +40 -0
  29. package/lib/gateway/httpsig.mjs +7 -3
  30. package/lib/pod/actor.mjs +2 -2
  31. package/lib/pod/transport.mjs +9 -3
  32. package/package.json +1 -1
  33. package/run-agent.mjs +11 -0
  34. package/scripts/refresh-contexts.mjs +5 -0
  35. package/scripts/stage-site.mjs +1 -0
  36. package/web/admin/index.html +2 -0
  37. package/web/admin/upkeep.js +9 -1
  38. package/web/app/README.md +1 -1
  39. package/web/app/agent.mjs +12 -0
  40. package/web/app/boot.mjs +49 -10
  41. package/web/app/dist/boot.js +47 -7
  42. package/web/app/dist/boot.js.map +2 -2
  43. package/web/app/dist/sw.js +1604 -722
  44. package/web/app/dist/sw.js.map +4 -4
  45. package/web/app/index.html +16 -6
  46. package/web/app/site/_redirects +1 -0
  47. package/web/app/site/admin/index.html +2 -0
  48. package/web/app/site/admin/upkeep.js +9 -1
  49. package/web/app/site/boot.js +89 -37
  50. package/web/app/site/index.html +16 -6
  51. package/web/app/site/sw.js +1605 -723
  52. package/web/front/#new-account.html# +0 -43
  53. package/web/front/new-account.html~ +0 -50
@@ -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,
@@ -9596,6 +9597,7 @@ __export(wire_exports, {
9596
9597
  acceptActivity: () => acceptActivity,
9597
9598
  actorDoc: () => actorDoc,
9598
9599
  addRemoveActivity: () => addRemoveActivity,
9600
+ addressing: () => addressing,
9599
9601
  announceActivity: () => announceActivity,
9600
9602
  apUrls: () => apUrls2,
9601
9603
  assertionKeyId: () => assertionKeyId,
@@ -9614,6 +9616,7 @@ __export(wire_exports, {
9614
9616
  followersPageId: () => followersPageId,
9615
9617
  followersPaging: () => followersPaging,
9616
9618
  followsNeedApproval: () => followsNeedApproval,
9619
+ hashtagsIn: () => hashtagsIn,
9617
9620
  hostMeta: () => hostMeta,
9618
9621
  jrd: () => jrd,
9619
9622
  likeActivity: () => likeActivity,
@@ -9626,11 +9629,13 @@ __export(wire_exports, {
9626
9629
  orderedCollection: () => orderedCollection,
9627
9630
  outboxHead: () => outboxHead,
9628
9631
  outboxItemId: () => outboxItemId,
9632
+ outboxLocalItem: () => outboxLocalItem,
9629
9633
  outboxPage: () => outboxPage,
9630
9634
  outboxPageCount: () => outboxPageCount,
9631
9635
  outboxPageId: () => outboxPageId,
9632
9636
  outboxPages: () => outboxPages,
9633
9637
  outboxPaging: () => outboxPaging,
9638
+ outboxWireItem: () => outboxWireItem,
9634
9639
  pageItems: () => pageItems,
9635
9640
  profilePageHtml: () => profilePageHtml,
9636
9641
  publicHandle: () => publicHandle,
@@ -9638,6 +9643,7 @@ __export(wire_exports, {
9638
9643
  rejectActivity: () => rejectActivity,
9639
9644
  repliesId: () => repliesId,
9640
9645
  sanitizeHtml: () => sanitizeHtml,
9646
+ tagHref: () => tagHref,
9641
9647
  titledContent: () => titledContent,
9642
9648
  tombstoneDoc: () => tombstoneDoc,
9643
9649
  undoActivity: () => undoActivity,
@@ -9658,11 +9664,14 @@ function hostMeta(base) {
9658
9664
  </XRD>
9659
9665
  `;
9660
9666
  }
9661
- function jrd({ handle: handle7, host, actor, aliases = [] }) {
9667
+ function jrd({ handle: handle7, host, actor, aliases = [], page: page2 = null }) {
9662
9668
  return {
9663
9669
  subject: `acct:${handle7}@${host}`,
9664
9670
  ...aliases.length ? { aliases } : {},
9665
- 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
+ ]
9666
9675
  };
9667
9676
  }
9668
9677
  function followsNeedApproval(config = {}) {
@@ -9925,7 +9934,7 @@ function outboxPage(outboxId, n, items) {
9925
9934
  id: outboxPageId(outboxId, n),
9926
9935
  type: "OrderedCollectionPage",
9927
9936
  partOf: outboxId,
9928
- orderedItems: items,
9937
+ orderedItems: items.map(outboxWireItem),
9929
9938
  ...n > 1 ? { next: outboxPageId(outboxId, n - 1) } : {}
9930
9939
  };
9931
9940
  }
@@ -10045,8 +10054,27 @@ function attachmentsOf(note) {
10045
10054
  function mentionsIn(text) {
10046
10055
  return [...new Set(String(text).match(MENTION_RE) || [])].map((m) => m.slice(1));
10047
10056
  }
10048
- function contentHtml(text, mentions = []) {
10057
+ function hashtagsIn(text) {
10058
+ const seen = /* @__PURE__ */ new Map();
10059
+ for (const m of String(text).matchAll(HASHTAG_RE)) {
10060
+ const key = m[2].toLowerCase();
10061
+ if (!seen.has(key)) seen.set(key, m[2]);
10062
+ }
10063
+ return [...seen.values()];
10064
+ }
10065
+ function tagHref(urls, name) {
10066
+ return urls.profileHtml + "?tag=" + encodeURIComponent(String(name).toLowerCase());
10067
+ }
10068
+ function contentHtml(text, mentions = [], hashtags = []) {
10049
10069
  let html = String(text).replace(/[&<>]/g, (c) => HTML_ESCAPES[c]);
10070
+ if (hashtags.length) {
10071
+ const byName = new Map(hashtags.filter((h) => h?.name && h?.href).map((h) => [h.name.toLowerCase(), h.href]));
10072
+ html = html.replace(HASHTAG_RE, (all, lead, name) => {
10073
+ const href = byName.get(name.toLowerCase());
10074
+ if (!href) return all;
10075
+ return `${lead}<a href="${String(href).replace(/[&<>"]/g, (c) => HTML_ESCAPES[c] || "&quot;")}" class="mention hashtag" rel="tag">#<span>${name}</span></a>`;
10076
+ });
10077
+ }
10050
10078
  for (const m of mentions) {
10051
10079
  if (!m?.handle || !m?.actor) continue;
10052
10080
  const href = String(m.page || m.actor).replace(/[&<>"]/g, (c) => HTML_ESCAPES[c] || "&quot;");
@@ -10056,6 +10084,14 @@ function contentHtml(text, mentions = []) {
10056
10084
  }
10057
10085
  return "<p>" + html.replace(/\n+/g, "</p><p>") + "</p>";
10058
10086
  }
10087
+ function addressing(urls, visibility, who = []) {
10088
+ return {
10089
+ public: { to: [PUBLIC], cc: [urls.followers, ...who] },
10090
+ unlisted: { to: [urls.followers], cc: [PUBLIC, ...who] },
10091
+ private: { to: [urls.followers], cc: who },
10092
+ direct: { to: who, cc: [] }
10093
+ }[visibility] || { to: [PUBLIC], cc: [urls.followers, ...who] };
10094
+ }
10059
10095
  function noteDoc({
10060
10096
  urls,
10061
10097
  slug,
@@ -10066,33 +10102,35 @@ function noteDoc({
10066
10102
  mentions = [],
10067
10103
  visibility = "public",
10068
10104
  summary = null,
10105
+ sensitive = false,
10069
10106
  updated = null,
10070
- container = null
10107
+ container = null,
10108
+ also = []
10071
10109
  }) {
10072
10110
  const id = (container || urls.notes) + slug;
10073
10111
  const who = mentions.map((m) => m.actor);
10074
- const addressing = {
10075
- public: { to: [PUBLIC], cc: [urls.followers, ...who] },
10076
- unlisted: { to: [urls.followers], cc: [PUBLIC, ...who] },
10077
- private: { to: [urls.followers], cc: who },
10078
- direct: { to: who, cc: [] }
10079
- }[visibility] || { to: [PUBLIC], cc: [urls.followers, ...who] };
10112
+ const extra = visibility === "direct" ? [] : also.filter((a) => a && !who.includes(a));
10113
+ const addressed = addressing(urls, visibility, [...who, ...extra]);
10114
+ const hashtags = hashtagsIn(content).map((name) => ({ name, href: tagHref(urls, name) }));
10080
10115
  const note = {
10081
10116
  "@context": AS_CTX,
10082
10117
  id,
10083
10118
  type: "Note",
10084
10119
  attributedTo: urls.actor,
10085
- content: contentHtml(content, mentions),
10120
+ content: contentHtml(content, mentions, hashtags),
10086
10121
  published,
10087
- to: addressing.to,
10088
- cc: addressing.cc,
10122
+ to: addressed.to,
10123
+ cc: addressed.cc,
10089
10124
  replies: repliesId(id)
10090
10125
  };
10091
10126
  if (summary) note.summary = summary;
10127
+ if (summary || sensitive) note.sensitive = true;
10092
10128
  if (updated) note.updated = updated;
10093
- if (mentions.length) {
10094
- note.tag = mentions.map((m) => ({ type: "Mention", href: m.actor, name: "@" + m.handle }));
10095
- }
10129
+ const tag = [
10130
+ ...mentions.map((m) => ({ type: "Mention", href: m.actor, name: "@" + m.handle })),
10131
+ ...hashtags.map((h) => ({ type: "Hashtag", href: h.href, name: "#" + h.name }))
10132
+ ];
10133
+ if (tag.length) note.tag = tag;
10096
10134
  if (inReplyTo) note.inReplyTo = inReplyTo;
10097
10135
  if (attachments?.length) {
10098
10136
  note.attachment = attachments.map((a) => ({
@@ -10270,7 +10308,7 @@ function addRemoveActivity({ urls, type, object, target, serial }) {
10270
10308
  target
10271
10309
  };
10272
10310
  }
10273
- var import_sanitize_html, AS_CTX, SEC_CTX, PUBLIC, DEFAULT_ROOT, assertionKeyId, OUTBOX_PAGE_SIZE, outboxPageId, outboxPageCount, outboxItemId, FOLLOWERS_PAGE_SIZE, followersPageId, followersPageCount, ALLOWED_TAGS, ALLOWED_ATTRS, MAX_ATTACHMENTS, MAX_ATTACHMENT_URL, attachmentUrl, HTML_ESCAPES, MENTION_RE;
10311
+ var import_sanitize_html, AS_CTX, SEC_CTX, PUBLIC, DEFAULT_ROOT, assertionKeyId, OUTBOX_PAGE_SIZE, outboxPageId, outboxPageCount, outboxItemId, outboxWireItem, outboxLocalItem, FOLLOWERS_PAGE_SIZE, followersPageId, followersPageCount, ALLOWED_TAGS, ALLOWED_ATTRS, MAX_ATTACHMENTS, MAX_ATTACHMENT_URL, attachmentUrl, HTML_ESCAPES, MENTION_RE, HASHTAG_RE;
10274
10312
  var init_wire = __esm({
10275
10313
  "lib/core/wire.mjs"() {
10276
10314
  init_urls();
@@ -10279,12 +10317,14 @@ var init_wire = __esm({
10279
10317
  AS_CTX = "https://www.w3.org/ns/activitystreams";
10280
10318
  SEC_CTX = "https://w3id.org/security/v1";
10281
10319
  PUBLIC = "https://www.w3.org/ns/activitystreams#Public";
10282
- DEFAULT_ROOT = "activitypods-js/";
10320
+ DEFAULT_ROOT = "fedipod/";
10283
10321
  assertionKeyId = (urls) => urls.actor + "#ed25519-key";
10284
10322
  OUTBOX_PAGE_SIZE = 20;
10285
10323
  outboxPageId = (outboxId, n) => `${outboxId}-${n}`;
10286
10324
  outboxPageCount = (total) => Math.max(1, Math.ceil(total / OUTBOX_PAGE_SIZE));
10287
10325
  outboxItemId = (i) => typeof i === "string" ? i : i?.id || null;
10326
+ outboxWireItem = (i) => typeof i === "string" ? createActivityId(i) : i;
10327
+ outboxLocalItem = (i) => typeof i === "string" && i.endsWith("-create") ? i.slice(0, -"-create".length) : i;
10288
10328
  FOLLOWERS_PAGE_SIZE = 20;
10289
10329
  followersPageId = (id, n) => `${id}-${n}`;
10290
10330
  followersPageCount = (total) => Math.max(1, Math.ceil(total / FOLLOWERS_PAGE_SIZE));
@@ -10325,6 +10365,7 @@ var init_wire = __esm({
10325
10365
  };
10326
10366
  HTML_ESCAPES = { "&": "&amp;", "<": "&lt;", ">": "&gt;" };
10327
10367
  MENTION_RE = /@([A-Za-z0-9_.-]+)@([A-Za-z0-9.-]+\.[A-Za-z]{2,}(?::\d+)?)/g;
10368
+ HASHTAG_RE = /(^|[^\/\w)])#([\p{L}\p{N}_]*[\p{L}_][\p{L}\p{N}_]*)/gu;
10328
10369
  }
10329
10370
  });
10330
10371
 
@@ -24128,20 +24169,20 @@ var require_yallist = __commonJS({
24128
24169
  return acc;
24129
24170
  };
24130
24171
  Yallist.prototype.toArray = function() {
24131
- var arr = new Array(this.length);
24172
+ var arr2 = new Array(this.length);
24132
24173
  for (var i = 0, walker = this.head; walker !== null; i++) {
24133
- arr[i] = walker.value;
24174
+ arr2[i] = walker.value;
24134
24175
  walker = walker.next;
24135
24176
  }
24136
- return arr;
24177
+ return arr2;
24137
24178
  };
24138
24179
  Yallist.prototype.toArrayReverse = function() {
24139
- var arr = new Array(this.length);
24180
+ var arr2 = new Array(this.length);
24140
24181
  for (var i = 0, walker = this.tail; walker !== null; i++) {
24141
- arr[i] = walker.value;
24182
+ arr2[i] = walker.value;
24142
24183
  walker = walker.prev;
24143
24184
  }
24144
- return arr;
24185
+ return arr2;
24145
24186
  };
24146
24187
  Yallist.prototype.slice = function(from, to) {
24147
24188
  to = to || this.length;
@@ -24469,11 +24510,11 @@ var require_lru_cache = __commonJS({
24469
24510
  del(key) {
24470
24511
  del(this, this[CACHE].get(key));
24471
24512
  }
24472
- load(arr) {
24513
+ load(arr2) {
24473
24514
  this.reset();
24474
24515
  const now = Date.now();
24475
- for (let l = arr.length - 1; l >= 0; l--) {
24476
- const hit = arr[l];
24516
+ for (let l = arr2.length - 1; l >= 0; l--) {
24517
+ const hit = arr2[l];
24477
24518
  const expiresAt = hit.e || 0;
24478
24519
  if (expiresAt === 0)
24479
24520
  this.set(hit.k, hit.v);
@@ -30207,26 +30248,26 @@ var require_base64_js = __commonJS({
30207
30248
  var lens = getLens(b642);
30208
30249
  var validLen = lens[0];
30209
30250
  var placeHoldersLen = lens[1];
30210
- var arr = new Arr(_byteLength(b642, validLen, placeHoldersLen));
30251
+ var arr2 = new Arr(_byteLength(b642, validLen, placeHoldersLen));
30211
30252
  var curByte = 0;
30212
30253
  var len2 = placeHoldersLen > 0 ? validLen - 4 : validLen;
30213
30254
  var i2;
30214
30255
  for (i2 = 0; i2 < len2; i2 += 4) {
30215
30256
  tmp = revLookup[b642.charCodeAt(i2)] << 18 | revLookup[b642.charCodeAt(i2 + 1)] << 12 | revLookup[b642.charCodeAt(i2 + 2)] << 6 | revLookup[b642.charCodeAt(i2 + 3)];
30216
- arr[curByte++] = tmp >> 16 & 255;
30217
- arr[curByte++] = tmp >> 8 & 255;
30218
- arr[curByte++] = tmp & 255;
30257
+ arr2[curByte++] = tmp >> 16 & 255;
30258
+ arr2[curByte++] = tmp >> 8 & 255;
30259
+ arr2[curByte++] = tmp & 255;
30219
30260
  }
30220
30261
  if (placeHoldersLen === 2) {
30221
30262
  tmp = revLookup[b642.charCodeAt(i2)] << 2 | revLookup[b642.charCodeAt(i2 + 1)] >> 4;
30222
- arr[curByte++] = tmp & 255;
30263
+ arr2[curByte++] = tmp & 255;
30223
30264
  }
30224
30265
  if (placeHoldersLen === 1) {
30225
30266
  tmp = revLookup[b642.charCodeAt(i2)] << 10 | revLookup[b642.charCodeAt(i2 + 1)] << 4 | revLookup[b642.charCodeAt(i2 + 2)] >> 2;
30226
- arr[curByte++] = tmp >> 8 & 255;
30227
- arr[curByte++] = tmp & 255;
30267
+ arr2[curByte++] = tmp >> 8 & 255;
30268
+ arr2[curByte++] = tmp & 255;
30228
30269
  }
30229
- return arr;
30270
+ return arr2;
30230
30271
  }
30231
30272
  function tripletToBase64(num) {
30232
30273
  return lookup2[num >> 18 & 63] + lookup2[num >> 12 & 63] + lookup2[num >> 6 & 63] + lookup2[num & 63];
@@ -30368,13 +30409,13 @@ var require_buffer = __commonJS({
30368
30409
  }
30369
30410
  function typedArraySupport() {
30370
30411
  try {
30371
- const arr = new Uint8Array(1);
30412
+ const arr2 = new Uint8Array(1);
30372
30413
  const proto = { foo: function() {
30373
30414
  return 42;
30374
30415
  } };
30375
30416
  Object.setPrototypeOf(proto, Uint8Array.prototype);
30376
- Object.setPrototypeOf(arr, proto);
30377
- return arr.foo() === 42;
30417
+ Object.setPrototypeOf(arr2, proto);
30418
+ return arr2.foo() === 42;
30378
30419
  } catch (e) {
30379
30420
  return false;
30380
30421
  }
@@ -30894,14 +30935,14 @@ var require_buffer = __commonJS({
30894
30935
  }
30895
30936
  throw new TypeError("val must be string, number or Buffer");
30896
30937
  }
30897
- function arrayIndexOf(arr, val, byteOffset, encoding, dir) {
30938
+ function arrayIndexOf(arr2, val, byteOffset, encoding, dir) {
30898
30939
  let indexSize = 1;
30899
- let arrLength = arr.length;
30940
+ let arrLength = arr2.length;
30900
30941
  let valLength = val.length;
30901
30942
  if (encoding !== void 0) {
30902
30943
  encoding = String(encoding).toLowerCase();
30903
30944
  if (encoding === "ucs2" || encoding === "ucs-2" || encoding === "utf16le" || encoding === "utf-16le") {
30904
- if (arr.length < 2 || val.length < 2) {
30945
+ if (arr2.length < 2 || val.length < 2) {
30905
30946
  return -1;
30906
30947
  }
30907
30948
  indexSize = 2;
@@ -30921,7 +30962,7 @@ var require_buffer = __commonJS({
30921
30962
  if (dir) {
30922
30963
  let foundIndex = -1;
30923
30964
  for (i = byteOffset; i < arrLength; i++) {
30924
- if (read2(arr, i) === read2(val, foundIndex === -1 ? 0 : i - foundIndex)) {
30965
+ if (read2(arr2, i) === read2(val, foundIndex === -1 ? 0 : i - foundIndex)) {
30925
30966
  if (foundIndex === -1) foundIndex = i;
30926
30967
  if (i - foundIndex + 1 === valLength) return foundIndex * indexSize;
30927
30968
  } else {
@@ -30934,7 +30975,7 @@ var require_buffer = __commonJS({
30934
30975
  for (i = byteOffset; i >= 0; i--) {
30935
30976
  let found = true;
30936
30977
  for (let j = 0; j < valLength; j++) {
30937
- if (read2(arr, i + j) !== read2(val, j)) {
30978
+ if (read2(arr2, i + j) !== read2(val, j)) {
30938
30979
  found = false;
30939
30980
  break;
30940
30981
  }
@@ -32646,11 +32687,12 @@ async function verifyHttpSignature(request, { documentLoader, keyCache, timeWind
32646
32687
  } catch {
32647
32688
  result = null;
32648
32689
  }
32690
+ const method = request.headers.has("signature-input") ? "rfc9421" : "draft-cavage";
32649
32691
  if (result?.verified) {
32650
32692
  const key = result.key;
32651
32693
  return {
32652
32694
  verified: true,
32653
- method: "draft-cavage",
32695
+ method,
32654
32696
  keyId: key.id?.href ?? null,
32655
32697
  actor: key.ownerId?.href ?? null,
32656
32698
  reason: null,
@@ -32671,7 +32713,7 @@ async function verifyHttpSignature(request, { documentLoader, keyCache, timeWind
32671
32713
  if (kind === "keyFetchError") {
32672
32714
  return {
32673
32715
  verified: false,
32674
- method: "draft-cavage",
32716
+ method,
32675
32717
  keyId: null,
32676
32718
  actor: null,
32677
32719
  reason: "key-unfetchable",
@@ -32680,7 +32722,7 @@ async function verifyHttpSignature(request, { documentLoader, keyCache, timeWind
32680
32722
  }
32681
32723
  return {
32682
32724
  verified: false,
32683
- method: "draft-cavage",
32725
+ method,
32684
32726
  keyId: null,
32685
32727
  actor: null,
32686
32728
  reason: "bad-signature",
@@ -33960,11 +34002,11 @@ var require_src = __commonJS({
33960
34002
  function serializeFunction(fn) {
33961
34003
  return JSON.stringify(fn);
33962
34004
  }
33963
- function serializeArray(arr) {
34005
+ function serializeArray(arr2) {
33964
34006
  let str = "[";
33965
- const length = arr.length;
34007
+ const length = arr2.length;
33966
34008
  for (let i = 0; i < length; i++) {
33967
- const val = arr[i];
34009
+ const val = arr2[i];
33968
34010
  if (i !== 0) str += ",";
33969
34011
  str += serialize3(val);
33970
34012
  }
@@ -34119,6 +34161,7 @@ function dropFollower(contacts, actor, why) {
34119
34161
  }
34120
34162
  var PodStore = class {
34121
34163
  constructor({ storage = null, log: log2 = console.log } = {}) {
34164
+ this.lastSkipped = [];
34122
34165
  this.storage = storage;
34123
34166
  this.log = log2;
34124
34167
  this.cache = /* @__PURE__ */ new Map();
@@ -34181,6 +34224,7 @@ var PodStore = class {
34181
34224
  this.log(`state load ${name}: unparsable (${e.message})`);
34182
34225
  }
34183
34226
  }
34227
+ this.lastSkipped = skipped;
34184
34228
  if (skipped.length) this.log(`state load skipped ${skipped.length}: ${skipped.join(", ")}`);
34185
34229
  this.log(`state loaded: ${this.cache.size} doc(s) from ${this.base} (${fetched} re-fetched)`);
34186
34230
  }
@@ -34475,6 +34519,14 @@ var PodStore = class {
34475
34519
  // — not from the pod, not from the RDF. Unverified entries are dropped first
34476
34520
  // now, so a flood can only ever evict itself, and a stranger's genuine
34477
34521
  // favourite still shows up, which is what recording them at all is for.
34522
+ // What an Undo unsays: the favourite or boost it names, matched by a
34523
+ // predicate on what was recorded. Returns how many went.
34524
+ removeNotifications(pred) {
34525
+ const all = this.getNotifications();
34526
+ const kept = all.filter((n) => !pred(n));
34527
+ if (kept.length !== all.length) this.write("notifications.json", kept);
34528
+ return all.length - kept.length;
34529
+ }
34478
34530
  addNotification(n) {
34479
34531
  const all = this.getNotifications();
34480
34532
  const id = node_crypto_default.createHash("sha256").update(JSON.stringify(n)).digest("hex").slice(0, 16);
@@ -36882,11 +36934,11 @@ function arrayToStatements(rdfFactory, subject, data) {
36882
36934
  }, subject);
36883
36935
  return statements;
36884
36936
  }
36885
- function ArrayIndexOf(arr, item, i = 0) {
36886
- var length = arr.length;
36937
+ function ArrayIndexOf(arr2, item, i = 0) {
36938
+ var length = arr2.length;
36887
36939
  if (i < 0) i = length + i;
36888
36940
  for (; i < length; i++) {
36889
- if (arr[i] === item) {
36941
+ if (arr2[i] === item) {
36890
36942
  return i;
36891
36943
  }
36892
36944
  }
@@ -45218,8 +45270,8 @@ async function writeProfilePage(pod, urls, html) {
45218
45270
  await pod.put(urls.profileHtml, html, "text/html");
45219
45271
  await pod.setAcl(urls.profileHtml, PUBLIC_READ2);
45220
45272
  }
45221
- function linkInWebIdProfile(pod, { actorUrl, accountName, kind = "person" }) {
45222
- return pod.linkAccountInProfile({ actorUrl, accountName, kind });
45273
+ function linkInWebIdProfile(pod, { actorUrl, accountName, kind = "person", outbox = null }) {
45274
+ return pod.linkAccountInProfile({ actorUrl, accountName, kind, outbox });
45223
45275
  }
45224
45276
 
45225
45277
  // lib/pod/inbox.mjs
@@ -45427,8 +45479,9 @@ async function publishOutbox(publisher, outbox, { acls = false, force = false }
45427
45479
  );
45428
45480
  return wrote;
45429
45481
  }
45430
- function readPublishedOutbox(publisher) {
45431
- return readPublished(publisher.remote, publisher.urls);
45482
+ async function readPublishedOutbox(publisher) {
45483
+ const items = await readPublished(publisher.remote, publisher.urls);
45484
+ return Array.isArray(items) ? items.map(outboxLocalItem) : items;
45432
45485
  }
45433
45486
  async function publishFollowers(publisher, actors, { acls = false, force = false } = {}) {
45434
45487
  const { urls } = publisher;
@@ -55489,6 +55542,126 @@ var miscellany_default = {
55489
55542
  }
55490
55543
  };
55491
55544
 
55545
+ // lib/core/contexts/anno.json
55546
+ var anno_default = {
55547
+ "@context": {
55548
+ oa: "http://www.w3.org/ns/oa#",
55549
+ dc: "http://purl.org/dc/elements/1.1/",
55550
+ dcterms: "http://purl.org/dc/terms/",
55551
+ dctypes: "http://purl.org/dc/dcmitype/",
55552
+ foaf: "http://xmlns.com/foaf/0.1/",
55553
+ rdf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
55554
+ rdfs: "http://www.w3.org/2000/01/rdf-schema#",
55555
+ skos: "http://www.w3.org/2004/02/skos/core#",
55556
+ xsd: "http://www.w3.org/2001/XMLSchema#",
55557
+ iana: "http://www.iana.org/assignments/relation/",
55558
+ owl: "http://www.w3.org/2002/07/owl#",
55559
+ as: "http://www.w3.org/ns/activitystreams#",
55560
+ schema: "http://schema.org/",
55561
+ id: { "@type": "@id", "@id": "@id" },
55562
+ type: { "@type": "@id", "@id": "@type" },
55563
+ Annotation: "oa:Annotation",
55564
+ Dataset: "dctypes:Dataset",
55565
+ Image: "dctypes:StillImage",
55566
+ Video: "dctypes:MovingImage",
55567
+ Audio: "dctypes:Sound",
55568
+ Text: "dctypes:Text",
55569
+ TextualBody: "oa:TextualBody",
55570
+ ResourceSelection: "oa:ResourceSelection",
55571
+ SpecificResource: "oa:SpecificResource",
55572
+ FragmentSelector: "oa:FragmentSelector",
55573
+ CssSelector: "oa:CssSelector",
55574
+ XPathSelector: "oa:XPathSelector",
55575
+ TextQuoteSelector: "oa:TextQuoteSelector",
55576
+ TextPositionSelector: "oa:TextPositionSelector",
55577
+ DataPositionSelector: "oa:DataPositionSelector",
55578
+ SvgSelector: "oa:SvgSelector",
55579
+ RangeSelector: "oa:RangeSelector",
55580
+ TimeState: "oa:TimeState",
55581
+ HttpRequestState: "oa:HttpRequestState",
55582
+ CssStylesheet: "oa:CssStyle",
55583
+ Choice: "oa:Choice",
55584
+ Person: "foaf:Person",
55585
+ Software: "as:Application",
55586
+ Organization: "foaf:Organization",
55587
+ AnnotationCollection: "as:OrderedCollection",
55588
+ AnnotationPage: "as:OrderedCollectionPage",
55589
+ Audience: "schema:Audience",
55590
+ Motivation: "oa:Motivation",
55591
+ bookmarking: "oa:bookmarking",
55592
+ classifying: "oa:classifying",
55593
+ commenting: "oa:commenting",
55594
+ describing: "oa:describing",
55595
+ editing: "oa:editing",
55596
+ highlighting: "oa:highlighting",
55597
+ identifying: "oa:identifying",
55598
+ linking: "oa:linking",
55599
+ moderating: "oa:moderating",
55600
+ questioning: "oa:questioning",
55601
+ replying: "oa:replying",
55602
+ reviewing: "oa:reviewing",
55603
+ assessing: "oa:assessing",
55604
+ tagging: "oa:tagging",
55605
+ auto: "oa:autoDirection",
55606
+ ltr: "oa:ltrDirection",
55607
+ rtl: "oa:rtlDirection",
55608
+ body: { "@type": "@id", "@id": "oa:hasBody" },
55609
+ target: { "@type": "@id", "@id": "oa:hasTarget" },
55610
+ source: { "@type": "@id", "@id": "oa:hasSource" },
55611
+ selector: { "@type": "@id", "@id": "oa:hasSelector" },
55612
+ state: { "@type": "@id", "@id": "oa:hasState" },
55613
+ scope: { "@type": "@id", "@id": "oa:hasScope" },
55614
+ refinedBy: { "@type": "@id", "@id": "oa:refinedBy" },
55615
+ startSelector: { "@type": "@id", "@id": "oa:hasStartSelector" },
55616
+ endSelector: { "@type": "@id", "@id": "oa:hasEndSelector" },
55617
+ renderedVia: { "@type": "@id", "@id": "oa:renderedVia" },
55618
+ creator: { "@type": "@id", "@id": "dcterms:creator" },
55619
+ generator: { "@type": "@id", "@id": "as:generator" },
55620
+ rights: { "@type": "@id", "@id": "dcterms:rights" },
55621
+ homepage: { "@type": "@id", "@id": "foaf:homepage" },
55622
+ via: { "@type": "@id", "@id": "oa:via" },
55623
+ canonical: { "@type": "@id", "@id": "oa:canonical" },
55624
+ stylesheet: { "@type": "@id", "@id": "oa:styledBy" },
55625
+ cached: { "@type": "@id", "@id": "oa:cachedSource" },
55626
+ conformsTo: { "@type": "@id", "@id": "dcterms:conformsTo" },
55627
+ items: { "@type": "@id", "@id": "as:items", "@container": "@list" },
55628
+ partOf: { "@type": "@id", "@id": "as:partOf" },
55629
+ first: { "@type": "@id", "@id": "as:first" },
55630
+ last: { "@type": "@id", "@id": "as:last" },
55631
+ next: { "@type": "@id", "@id": "as:next" },
55632
+ prev: { "@type": "@id", "@id": "as:prev" },
55633
+ audience: { "@type": "@id", "@id": "schema:audience" },
55634
+ motivation: { "@type": "@vocab", "@id": "oa:motivatedBy" },
55635
+ purpose: { "@type": "@vocab", "@id": "oa:hasPurpose" },
55636
+ textDirection: { "@type": "@vocab", "@id": "oa:textDirection" },
55637
+ accessibility: "schema:accessibilityFeature",
55638
+ bodyValue: "oa:bodyValue",
55639
+ format: "dc:format",
55640
+ language: "dc:language",
55641
+ processingLanguage: "oa:processingLanguage",
55642
+ value: "rdf:value",
55643
+ exact: "oa:exact",
55644
+ prefix: "oa:prefix",
55645
+ suffix: "oa:suffix",
55646
+ styleClass: "oa:styleClass",
55647
+ name: "foaf:name",
55648
+ email: "foaf:mbox",
55649
+ email_sha1: "foaf:mbox_sha1sum",
55650
+ nickname: "foaf:nick",
55651
+ label: "rdfs:label",
55652
+ created: { "@id": "dcterms:created", "@type": "xsd:dateTime" },
55653
+ modified: { "@id": "dcterms:modified", "@type": "xsd:dateTime" },
55654
+ generated: { "@id": "dcterms:issued", "@type": "xsd:dateTime" },
55655
+ sourceDate: { "@id": "oa:sourceDate", "@type": "xsd:dateTime" },
55656
+ sourceDateStart: { "@id": "oa:sourceDateStart", "@type": "xsd:dateTime" },
55657
+ sourceDateEnd: { "@id": "oa:sourceDateEnd", "@type": "xsd:dateTime" },
55658
+ start: { "@id": "oa:start", "@type": "xsd:nonNegativeInteger" },
55659
+ end: { "@id": "oa:end", "@type": "xsd:nonNegativeInteger" },
55660
+ total: { "@id": "as:totalItems", "@type": "xsd:nonNegativeInteger" },
55661
+ startIndex: { "@id": "as:startIndex", "@type": "xsd:nonNegativeInteger" }
55662
+ }
55663
+ };
55664
+
55492
55665
  // lib/core/contexts/index.mjs
55493
55666
  var CONTEXTS = {
55494
55667
  "https://www.w3.org/ns/activitystreams": activitystreams_default,
@@ -55504,7 +55677,8 @@ var CONTEXTS = {
55504
55677
  "https://w3id.org/fep/5711": fep_5711_default,
55505
55678
  "https://join-lemmy.org/context.json": join_lemmy_default,
55506
55679
  "http://joinmastodon.org/ns": joinmastodon_default,
55507
- "https://purl.archive.org/miscellany": miscellany_default
55680
+ "https://purl.archive.org/miscellany": miscellany_default,
55681
+ "http://www.w3.org/ns/anno.jsonld": anno_default
55508
55682
  };
55509
55683
 
55510
55684
  // lib/core/graphview.mjs
@@ -55762,146 +55936,6 @@ async function readLenient(raw) {
55762
55936
  }
55763
55937
  }
55764
55938
 
55765
- // lib/core/publisher/restore.mjs
55766
- var ACCEPT_AP = 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"';
55767
- var REBUILD_MAX_PER_RUN = 200;
55768
- async function reconcileFollowers(publisher, contacts) {
55769
- let published = [];
55770
- try {
55771
- published = await publisher.readPublishedFollowers() || [];
55772
- } catch {
55773
- return 0;
55774
- }
55775
- const known2 = new Set(contacts.followers.map((f) => f.actor));
55776
- const removed = new Set((contacts.removedFollowers || []).map((r) => r.actor));
55777
- const missing = published.filter((a) => typeof a === "string" && !known2.has(a) && !removed.has(a));
55778
- if (!missing.length) return 0;
55779
- let recovered = 0;
55780
- for (const actor of missing.slice(0, 200)) {
55781
- try {
55782
- const res = await publisher.deliverer.signedFetch(actor, { headers: { accept: ACCEPT_AP } });
55783
- if (!res.ok) continue;
55784
- const { doc, view, degraded } = await readLenient(await res.json());
55785
- const actorDoc2 = view ?? doc;
55786
- if (degraded) publisher.log?.(`actor ${actor} grounded to read: ${degraded}`);
55787
- if (!actorDoc2?.inbox) continue;
55788
- contacts.followers.push({
55789
- actor,
55790
- inbox: actorDoc2.inbox,
55791
- sharedInbox: actorDoc2.endpoints?.sharedInbox || null,
55792
- recovered: true,
55793
- // Said explicitly, because onUndo reads it: the pod publishes WHO
55794
- // follows, never the id of the Follow that did it, so a recovered
55795
- // record has nothing an Undo can be matched against and must not be
55796
- // evictable by one naming anything at all.
55797
- followId: null
55798
- });
55799
- recovered++;
55800
- } catch {
55801
- }
55802
- }
55803
- if (recovered) {
55804
- publisher.store.setContacts(contacts);
55805
- publisher.log(`reconciled ${recovered} follower(s) the pod knew about and this machine did not \u2014 a restored or copied state was behind`);
55806
- }
55807
- return recovered;
55808
- }
55809
- async function reconcileOutbox(publisher, outbox) {
55810
- let published = [];
55811
- try {
55812
- published = await publisher.readPublishedOutbox() || [];
55813
- } catch {
55814
- return 0;
55815
- }
55816
- if (!Array.isArray(published) || !published.length) return 0;
55817
- const idOf = (i) => typeof i === "string" ? i : i?.id || null;
55818
- const known2 = new Set(outbox.map(idOf).filter(Boolean));
55819
- const removed = new Set(publisher.store.read("outbox-removed.json", []).map((r) => r.id));
55820
- const missing = published.filter((i) => {
55821
- const id = idOf(i);
55822
- return id && !known2.has(id) && !removed.has(id);
55823
- });
55824
- if (!missing.length) return 0;
55825
- outbox.push(...missing);
55826
- publisher.store.write("outbox.json", outbox);
55827
- publisher.log(`reconciled ${missing.length} outbox entr(ies) the pod carried and this machine did not`);
55828
- return missing.length;
55829
- }
55830
- async function rebuildStatuses(publisher, { fromNotes = false } = {}) {
55831
- const { urls } = publisher;
55832
- const ids = /* @__PURE__ */ new Set();
55833
- const boosts = [];
55834
- let indexed = false;
55835
- const published = await publisher.readPublishedOutbox().catch(() => null);
55836
- if (published) {
55837
- indexed = true;
55838
- for (const item of published) {
55839
- if (typeof item === "string") {
55840
- if (item.startsWith(urls.notes)) ids.add(item);
55841
- } else if (item?.type === "Announce") boosts.push(item);
55842
- }
55843
- }
55844
- if (fromNotes) {
55845
- for (const child of await list2(publisher.remote, urls).catch(() => [])) {
55846
- ids.add(child.url);
55847
- }
55848
- indexed = true;
55849
- }
55850
- if (!indexed) return { indexed: 0, recovered: 0, reblogs: 0, landed: false, why: "the pod would not answer for its outbox" };
55851
- const statuses = publisher.store.getStatuses();
55852
- const have = new Set(statuses.map((s) => s.noteId));
55853
- const removed = new Set(publisher.store.read("outbox-removed.json", []).map((r) => r.id));
55854
- const recovered = [];
55855
- let budget = REBUILD_MAX_PER_RUN;
55856
- for (const id of ids) {
55857
- if (budget <= 0) {
55858
- publisher.log(`rebuild: stopping at ${REBUILD_MAX_PER_RUN} this run \u2014 run it again for the rest`);
55859
- break;
55860
- }
55861
- if (have.has(id) || removed.has(id)) continue;
55862
- budget--;
55863
- const note = await read(publisher.remote, id).catch(() => null);
55864
- if (note?.type !== "Note" || note.id !== id || note.attributedTo !== urls.actor) continue;
55865
- const attachments = attachmentsOf(note);
55866
- const mentions = (Array.isArray(note.tag) ? note.tag : []).filter((t) => t?.type === "Mention" && t.href).map((t) => ({ href: t.href, name: t.name }));
55867
- recovered.push({
55868
- noteId: note.id,
55869
- actor: urls.actor,
55870
- content: note.content || "",
55871
- published: note.published || null,
55872
- ...note.inReplyTo ? { inReplyTo: note.inReplyTo } : {},
55873
- kind: "post",
55874
- slug: note.id.slice(urls.notes.length),
55875
- ...attachments.length ? { attachments } : {},
55876
- ...mentions.length ? { mentions } : {},
55877
- recovered: true
55878
- });
55879
- }
55880
- const merged = [...statuses, ...recovered];
55881
- let reblogs = 0;
55882
- for (const act of boosts) {
55883
- const object = typeof act.object === "string" ? act.object : act.object?.id;
55884
- const s = object && merged.find((x) => x.noteId === object);
55885
- if (!s || s.reblogged) continue;
55886
- s.reblogged = true;
55887
- s.announceActivity = act;
55888
- reblogs++;
55889
- }
55890
- if (!recovered.length && !reblogs) return { indexed: ids.size, recovered: 0, reblogs: 0, landed: true };
55891
- merged.sort((a, b) => String(b.published || "").localeCompare(String(a.published || "")));
55892
- const kept = merged.slice(0, 1e3);
55893
- publisher.store.write("statuses.json", kept);
55894
- const landed = await publisher.store.commit();
55895
- publisher.log(`rebuilt ${recovered.length} post(s) and ${reblogs} boost(s) from the pod${landed ? "" : " \u2014 THE STATE WRITE DID NOT LAND"}`);
55896
- return {
55897
- indexed: ids.size,
55898
- recovered: recovered.length,
55899
- reblogs,
55900
- landed,
55901
- dropped: Math.max(0, merged.length - kept.length)
55902
- };
55903
- }
55904
-
55905
55939
  // lib/core/publisher/notes.mjs
55906
55940
  init_node_crypto();
55907
55941
  init_wire();
@@ -55954,6 +55988,35 @@ async function mentionsFor(publisher, content, inReplyTo) {
55954
55988
  }
55955
55989
  return mentions;
55956
55990
  }
55991
+ async function replyTarget(publisher, inReplyTo, mentions = [], visibility = "public") {
55992
+ if (!inReplyTo || visibility === "direct" || !publisher.resolveActor) return null;
55993
+ const parent = publisher.store.getStatuses().find((s) => s.noteId === inReplyTo);
55994
+ let actor = parent?.actor || null;
55995
+ if (!parent) {
55996
+ const doc2 = await publisher.resolveActor(inReplyTo).catch(() => null);
55997
+ const by = doc2?.attributedTo;
55998
+ actor = typeof by === "string" ? by : (Array.isArray(by) ? by[0] : by)?.id || null;
55999
+ if (typeof actor === "object") actor = actor?.id || null;
56000
+ }
56001
+ if (!actor || actor === publisher.urls.actor) return null;
56002
+ if (mentions.some((m) => m.actor === actor)) return null;
56003
+ if (publisher.store.isBlocked?.(actor)) return null;
56004
+ const doc = await publisher.resolveActor(actor).catch(() => null);
56005
+ const inbox = doc?.endpoints?.sharedInbox || doc?.inbox || null;
56006
+ if (!inbox) publisher.log(`reply: no inbox found for ${actor} \u2014 they get it only through followers`);
56007
+ return { actor, inbox };
56008
+ }
56009
+ async function inboxesFor(publisher, actorIds = []) {
56010
+ const out = [];
56011
+ for (const id of [...new Set(actorIds.filter((a) => typeof a === "string" && a && a !== publisher.urls.actor))]) {
56012
+ if (!publisher.resolveActor) break;
56013
+ const doc = await publisher.resolveActor(id).catch(() => null);
56014
+ const inbox = doc?.endpoints?.sharedInbox || doc?.inbox;
56015
+ if (inbox) out.push(inbox);
56016
+ else publisher.log(`addressed actor ${id} has no inbox the agent can find \u2014 not delivered to`);
56017
+ }
56018
+ return out;
56019
+ }
55957
56020
  function assertDirectAddressed(content, mentions) {
55958
56021
  const wanted = [...new Set(mentionsIn(content))];
55959
56022
  const missing = wanted.filter((h) => !mentions.some((m) => m.handle === h));
@@ -55965,7 +56028,21 @@ function assertDirectAddressed(content, mentions) {
55965
56028
  e.code = "unaddressed";
55966
56029
  throw e;
55967
56030
  }
55968
- async function publishNote(publisher, content, { inReplyTo, attachments, visibility = "public", spoilerText = null } = {}) {
56031
+ var SLUG_OK = /^[A-Za-z0-9._-]{1,64}$/u;
56032
+ var safeSlug = (s) => typeof s === "string" && SLUG_OK.test(s) && !/^\.+$/u.test(s) ? s : null;
56033
+ async function slugFor(publisher, container, wanted, published) {
56034
+ const name = safeSlug(wanted);
56035
+ if (name && !await read(publisher.remote, container + name).catch(() => null)) return name;
56036
+ return published.slice(0, 10) + "-" + node_crypto_default.randomBytes(4).toString("hex");
56037
+ }
56038
+ function rowContent(obj) {
56039
+ if (typeof obj?.content === "string" && obj.content.trim()) return sanitizeHtml(obj.content);
56040
+ const plain = [obj?.bodyValue, obj?.name, obj?.summary].find((v) => typeof v === "string" && v.trim());
56041
+ if (plain) return contentHtml(plain);
56042
+ const esc = (v) => String(v).replace(/[&<>"]/gu, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
56043
+ return `<p><a href="${esc(obj?.id || "")}">${esc(obj?.type || "object")}</a></p>`;
56044
+ }
56045
+ async function publishNote(publisher, content, { inReplyTo, attachments, visibility = "public", spoilerText = null, sensitive = false, slug: wanted = null, also = [], deliverTo = [] } = {}) {
55969
56046
  const { urls } = publisher;
55970
56047
  const priv = visibility === "private" || visibility === "direct";
55971
56048
  if (priv) {
@@ -55973,9 +56050,10 @@ async function publishNote(publisher, content, { inReplyTo, attachments, visibil
55973
56050
  if (ready !== true) throw new Error(ready);
55974
56051
  }
55975
56052
  const published = (/* @__PURE__ */ new Date()).toISOString();
55976
- const slug = published.slice(0, 10) + "-" + node_crypto_default.randomBytes(4).toString("hex");
56053
+ const slug = await slugFor(publisher, priv ? urls.privateNotes : urls.notes, wanted, published);
55977
56054
  const mentions = await publisher._mentionsFor(content, inReplyTo);
55978
56055
  if (visibility === "direct") assertDirectAddressed(content, mentions);
56056
+ const reply = await replyTarget(publisher, inReplyTo, mentions, visibility);
55979
56057
  const note = noteDoc({
55980
56058
  urls,
55981
56059
  slug,
@@ -55986,8 +56064,11 @@ async function publishNote(publisher, content, { inReplyTo, attachments, visibil
55986
56064
  mentions,
55987
56065
  visibility,
55988
56066
  summary: spoilerText,
55989
- container: priv ? urls.privateNotes : urls.notes
56067
+ sensitive,
56068
+ container: priv ? urls.privateNotes : urls.notes,
56069
+ also: [...also, ...reply ? [reply.actor] : []]
55990
56070
  });
56071
+ const addressedInboxes = await inboxesFor(publisher, [...also, ...deliverTo]);
55991
56072
  await write4(publisher.remote, note.id, note);
55992
56073
  await writeEmptyReplies(
55993
56074
  publisher.remote,
@@ -56006,15 +56087,19 @@ async function publishNote(publisher, content, { inReplyTo, attachments, visibil
56006
56087
  text: content,
56007
56088
  visibility,
56008
56089
  ...spoilerText ? { spoiler: spoilerText } : {},
56090
+ ...note.sensitive ? { sensitive: true } : {},
56009
56091
  ...attachments?.length ? { attachments } : {},
56010
- ...note.tag?.length ? { mentions: note.tag.map((t) => ({ href: t.href, name: t.name })) } : {}
56092
+ ...rowTags(note)
56011
56093
  });
56094
+ if (await publisher.store.commit?.() === false) publisher.log(`post published but its timeline row was refused: ${note.id}`);
56012
56095
  const create = createActivity(note, urls);
56013
56096
  await writeCreate(publisher.remote, create.id, create);
56014
56097
  const contacts = publisher.store.getContacts();
56015
56098
  const inboxes = [...new Set([
56016
56099
  ...visibility === "direct" ? [] : contacts.followers.map((f) => f.sharedInbox || f.inbox),
56017
- ...mentions.map((m) => m.inbox)
56100
+ ...mentions.map((m) => m.inbox),
56101
+ reply?.inbox,
56102
+ ...addressedInboxes
56018
56103
  ].filter(Boolean))];
56019
56104
  await publisher.deliverer.deliverToAll(inboxes, create);
56020
56105
  publisher.log(`note published: ${note.id} \u2192 ${inboxes.length} inbox(es)`);
@@ -56033,7 +56118,69 @@ async function publishNote(publisher, content, { inReplyTo, attachments, visibil
56033
56118
  }
56034
56119
  return note;
56035
56120
  }
56036
- async function updateNote(publisher, s, { content, spoilerText = null, attachments = null } = {}) {
56121
+ async function publishObject(publisher, object, { visibility = "public", slug: wanted = null, also = [], deliverTo = [] } = {}) {
56122
+ const { urls } = publisher;
56123
+ const priv = visibility === "private" || visibility === "direct";
56124
+ if (priv) {
56125
+ const ready = await publisher.privateReady();
56126
+ if (ready !== true) throw new Error(ready);
56127
+ }
56128
+ const published = (/* @__PURE__ */ new Date()).toISOString();
56129
+ const container = priv ? urls.privateNotes : urls.notes;
56130
+ const pod = publisher.config?.remotePod || urls.base;
56131
+ const ownId = typeof object.id === "string" && /^https?:\/\//u.test(object.id) && object.id.startsWith(pod) ? object.id : null;
56132
+ const name = await slugFor(publisher, container, wanted, published);
56133
+ const addressed = addressing(urls, visibility, visibility === "direct" ? also : []);
56134
+ if (visibility !== "direct") addressed.cc = [.../* @__PURE__ */ new Set([...addressed.cc, ...also])];
56135
+ let doc = null;
56136
+ const id = ownId || container + name;
56137
+ if (!ownId) {
56138
+ const { bto, bcc, ...sent } = object;
56139
+ doc = { ...sent, id, attributedTo: urls.actor, published: object.published || published, to: addressed.to, cc: addressed.cc };
56140
+ if (!doc["@context"]) doc["@context"] = AS_CTX;
56141
+ if (typeof doc.content === "string") doc.content = sanitizeHtml(doc.content);
56142
+ await write4(publisher.remote, id, doc);
56143
+ }
56144
+ if (!priv) await publisher.recordOutbox(id);
56145
+ const row = doc || object;
56146
+ publisher.store.addStatus({
56147
+ noteId: id,
56148
+ actor: urls.actor,
56149
+ content: rowContent(row),
56150
+ published: row.published || published,
56151
+ kind: "post",
56152
+ slug: name,
56153
+ visibility
56154
+ });
56155
+ if (await publisher.store.commit?.() === false) publisher.log(`object published but its timeline row was refused: ${id}`);
56156
+ const createId = doc ? createActivityId(id) : createActivityId(container + name);
56157
+ const create = {
56158
+ "@context": AS_CTX,
56159
+ id: createId,
56160
+ type: "Create",
56161
+ actor: urls.actor,
56162
+ published: row.published || published,
56163
+ to: addressed.to,
56164
+ cc: addressed.cc,
56165
+ object: doc || id
56166
+ };
56167
+ await writeCreate(publisher.remote, create.id, create);
56168
+ const contacts = publisher.store.getContacts();
56169
+ const inboxes = [...new Set([
56170
+ ...visibility === "direct" ? [] : contacts.followers.map((f) => f.sharedInbox || f.inbox),
56171
+ ...await inboxesFor(publisher, [...also, ...deliverTo])
56172
+ ].filter(Boolean))];
56173
+ await publisher.deliverer.deliverToAll(inboxes, create);
56174
+ publisher.log(`${row.type || "object"} published: ${id} \u2192 ${inboxes.length} inbox(es)`);
56175
+ return { id, createId, copied: !ownId };
56176
+ }
56177
+ function rowTags(note) {
56178
+ const tags = [].concat(note.tag || []);
56179
+ const mentions = tags.filter((t) => t.type === "Mention").map((t) => ({ href: t.href, name: t.name }));
56180
+ const hashtags = tags.filter((t) => t.type === "Hashtag").map((t) => ({ name: String(t.name).replace(/^#/, ""), url: t.href }));
56181
+ return { ...mentions.length ? { mentions } : {}, ...hashtags.length ? { tags: hashtags } : {} };
56182
+ }
56183
+ async function updateNote(publisher, s, { content, spoilerText = null, sensitive = null, attachments = null } = {}) {
56037
56184
  const { urls } = publisher;
56038
56185
  const updated = (/* @__PURE__ */ new Date()).toISOString();
56039
56186
  const inText = new Set(mentionsIn(content));
@@ -56050,6 +56197,7 @@ async function updateNote(publisher, s, { content, spoilerText = null, attachmen
56050
56197
  const atts = attachments ?? s.attachments ?? [];
56051
56198
  const container = String(s.noteId).startsWith(urls.privateNotes) ? urls.privateNotes : urls.notes;
56052
56199
  const slug = s.slug || String(s.noteId).slice(container.length);
56200
+ const reply = await replyTarget(publisher, s.inReplyTo, mentions, s.visibility || "public");
56053
56201
  const note = noteDoc({
56054
56202
  urls,
56055
56203
  slug,
@@ -56060,8 +56208,10 @@ async function updateNote(publisher, s, { content, spoilerText = null, attachmen
56060
56208
  mentions,
56061
56209
  visibility: s.visibility || "public",
56062
56210
  summary: spoilerText,
56211
+ sensitive: sensitive ?? !!s.sensitive,
56063
56212
  updated,
56064
- container
56213
+ container,
56214
+ also: reply ? [reply.actor] : []
56065
56215
  });
56066
56216
  await write4(publisher.remote, note.id, note);
56067
56217
  await writeCreate(publisher.remote, createActivityId(note.id), createActivity(note, urls));
@@ -56070,20 +56220,175 @@ async function updateNote(publisher, s, { content, spoilerText = null, attachmen
56070
56220
  text: content,
56071
56221
  editedAt: updated,
56072
56222
  spoiler: spoilerText || void 0,
56223
+ sensitive: note.sensitive || void 0,
56073
56224
  attachments: atts.length ? atts : void 0,
56074
- mentions: note.tag?.length ? note.tag.map((t) => ({ href: t.href, name: t.name })) : void 0
56225
+ mentions: rowTags(note).mentions,
56226
+ tags: rowTags(note).tags
56075
56227
  });
56076
56228
  const update = updateActivity(note, urls);
56077
56229
  const contacts = publisher.store.getContacts();
56078
56230
  const inboxes = [...new Set([
56079
56231
  ...s.visibility === "direct" ? [] : contacts.followers.map((f) => f.sharedInbox || f.inbox),
56080
- ...mentions.map((m) => m.inbox)
56232
+ ...mentions.map((m) => m.inbox),
56233
+ reply?.inbox
56081
56234
  ].filter(Boolean))];
56082
56235
  await publisher.deliverer.deliverToAll(inboxes, update);
56083
56236
  publisher.log(`note edited: ${note.id} \u2192 ${inboxes.length} inbox(es)`);
56084
56237
  return patched;
56085
56238
  }
56086
56239
 
56240
+ // lib/core/publisher/restore.mjs
56241
+ var ACCEPT_AP = 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"';
56242
+ var REBUILD_MAX_PER_RUN = 200;
56243
+ async function reconcileFollowers(publisher, contacts) {
56244
+ let published = [];
56245
+ try {
56246
+ published = await publisher.readPublishedFollowers() || [];
56247
+ } catch {
56248
+ return 0;
56249
+ }
56250
+ const known2 = new Set(contacts.followers.map((f) => f.actor));
56251
+ const removed = new Set((contacts.removedFollowers || []).map((r) => r.actor));
56252
+ const missing = published.filter((a) => typeof a === "string" && !known2.has(a) && !removed.has(a));
56253
+ if (!missing.length) return 0;
56254
+ let recovered = 0;
56255
+ for (const actor of missing.slice(0, 200)) {
56256
+ try {
56257
+ const res = await publisher.deliverer.signedFetch(actor, { headers: { accept: ACCEPT_AP } });
56258
+ if (!res.ok) continue;
56259
+ const { doc, view, degraded } = await readLenient(await res.json());
56260
+ const actorDoc2 = view ?? doc;
56261
+ if (degraded) publisher.log?.(`actor ${actor} grounded to read: ${degraded}`);
56262
+ if (!actorDoc2?.inbox) continue;
56263
+ contacts.followers.push({
56264
+ actor,
56265
+ inbox: actorDoc2.inbox,
56266
+ sharedInbox: actorDoc2.endpoints?.sharedInbox || null,
56267
+ recovered: true,
56268
+ // Said explicitly, because onUndo reads it: the pod publishes WHO
56269
+ // follows, never the id of the Follow that did it, so a recovered
56270
+ // record has nothing an Undo can be matched against and must not be
56271
+ // evictable by one naming anything at all.
56272
+ followId: null
56273
+ });
56274
+ recovered++;
56275
+ } catch {
56276
+ }
56277
+ }
56278
+ if (recovered) {
56279
+ publisher.store.setContacts(contacts);
56280
+ publisher.log(`reconciled ${recovered} follower(s) the pod knew about and this machine did not \u2014 a restored or copied state was behind`);
56281
+ }
56282
+ return recovered;
56283
+ }
56284
+ async function reconcileOutbox(publisher, outbox) {
56285
+ let published = [];
56286
+ try {
56287
+ published = await publisher.readPublishedOutbox() || [];
56288
+ } catch {
56289
+ return 0;
56290
+ }
56291
+ if (!Array.isArray(published) || !published.length) return 0;
56292
+ const idOf2 = (i) => typeof i === "string" ? i : i?.id || null;
56293
+ const known2 = new Set(outbox.map(idOf2).filter(Boolean));
56294
+ const removed = new Set(publisher.store.read("outbox-removed.json", []).map((r) => r.id));
56295
+ const missing = published.filter((i) => {
56296
+ const id = idOf2(i);
56297
+ return id && !known2.has(id) && !removed.has(id);
56298
+ });
56299
+ if (!missing.length) return 0;
56300
+ outbox.push(...missing);
56301
+ publisher.store.write("outbox.json", outbox);
56302
+ publisher.log(`reconciled ${missing.length} outbox entr(ies) the pod carried and this machine did not`);
56303
+ return missing.length;
56304
+ }
56305
+ async function rebuildStatuses(publisher, { fromNotes = false } = {}) {
56306
+ const { urls } = publisher;
56307
+ const ids = /* @__PURE__ */ new Set();
56308
+ const boosts = [];
56309
+ let indexed = false;
56310
+ const published = await publisher.readPublishedOutbox().catch(() => null);
56311
+ if (published) {
56312
+ indexed = true;
56313
+ for (const item of published) {
56314
+ if (typeof item === "string") {
56315
+ if (item.startsWith(urls.notes)) ids.add(item);
56316
+ } else if (item?.type === "Announce") boosts.push(item);
56317
+ }
56318
+ }
56319
+ if (fromNotes) {
56320
+ for (const child of await list2(publisher.remote, urls).catch(() => [])) {
56321
+ ids.add(child.url);
56322
+ }
56323
+ indexed = true;
56324
+ }
56325
+ if (!indexed) return { indexed: 0, recovered: 0, reblogs: 0, landed: false, why: "the pod would not answer for its outbox" };
56326
+ const statuses = publisher.store.getStatuses();
56327
+ const have = new Set(statuses.map((s) => s.noteId));
56328
+ const removed = new Set(publisher.store.read("outbox-removed.json", []).map((r) => r.id));
56329
+ const recovered = [];
56330
+ let budget = REBUILD_MAX_PER_RUN;
56331
+ for (const id of ids) {
56332
+ if (budget <= 0) {
56333
+ publisher.log(`rebuild: stopping at ${REBUILD_MAX_PER_RUN} this run \u2014 run it again for the rest`);
56334
+ break;
56335
+ }
56336
+ if (have.has(id) || removed.has(id)) continue;
56337
+ budget--;
56338
+ const note = await read(publisher.remote, id).catch(() => null);
56339
+ if (!note?.type || note.id !== id || note.attributedTo !== urls.actor) continue;
56340
+ const attachments = attachmentsOf(note);
56341
+ const mentions = (Array.isArray(note.tag) ? note.tag : []).filter((t) => t?.type === "Mention" && t.href).map((t) => ({ href: t.href, name: t.name }));
56342
+ recovered.push({
56343
+ noteId: note.id,
56344
+ actor: urls.actor,
56345
+ content: rowContent(note),
56346
+ published: note.published || null,
56347
+ ...note.inReplyTo ? { inReplyTo: note.inReplyTo } : {},
56348
+ kind: "post",
56349
+ slug: note.id.slice(urls.notes.length),
56350
+ ...attachments.length ? { attachments } : {},
56351
+ ...mentions.length ? { mentions } : {},
56352
+ recovered: true
56353
+ });
56354
+ }
56355
+ const merged = [...statuses, ...recovered];
56356
+ let reblogs = 0;
56357
+ for (const act of boosts) {
56358
+ const object = typeof act.object === "string" ? act.object : act.object?.id;
56359
+ const s = object && merged.find((x) => x.noteId === object);
56360
+ if (!s || s.reblogged) continue;
56361
+ s.reblogged = true;
56362
+ s.announceActivity = act;
56363
+ reblogs++;
56364
+ }
56365
+ if (!recovered.length && !reblogs) return { indexed: ids.size, recovered: 0, reblogs: 0, landed: true };
56366
+ merged.sort((a, b) => String(b.published || "").localeCompare(String(a.published || "")));
56367
+ const kept = merged.slice(0, 1e3);
56368
+ publisher.store.write("statuses.json", kept);
56369
+ const landed = await publisher.store.commit();
56370
+ publisher.log(`rebuilt ${recovered.length} post(s) and ${reblogs} boost(s) from the pod${landed ? "" : " \u2014 THE STATE WRITE DID NOT LAND"}`);
56371
+ return {
56372
+ indexed: ids.size,
56373
+ recovered: recovered.length,
56374
+ reblogs,
56375
+ landed,
56376
+ dropped: Math.max(0, merged.length - kept.length)
56377
+ };
56378
+ }
56379
+ async function healStatuses(publisher) {
56380
+ const { urls, store } = publisher;
56381
+ const own = store.read("outbox.json", []).map((i) => typeof i === "string" ? i : null).filter((id) => id && id.startsWith(urls.notes));
56382
+ if (!own.length) return { missing: 0, recovered: 0 };
56383
+ const have = new Set(store.getStatuses().map((s) => s.noteId));
56384
+ const removed = new Set(store.read("outbox-removed.json", []).map((r2) => r2.id));
56385
+ const missing = own.filter((id) => !have.has(id) && !removed.has(id));
56386
+ if (!missing.length) return { missing: 0, recovered: 0 };
56387
+ const r = await rebuildStatuses(publisher);
56388
+ publisher.log(`timeline index healed: ${missing.length} own post(s) were missing, ${r.recovered} recovered`);
56389
+ return { missing: missing.length, recovered: r.recovered };
56390
+ }
56391
+
56087
56392
  // lib/core/publisher/questions.mjs
56088
56393
  init_node_crypto();
56089
56394
  init_wire();
@@ -56151,7 +56456,8 @@ async function publishQuestion(publisher, content, {
56151
56456
  expiresAt = null,
56152
56457
  inReplyTo = void 0,
56153
56458
  visibility = "public",
56154
- spoilerText = null
56459
+ spoilerText = null,
56460
+ sensitive = false
56155
56461
  } = {}) {
56156
56462
  const { urls } = publisher;
56157
56463
  const priv = visibility === "private" || visibility === "direct";
@@ -56178,6 +56484,7 @@ async function publishQuestion(publisher, content, {
56178
56484
  const slug = published.slice(0, 10) + "-" + node_crypto_default.randomBytes(4).toString("hex");
56179
56485
  const mentions = await publisher._mentionsFor(content, inReplyTo);
56180
56486
  if (visibility === "direct") assertDirectAddressed(content, mentions);
56487
+ const reply = await replyTarget(publisher, inReplyTo, mentions, visibility);
56181
56488
  const poll = {
56182
56489
  multiple: !!multiple,
56183
56490
  expiresAt: expiresAt || null,
@@ -56186,7 +56493,7 @@ async function publishQuestion(publisher, content, {
56186
56493
  votersCount: 0,
56187
56494
  // Resolved once, here: a tally rewrite must not cost a webfinger lookup
56188
56495
  // per vote for people the poll named.
56189
- mentionInboxes: [...new Set(mentions.map((m) => m.inbox).filter(Boolean))]
56496
+ mentionInboxes: [...new Set([...mentions.map((m) => m.inbox), reply?.inbox].filter(Boolean))]
56190
56497
  };
56191
56498
  const question = questionDoc({
56192
56499
  urls,
@@ -56198,6 +56505,8 @@ async function publishQuestion(publisher, content, {
56198
56505
  mentions,
56199
56506
  visibility,
56200
56507
  summary: spoilerText,
56508
+ sensitive,
56509
+ also: reply ? [reply.actor] : [],
56201
56510
  container: priv ? urls.privateNotes : urls.notes,
56202
56511
  options: poll.options,
56203
56512
  multiple: poll.multiple,
@@ -56222,9 +56531,12 @@ async function publishQuestion(publisher, content, {
56222
56531
  visibility,
56223
56532
  poll,
56224
56533
  inReplyTo,
56534
+ ...reply ? { replyActor: reply.actor } : {},
56225
56535
  ...spoilerText ? { spoiler: spoilerText } : {},
56226
- ...question.tag?.length ? { mentions: question.tag.map((t) => ({ href: t.href, name: t.name })) } : {}
56536
+ ...question.sensitive ? { sensitive: true } : {},
56537
+ ...rowTags(question)
56227
56538
  });
56539
+ if (await publisher.store.commit?.() === false) publisher.log(`poll published but its timeline row was refused: ${question.id}`);
56228
56540
  const create = publisher._pollActivity("Create", question, createActivityId(question.id));
56229
56541
  await writeCreate(publisher.remote, create.id, create);
56230
56542
  const contacts = publisher.store.getContacts();
@@ -56301,8 +56613,10 @@ async function republishPoll(publisher, questionId, { closing = null } = {}) {
56301
56613
  inReplyTo: s.inReplyTo,
56302
56614
  attachments: [],
56303
56615
  mentions,
56616
+ also: s.replyActor ? [s.replyActor] : [],
56304
56617
  visibility: s.visibility || "public",
56305
56618
  summary: s.spoiler || null,
56619
+ sensitive: !!s.sensitive,
56306
56620
  container,
56307
56621
  options: poll.options,
56308
56622
  multiple: !!poll.multiple,
@@ -56374,6 +56688,7 @@ var Publisher = class {
56374
56688
  log: log2 = console.log,
56375
56689
  probeFetch = null,
56376
56690
  resolveMention = null,
56691
+ resolveActor = null,
56377
56692
  clientOrigin = null
56378
56693
  }) {
56379
56694
  this.config = config;
@@ -56388,6 +56703,7 @@ var Publisher = class {
56388
56703
  if (this.urls.toPod && this.remote?.setUrlMap) this.remote.setUrlMap(this.urls.toPod);
56389
56704
  this.probeFetch = probeFetch || ((u, i) => this.remote.probe(u, i));
56390
56705
  this.resolveMention = resolveMention;
56706
+ this.resolveActor = resolveActor;
56391
56707
  this.pollTimers = /* @__PURE__ */ new Map();
56392
56708
  this.log = log2;
56393
56709
  }
@@ -56405,19 +56721,25 @@ var Publisher = class {
56405
56721
  // republish button. Without it, an actor lost from the pod would match the
56406
56722
  // digest, be skipped, and leave the agent reporting success while nobody can
56407
56723
  // resolve it.
56408
- async publishProfile({ force = false } = {}) {
56409
- const { urls } = this;
56410
- const host = new URL(urls.base).host;
56411
- const priv = await this.privateReady() === true;
56412
- const moderators = (this.config.moderators || []).length ? urls.moderators : null;
56724
+ // The Gateway's outbox door for this account, when a Gateway is attached:
56725
+ // beside its inbox door, or under the fronted actor.
56726
+ gatewayOutbox() {
56413
56727
  const gw = this.config.gateway;
56414
- const gwActive = gw && gw.url && gw.mode && gw.mode !== "off";
56415
- const actorDoc2 = actorDoc({
56728
+ if (!(gw && gw.url && gw.mode && gw.mode !== "off")) return null;
56729
+ if (gw.frontActor) return String(gw.frontActor).replace(/ap\/actor\/?$/u, "ap/outbox");
56730
+ return String(gw.url).replace(/ap\/inbox\/?$/u, "ap/outbox");
56731
+ }
56732
+ // Every actor document this identity publishes, from one place: the
56733
+ // profile publish and a Move both write it, and a Move that rebuilt it
56734
+ // by hand dropped the gateway inbox, the outbox door and the collections.
56735
+ actorDocFor({ priv = false, moderators = null, inbox = null, movedTo = this.config.movedTo || null } = {}) {
56736
+ const { urls } = this;
56737
+ return actorDoc({
56416
56738
  urls,
56417
56739
  handle: publicHandle(this.config),
56418
56740
  name: this.config.name,
56419
56741
  publicKeyPem: this.publicKeyPem,
56420
- movedTo: this.config.movedTo || null,
56742
+ movedTo,
56421
56743
  kind: this.config.kind,
56422
56744
  approveJoins: followsNeedApproval(this.config),
56423
56745
  assertionKey: this.assertionKey,
@@ -56431,15 +56753,25 @@ var Publisher = class {
56431
56753
  pendingFollowers: priv ? urls.pendingFollowers : null,
56432
56754
  pendingFollowing: priv ? urls.pendingFollowing : null,
56433
56755
  blocked: priv ? urls.blocked : null,
56434
- inbox: gwActive ? gw.url : null,
56756
+ inbox,
56435
56757
  // The agent's own outbox endpoint, where it is reachable: a client
56436
56758
  // following the actor must arrive somewhere that will take a write.
56437
- outbox: this.clientOrigin ? `${this.clientOrigin}ap/outbox` : null,
56759
+ // Otherwise the Gateway's outbox door, when one is attached.
56760
+ outbox: this.clientOrigin ? `${this.clientOrigin}ap/outbox` : this.gatewayOutbox(),
56438
56761
  // How a client-to-server client finds the way in with nothing configured
56439
56762
  // by hand. Advertised only where the surface is publicly reachable.
56440
56763
  oauthAuthorize: this.clientOrigin ? `${this.clientOrigin}oauth/authorize` : null,
56441
56764
  oauthToken: this.clientOrigin ? `${this.clientOrigin}oauth/token` : null
56442
56765
  });
56766
+ }
56767
+ async publishProfile({ force = false } = {}) {
56768
+ const { urls } = this;
56769
+ const host = new URL(urls.base).host;
56770
+ const priv = await this.privateReady() === true;
56771
+ const moderators = (this.config.moderators || []).length ? urls.moderators : null;
56772
+ const gw = this.config.gateway;
56773
+ const gwActive = gw && gw.url && gw.mode && gw.mode !== "off";
56774
+ const actorDoc2 = this.actorDocFor({ priv, moderators, inbox: gwActive ? gw.url : null });
56443
56775
  const surface = node_crypto_default.createHash("sha256").update(JSON.stringify({
56444
56776
  actor: actorDoc2,
56445
56777
  handle: this.config.handle,
@@ -56455,7 +56787,7 @@ var Publisher = class {
56455
56787
  await writeWebfinger(
56456
56788
  this.remote,
56457
56789
  urls,
56458
- jrd({ handle: this.config.handle, host, actor: urls.actor })
56790
+ jrd({ handle: this.config.handle, host, actor: urls.actor, page: urls.profileHtml })
56459
56791
  );
56460
56792
  await writeHostMeta(this.remote, urls, hostMeta(urls.base));
56461
56793
  const nodeinfoDocUrl = urls.home + "ap/nodeinfo-2.0";
@@ -56485,7 +56817,9 @@ var Publisher = class {
56485
56817
  const wrote = await linkInWebIdProfile(this.remote, {
56486
56818
  actorUrl: urls.actor,
56487
56819
  accountName: `@${this.config.handle}@${host}`,
56488
- kind: this.config.kind
56820
+ kind: this.config.kind,
56821
+ // Where a Solid client posts: dokieli reads `as:outbox` off the WebID.
56822
+ outbox: this.clientOrigin ? `${this.clientOrigin}ap/outbox` : this.gatewayOutbox()
56489
56823
  });
56490
56824
  if (wrote) this.log("WebID profile now lists the actor as a foaf:account");
56491
56825
  } catch (e) {
@@ -56648,21 +56982,15 @@ var Publisher = class {
56648
56982
  await this.deliverer.deliverToAll(inboxes, moveActivity(urls, target, Date.parse(at)));
56649
56983
  this.config.movedTo = target;
56650
56984
  this.store.setConfig({ ...this.store.getConfig(), movedTo: target, movedAt: at });
56651
- await writeMoved(this.remote, urls, actorDoc({
56652
- urls,
56653
- handle: publicHandle(this.config),
56654
- name: this.config.name,
56655
- publicKeyPem: this.publicKeyPem,
56656
- movedTo: target,
56657
- kind: this.config.kind,
56658
- approveJoins: followsNeedApproval(this.config),
56659
- assertionKey: this.assertionKey,
56660
- summary: this.config.summary || null,
56661
- icon: this.config.icon || null,
56662
- image: this.config.image || null,
56663
- fields: this.config.fields || [],
56664
- webId: this.remote.webId || null,
56665
- aliases: this.config.aliases || []
56985
+ const gw = this.config.gateway;
56986
+ const gwActive = gw && gw.url && gw.mode && gw.mode !== "off";
56987
+ const priv = await this.privateReady() === true;
56988
+ const moderators = (this.config.moderators || []).length ? urls.moderators : null;
56989
+ await writeMoved(this.remote, urls, this.actorDocFor({
56990
+ priv,
56991
+ moderators,
56992
+ inbox: gwActive ? gw.url : null,
56993
+ movedTo: target
56666
56994
  }));
56667
56995
  await this.store.flush();
56668
56996
  this.log(`moved to ${target}: Move sent to ${inboxes.length} inbox(es), actor now advertises movedTo`);
@@ -56732,6 +57060,9 @@ var Publisher = class {
56732
57060
  rebuildStatuses(...a) {
56733
57061
  return rebuildStatuses(this, ...a);
56734
57062
  }
57063
+ healStatuses(...a) {
57064
+ return healStatuses(this, ...a);
57065
+ }
56735
57066
  // notes.mjs
56736
57067
  ensureMediaContainer(...a) {
56737
57068
  return ensureMediaContainer(this, ...a);
@@ -56751,6 +57082,9 @@ var Publisher = class {
56751
57082
  publishNote(...a) {
56752
57083
  return publishNote(this, ...a);
56753
57084
  }
57085
+ publishObject(...a) {
57086
+ return publishObject(this, ...a);
57087
+ }
56754
57088
  updateNote(...a) {
56755
57089
  return updateNote(this, ...a);
56756
57090
  }
@@ -56833,13 +57167,13 @@ var MAX_FORWARDS_PER_DRAIN = 20;
56833
57167
  var ACCEPT_AP2 = 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"';
56834
57168
  function trimActivity(a) {
56835
57169
  if (!a || typeof a !== "object") return a ?? null;
56836
- const idOf = (v) => typeof v === "string" ? v : v?.id ?? null;
57170
+ const idOf2 = (v) => typeof v === "string" ? v : v?.id ?? null;
56837
57171
  const out = {};
56838
57172
  for (const k of ["id", "type", "actor", "target"]) {
56839
- const v = idOf(a[k]);
57173
+ const v = idOf2(a[k]);
56840
57174
  if (v) out[k] = String(v).slice(0, 2048);
56841
57175
  }
56842
- const obj = idOf(a.object);
57176
+ const obj = idOf2(a.object);
56843
57177
  if (obj) out.object = String(obj).slice(0, 2048);
56844
57178
  else if (a.object && typeof a.object === "object" && a.object.type) {
56845
57179
  out.object = { type: String(a.object.type).slice(0, 64) };
@@ -57360,6 +57694,8 @@ async function onFollow(intake, activity, actor, { trusted = false } = {}) {
57360
57694
  intake.log(`Accept sent \u2192 ${doc.inbox}`);
57361
57695
  }
57362
57696
  async function onUndo(intake, activity, actor, { trusted = false } = {}) {
57697
+ const inner = activity.object && typeof activity.object === "object" ? activity.object : null;
57698
+ if (inner?.type === "Like" || inner?.type === "Announce") return onUndoReaction(intake, inner, actor, { trusted });
57363
57699
  if (typeof activity.object === "object" && activity.object?.type && activity.object.type !== "Follow") return;
57364
57700
  const named = typeof activity.object === "string" ? activity.object : activity.object?.id;
57365
57701
  if (!named && !trusted) return;
@@ -57447,6 +57783,47 @@ async function onAnnounce(intake, activity, actor, objectId) {
57447
57783
  }
57448
57784
  return intake.ingestNote(objectId, actor, { via: actor });
57449
57785
  }
57786
+ async function onUndoReaction(intake, inner, actor, { trusted = false } = {}) {
57787
+ const innerActor = typeof inner.actor === "string" ? inner.actor : inner.actor?.id;
57788
+ if (innerActor && innerActor !== actor) return;
57789
+ const wrapped = inner.object;
57790
+ const target = wrapped && typeof wrapped === "object" && (wrapped.type === "Create" || wrapped.type === "Update") ? wrapped.object : wrapped;
57791
+ const objectId = typeof target === "string" ? target : target?.id;
57792
+ if (!objectId) return;
57793
+ if (objectId.startsWith(intake.urls.notes)) {
57794
+ const type = inner.type === "Like" ? "favourite" : "reblog";
57795
+ const gone = intake.store.removeNotifications((n) => n.type === type && n.actor === actor && n.noteId === objectId);
57796
+ if (gone) intake.log(`${inner.type} withdrawn by ${actor} on ${objectId}`);
57797
+ return;
57798
+ }
57799
+ if (inner.type !== "Announce") return;
57800
+ const followed = trusted || intake.store.getContacts().following.some((f) => f.actor === actor && f.accepted);
57801
+ if (!followed) return;
57802
+ const s = intake.store.getStatuses().find((x) => x.noteId === objectId);
57803
+ if (!s || s.kind === "post" || s.via !== actor) return;
57804
+ await intake.forget(s);
57805
+ intake.log(`boost withdrawn by ${actor}: ${objectId} left the timeline`);
57806
+ }
57807
+ async function onBlock(intake, activity, actor, { trusted = false } = {}) {
57808
+ const objectId = typeof activity.object === "string" ? activity.object : activity.object?.id;
57809
+ if (objectId !== intake.urls.actor) return;
57810
+ if (!trusted) {
57811
+ intake.log(`Block from ${actor} arrived unverified \u2014 ignored`);
57812
+ return;
57813
+ }
57814
+ const contacts = intake.store.getContacts();
57815
+ const wasFollower = contacts.followers.some((f) => f.actor === actor);
57816
+ const wasFollowing = contacts.following.some((f) => f.actor === actor);
57817
+ if (wasFollower) dropFollower(contacts, actor, "blocked-us");
57818
+ contacts.following = contacts.following.filter((f) => f.actor !== actor);
57819
+ intake.store.setContacts(contacts);
57820
+ const reqs = intake.store.getRequests();
57821
+ const left = reqs.filter((r) => r.actor !== actor);
57822
+ if (left.length !== reqs.length) intake.store.setRequests(left);
57823
+ await intake.republish({ followers: wasFollower, following: wasFollowing, pending: true });
57824
+ const what = [wasFollower && "their follow of us", wasFollowing && "our follow of them"].filter(Boolean).join(" and ");
57825
+ intake.log(`blocked by ${actor}: ${what || "no relationship"} dropped`);
57826
+ }
57450
57827
  async function onDelete(intake, activity, actor) {
57451
57828
  const objectId = typeof activity.object === "string" ? activity.object : activity.object?.id;
57452
57829
  if (!objectId) return "Delete without object id";
@@ -57652,6 +58029,7 @@ async function ingestNote(intake, objectId, actor, { via, inline = null } = {})
57652
58029
  const followed = via || known2 || !known2 && await intake.isCoMember(author);
57653
58030
  const kind = followed ? "timeline" : "mention";
57654
58031
  const mentions = [].concat(note.tag || []).filter((t) => t?.type === "Mention" && t.href && t.name).slice(0, MAX_MENTIONS).map((t) => ({ href: httpOnly(String(t.href).slice(0, MAX_URL_CHARS)), name: String(t.name).slice(0, 256) })).filter((m) => m.href);
58032
+ const hashtags = [].concat(note.tag || []).filter((t) => t?.type === "Hashtag" && t.name).slice(0, MAX_MENTIONS).map((t) => ({ name: String(t.name).replace(/^#/, "").slice(0, 128), url: httpOnly(String(t.href || "").slice(0, MAX_URL_CHARS)) })).filter((t) => t.name);
57655
58033
  const emojis = emojisOf(note);
57656
58034
  const poll = pollOf(note);
57657
58035
  const audience = [].concat(note.to || [], note.cc || []).map(String);
@@ -57668,6 +58046,8 @@ async function ingestNote(intake, objectId, actor, { via, inline = null } = {})
57668
58046
  ...nonPublic ? { nonPublic: true } : {},
57669
58047
  // The author's content warning, shown as one: plain text only.
57670
58048
  ...note.summary ? { spoiler: String(note.summary).replace(/<[^>]*>/g, "") } : {},
58049
+ ...note.sensitive === true ? { sensitive: true } : {},
58050
+ ...hashtags.length ? { tags: hashtags } : {},
57671
58051
  ...poll ? { poll } : {},
57672
58052
  ...emojis.length ? { emojis } : {},
57673
58053
  ...mentions.length ? { mentions } : {},
@@ -60814,8 +61194,8 @@ function walkPath(graph2, subject, path, visited = new node_set_default()) {
60814
61194
  pathValues.addAll(deeperValues);
60815
61195
  return pathValues;
60816
61196
  }
60817
- function flatMap(arr, func) {
60818
- return [...arr].reduce((acc, x) => acc.concat(func(x)), []);
61197
+ function flatMap(arr2, func) {
61198
+ return [...arr2].reduce((acc, x) => acc.concat(func(x)), []);
60819
61199
  }
60820
61200
 
60821
61201
  // node_modules/rdf-validate-shacl/src/dataset-utils.js
@@ -62764,8 +63144,10 @@ var ATTEMPTS_TTL_MS = 7 * 24 * 60 * 6e4;
62764
63144
  var MAX_ITEM_ATTEMPTS = 5;
62765
63145
  var MAX_ITEMS_PER_DRAIN = 50;
62766
63146
  var Intake = class {
62767
- constructor({ config, urls, remote, store, deliverer, publisher, log: log2 = console.log, lease = null, archive = null, push = true, pollSeconds = null }) {
62768
- Object.assign(this, { config, urls, remote, store, deliverer, publisher, log: log2, lease, archive, push, pollSeconds });
63147
+ // `ownerPost(activity, { raw, slug })` is the client-to-server dispatcher,
63148
+ // for an item the Gateway's outbox door took on the owner's behalf.
63149
+ constructor({ config, urls, remote, store, deliverer, publisher, log: log2 = console.log, lease = null, archive = null, push = true, pollSeconds = null, ownerPost = null }) {
63150
+ Object.assign(this, { config, urls, remote, store, deliverer, publisher, log: log2, lease, archive, push, pollSeconds, ownerPost });
62769
63151
  this.serial = Date.now();
62770
63152
  this.stopped = false;
62771
63153
  this.lastDrain = null;
@@ -63073,9 +63455,10 @@ var Intake = class {
63073
63455
  }
63074
63456
  const receipt = activity ? await this._readReceipt(url) : null;
63075
63457
  if (activity && this.gatewaySecret()) this._bumpGatewayStat(!!receipt?.verified);
63076
- const rejection = activity ? await this.handle(activity, receipt) : "unparsable JSON";
63077
- if (!rejection && raw) await this._archive(url, raw, activity);
63078
- if (!rejection) await this._maybeForward(activity);
63458
+ const owned = activity && this.isOwnerPost(receipt);
63459
+ const rejection = !activity ? "unparsable JSON" : owned ? await this.ownerPostFrom(activity, raw, receipt) : await this.handle(activity, receipt);
63460
+ if (!rejection && raw && !owned) await this._archive(url, raw, activity);
63461
+ if (!rejection && !owned) await this._maybeForward(activity);
63079
63462
  if (rejection) {
63080
63463
  this.store.addDeadLetter({
63081
63464
  inboxUrl: url,
@@ -63187,6 +63570,8 @@ var Intake = class {
63187
63570
  case "Add":
63188
63571
  case "Remove":
63189
63572
  return this.onAddRemove(activity, actor);
63573
+ case "Block":
63574
+ return this.onBlock(activity, actor, { trusted });
63190
63575
  default:
63191
63576
  this.log(`ignored ${activity.type} from ${actor}`);
63192
63577
  }
@@ -63223,6 +63608,28 @@ var Intake = class {
63223
63608
  _readReceipt(...a) {
63224
63609
  return readReceipt(this, ...a);
63225
63610
  }
63611
+ // A receipt the door stamped `c2s` for THIS actor, and only a receipt whose
63612
+ // HMAC verified (readReceipt returns nothing else). A stranger appending an
63613
+ // item that claims to be ours has no such receipt and is read as mail.
63614
+ isOwnerPost(receipt) {
63615
+ return !!(this.ownerPost && receipt?.verified && receipt.method === "c2s" && receipt.actor === this.urls.actor);
63616
+ }
63617
+ // Publish what the owner posted at the door. The bytes as the client wrote
63618
+ // them go to the dispatcher (an object stored as sent must be the client's
63619
+ // document, not our reading of it); the graph is what it decides from.
63620
+ // Returns null when published, else the reason it was not — a dead letter.
63621
+ async ownerPostFrom(activity, raw, receipt) {
63622
+ let asSent = null;
63623
+ try {
63624
+ asSent = raw ? JSON.parse(raw) : null;
63625
+ } catch {
63626
+ asSent = null;
63627
+ }
63628
+ const r = await this.ownerPost(activity, { raw: asSent, slug: receipt.slug || null });
63629
+ if (!r || r.status >= 300) return `owner post refused (${r?.status || "?"}): ${r?.body?.error || ""}`;
63630
+ this.log(`owner post from the outbox door published: ${r.body?.object || r.body?.id || activity.type}`);
63631
+ return null;
63632
+ }
63226
63633
  receiptVouchesFor(...a) {
63227
63634
  return receiptVouchesFor(this, ...a);
63228
63635
  }
@@ -63258,6 +63665,9 @@ var Intake = class {
63258
63665
  onUndo(...a) {
63259
63666
  return onUndo(this, ...a);
63260
63667
  }
63668
+ onBlock(...a) {
63669
+ return onBlock(this, ...a);
63670
+ }
63261
63671
  onCreate(...a) {
63262
63672
  return onCreate(this, ...a);
63263
63673
  }
@@ -63306,6 +63716,859 @@ var Intake = class {
63306
63716
  }
63307
63717
  };
63308
63718
 
63719
+ // lib/core/social.mjs
63720
+ var social_exports = {};
63721
+ __export(social_exports, {
63722
+ admitRequest: () => admitRequest,
63723
+ announceModeration: () => announceModeration,
63724
+ applyModeration: () => applyModeration,
63725
+ blockActor: () => blockActor,
63726
+ confirmDelegation: () => confirmDelegation,
63727
+ deleteNote: () => deleteNote,
63728
+ ejectFollower: () => ejectFollower,
63729
+ favourite: () => favourite,
63730
+ followActor: () => followActor,
63731
+ followHandle: () => followHandle,
63732
+ lookupWebFinger: () => lookupWebFinger,
63733
+ pinStatus: () => pinStatus,
63734
+ reblog: () => reblog,
63735
+ refuseRequest: () => refuseRequest,
63736
+ resolveHandle: () => resolveHandle,
63737
+ retractAnnouncement: () => retractAnnouncement,
63738
+ selfLink: () => selfLink,
63739
+ unblockActor: () => unblockActor,
63740
+ unfavourite: () => unfavourite,
63741
+ unfollowActor: () => unfollowActor,
63742
+ unreblog: () => unreblog,
63743
+ votePoll: () => votePoll
63744
+ });
63745
+ init_node_crypto();
63746
+ init_wire();
63747
+ async function lookupWebFinger(acct, { fetchImpl = null } = {}) {
63748
+ const clean = String(acct || "").replace(/^acct:/, "");
63749
+ const at = clean.lastIndexOf("@");
63750
+ if (at < 1) return null;
63751
+ const [user, host] = [clean.slice(0, at), clean.slice(at + 1)];
63752
+ if (!host || /[/\\?#]/.test(host)) return null;
63753
+ const { safeFetch: safeFetch2, readCapped: readCapped3 } = await Promise.resolve().then(() => (init_safefetch(), safefetch_exports));
63754
+ const url = `https://${host}/.well-known/webfinger?resource=${encodeURIComponent(`acct:${clean}`)}`;
63755
+ const headers = { accept: "application/jrd+json, application/json" };
63756
+ const res = await (fetchImpl ? fetchImpl(url, { headers }) : safeFetch2(url, { headers })).catch(() => null);
63757
+ if (!res || res.status >= 400) return null;
63758
+ let jrd2;
63759
+ try {
63760
+ jrd2 = JSON.parse(await readCapped3(res, 256 * 1024));
63761
+ } catch {
63762
+ return null;
63763
+ }
63764
+ const subject = String(jrd2?.subject || "").replace(/^acct:/, "").toLowerCase();
63765
+ if (subject && subject !== clean.toLowerCase()) {
63766
+ return null;
63767
+ }
63768
+ return jrd2;
63769
+ }
63770
+ var selfLink = (jrd2) => jrd2?.links?.find((l) => l.rel === "self" && /application\/(activity\+json|ld\+json)/.test(l.type || ""));
63771
+ async function confirmDelegation(asked, doc, lookup2 = lookupWebFinger) {
63772
+ let home;
63773
+ try {
63774
+ home = new URL(doc.id).host.toLowerCase();
63775
+ } catch {
63776
+ throw new Error(`actor id is not a URL (${doc.id})`);
63777
+ }
63778
+ if (home === asked.toLowerCase()) return;
63779
+ const user = doc.preferredUsername;
63780
+ if (!user) {
63781
+ throw new Error(`${asked} points at ${doc.id} on another host, which names no username to confirm it by`);
63782
+ }
63783
+ const back = await lookup2(`acct:${user}@${home}`);
63784
+ if (selfLink(back)?.href !== doc.id) {
63785
+ throw new Error(`${asked} claims ${doc.id}, but ${home} does not agree that is @${user}@${home} \u2014 refusing to take one host's word for another's actor`);
63786
+ }
63787
+ }
63788
+ async function resolveHandle(agent2, handle7) {
63789
+ const clean = String(handle7 || "").replace(/^@/, "");
63790
+ if (!/^[^@]+@[^@]+$/.test(clean)) throw new Error("handle must look like user@host");
63791
+ if (agent2.store.isBlocked("https://" + clean.split("@")[1] + "/")) throw new Error("domain is blocked");
63792
+ const remote = agent2.intake?.deliverer?.signedFetch ? (u, i) => agent2.intake.deliverer.signedFetch(u, i) : null;
63793
+ const lookup2 = (a) => lookupWebFinger(a, { fetchImpl: remote });
63794
+ const jrd2 = await lookup2("acct:" + clean);
63795
+ const self2 = selfLink(jrd2);
63796
+ if (!self2?.href) throw new Error(`webfinger found no actor for ${clean}`);
63797
+ const doc = await agent2.intake.fetchAP(self2.href);
63798
+ if (!doc?.id) throw new Error(`actor document unusable for ${clean}`);
63799
+ if (agent2.store.isBlocked(doc.id)) throw new Error("actor is blocked");
63800
+ await confirmDelegation(clean.slice(clean.lastIndexOf("@") + 1), doc, lookup2);
63801
+ await cacheCounts(agent2, doc);
63802
+ return doc;
63803
+ }
63804
+ async function cacheCounts(agent2, doc) {
63805
+ const total = async (url) => {
63806
+ if (!url) return null;
63807
+ try {
63808
+ const res = await agent2.intake.fetchAP(url);
63809
+ return Number.isInteger(res?.totalItems) ? res.totalItems : null;
63810
+ } catch {
63811
+ return null;
63812
+ }
63813
+ };
63814
+ const [followers, following] = await Promise.all([total(doc.followers), total(doc.following)]);
63815
+ if (followers === null && following === null) return;
63816
+ const actors = agent2.store.getActors();
63817
+ if (!actors[doc.id]) return;
63818
+ actors[doc.id] = { ...actors[doc.id], counts: { followers, following } };
63819
+ agent2.store.write("actors.json", actors);
63820
+ }
63821
+ async function followActor(agent2, actorUrl, { publish = true, doc: fetched = null } = {}) {
63822
+ const doc = fetched || await agent2.intake.fetchAP(actorUrl);
63823
+ if (!doc?.inbox) throw new Error(`actor document unusable (${actorUrl})`);
63824
+ if (agent2.store.isBlocked(doc.id)) throw new Error("actor is blocked");
63825
+ const contacts = agent2.store.getContacts();
63826
+ let rec = contacts.following.find((f) => f.actor === doc.id);
63827
+ if (!rec) {
63828
+ rec = { actor: doc.id, inbox: doc.inbox, accepted: false };
63829
+ contacts.following.push(rec);
63830
+ }
63831
+ const follow = followActivity({ urls: agent2.publisher.urls, targetActor: doc.id, serial: Date.now() });
63832
+ rec.followActivity = follow;
63833
+ agent2.store.setContacts(contacts);
63834
+ await agent2.deliverer.deliver(doc.inbox, follow);
63835
+ if (publish) await agent2.publisher.publishCollections({ following: true, pending: true });
63836
+ return doc;
63837
+ }
63838
+ async function followHandle(agent2, handle7) {
63839
+ const doc = await resolveHandle(agent2, handle7);
63840
+ await followActor(agent2, doc.id);
63841
+ const clean = String(handle7 || "").replace(/^@/, "");
63842
+ const contacts = agent2.store.getContacts();
63843
+ const rec = contacts.following.find((f) => f.actor === doc.id);
63844
+ if (rec && !rec.handle) {
63845
+ rec.handle = clean;
63846
+ agent2.store.setContacts(contacts);
63847
+ }
63848
+ return { ok: true, actor: doc.id };
63849
+ }
63850
+ async function unfollowActor(agent2, actor) {
63851
+ const contacts = agent2.store.getContacts();
63852
+ const rec = contacts.following.find((f) => f.actor === actor);
63853
+ if (!rec) throw new Error("not following that actor");
63854
+ if (rec.followActivity) {
63855
+ await agent2.deliverer.deliver(
63856
+ rec.inbox,
63857
+ undoActivity({ urls: agent2.publisher.urls, activity: rec.followActivity, serial: Date.now() })
63858
+ );
63859
+ }
63860
+ contacts.following = contacts.following.filter((f) => f.actor !== actor);
63861
+ agent2.store.setContacts(contacts);
63862
+ await agent2.publisher.publishCollections({ following: true, pending: true });
63863
+ return { ok: true };
63864
+ }
63865
+ async function favourite(agent2, s) {
63866
+ if (s.favourited) return s;
63867
+ const doc = await agent2.intake.fetchAP(s.actor);
63868
+ if (!doc?.inbox) throw new Error("author inbox unavailable");
63869
+ const act = likeActivity({ urls: agent2.publisher.urls, noteId: s.noteId, serial: Date.now() });
63870
+ await agent2.deliverer.deliver(doc.endpoints?.sharedInbox || doc.inbox, act);
63871
+ return agent2.store.updateStatus(s.noteId, { favourited: true, likeActivity: act });
63872
+ }
63873
+ async function unfavourite(agent2, s) {
63874
+ if (s.likeActivity) {
63875
+ const doc = await agent2.intake.fetchAP(s.actor).catch(() => null);
63876
+ if (doc?.inbox) {
63877
+ await agent2.deliverer.deliver(
63878
+ doc.endpoints?.sharedInbox || doc.inbox,
63879
+ undoActivity({ urls: agent2.publisher.urls, activity: s.likeActivity, serial: Date.now() })
63880
+ );
63881
+ }
63882
+ }
63883
+ return agent2.store.updateStatus(s.noteId, { favourited: false, likeActivity: void 0 });
63884
+ }
63885
+ async function reblog(agent2, s) {
63886
+ if (s.reblogged) return s;
63887
+ const { urls } = agent2.publisher;
63888
+ const act = announceActivity({ urls, object: s.noteId, serial: Date.now() });
63889
+ const inboxes = new Set(agent2.store.getContacts().followers.map((f) => f.sharedInbox || f.inbox).filter(Boolean));
63890
+ if (s.actor !== urls.actor) {
63891
+ const doc = await agent2.intake.fetchAP(s.actor).catch(() => null);
63892
+ if (doc?.inbox) inboxes.add(doc.endpoints?.sharedInbox || doc.inbox);
63893
+ }
63894
+ await agent2.deliverer.deliverToAll([...inboxes], act);
63895
+ const updated = agent2.store.updateStatus(s.noteId, { reblogged: true, announceActivity: act });
63896
+ await agent2.publisher.recordOutbox(act);
63897
+ if (await agent2.store.commit?.() === false) agent2.log(`boost sent but its timeline row was refused: ${s.noteId}`);
63898
+ return updated;
63899
+ }
63900
+ async function unreblog(agent2, s) {
63901
+ if (s.announceActivity) {
63902
+ const undo = undoActivity({ urls: agent2.publisher.urls, activity: s.announceActivity, serial: Date.now() });
63903
+ const inboxes = new Set(agent2.store.getContacts().followers.map((f) => f.sharedInbox || f.inbox).filter(Boolean));
63904
+ const doc = await agent2.intake.fetchAP(s.actor).catch(() => null);
63905
+ if (doc?.inbox && s.actor !== agent2.publisher.urls.actor) inboxes.add(doc.endpoints?.sharedInbox || doc.inbox);
63906
+ await agent2.deliverer.deliverToAll([...inboxes], undo);
63907
+ await agent2.publisher.unrecordOutbox((i) => i?.id === s.announceActivity.id);
63908
+ }
63909
+ return agent2.store.updateStatus(s.noteId, { reblogged: false, announceActivity: void 0 });
63910
+ }
63911
+ async function ejectFollower(agent2, actor) {
63912
+ const { urls } = agent2.publisher;
63913
+ const contacts = agent2.store.getContacts();
63914
+ const rec = contacts.followers.find((f) => f.actor === actor);
63915
+ if (!rec) throw new Error("not a member");
63916
+ dropFollower(contacts, actor, "ejected");
63917
+ agent2.store.setContacts(contacts);
63918
+ const muted = agent2.store.getMuted();
63919
+ muted.actors = [.../* @__PURE__ */ new Set([...muted.actors, actor])];
63920
+ agent2.store.setMuted(muted);
63921
+ await agent2.publisher.publishCollections({ followers: true });
63922
+ await announceModeration(
63923
+ agent2,
63924
+ blockActivity({ urls, targetActor: actor, serial: Date.now() })
63925
+ ).catch((e) => agent2.log?.(`moderation announce failed: ${e.message}`));
63926
+ if (rec.bsky) {
63927
+ await agent2.bskygroup?.blockDid(rec.bsky.did).catch((e) => agent2.log?.(`bluesky block failed for ${rec.bsky.handle}: ${e.message}`));
63928
+ return { ok: true, actor, told: true };
63929
+ }
63930
+ if (rec.inbox) {
63931
+ await agent2.deliverer.deliver(rec.inbox, rejectActivity({
63932
+ urls,
63933
+ followActivity: { id: rec.followId, type: "Follow", actor, object: urls.actor },
63934
+ serial: Date.now()
63935
+ }));
63936
+ }
63937
+ return { ok: true, actor, told: !!rec.inbox };
63938
+ }
63939
+ async function admitRequest(agent2, actor, { publish = true } = {}) {
63940
+ const reqs = agent2.store.getRequests();
63941
+ const req = reqs.find((r) => r.actor === actor);
63942
+ if (!req) throw new Error("no such request");
63943
+ const contacts = agent2.store.getContacts();
63944
+ if (!contacts.followers.some((f) => f.actor === actor)) {
63945
+ const bridgedDid = /^https:\/\/bsky\.brid\.gy\/ap\/(did:[^/]+)$/.exec(actor)?.[1];
63946
+ if (bridgedDid) contacts.followers = contacts.followers.filter((f) => f.bsky?.did !== bridgedDid);
63947
+ contacts.followers.push(req.bsky ? { actor, bsky: req.bsky } : { actor, inbox: req.inbox, sharedInbox: req.sharedInbox, followId: req.activity?.id });
63948
+ agent2.store.setContacts(contacts);
63949
+ agent2.store.addNotification({ type: "follow", actor, ...req.bsky ? { bsky: true } : {} });
63950
+ }
63951
+ agent2.store.setRequests(reqs.filter((r) => r.actor !== actor));
63952
+ if (req.bsky) {
63953
+ await agent2.bskygroup?.nudgeOnce(contacts.followers.find((f) => f.actor === actor));
63954
+ return { ok: true, actor };
63955
+ }
63956
+ if (publish) await agent2.publisher.publishCollections({ followers: true, pending: true });
63957
+ await agent2.deliverer.deliver(req.inbox, acceptActivity({
63958
+ urls: agent2.publisher.urls,
63959
+ followActivity: req.activity,
63960
+ serial: Date.now()
63961
+ }));
63962
+ return { ok: true, actor };
63963
+ }
63964
+ async function refuseRequest(agent2, actor) {
63965
+ const reqs = agent2.store.getRequests();
63966
+ const req = reqs.find((r) => r.actor === actor);
63967
+ if (!req) throw new Error("no such request");
63968
+ agent2.store.setRequests(reqs.filter((r) => r.actor !== actor));
63969
+ await agent2.publisher?.publishCollections({ pending: true }).catch(() => {
63970
+ });
63971
+ if (req.bsky) {
63972
+ await agent2.bskygroup?.blockDid(req.bsky.did).catch((e) => agent2.log?.(`bluesky block failed for ${req.bsky.handle}: ${e.message}`));
63973
+ return { ok: true, actor };
63974
+ }
63975
+ await agent2.deliverer.deliver(req.inbox, rejectActivity({
63976
+ urls: agent2.publisher.urls,
63977
+ followActivity: req.activity,
63978
+ serial: Date.now()
63979
+ }));
63980
+ return { ok: true, actor };
63981
+ }
63982
+ async function retractAnnouncement(agent2, noteId) {
63983
+ return agent2.intake.retract(noteId);
63984
+ }
63985
+ async function votePoll(agent2, s, choices) {
63986
+ const { urls } = agent2.publisher;
63987
+ const poll = s.poll || {};
63988
+ const opts = poll.options || [];
63989
+ if (poll.closed || poll.expiresAt && Date.parse(poll.expiresAt) < Date.now()) {
63990
+ return { ok: false, error: "this poll has closed" };
63991
+ }
63992
+ if (poll.voted) return { ok: false, error: "already voted" };
63993
+ if (s.actor === urls.actor) return { ok: false, error: "this is your own poll" };
63994
+ const picks = [...new Set(choices)].filter((i) => i >= 0 && i < opts.length);
63995
+ if (!picks.length) return { ok: false, error: "invalid choice" };
63996
+ if (!poll.multiple && picks.length > 1) return { ok: false, error: "this poll takes one choice" };
63997
+ const author = await agent2.intake.fetchAP(s.actor).catch(() => null);
63998
+ const inbox = author?.endpoints?.sharedInbox || author?.inbox;
63999
+ if (!inbox) return { ok: false, error: "could not reach the poll author" };
64000
+ const published = (/* @__PURE__ */ new Date()).toISOString();
64001
+ for (const i of picks) {
64002
+ const id = urls.notes + published.slice(0, 10) + "-vote-" + node_crypto_default.randomBytes(4).toString("hex");
64003
+ const note = {
64004
+ "@context": AS_CTX,
64005
+ id,
64006
+ type: "Note",
64007
+ attributedTo: urls.actor,
64008
+ name: opts[i].title,
64009
+ inReplyTo: s.noteId,
64010
+ published,
64011
+ to: [s.actor]
64012
+ };
64013
+ const create = {
64014
+ "@context": AS_CTX,
64015
+ id: createActivityId(id),
64016
+ type: "Create",
64017
+ actor: urls.actor,
64018
+ published,
64019
+ to: [s.actor],
64020
+ object: note
64021
+ };
64022
+ await write4(agent2.remote, id, note);
64023
+ await writeCreate(agent2.remote, create.id, create);
64024
+ await agent2.deliverer.deliverToAll([inbox], create);
64025
+ }
64026
+ agent2.store.updateStatus(s.noteId, {
64027
+ poll: {
64028
+ ...poll,
64029
+ voted: true,
64030
+ ownVotes: picks,
64031
+ options: opts.map((o, j) => picks.includes(j) ? { ...o, votes: (o.votes || 0) + 1 } : o)
64032
+ }
64033
+ });
64034
+ return { ok: true };
64035
+ }
64036
+ async function deleteNote(agent2, s) {
64037
+ const { urls } = agent2.publisher;
64038
+ const inboxes = agent2.store.getContacts().followers.map((f) => f.sharedInbox || f.inbox).filter(Boolean);
64039
+ await agent2.deliverer.deliverToAll(inboxes, deleteActivity({ urls, noteId: s.noteId }));
64040
+ const stuck = [];
64041
+ const deletedAt = (/* @__PURE__ */ new Date()).toISOString();
64042
+ try {
64043
+ await writeTombstone2(agent2.remote, s.noteId, noteTombstone(s.noteId, deletedAt));
64044
+ } catch {
64045
+ stuck.push(s.noteId);
64046
+ }
64047
+ if (!await dropCreate(agent2.remote, createActivityId(s.noteId))) {
64048
+ stuck.push(createActivityId(s.noteId));
64049
+ }
64050
+ if (stuck.length) {
64051
+ agent2.log?.(`delete ${s.noteId}: the pod kept ${stuck.length} document(s) \u2014 the post is STILL PUBLISHED`);
64052
+ return {
64053
+ ok: false,
64054
+ stillPublished: stuck,
64055
+ error: "the pod would not remove the post \u2014 it is still published, and has been kept here so you can try again"
64056
+ };
64057
+ }
64058
+ await dropReplies(agent2.remote, repliesId(s.noteId));
64059
+ if (s.atproto?.uri && agent2.atproto?.connected()) {
64060
+ await agent2.atproto.deleteCrossPost(s.atproto.uri).then(() => agent2.log?.(`bluesky mirror deleted: ${s.atproto.uri}`)).catch((e) => agent2.log?.(`bluesky mirror not deleted (${e.message}): ${s.atproto.uri}`));
64061
+ }
64062
+ await agent2.publisher.unrecordOutbox((i) => i === s.noteId || i?.id && i.id === s.announceActivity?.id);
64063
+ agent2.store.removeStatus(s.noteId);
64064
+ return { ok: true };
64065
+ }
64066
+ async function announceModeration(agent2, activity) {
64067
+ if (agent2.store.getConfig()?.kind !== "group") return null;
64068
+ const { urls } = agent2.publisher;
64069
+ const act = announceActivity({ urls, object: activity, serial: Date.now(), audience: urls.actor });
64070
+ const inboxes = [...new Set(agent2.store.getContacts().followers.map((f) => f.sharedInbox || f.inbox).filter(Boolean))];
64071
+ await agent2.deliverer.deliverToAll(inboxes, act);
64072
+ await agent2.publisher.recordOutbox(act);
64073
+ return act;
64074
+ }
64075
+ async function blockActor(agent2, actorUrl) {
64076
+ if (!/^https?:\/\/\S+$/.test(String(actorUrl || ""))) throw new Error("actor must be a URL");
64077
+ const b = agent2.store.getBlocklist();
64078
+ if (!b.actors.includes(actorUrl)) b.actors.push(actorUrl);
64079
+ agent2.store.setBlocklist(b);
64080
+ await unfollowActor(agent2, actorUrl).catch(() => {
64081
+ });
64082
+ await agent2.publisher?.publishCollections({ blocked: true }).catch(() => {
64083
+ });
64084
+ await announceModeration(
64085
+ agent2,
64086
+ blockActivity({ urls: agent2.publisher.urls, targetActor: actorUrl, serial: Date.now() })
64087
+ ).catch((e) => agent2.log?.(`moderation announce failed: ${e.message}`));
64088
+ return { ok: true, actor: actorUrl };
64089
+ }
64090
+ async function unblockActor(agent2, actorUrl) {
64091
+ const b = agent2.store.getBlocklist();
64092
+ b.actors = b.actors.filter((a) => a !== actorUrl);
64093
+ agent2.store.setBlocklist(b);
64094
+ await agent2.publisher?.publishCollections({ blocked: true }).catch(() => {
64095
+ });
64096
+ await announceModeration(agent2, undoActivity({
64097
+ urls: agent2.publisher.urls,
64098
+ activity: { type: "Block", actor: agent2.publisher.urls.actor, object: actorUrl },
64099
+ serial: Date.now()
64100
+ })).catch((e) => agent2.log?.(`moderation announce failed: ${e.message}`));
64101
+ return { ok: true, actor: actorUrl };
64102
+ }
64103
+ async function applyModeration(agent2, entry) {
64104
+ const { urls } = agent2.publisher;
64105
+ const target = typeof entry.activity?.object === "string" ? entry.activity.object : entry.activity?.object?.id;
64106
+ switch (entry.type) {
64107
+ case "Block": {
64108
+ if (!target) throw new Error("the queued Block names nobody");
64109
+ const member = agent2.store.getContacts().followers.some((f) => f.actor === target);
64110
+ return member ? ejectFollower(agent2, target) : blockActor(agent2, target);
64111
+ }
64112
+ case "Undo": {
64113
+ const inner = entry.activity?.object;
64114
+ const unbanned = typeof inner === "object" && inner?.type === "Block" ? typeof inner.object === "string" ? inner.object : inner.object?.id : null;
64115
+ if (!unbanned) throw new Error("the queued Undo does not name a Block");
64116
+ const muted = agent2.store.getMuted();
64117
+ muted.actors = muted.actors.filter((a) => a !== unbanned);
64118
+ agent2.store.setMuted(muted);
64119
+ return unblockActor(agent2, unbanned);
64120
+ }
64121
+ case "Delete": {
64122
+ if (!target) throw new Error("the queued Delete names nothing");
64123
+ return agent2.intake.retract(target);
64124
+ }
64125
+ case "Add":
64126
+ case "Remove": {
64127
+ if (!target) throw new Error("the queued roster change names nobody");
64128
+ const cfg = { ...agent2.store.getConfig() };
64129
+ const prev = cfg.moderators || [];
64130
+ cfg.moderators = entry.type === "Add" ? [.../* @__PURE__ */ new Set([...prev, target])] : prev.filter((m) => m !== target);
64131
+ agent2.store.setConfig(cfg);
64132
+ if (agent2.publisher) agent2.publisher.config.moderators = cfg.moderators;
64133
+ await agent2.publisher.publishProfile();
64134
+ await announceModeration(agent2, addRemoveActivity({
64135
+ urls,
64136
+ type: entry.type,
64137
+ object: target,
64138
+ target: urls.moderators,
64139
+ serial: Date.now()
64140
+ })).catch((e) => agent2.log?.(`moderation announce failed: ${e.message}`));
64141
+ return { ok: true, moderators: cfg.moderators };
64142
+ }
64143
+ default:
64144
+ throw new Error(`no way to apply a queued ${entry.type}`);
64145
+ }
64146
+ }
64147
+ async function pinStatus(agent2, s, pinned) {
64148
+ if (s.actor !== agent2.publisher.urls.actor) throw new Error("not your status");
64149
+ const updated = agent2.store.updateStatus(s.noteId, { pinned: !!pinned });
64150
+ await agent2.publisher.publishFeatured().catch((e) => agent2.log?.(`featured: ${e.message}`));
64151
+ agent2.publisher.publishProfile?.().catch(() => {
64152
+ });
64153
+ return updated || s;
64154
+ }
64155
+
64156
+ // lib/client/c2s.mjs
64157
+ init_wire();
64158
+ var MAX_BODY = 512 * 1024;
64159
+ var MAX_INBOX_PAGE = 500;
64160
+ var ACTIVITY_TYPES = /* @__PURE__ */ new Set([
64161
+ "Create",
64162
+ "Update",
64163
+ "Delete",
64164
+ "Follow",
64165
+ "Like",
64166
+ "Announce",
64167
+ "Undo",
64168
+ "Block",
64169
+ "Add",
64170
+ "Remove",
64171
+ "Accept",
64172
+ "Reject",
64173
+ "Move"
64174
+ ]);
64175
+ var arr = (v) => v === void 0 || v === null ? [] : Array.isArray(v) ? v : [v];
64176
+ var idOf = (v) => typeof v === "string" ? v : v?.id || null;
64177
+ function readBody(req) {
64178
+ return new Promise((resolve2, reject) => {
64179
+ let size = 0;
64180
+ const chunks = [];
64181
+ req.on("data", (c) => {
64182
+ size += c.length;
64183
+ if (size > MAX_BODY) {
64184
+ reject(new Error("body too large"));
64185
+ req.destroy();
64186
+ return;
64187
+ }
64188
+ chunks.push(c);
64189
+ });
64190
+ req.on("end", () => resolve2(Buffer.concat(chunks).toString("utf8")));
64191
+ req.on("error", reject);
64192
+ });
64193
+ }
64194
+ var C2S = class {
64195
+ constructor({ agent: agent2, log: log2 = console.log, auth }) {
64196
+ this.agent = agent2;
64197
+ this.log = log2;
64198
+ this.auth = auth;
64199
+ }
64200
+ get store() {
64201
+ return this.agent.store;
64202
+ }
64203
+ get urls() {
64204
+ return this.agent.publisher?.urls;
64205
+ }
64206
+ send(res, status2, obj, headers = {}) {
64207
+ res.writeHead(status2, {
64208
+ "content-type": "application/json; charset=utf-8",
64209
+ "cache-control": "no-store",
64210
+ ...headers
64211
+ });
64212
+ res.end(JSON.stringify(obj));
64213
+ return true;
64214
+ }
64215
+ byIri(iri) {
64216
+ return iri ? this.store.getStatuses().find((s) => s.noteId === iri) : null;
64217
+ }
64218
+ /** The months the archive holds, newest first. One container listing. */
64219
+ async archiveMonths() {
64220
+ const archive = this.agent.intake?.archive;
64221
+ if (!archive) return [];
64222
+ const { names } = await archive.list("");
64223
+ return (names || []).map((n) => n.replace(/\/$/u, "")).filter((n) => /^\d{4}-\d{2}$/u.test(n)).sort().reverse();
64224
+ }
64225
+ /**
64226
+ * The owner's own inbox, §5.2. Paged by month because that is how the
64227
+ * archive is stored, so a page costs one listing and a read per item and no
64228
+ * page is dearer for another month being large.
64229
+ */
64230
+ async sendInbox(res, url) {
64231
+ const id = `${this.urls.base}ap/inbox`;
64232
+ const page2 = url?.searchParams?.get("page") || null;
64233
+ let months;
64234
+ try {
64235
+ months = await this.archiveMonths();
64236
+ } catch (e) {
64237
+ return this.send(res, 502, { error: `the archive could not be read: ${e.message}` });
64238
+ }
64239
+ if (!page2) {
64240
+ if (!months.length && this.store.getConfig()?.archiveInbox === false) {
64241
+ this.log("inbox read: nothing to show \u2014 this identity does not keep what it receives");
64242
+ }
64243
+ return this.send(res, 200, {
64244
+ "@context": AS_CTX,
64245
+ id,
64246
+ type: "OrderedCollection",
64247
+ ...months.length ? { first: `${id}?page=${months[0]}` } : { orderedItems: [] }
64248
+ });
64249
+ }
64250
+ if (!/^\d{4}-\d{2}$/u.test(page2)) {
64251
+ return this.send(res, 400, { error: "page names a month, written 2026-09" });
64252
+ }
64253
+ const archive = this.agent.intake?.archive;
64254
+ let names = [];
64255
+ try {
64256
+ ({ names } = await archive.list(`${page2}/`));
64257
+ } catch (e) {
64258
+ return this.send(res, 502, { error: `the archive could not be read: ${e.message}` });
64259
+ }
64260
+ const files = (names || []).filter((n) => n.endsWith(".json")).sort();
64261
+ if (files.length > MAX_INBOX_PAGE) {
64262
+ this.log(`inbox read: ${page2} holds ${files.length} items; serving the first ${MAX_INBOX_PAGE}`);
64263
+ }
64264
+ const kept = [];
64265
+ for (const file of files.slice(0, MAX_INBOX_PAGE)) {
64266
+ const read2 = await archive.read(`${page2}/${file}`, { accept: "*/*" });
64267
+ if (!read2?.ok || !read2.body) continue;
64268
+ try {
64269
+ const record = JSON.parse(read2.body);
64270
+ kept.push({ at: record.receivedAt || "", activity: JSON.parse(record.raw) });
64271
+ } catch {
64272
+ }
64273
+ }
64274
+ kept.sort((a, b) => String(b.at).localeCompare(String(a.at)));
64275
+ const older = months.filter((m) => m < page2)[0] || null;
64276
+ return this.send(res, 200, {
64277
+ "@context": AS_CTX,
64278
+ id: `${id}?page=${page2}`,
64279
+ type: "OrderedCollectionPage",
64280
+ partOf: id,
64281
+ ...older ? { next: `${id}?page=${older}` } : {},
64282
+ orderedItems: kept.map((k) => k.activity)
64283
+ });
64284
+ }
64285
+ async handle(req, res, pathname, url) {
64286
+ if (pathname !== "/ap/outbox" && pathname !== "/ap/actor" && pathname !== "/ap/inbox") return false;
64287
+ if (req.method === "OPTIONS") {
64288
+ res.writeHead(204, pathname === "/ap/inbox" ? { allow: "GET, OPTIONS" } : { allow: "GET, POST, OPTIONS", "accept-post": "application/ld+json, application/activity+json" });
64289
+ res.end();
64290
+ return true;
64291
+ }
64292
+ if (!this.agent.configured() || !this.urls) {
64293
+ return this.send(res, 409, { error: "agent not configured" });
64294
+ }
64295
+ if (pathname === "/ap/inbox") {
64296
+ if (req.method !== "GET" && req.method !== "HEAD") {
64297
+ return this.send(res, 405, { error: "deliveries go to this actor's inbox on the pod, which the actor document names; this address is the owner reading their own" });
64298
+ }
64299
+ const reader = await this.auth(req, pathname);
64300
+ if (!reader.ok) return this.send(res, reader.status, { error: reader.error }, reader.headers || {});
64301
+ return this.sendInbox(res, url);
64302
+ }
64303
+ if (req.method === "GET" || req.method === "HEAD") {
64304
+ const target = pathname === "/ap/actor" ? this.urls.actor : this.urls.outbox;
64305
+ res.writeHead(303, { location: target, "cache-control": "no-store" });
64306
+ res.end();
64307
+ return true;
64308
+ }
64309
+ if (pathname !== "/ap/outbox" || req.method !== "POST") {
64310
+ return this.send(res, 405, { error: "POST the outbox; GET redirects to the pod" });
64311
+ }
64312
+ const who = await this.auth(req, pathname);
64313
+ if (!who.ok) return this.send(res, who.status, { error: who.error }, who.headers || {});
64314
+ if (this.agent.viewer) {
64315
+ const took = await this.agent.requestTakeover?.();
64316
+ if (!took) return this.send(res, 503, { error: "another agent is active for this pod \u2014 takeover failed, try again" });
64317
+ }
64318
+ let activity;
64319
+ let raw = null;
64320
+ try {
64321
+ const body = await readBody(req);
64322
+ try {
64323
+ raw = JSON.parse(body);
64324
+ } catch {
64325
+ raw = null;
64326
+ }
64327
+ const read2 = await readLenient(body);
64328
+ activity = read2.view ?? read2.doc;
64329
+ } catch (e) {
64330
+ return this.send(res, 400, { error: `unreadable body: ${e.message}` });
64331
+ }
64332
+ if (!activity || typeof activity !== "object" || Array.isArray(activity) || !activity.type) {
64333
+ return this.send(res, 400, { error: "a typed ActivityStreams object is required" });
64334
+ }
64335
+ const slug = safeSlug(req.headers.slug) || null;
64336
+ const r = await this.dispatch(activity, { slug, raw });
64337
+ return this.send(res, r.status, r.body, r.headers);
64338
+ }
64339
+ // Addressing → the facade's four visibilities, inverting the table the
64340
+ // composer writes (wire.addressing). Nothing stated is a public post — what
64341
+ // every client means by a post with no audience chosen, and what a client
64342
+ // that never addresses (dokieli's annotations) needs.
64343
+ visibilityOf(activity, object) {
64344
+ const to = arr(activity.to ?? object?.to).map(idOf);
64345
+ const cc = arr(activity.cc ?? object?.cc).map(idOf);
64346
+ if (!to.length && !cc.length) return "public";
64347
+ if (to.includes(PUBLIC)) return "public";
64348
+ if (cc.includes(PUBLIC)) return "unlisted";
64349
+ if (to.includes(this.urls.followers)) return "private";
64350
+ return "direct";
64351
+ }
64352
+ async dispatch(activity, { slug = null, raw = null } = {}) {
64353
+ const reply = (status2, body, headers = {}) => ({ status: status2, body, headers });
64354
+ if (!activity || typeof activity !== "object" || Array.isArray(activity) || !activity.type) {
64355
+ return reply(400, { error: "a typed ActivityStreams object is required" });
64356
+ }
64357
+ if (!ACTIVITY_TYPES.has(activity.type)) {
64358
+ activity = { type: "Create", object: activity, to: activity.to, cc: activity.cc };
64359
+ }
64360
+ try {
64361
+ return await this._dispatch(activity, { slug, raw, reply });
64362
+ } catch (e) {
64363
+ this.log(`c2s ${activity?.type}: ${e.message}`);
64364
+ return reply(422, { error: e.message || String(e) });
64365
+ }
64366
+ }
64367
+ // The actors a client addressed by id, beyond the audience words: the ones
64368
+ // in to/cc are listed and delivered to; the ones in bto/bcc are delivered
64369
+ // to and never listed (§6).
64370
+ addressedActors(activity, object) {
64371
+ const pick = (...fields) => [...new Set(fields.flatMap((f) => arr(activity[f] ?? object?.[f]).map(idOf)))].filter((a) => typeof a === "string" && /^https?:\/\//u.test(a) && a !== PUBLIC && a !== this.urls.followers && a !== this.urls.actor);
64372
+ return { also: pick("to", "cc"), deliverTo: pick("bto", "bcc") };
64373
+ }
64374
+ async _dispatch(activity, { slug, raw, reply }) {
64375
+ const agent2 = this.agent;
64376
+ const object = typeof activity.object === "object" && activity.object !== null ? activity.object : null;
64377
+ const objectId = idOf(activity.object);
64378
+ switch (activity.type) {
64379
+ case "Create": {
64380
+ const makes = object?.type || "Note";
64381
+ if (!object) return reply(422, { error: "a Create carries the object it creates" });
64382
+ const visibility = this.visibilityOf(activity, object);
64383
+ const { also, deliverTo } = this.addressedActors(activity, object);
64384
+ if (makes !== "Note" && makes !== "Question") {
64385
+ const asSent = raw && typeof raw === "object" && !Array.isArray(raw) ? ACTIVITY_TYPES.has(raw.type) ? raw.object && typeof raw.object === "object" ? raw.object : null : raw : null;
64386
+ const made = await agent2.publisher.publishObject(asSent || object, { visibility, slug, also, deliverTo });
64387
+ return reply(201, { id: made.createId, object: made.id }, { location: made.createId });
64388
+ }
64389
+ const text = String(object.source?.content ?? object.content ?? "");
64390
+ if (!text.trim()) return reply(422, { error: "the note has no content" });
64391
+ if (makes === "Question") {
64392
+ const one = arr(object.oneOf);
64393
+ const many = arr(object.anyOf);
64394
+ const titles = (one.length ? one : many).map((c) => String(c?.name ?? "").trim()).filter(Boolean);
64395
+ try {
64396
+ const question = await agent2.publisher.publishQuestion(text, {
64397
+ options: titles,
64398
+ multiple: !one.length && many.length > 0,
64399
+ expiresAt: object.endTime || null,
64400
+ inReplyTo: idOf(object.inReplyTo) || void 0,
64401
+ visibility,
64402
+ spoilerText: object.summary || null,
64403
+ sensitive: object.sensitive === true
64404
+ });
64405
+ return reply(
64406
+ 201,
64407
+ { id: createActivityId(question.id), object: question.id },
64408
+ { location: createActivityId(question.id) }
64409
+ );
64410
+ } catch (e) {
64411
+ return reply(422, { error: e.message });
64412
+ }
64413
+ }
64414
+ const attachments = arr(object.attachment).map((a) => ({
64415
+ url: a?.url,
64416
+ mediaType: a?.mediaType,
64417
+ ...a?.name ? { description: a.name } : {}
64418
+ })).filter((a) => a.url);
64419
+ const note = await agent2.publisher.publishNote(text, {
64420
+ inReplyTo: idOf(object.inReplyTo) || void 0,
64421
+ attachments,
64422
+ visibility,
64423
+ spoilerText: object.summary || null,
64424
+ sensitive: object.sensitive === true,
64425
+ slug,
64426
+ also,
64427
+ deliverTo
64428
+ });
64429
+ return reply(
64430
+ 201,
64431
+ { id: createActivityId(note.id), object: note.id },
64432
+ { location: createActivityId(note.id) }
64433
+ );
64434
+ }
64435
+ case "Update": {
64436
+ if (objectId === this.urls.actor) {
64437
+ return reply(422, { error: "edit the profile on the admin surface; actor updates are not taken here" });
64438
+ }
64439
+ const s = this.byIri(objectId);
64440
+ if (!s) return reply(404, { error: "no such note here" });
64441
+ if (s.actor !== this.urls.actor || s.kind !== "post") {
64442
+ return reply(403, { error: "not your note" });
64443
+ }
64444
+ const text = String(object?.source?.content ?? object?.content ?? "");
64445
+ if (!text.trim()) return reply(422, { error: "the edit has no content" });
64446
+ const attachments = object?.attachment !== void 0 ? arr(object.attachment).map((a) => ({
64447
+ url: a?.url,
64448
+ mediaType: a?.mediaType,
64449
+ ...a?.name ? { description: a.name } : {}
64450
+ })).filter((a) => a.url) : null;
64451
+ await agent2.publisher.updateNote(s, {
64452
+ content: text,
64453
+ spoilerText: object?.summary || null,
64454
+ sensitive: object?.sensitive === void 0 ? null : object.sensitive === true,
64455
+ attachments
64456
+ });
64457
+ return reply(200, { ok: true, object: s.noteId });
64458
+ }
64459
+ case "Delete": {
64460
+ if (objectId === this.urls.actor) {
64461
+ return reply(422, { error: "retiring the actor is done on the admin surface, where it asks twice" });
64462
+ }
64463
+ const s = this.byIri(objectId);
64464
+ if (!s) return reply(404, { error: "no such note here" });
64465
+ if (s.actor !== this.urls.actor || s.kind !== "post") {
64466
+ return reply(403, { error: "not your note" });
64467
+ }
64468
+ const r = await deleteNote(agent2, s);
64469
+ if (!r.ok) return reply(502, { error: r.error, stillPublished: r.stillPublished });
64470
+ return reply(200, { ok: true });
64471
+ }
64472
+ case "Follow": {
64473
+ if (!objectId) return reply(400, { error: "whom? object must name an actor" });
64474
+ if (/^acct:|^@|^[^/@]+@[^/@]+$/.test(objectId) && !/^https?:/.test(objectId)) {
64475
+ const r = await followHandle(agent2, objectId.replace(/^acct:/, ""));
64476
+ const rec2 = this.store.getContacts().following.find((f) => f.actor === r.actor);
64477
+ return reply(
64478
+ 201,
64479
+ { id: rec2?.followActivity?.id, object: r.actor },
64480
+ rec2?.followActivity?.id ? { location: rec2.followActivity.id } : {}
64481
+ );
64482
+ }
64483
+ const doc = await followActor(agent2, objectId);
64484
+ const rec = this.store.getContacts().following.find((f) => f.actor === doc.id);
64485
+ return reply(
64486
+ 201,
64487
+ { id: rec?.followActivity?.id, object: doc.id },
64488
+ rec?.followActivity?.id ? { location: rec.followActivity.id } : {}
64489
+ );
64490
+ }
64491
+ case "Like": {
64492
+ const s = this.byIri(objectId);
64493
+ if (!s) return reply(422, { error: "that note is not held here \u2014 like what the timeline holds" });
64494
+ const updated = await favourite(agent2, s);
64495
+ return reply(
64496
+ 201,
64497
+ { id: updated.likeActivity?.id, object: s.noteId },
64498
+ updated.likeActivity?.id ? { location: updated.likeActivity.id } : {}
64499
+ );
64500
+ }
64501
+ case "Announce": {
64502
+ const s = this.byIri(objectId);
64503
+ if (!s) return reply(422, { error: "that note is not held here \u2014 boost what the timeline holds" });
64504
+ const updated = await reblog(agent2, s);
64505
+ return reply(
64506
+ 201,
64507
+ { id: updated.announceActivity?.id, object: s.noteId },
64508
+ updated.announceActivity?.id ? { location: updated.announceActivity.id } : {}
64509
+ );
64510
+ }
64511
+ case "Undo": {
64512
+ const inner = object;
64513
+ const innerId = idOf(activity.object);
64514
+ if (inner?.type === "Block") {
64515
+ const target = idOf(inner.object);
64516
+ if (!target) return reply(400, { error: "unblock whom?" });
64517
+ await unblockActor(agent2, target);
64518
+ return reply(200, { ok: true, object: target });
64519
+ }
64520
+ const statuses = this.store.getStatuses();
64521
+ let s = innerId ? statuses.find((x) => x.likeActivity?.id === innerId) : null;
64522
+ if (!s && inner?.type === "Like") s = this.byIri(idOf(inner.object));
64523
+ if (s?.favourited) {
64524
+ const updated = await unfavourite(agent2, s);
64525
+ return reply(200, { ok: true, object: updated.noteId });
64526
+ }
64527
+ s = innerId ? statuses.find((x) => x.announceActivity?.id === innerId) : null;
64528
+ if (!s && inner?.type === "Announce") s = this.byIri(idOf(inner.object));
64529
+ if (s?.reblogged) {
64530
+ const updated = await unreblog(agent2, s);
64531
+ return reply(200, { ok: true, object: updated.noteId });
64532
+ }
64533
+ const following = this.store.getContacts().following;
64534
+ const rec = following.find((f) => f.followActivity?.id === innerId) || (inner?.type === "Follow" ? following.find((f) => f.actor === idOf(inner.object)) : null);
64535
+ if (rec) {
64536
+ await unfollowActor(agent2, rec.actor);
64537
+ return reply(200, { ok: true, object: rec.actor });
64538
+ }
64539
+ return reply(422, { error: "nothing here matches what that Undo names" });
64540
+ }
64541
+ case "Block": {
64542
+ if (!objectId) return reply(400, { error: "block whom? object must name an actor" });
64543
+ await blockActor(agent2, objectId);
64544
+ return reply(200, { ok: true, object: objectId });
64545
+ }
64546
+ case "Add":
64547
+ case "Remove": {
64548
+ if (idOf(activity.target) !== this.urls.featured) {
64549
+ return reply(422, { error: "the featured collection is the one Add/Remove edits here" });
64550
+ }
64551
+ const s = this.byIri(objectId);
64552
+ if (!s) return reply(404, { error: "no such note here" });
64553
+ const updated = await pinStatus(agent2, s, activity.type === "Add");
64554
+ return reply(200, { ok: true, object: updated.noteId, pinned: !!updated.pinned });
64555
+ }
64556
+ case "Accept":
64557
+ case "Reject": {
64558
+ const requester = object?.actor ? idOf(object.actor) : objectId;
64559
+ if (!requester) return reply(400, { error: "whose request? object must name the Follow or its actor" });
64560
+ const r = activity.type === "Accept" ? await admitRequest(agent2, requester).catch((e) => ({ error: e.message })) : await refuseRequest(agent2, requester).catch((e) => ({ error: e.message }));
64561
+ if (r.error) return reply(404, { error: r.error });
64562
+ return reply(200, { ok: true, object: requester });
64563
+ }
64564
+ case "Move":
64565
+ return reply(422, { error: "moving the account is done on the admin surface, where it asks twice" });
64566
+ default:
64567
+ return reply(422, { error: `no handler for ${activity.type}` });
64568
+ }
64569
+ }
64570
+ };
64571
+
63309
64572
  // lib/core/lease.mjs
63310
64573
  init_node_crypto();
63311
64574
  var TTL_MS = 3e5;
@@ -63618,7 +64881,7 @@ function pollParams(body) {
63618
64881
  function htmlToText(html) {
63619
64882
  return String(html).replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>\s*<p[^>]*>/gi, "\n\n").replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").trim();
63620
64883
  }
63621
- function readBody(req) {
64884
+ function readBody2(req) {
63622
64885
  return new Promise((resolve2, reject) => {
63623
64886
  let data = "";
63624
64887
  req.on("data", (c) => {
@@ -63913,7 +65176,7 @@ function rateLimited(api) {
63913
65176
  async function handle(api, ctx) {
63914
65177
  const { req, res, pathname, url, send } = ctx;
63915
65178
  if (pathname === "/api/v1/apps" && req.method === "POST") {
63916
- const body = await readBody(req);
65179
+ const body = await readBody2(req);
63917
65180
  const redirectUris = parseRedirects(body.redirect_uris);
63918
65181
  const app = api.registerApp({
63919
65182
  name: body.client_name,
@@ -63936,7 +65199,7 @@ async function handle(api, ctx) {
63936
65199
  let params = url.searchParams;
63937
65200
  let body = null;
63938
65201
  if (req.method === "POST") {
63939
- body = await readBody(req);
65202
+ body = await readBody2(req);
63940
65203
  params = new URLSearchParams(body);
63941
65204
  }
63942
65205
  const redirect = params.get("redirect_uri") || "";
@@ -64011,7 +65274,7 @@ async function handle(api, ctx) {
64011
65274
  return true;
64012
65275
  }
64013
65276
  if (pathname === "/oauth/token" && req.method === "POST") {
64014
- const body = await readBody(req);
65277
+ const body = await readBody2(req);
64015
65278
  const app = api.findApp(body.client_id || "");
64016
65279
  if (!app && body.code_verifier && /^https:\/\//iu.test(String(body.client_id || ""))) {
64017
65280
  const rec = api.consumeCode(body.code || "");
@@ -64091,7 +65354,7 @@ async function handle(api, ctx) {
64091
65354
  });
64092
65355
  }
64093
65356
  if (pathname === "/oauth/revoke" && req.method === "POST") {
64094
- const body = await readBody(req).catch(() => ({}));
65357
+ const body = await readBody2(req).catch(() => ({}));
64095
65358
  const gone = body?.token;
64096
65359
  if (gone) {
64097
65360
  const kept = api.tokenRecords().filter((r) => r.token !== gone);
@@ -64103,442 +65366,6 @@ async function handle(api, ctx) {
64103
65366
  return false;
64104
65367
  }
64105
65368
 
64106
- // lib/core/social.mjs
64107
- var social_exports = {};
64108
- __export(social_exports, {
64109
- admitRequest: () => admitRequest,
64110
- announceModeration: () => announceModeration,
64111
- applyModeration: () => applyModeration,
64112
- blockActor: () => blockActor,
64113
- confirmDelegation: () => confirmDelegation,
64114
- deleteNote: () => deleteNote,
64115
- ejectFollower: () => ejectFollower,
64116
- favourite: () => favourite,
64117
- followActor: () => followActor,
64118
- followHandle: () => followHandle,
64119
- lookupWebFinger: () => lookupWebFinger,
64120
- pinStatus: () => pinStatus,
64121
- reblog: () => reblog,
64122
- refuseRequest: () => refuseRequest,
64123
- resolveHandle: () => resolveHandle,
64124
- retractAnnouncement: () => retractAnnouncement,
64125
- selfLink: () => selfLink,
64126
- unblockActor: () => unblockActor,
64127
- unfavourite: () => unfavourite,
64128
- unfollowActor: () => unfollowActor,
64129
- unreblog: () => unreblog,
64130
- votePoll: () => votePoll
64131
- });
64132
- init_node_crypto();
64133
- init_wire();
64134
- async function lookupWebFinger(acct, { fetchImpl = null } = {}) {
64135
- const clean = String(acct || "").replace(/^acct:/, "");
64136
- const at = clean.lastIndexOf("@");
64137
- if (at < 1) return null;
64138
- const [user, host] = [clean.slice(0, at), clean.slice(at + 1)];
64139
- if (!host || /[/\\?#]/.test(host)) return null;
64140
- const { safeFetch: safeFetch2, readCapped: readCapped3 } = await Promise.resolve().then(() => (init_safefetch(), safefetch_exports));
64141
- const url = `https://${host}/.well-known/webfinger?resource=${encodeURIComponent(`acct:${clean}`)}`;
64142
- const headers = { accept: "application/jrd+json, application/json" };
64143
- const res = await (fetchImpl ? fetchImpl(url, { headers }) : safeFetch2(url, { headers })).catch(() => null);
64144
- if (!res || res.status >= 400) return null;
64145
- let jrd2;
64146
- try {
64147
- jrd2 = JSON.parse(await readCapped3(res, 256 * 1024));
64148
- } catch {
64149
- return null;
64150
- }
64151
- const subject = String(jrd2?.subject || "").replace(/^acct:/, "").toLowerCase();
64152
- if (subject && subject !== clean.toLowerCase()) {
64153
- return null;
64154
- }
64155
- return jrd2;
64156
- }
64157
- var selfLink = (jrd2) => jrd2?.links?.find((l) => l.rel === "self" && /application\/(activity\+json|ld\+json)/.test(l.type || ""));
64158
- async function confirmDelegation(asked, doc, lookup2 = lookupWebFinger) {
64159
- let home;
64160
- try {
64161
- home = new URL(doc.id).host.toLowerCase();
64162
- } catch {
64163
- throw new Error(`actor id is not a URL (${doc.id})`);
64164
- }
64165
- if (home === asked.toLowerCase()) return;
64166
- const user = doc.preferredUsername;
64167
- if (!user) {
64168
- throw new Error(`${asked} points at ${doc.id} on another host, which names no username to confirm it by`);
64169
- }
64170
- const back = await lookup2(`acct:${user}@${home}`);
64171
- if (selfLink(back)?.href !== doc.id) {
64172
- throw new Error(`${asked} claims ${doc.id}, but ${home} does not agree that is @${user}@${home} \u2014 refusing to take one host's word for another's actor`);
64173
- }
64174
- }
64175
- async function resolveHandle(agent2, handle7) {
64176
- const clean = String(handle7 || "").replace(/^@/, "");
64177
- if (!/^[^@]+@[^@]+$/.test(clean)) throw new Error("handle must look like user@host");
64178
- if (agent2.store.isBlocked("https://" + clean.split("@")[1] + "/")) throw new Error("domain is blocked");
64179
- const remote = agent2.intake?.deliverer?.signedFetch ? (u, i) => agent2.intake.deliverer.signedFetch(u, i) : null;
64180
- const lookup2 = (a) => lookupWebFinger(a, { fetchImpl: remote });
64181
- const jrd2 = await lookup2("acct:" + clean);
64182
- const self2 = selfLink(jrd2);
64183
- if (!self2?.href) throw new Error(`webfinger found no actor for ${clean}`);
64184
- const doc = await agent2.intake.fetchAP(self2.href);
64185
- if (!doc?.id) throw new Error(`actor document unusable for ${clean}`);
64186
- if (agent2.store.isBlocked(doc.id)) throw new Error("actor is blocked");
64187
- await confirmDelegation(clean.slice(clean.lastIndexOf("@") + 1), doc, lookup2);
64188
- await cacheCounts(agent2, doc);
64189
- return doc;
64190
- }
64191
- async function cacheCounts(agent2, doc) {
64192
- const total = async (url) => {
64193
- if (!url) return null;
64194
- try {
64195
- const res = await agent2.intake.fetchAP(url);
64196
- return Number.isInteger(res?.totalItems) ? res.totalItems : null;
64197
- } catch {
64198
- return null;
64199
- }
64200
- };
64201
- const [followers, following] = await Promise.all([total(doc.followers), total(doc.following)]);
64202
- if (followers === null && following === null) return;
64203
- const actors = agent2.store.getActors();
64204
- if (!actors[doc.id]) return;
64205
- actors[doc.id] = { ...actors[doc.id], counts: { followers, following } };
64206
- agent2.store.write("actors.json", actors);
64207
- }
64208
- async function followActor(agent2, actorUrl, { publish = true, doc: fetched = null } = {}) {
64209
- const doc = fetched || await agent2.intake.fetchAP(actorUrl);
64210
- if (!doc?.inbox) throw new Error(`actor document unusable (${actorUrl})`);
64211
- if (agent2.store.isBlocked(doc.id)) throw new Error("actor is blocked");
64212
- const contacts = agent2.store.getContacts();
64213
- let rec = contacts.following.find((f) => f.actor === doc.id);
64214
- if (!rec) {
64215
- rec = { actor: doc.id, inbox: doc.inbox, accepted: false };
64216
- contacts.following.push(rec);
64217
- }
64218
- const follow = followActivity({ urls: agent2.publisher.urls, targetActor: doc.id, serial: Date.now() });
64219
- rec.followActivity = follow;
64220
- agent2.store.setContacts(contacts);
64221
- await agent2.deliverer.deliver(doc.inbox, follow);
64222
- if (publish) await agent2.publisher.publishCollections({ following: true, pending: true });
64223
- return doc;
64224
- }
64225
- async function followHandle(agent2, handle7) {
64226
- const doc = await resolveHandle(agent2, handle7);
64227
- await followActor(agent2, doc.id);
64228
- const clean = String(handle7 || "").replace(/^@/, "");
64229
- const contacts = agent2.store.getContacts();
64230
- const rec = contacts.following.find((f) => f.actor === doc.id);
64231
- if (rec && !rec.handle) {
64232
- rec.handle = clean;
64233
- agent2.store.setContacts(contacts);
64234
- }
64235
- return { ok: true, actor: doc.id };
64236
- }
64237
- async function unfollowActor(agent2, actor) {
64238
- const contacts = agent2.store.getContacts();
64239
- const rec = contacts.following.find((f) => f.actor === actor);
64240
- if (!rec) throw new Error("not following that actor");
64241
- if (rec.followActivity) {
64242
- await agent2.deliverer.deliver(
64243
- rec.inbox,
64244
- undoActivity({ urls: agent2.publisher.urls, activity: rec.followActivity, serial: Date.now() })
64245
- );
64246
- }
64247
- contacts.following = contacts.following.filter((f) => f.actor !== actor);
64248
- agent2.store.setContacts(contacts);
64249
- await agent2.publisher.publishCollections({ following: true, pending: true });
64250
- return { ok: true };
64251
- }
64252
- async function favourite(agent2, s) {
64253
- if (s.favourited) return s;
64254
- const doc = await agent2.intake.fetchAP(s.actor);
64255
- if (!doc?.inbox) throw new Error("author inbox unavailable");
64256
- const act = likeActivity({ urls: agent2.publisher.urls, noteId: s.noteId, serial: Date.now() });
64257
- await agent2.deliverer.deliver(doc.endpoints?.sharedInbox || doc.inbox, act);
64258
- return agent2.store.updateStatus(s.noteId, { favourited: true, likeActivity: act });
64259
- }
64260
- async function unfavourite(agent2, s) {
64261
- if (s.likeActivity) {
64262
- const doc = await agent2.intake.fetchAP(s.actor).catch(() => null);
64263
- if (doc?.inbox) {
64264
- await agent2.deliverer.deliver(
64265
- doc.endpoints?.sharedInbox || doc.inbox,
64266
- undoActivity({ urls: agent2.publisher.urls, activity: s.likeActivity, serial: Date.now() })
64267
- );
64268
- }
64269
- }
64270
- return agent2.store.updateStatus(s.noteId, { favourited: false, likeActivity: void 0 });
64271
- }
64272
- async function reblog(agent2, s) {
64273
- if (s.reblogged) return s;
64274
- const { urls } = agent2.publisher;
64275
- const act = announceActivity({ urls, object: s.noteId, serial: Date.now() });
64276
- const inboxes = new Set(agent2.store.getContacts().followers.map((f) => f.sharedInbox || f.inbox).filter(Boolean));
64277
- if (s.actor !== urls.actor) {
64278
- const doc = await agent2.intake.fetchAP(s.actor).catch(() => null);
64279
- if (doc?.inbox) inboxes.add(doc.endpoints?.sharedInbox || doc.inbox);
64280
- }
64281
- await agent2.deliverer.deliverToAll([...inboxes], act);
64282
- const updated = agent2.store.updateStatus(s.noteId, { reblogged: true, announceActivity: act });
64283
- await agent2.publisher.recordOutbox(act);
64284
- return updated;
64285
- }
64286
- async function unreblog(agent2, s) {
64287
- if (s.announceActivity) {
64288
- const undo = undoActivity({ urls: agent2.publisher.urls, activity: s.announceActivity, serial: Date.now() });
64289
- const inboxes = new Set(agent2.store.getContacts().followers.map((f) => f.sharedInbox || f.inbox).filter(Boolean));
64290
- const doc = await agent2.intake.fetchAP(s.actor).catch(() => null);
64291
- if (doc?.inbox && s.actor !== agent2.publisher.urls.actor) inboxes.add(doc.endpoints?.sharedInbox || doc.inbox);
64292
- await agent2.deliverer.deliverToAll([...inboxes], undo);
64293
- await agent2.publisher.unrecordOutbox((i) => i?.id === s.announceActivity.id);
64294
- }
64295
- return agent2.store.updateStatus(s.noteId, { reblogged: false, announceActivity: void 0 });
64296
- }
64297
- async function ejectFollower(agent2, actor) {
64298
- const { urls } = agent2.publisher;
64299
- const contacts = agent2.store.getContacts();
64300
- const rec = contacts.followers.find((f) => f.actor === actor);
64301
- if (!rec) throw new Error("not a member");
64302
- dropFollower(contacts, actor, "ejected");
64303
- agent2.store.setContacts(contacts);
64304
- const muted = agent2.store.getMuted();
64305
- muted.actors = [.../* @__PURE__ */ new Set([...muted.actors, actor])];
64306
- agent2.store.setMuted(muted);
64307
- await agent2.publisher.publishCollections({ followers: true });
64308
- await announceModeration(
64309
- agent2,
64310
- blockActivity({ urls, targetActor: actor, serial: Date.now() })
64311
- ).catch((e) => agent2.log?.(`moderation announce failed: ${e.message}`));
64312
- if (rec.bsky) {
64313
- await agent2.bskygroup?.blockDid(rec.bsky.did).catch((e) => agent2.log?.(`bluesky block failed for ${rec.bsky.handle}: ${e.message}`));
64314
- return { ok: true, actor, told: true };
64315
- }
64316
- if (rec.inbox) {
64317
- await agent2.deliverer.deliver(rec.inbox, rejectActivity({
64318
- urls,
64319
- followActivity: { id: rec.followId, type: "Follow", actor, object: urls.actor },
64320
- serial: Date.now()
64321
- }));
64322
- }
64323
- return { ok: true, actor, told: !!rec.inbox };
64324
- }
64325
- async function admitRequest(agent2, actor, { publish = true } = {}) {
64326
- const reqs = agent2.store.getRequests();
64327
- const req = reqs.find((r) => r.actor === actor);
64328
- if (!req) throw new Error("no such request");
64329
- const contacts = agent2.store.getContacts();
64330
- if (!contacts.followers.some((f) => f.actor === actor)) {
64331
- const bridgedDid = /^https:\/\/bsky\.brid\.gy\/ap\/(did:[^/]+)$/.exec(actor)?.[1];
64332
- if (bridgedDid) contacts.followers = contacts.followers.filter((f) => f.bsky?.did !== bridgedDid);
64333
- contacts.followers.push(req.bsky ? { actor, bsky: req.bsky } : { actor, inbox: req.inbox, sharedInbox: req.sharedInbox, followId: req.activity?.id });
64334
- agent2.store.setContacts(contacts);
64335
- agent2.store.addNotification({ type: "follow", actor, ...req.bsky ? { bsky: true } : {} });
64336
- }
64337
- agent2.store.setRequests(reqs.filter((r) => r.actor !== actor));
64338
- if (req.bsky) {
64339
- await agent2.bskygroup?.nudgeOnce(contacts.followers.find((f) => f.actor === actor));
64340
- return { ok: true, actor };
64341
- }
64342
- if (publish) await agent2.publisher.publishCollections({ followers: true, pending: true });
64343
- await agent2.deliverer.deliver(req.inbox, acceptActivity({
64344
- urls: agent2.publisher.urls,
64345
- followActivity: req.activity,
64346
- serial: Date.now()
64347
- }));
64348
- return { ok: true, actor };
64349
- }
64350
- async function refuseRequest(agent2, actor) {
64351
- const reqs = agent2.store.getRequests();
64352
- const req = reqs.find((r) => r.actor === actor);
64353
- if (!req) throw new Error("no such request");
64354
- agent2.store.setRequests(reqs.filter((r) => r.actor !== actor));
64355
- await agent2.publisher?.publishCollections({ pending: true }).catch(() => {
64356
- });
64357
- if (req.bsky) {
64358
- await agent2.bskygroup?.blockDid(req.bsky.did).catch((e) => agent2.log?.(`bluesky block failed for ${req.bsky.handle}: ${e.message}`));
64359
- return { ok: true, actor };
64360
- }
64361
- await agent2.deliverer.deliver(req.inbox, rejectActivity({
64362
- urls: agent2.publisher.urls,
64363
- followActivity: req.activity,
64364
- serial: Date.now()
64365
- }));
64366
- return { ok: true, actor };
64367
- }
64368
- async function retractAnnouncement(agent2, noteId) {
64369
- return agent2.intake.retract(noteId);
64370
- }
64371
- async function votePoll(agent2, s, choices) {
64372
- const { urls } = agent2.publisher;
64373
- const poll = s.poll || {};
64374
- const opts = poll.options || [];
64375
- if (poll.closed || poll.expiresAt && Date.parse(poll.expiresAt) < Date.now()) {
64376
- return { ok: false, error: "this poll has closed" };
64377
- }
64378
- if (poll.voted) return { ok: false, error: "already voted" };
64379
- if (s.actor === urls.actor) return { ok: false, error: "this is your own poll" };
64380
- const picks = [...new Set(choices)].filter((i) => i >= 0 && i < opts.length);
64381
- if (!picks.length) return { ok: false, error: "invalid choice" };
64382
- if (!poll.multiple && picks.length > 1) return { ok: false, error: "this poll takes one choice" };
64383
- const author = await agent2.intake.fetchAP(s.actor).catch(() => null);
64384
- const inbox = author?.endpoints?.sharedInbox || author?.inbox;
64385
- if (!inbox) return { ok: false, error: "could not reach the poll author" };
64386
- const published = (/* @__PURE__ */ new Date()).toISOString();
64387
- for (const i of picks) {
64388
- const id = urls.notes + published.slice(0, 10) + "-vote-" + node_crypto_default.randomBytes(4).toString("hex");
64389
- const note = {
64390
- "@context": AS_CTX,
64391
- id,
64392
- type: "Note",
64393
- attributedTo: urls.actor,
64394
- name: opts[i].title,
64395
- inReplyTo: s.noteId,
64396
- published,
64397
- to: [s.actor]
64398
- };
64399
- const create = {
64400
- "@context": AS_CTX,
64401
- id: createActivityId(id),
64402
- type: "Create",
64403
- actor: urls.actor,
64404
- published,
64405
- to: [s.actor],
64406
- object: note
64407
- };
64408
- await write4(agent2.remote, id, note);
64409
- await writeCreate(agent2.remote, create.id, create);
64410
- await agent2.deliverer.deliverToAll([inbox], create);
64411
- }
64412
- agent2.store.updateStatus(s.noteId, {
64413
- poll: {
64414
- ...poll,
64415
- voted: true,
64416
- ownVotes: picks,
64417
- options: opts.map((o, j) => picks.includes(j) ? { ...o, votes: (o.votes || 0) + 1 } : o)
64418
- }
64419
- });
64420
- return { ok: true };
64421
- }
64422
- async function deleteNote(agent2, s) {
64423
- const { urls } = agent2.publisher;
64424
- const inboxes = agent2.store.getContacts().followers.map((f) => f.sharedInbox || f.inbox).filter(Boolean);
64425
- await agent2.deliverer.deliverToAll(inboxes, deleteActivity({ urls, noteId: s.noteId }));
64426
- const stuck = [];
64427
- const deletedAt = (/* @__PURE__ */ new Date()).toISOString();
64428
- try {
64429
- await writeTombstone2(agent2.remote, s.noteId, noteTombstone(s.noteId, deletedAt));
64430
- } catch {
64431
- stuck.push(s.noteId);
64432
- }
64433
- if (!await dropCreate(agent2.remote, createActivityId(s.noteId))) {
64434
- stuck.push(createActivityId(s.noteId));
64435
- }
64436
- if (stuck.length) {
64437
- agent2.log?.(`delete ${s.noteId}: the pod kept ${stuck.length} document(s) \u2014 the post is STILL PUBLISHED`);
64438
- return {
64439
- ok: false,
64440
- stillPublished: stuck,
64441
- error: "the pod would not remove the post \u2014 it is still published, and has been kept here so you can try again"
64442
- };
64443
- }
64444
- await dropReplies(agent2.remote, repliesId(s.noteId));
64445
- if (s.atproto?.uri && agent2.atproto?.connected()) {
64446
- await agent2.atproto.deleteCrossPost(s.atproto.uri).then(() => agent2.log?.(`bluesky mirror deleted: ${s.atproto.uri}`)).catch((e) => agent2.log?.(`bluesky mirror not deleted (${e.message}): ${s.atproto.uri}`));
64447
- }
64448
- await agent2.publisher.unrecordOutbox((i) => i === s.noteId || i?.id && i.id === s.announceActivity?.id);
64449
- agent2.store.removeStatus(s.noteId);
64450
- return { ok: true };
64451
- }
64452
- async function announceModeration(agent2, activity) {
64453
- if (agent2.store.getConfig()?.kind !== "group") return null;
64454
- const { urls } = agent2.publisher;
64455
- const act = announceActivity({ urls, object: activity, serial: Date.now(), audience: urls.actor });
64456
- const inboxes = [...new Set(agent2.store.getContacts().followers.map((f) => f.sharedInbox || f.inbox).filter(Boolean))];
64457
- await agent2.deliverer.deliverToAll(inboxes, act);
64458
- await agent2.publisher.recordOutbox(act);
64459
- return act;
64460
- }
64461
- async function blockActor(agent2, actorUrl) {
64462
- if (!/^https?:\/\/\S+$/.test(String(actorUrl || ""))) throw new Error("actor must be a URL");
64463
- const b = agent2.store.getBlocklist();
64464
- if (!b.actors.includes(actorUrl)) b.actors.push(actorUrl);
64465
- agent2.store.setBlocklist(b);
64466
- await unfollowActor(agent2, actorUrl).catch(() => {
64467
- });
64468
- await agent2.publisher?.publishCollections({ blocked: true }).catch(() => {
64469
- });
64470
- await announceModeration(
64471
- agent2,
64472
- blockActivity({ urls: agent2.publisher.urls, targetActor: actorUrl, serial: Date.now() })
64473
- ).catch((e) => agent2.log?.(`moderation announce failed: ${e.message}`));
64474
- return { ok: true, actor: actorUrl };
64475
- }
64476
- async function unblockActor(agent2, actorUrl) {
64477
- const b = agent2.store.getBlocklist();
64478
- b.actors = b.actors.filter((a) => a !== actorUrl);
64479
- agent2.store.setBlocklist(b);
64480
- await agent2.publisher?.publishCollections({ blocked: true }).catch(() => {
64481
- });
64482
- await announceModeration(agent2, undoActivity({
64483
- urls: agent2.publisher.urls,
64484
- activity: { type: "Block", actor: agent2.publisher.urls.actor, object: actorUrl },
64485
- serial: Date.now()
64486
- })).catch((e) => agent2.log?.(`moderation announce failed: ${e.message}`));
64487
- return { ok: true, actor: actorUrl };
64488
- }
64489
- async function applyModeration(agent2, entry) {
64490
- const { urls } = agent2.publisher;
64491
- const target = typeof entry.activity?.object === "string" ? entry.activity.object : entry.activity?.object?.id;
64492
- switch (entry.type) {
64493
- case "Block": {
64494
- if (!target) throw new Error("the queued Block names nobody");
64495
- const member = agent2.store.getContacts().followers.some((f) => f.actor === target);
64496
- return member ? ejectFollower(agent2, target) : blockActor(agent2, target);
64497
- }
64498
- case "Undo": {
64499
- const inner = entry.activity?.object;
64500
- const unbanned = typeof inner === "object" && inner?.type === "Block" ? typeof inner.object === "string" ? inner.object : inner.object?.id : null;
64501
- if (!unbanned) throw new Error("the queued Undo does not name a Block");
64502
- const muted = agent2.store.getMuted();
64503
- muted.actors = muted.actors.filter((a) => a !== unbanned);
64504
- agent2.store.setMuted(muted);
64505
- return unblockActor(agent2, unbanned);
64506
- }
64507
- case "Delete": {
64508
- if (!target) throw new Error("the queued Delete names nothing");
64509
- return agent2.intake.retract(target);
64510
- }
64511
- case "Add":
64512
- case "Remove": {
64513
- if (!target) throw new Error("the queued roster change names nobody");
64514
- const cfg = { ...agent2.store.getConfig() };
64515
- const prev = cfg.moderators || [];
64516
- cfg.moderators = entry.type === "Add" ? [.../* @__PURE__ */ new Set([...prev, target])] : prev.filter((m) => m !== target);
64517
- agent2.store.setConfig(cfg);
64518
- if (agent2.publisher) agent2.publisher.config.moderators = cfg.moderators;
64519
- await agent2.publisher.publishProfile();
64520
- await announceModeration(agent2, addRemoveActivity({
64521
- urls,
64522
- type: entry.type,
64523
- object: target,
64524
- target: urls.moderators,
64525
- serial: Date.now()
64526
- })).catch((e) => agent2.log?.(`moderation announce failed: ${e.message}`));
64527
- return { ok: true, moderators: cfg.moderators };
64528
- }
64529
- default:
64530
- throw new Error(`no way to apply a queued ${entry.type}`);
64531
- }
64532
- }
64533
- async function pinStatus(agent2, s, pinned) {
64534
- if (s.actor !== agent2.publisher.urls.actor) throw new Error("not your status");
64535
- const updated = agent2.store.updateStatus(s.noteId, { pinned: !!pinned });
64536
- await agent2.publisher.publishFeatured().catch((e) => agent2.log?.(`featured: ${e.message}`));
64537
- agent2.publisher.publishProfile?.().catch(() => {
64538
- });
64539
- return updated || s;
64540
- }
64541
-
64542
65369
  // lib/client/masto/render.mjs
64543
65370
  init_wire();
64544
65371
 
@@ -64809,11 +65636,11 @@ function account(api, actorUrl, { selfAcct } = {}) {
64809
65636
  fields: (self2 ? api.store.getConfig()?.fields : cached.fields) || []
64810
65637
  };
64811
65638
  }
64812
- function page(api, items, url, { limit = 20, max = 40, idOf = (s) => api.store.idFor(s.noteId) } = {}) {
65639
+ function page(api, items, url, { limit = 20, max = 40, idOf: idOf2 = (s) => api.store.idFor(s.noteId) } = {}) {
64813
65640
  const n = Math.min(Number(url.searchParams.get("limit")) || limit, max);
64814
65641
  const ids = /* @__PURE__ */ new Map();
64815
65642
  const idAt = (s) => {
64816
- if (!ids.has(s)) ids.set(s, idOf(s));
65643
+ if (!ids.has(s)) ids.set(s, idOf2(s));
64817
65644
  return ids.get(s);
64818
65645
  };
64819
65646
  const cut = (param) => {
@@ -64830,15 +65657,15 @@ function page(api, items, url, { limit = 20, max = 40, idOf = (s) => api.store.i
64830
65657
  }
64831
65658
  const pageItems2 = items.slice(0, n);
64832
65659
  if (!pageItems2.length) return { items: pageItems2, headers: {} };
64833
- const base = `http://${api.host}${url.pathname}`;
65660
+ const base = url.origin + url.pathname;
64834
65661
  const q = (extra) => {
64835
65662
  const u = new URL(base);
64836
65663
  for (const [k, v] of url.searchParams) if (!["max_id", "since_id", "min_id"].includes(k)) u.searchParams.set(k, v);
64837
65664
  for (const [k, v] of Object.entries(extra)) u.searchParams.set(k, v);
64838
65665
  return u.href;
64839
65666
  };
64840
- const links = [`<${q({ max_id: idOf(pageItems2[pageItems2.length - 1]) })}>; rel="next"`];
64841
- if (pageItems2.length) links.push(`<${q({ min_id: idOf(pageItems2[0]) })}>; rel="prev"`);
65667
+ const links = [`<${q({ max_id: idOf2(pageItems2[pageItems2.length - 1]) })}>; rel="next"`];
65668
+ if (pageItems2.length) links.push(`<${q({ min_id: idOf2(pageItems2[0]) })}>; rel="prev"`);
64842
65669
  return { items: pageItems2, headers: { link: links.join(", ") } };
64843
65670
  }
64844
65671
  async function bskyReply(api, send, body, parent, visibility) {
@@ -64956,7 +65783,7 @@ function status(api, s, { all } = {}) {
64956
65783
  created_at: s.published || (/* @__PURE__ */ new Date()).toISOString(),
64957
65784
  in_reply_to_id: s.inReplyTo ? api.store.idFor(s.inReplyTo) : null,
64958
65785
  in_reply_to_account_id: null,
64959
- sensitive: !!s.spoiler,
65786
+ sensitive: !!(s.spoiler || s.sensitive),
64960
65787
  spoiler_text: s.spoiler || "",
64961
65788
  visibility: s.visibility || "public",
64962
65789
  language: null,
@@ -64996,7 +65823,7 @@ function status(api, s, { all } = {}) {
64996
65823
  acct: bare.includes("@") ? bare : host ? `${user}@${host}` : bare
64997
65824
  };
64998
65825
  }),
64999
- tags: [],
65826
+ tags: (s.tags || []).map((t) => ({ name: t.name, url: t.url })),
65000
65827
  emojis: (s.emojis || []).map((e) => ({
65001
65828
  shortcode: e.shortcode,
65002
65829
  url: e.url,
@@ -65425,7 +66252,7 @@ async function handle3(api, ctx) {
65425
66252
  const entry = api.store.getMedia()[mMedia[1]];
65426
66253
  if (!entry) return send(404, { error: "Record not found" });
65427
66254
  if (req.method === "PUT") {
65428
- const body = await readBody(req);
66255
+ const body = await readBody2(req);
65429
66256
  if (typeof body.description === "string") {
65430
66257
  entry.description = body.description;
65431
66258
  api.store.setMedia(mMedia[1], entry);
@@ -65459,7 +66286,7 @@ async function handle4(api, ctx) {
65459
66286
  const ct = String(req.headers["content-type"] || "");
65460
66287
  let form = {}, files = {};
65461
66288
  if (ct.includes("multipart/form-data")) ({ fields: form, files } = await readMultipart(req));
65462
- else form = await readBody(req);
66289
+ else form = await readBody2(req);
65463
66290
  const cfg = { ...api.store.getConfig() };
65464
66291
  const putImage = async (f) => {
65465
66292
  const ext = (f.filename || "").includes(".") ? f.filename.split(".").pop().replace(/[^\w]/g, "") : "bin";
@@ -65521,7 +66348,7 @@ async function handle4(api, ctx) {
65521
66348
  }
65522
66349
  if (pathname === "/api/v1/markers") {
65523
66350
  if (req.method === "POST") {
65524
- const body = await readBody(req);
66351
+ const body = await readBody2(req);
65525
66352
  const markers = api.store.read("masto-markers.json", {});
65526
66353
  for (const [k, v] of Object.entries(body)) {
65527
66354
  const lastId = v?.last_read_id || v;
@@ -65596,7 +66423,7 @@ async function handle4(api, ctx) {
65596
66423
  return sub ? send(200, api.push.json(token, sub)) : send(404, { error: "Record not found" });
65597
66424
  }
65598
66425
  if (req.method === "POST") {
65599
- const body = await readBody(req);
66426
+ const body = await readBody2(req);
65600
66427
  const sub = api.push.set(token, {
65601
66428
  endpoint: body.subscription?.endpoint,
65602
66429
  keys: body.subscription?.keys,
@@ -65606,7 +66433,7 @@ async function handle4(api, ctx) {
65606
66433
  return send(200, api.push.json(token, sub));
65607
66434
  }
65608
66435
  if (req.method === "PUT") {
65609
- const body = await readBody(req);
66436
+ const body = await readBody2(req);
65610
66437
  const sub = api.push.setAlerts(token, body.data?.alerts);
65611
66438
  return sub ? send(200, api.push.json(token, sub)) : send(404, { error: "Record not found" });
65612
66439
  }
@@ -65699,7 +66526,7 @@ async function handle5(api, ctx) {
65699
66526
  return send(200, api.store.getLists().map(listJson));
65700
66527
  }
65701
66528
  if (pathname === "/api/v1/lists" && req.method === "POST") {
65702
- const body = await readBody(req);
66529
+ const body = await readBody2(req);
65703
66530
  const title = String(body.title || "").trim();
65704
66531
  if (!title) return send(422, { error: "a title is required" });
65705
66532
  const lists = api.store.getLists();
@@ -65718,7 +66545,7 @@ async function handle5(api, ctx) {
65718
66545
  return send(200, {});
65719
66546
  }
65720
66547
  if (req.method === "PUT") {
65721
- const body = await readBody(req);
66548
+ const body = await readBody2(req);
65722
66549
  if (body.title) l.title = String(body.title);
65723
66550
  if (body.replies_policy) l.repliesPolicy = body.replies_policy;
65724
66551
  api.store.setLists(lists);
@@ -65731,7 +66558,7 @@ async function handle5(api, ctx) {
65731
66558
  const l = lists.find((x) => x.id === mListAcc[1]);
65732
66559
  if (!l) return send(404, { error: "Record not found" });
65733
66560
  if (req.method === "GET") return send(200, (l.members || []).map((a) => api.account(a)));
65734
- const body = await readBody(req).catch(() => ({}));
66561
+ const body = await readBody2(req).catch(() => ({}));
65735
66562
  const ids = [].concat(body.account_ids || body["account_ids[]"] || url.searchParams.getAll("account_ids[]")).filter(Boolean);
65736
66563
  const actors = ids.map((id) => api.store.urlFor(id)).filter(Boolean);
65737
66564
  if (req.method === "POST") l.members = [.../* @__PURE__ */ new Set([...l.members || [], ...actors])];
@@ -65763,7 +66590,7 @@ async function handle5(api, ctx) {
65763
66590
  return send(200, api.store.getFilters().map(filterJson));
65764
66591
  }
65765
66592
  if (pathname === "/api/v2/filters" && req.method === "POST") {
65766
- const body = await readBody(req);
66593
+ const body = await readBody2(req);
65767
66594
  const title = String(body.title || "").trim();
65768
66595
  if (!title) return send(422, { error: "a title is required" });
65769
66596
  const filters = api.store.getFilters();
@@ -65789,7 +66616,7 @@ async function handle5(api, ctx) {
65789
66616
  return send(200, {});
65790
66617
  }
65791
66618
  if (req.method === "PUT") {
65792
- const body = await readBody(req);
66619
+ const body = await readBody2(req);
65793
66620
  if (body.title) f.title = String(body.title);
65794
66621
  if (body.context) f.context = [].concat(body.context);
65795
66622
  if (body.filter_action) f.action = body.filter_action;
@@ -65846,7 +66673,7 @@ async function handle5(api, ctx) {
65846
66673
  if (sinceId) cut(sinceId, true);
65847
66674
  const page2 = q.get("min_id") && !q.get("since_id") ? items.slice(Math.max(0, items.length - limit)) : items.slice(0, limit);
65848
66675
  if (page2.length) {
65849
- const base = `${api.scheme || (req.socket?.encrypted ? "https" : "http")}://${req.headers.host}${pathname}`;
66676
+ const base = `${api.scheme || (req.socket?.encrypted ? "https" : "http")}://${req.headers.host}${api.mount || ""}${pathname}`;
65850
66677
  const link = (params) => {
65851
66678
  const u = new URL(base);
65852
66679
  for (const [k, v] of q) if (k !== "max_id" && k !== "since_id" && k !== "min_id") u.searchParams.append(k, v);
@@ -65912,7 +66739,7 @@ init_node_crypto();
65912
66739
  async function handle6(api, ctx) {
65913
66740
  const { req, res, pathname, url, send } = ctx;
65914
66741
  if (pathname === "/api/v1/statuses" && req.method === "POST") {
65915
- const body = await readBody(req);
66742
+ const body = await readBody2(req);
65916
66743
  if (!body.status) return send(422, { error: "status text required" });
65917
66744
  const visibility = body.visibility || "public";
65918
66745
  if (!["public", "unlisted", "private", "direct"].includes(visibility)) {
@@ -65928,6 +66755,7 @@ async function handle6(api, ctx) {
65928
66755
  if (ready !== true) return send(422, { error: ready });
65929
66756
  }
65930
66757
  const spoilerText = String(body.spoiler_text || "").trim() || null;
66758
+ const sensitive = body.sensitive === true || body.sensitive === "true" || body.sensitive === "1";
65931
66759
  const mediaIds = [].concat(body.media_ids || body["media_ids[]"] || []).filter(Boolean);
65932
66760
  const media = api.store.getMedia();
65933
66761
  const attachments = mediaIds.map((id) => media[id] && { id, ...media[id] }).filter(Boolean);
@@ -65958,7 +66786,8 @@ async function handle6(api, ctx) {
65958
66786
  expiresAt: new Date(Date.now() + seconds * 1e3).toISOString(),
65959
66787
  inReplyTo,
65960
66788
  visibility,
65961
- spoilerText
66789
+ spoilerText,
66790
+ sensitive
65962
66791
  });
65963
66792
  return send(200, api.status(api.store.getStatuses().find((x) => x.noteId === q.id)));
65964
66793
  } catch (e) {
@@ -65987,7 +66816,7 @@ async function handle6(api, ctx) {
65987
66816
  try {
65988
66817
  note = await api.agent.publisher.publishNote(
65989
66818
  body.status,
65990
- { inReplyTo, attachments, visibility, spoilerText }
66819
+ { inReplyTo, attachments, visibility, spoilerText, sensitive }
65991
66820
  );
65992
66821
  } catch (e) {
65993
66822
  if (e.code === "unaddressed") return send(422, { error: e.message });
@@ -66011,7 +66840,7 @@ async function handle6(api, ctx) {
66011
66840
  return send(200, [{
66012
66841
  content: s.content || "",
66013
66842
  spoiler_text: s.spoiler || "",
66014
- sensitive: !!s.spoiler,
66843
+ sensitive: !!(s.spoiler || s.sensitive),
66015
66844
  created_at: s.editedAt || s.published,
66016
66845
  account: api.account(s.actor),
66017
66846
  media_attachments: (s.attachments || []).map((a) => api.mediaJson(a)),
@@ -66030,15 +66859,16 @@ async function handle6(api, ctx) {
66030
66859
  const s = noteUrl && api.store.getStatuses().find((x) => x.noteId === noteUrl);
66031
66860
  if (!s) return send(404, { error: "Record not found" });
66032
66861
  if (s.actor !== api.urls.actor) return send(403, { error: "not your status" });
66033
- const body = await readBody(req);
66862
+ const body = await readBody2(req);
66034
66863
  if (!body.status) return send(422, { error: "status text required" });
66035
66864
  const mediaIds = [].concat(body.media_ids || body["media_ids[]"] || []).filter(Boolean);
66036
66865
  const media = api.store.getMedia();
66037
66866
  const attachments = mediaIds.length ? mediaIds.map((id) => media[id] && { id, ...media[id] }).filter(Boolean) : null;
66038
66867
  const spoilerText = String(body.spoiler_text || "").trim() || null;
66868
+ const sensitive = body.sensitive === void 0 ? null : body.sensitive === true || body.sensitive === "true" || body.sensitive === "1";
66039
66869
  const patched = await api.agent.publisher.updateNote(
66040
66870
  s,
66041
- { content: body.status, spoilerText, attachments }
66871
+ { content: body.status, spoilerText, sensitive, attachments }
66042
66872
  );
66043
66873
  return send(200, api.status(patched || s));
66044
66874
  }
@@ -66148,7 +66978,7 @@ async function handle6(api, ctx) {
66148
66978
  return send(200, {});
66149
66979
  }
66150
66980
  if (req.method === "PUT") {
66151
- const body = await readBody(req);
66981
+ const body = await readBody2(req);
66152
66982
  const at = Date.parse(body.scheduled_at || "");
66153
66983
  if (!Number.isFinite(at)) return send(422, { error: "scheduled_at required" });
66154
66984
  e.scheduledAt = new Date(at).toISOString();
@@ -66162,7 +66992,7 @@ async function handle6(api, ctx) {
66162
66992
  const s = noteUrl && api.store.getStatuses().find((x) => x.noteId === noteUrl);
66163
66993
  if (!s?.poll) return send(404, { error: "Record not found" });
66164
66994
  if (mPoll[2] && req.method === "POST") {
66165
- const body = await readBody(req);
66995
+ const body = await readBody2(req);
66166
66996
  const choices = [].concat(body.choices || body["choices[]"] || []).map(Number).filter(Number.isInteger);
66167
66997
  if (!choices.length) return send(422, { error: "choices required" });
66168
66998
  const r = await votePoll(api.agent, s, choices);
@@ -66211,11 +67041,13 @@ var MastoApi = class _MastoApi {
66211
67041
  allowed = null,
66212
67042
  scheme = null,
66213
67043
  embedded = false,
67044
+ mount = "",
66214
67045
  streaming = true,
66215
67046
  webPush = true,
66216
67047
  scheduling = true
66217
67048
  }) {
66218
67049
  this.agent = agent2;
67050
+ this.mount = mount;
66219
67051
  this.embedded = embedded;
66220
67052
  this.log = log2;
66221
67053
  this.allowed = allowed;
@@ -66275,7 +67107,13 @@ var MastoApi = class _MastoApi {
66275
67107
  res.end(body);
66276
67108
  return true;
66277
67109
  };
66278
- const ctx = { req, res, pathname, url, send };
67110
+ const scheme = String(this.scheme || (req.headers?.["x-forwarded-proto"] === "https" || req.socket?.encrypted ? "https" : "http")).replace(/:.*$/, "");
67111
+ let self2 = url;
67112
+ try {
67113
+ self2 = new URL(`${scheme}://${req.headers?.host || "localhost"}${this.mount || ""}${pathname}${url.search || ""}`);
67114
+ } catch {
67115
+ }
67116
+ const ctx = { req, res, pathname, url: self2, send };
66279
67117
  if (await handle(this, ctx)) return true;
66280
67118
  if (!pathname.startsWith("/api/")) return false;
66281
67119
  if (await handle2(this, ctx)) return true;
@@ -66907,7 +67745,7 @@ var PodTransport = class {
66907
67745
  * says all of it. The parsed graph must mention the WebID before anything is
66908
67746
  * written back — an empty or foreign body must never become the new profile.
66909
67747
  */
66910
- async linkAccountInProfile({ actorUrl, accountName, kind = "person" }) {
67748
+ async linkAccountInProfile({ actorUrl, accountName, kind = "person", outbox = null }) {
66911
67749
  const docUrl = this.webId.split("#")[0];
66912
67750
  const res = await this.fetch(docUrl, { headers: { accept: "text/turtle" } });
66913
67751
  if (res.status >= 400) throw new Error(`[${this.label}] GET ${docUrl} \u2192 ${res.status}`);
@@ -66923,10 +67761,16 @@ var PodTransport = class {
66923
67761
  [me, FOAF("account"), actor],
66924
67762
  [actor, RDF4("type"), FOAF("OnlineAccount")],
66925
67763
  [actor, RDF4("type"), kind === "group" ? AS("Group") : AS("Person")],
66926
- [actor, FOAF("accountName"), literal2(accountName)]
67764
+ [actor, FOAF("accountName"), literal2(accountName)],
67765
+ // Where a Solid client posts on this person's behalf (`as:outbox` on the
67766
+ // WebID is what dokieli reads); only where a door exists to take it.
67767
+ ...outbox ? [[me, AS("outbox"), namedNode2(outbox)]] : []
66927
67768
  ];
66928
67769
  const missing = wanted.filter(([s, p, o]) => !g.holds(s, p, o, doc));
66929
- const stale = g.statementsMatching(actor, FOAF("accountName"), null, doc).filter((st2) => st2.object.value !== accountName);
67770
+ const stale = [
67771
+ ...g.statementsMatching(actor, FOAF("accountName"), null, doc).filter((st2) => st2.object.value !== accountName),
67772
+ ...outbox ? g.statementsMatching(me, AS("outbox"), null, doc).filter((st2) => st2.object.value !== outbox) : []
67773
+ ];
66930
67774
  if (!missing.length && !stale.length) return false;
66931
67775
  const deletes = stale.map((st2) => [st2.subject, st2.predicate, st2.object]);
66932
67776
  if (await this.patchDocument(docUrl, missing, deletes)) return true;
@@ -67280,6 +68124,7 @@ var hostOf = (inbox) => {
67280
68124
  }
67281
68125
  };
67282
68126
  var aboutTheHost = (e) => !e.status || e.status >= 500 || e.status === 429;
68127
+ var unsalvageable = (e) => e.status === 404 || e.status === 410;
67283
68128
  var Deliverer = class {
67284
68129
  // passive: signing-only (viewer-mode agents) — no queue drain timer, so a
67285
68130
  // read-only agent never mutates shared delivery state. startQueue() flips
@@ -67292,9 +68137,11 @@ var Deliverer = class {
67292
68137
  edPrivate = null,
67293
68138
  proofKeyId = null,
67294
68139
  log: log2 = console.log,
67295
- passive = false
68140
+ passive = false,
68141
+ onGone = null
67296
68142
  }) {
67297
68143
  this.store = store;
68144
+ this.onGone = onGone;
67298
68145
  this.keyId = keyId;
67299
68146
  this.rsaPrivate = rsaPrivate;
67300
68147
  this.actorId = actorId;
@@ -67390,6 +68237,10 @@ var Deliverer = class {
67390
68237
  await this.deliverNow(inbox, signed);
67391
68238
  this.log(`delivered ${signed.type} \u2192 ${inbox}`);
67392
68239
  } catch (e) {
68240
+ if (unsalvageable(e)) {
68241
+ await this._unsalvageable(inbox, e);
68242
+ return;
68243
+ }
67393
68244
  this.log(`delivery failed (${e.message}) \u2014 queued`);
67394
68245
  const wait = e.retryAfterMs || 6e4;
67395
68246
  if (aboutTheHost(e)) {
@@ -67399,6 +68250,26 @@ var Deliverer = class {
67399
68250
  this._enqueue({ inbox, activity: signed, attempts: 1, nextAt: Date.now() + wait });
67400
68251
  }
67401
68252
  }
68253
+ // Not retried, and on a 410 the followers who received there are dropped,
68254
+ // so the next fan-out stops asking; the followers collection is republished
68255
+ // through onGone. A 404 drops only this delivery.
68256
+ async _unsalvageable(inbox, e) {
68257
+ if (e.status !== 410) {
68258
+ this.log(`delivery dropped (${e.message}) \u2014 nothing answers there`);
68259
+ return;
68260
+ }
68261
+ const contacts = this.store.getContacts?.();
68262
+ const left = (contacts?.followers || []).filter((f) => f.inbox === inbox || f.sharedInbox === inbox);
68263
+ this.log(`${inbox} is gone (410) \u2014 delivery dropped${left.length ? `, ${left.length} follower(s) there forgotten` : ""}`);
68264
+ if (!left.length) return;
68265
+ for (const f of left) dropFollower(contacts, f.actor, "inbox-gone");
68266
+ this.store.setContacts(contacts);
68267
+ try {
68268
+ await this.onGone?.();
68269
+ } catch (err) {
68270
+ this.log(`followers republish after 410: ${err.message}`);
68271
+ }
68272
+ }
67402
68273
  // The queue has a ceiling: a fan-out into a sea of down servers must not grow
67403
68274
  // queue.json without bound. Overflow keeps the evidence as a dead letter.
67404
68275
  _enqueue(item) {
@@ -67483,6 +68354,11 @@ var Deliverer = class {
67483
68354
  changed = true;
67484
68355
  this.log(`retry ok: ${item.activity.type} \u2192 ${item.inbox}`);
67485
68356
  } catch (e) {
68357
+ if (unsalvageable(e)) {
68358
+ changed = true;
68359
+ await this._unsalvageable(item.inbox, e);
68360
+ continue;
68361
+ }
67486
68362
  item.attempts += 1;
67487
68363
  changed = true;
67488
68364
  const wait = e.retryAfterMs || ladderFor(item);
@@ -69664,6 +70540,7 @@ var BrowserAgent = class _BrowserAgent {
69664
70540
  this.lease.startRenewal();
69665
70541
  try {
69666
70542
  await this.store.load({ force: true }).catch((e) => this.log(`re-reading state: ${e.message}`));
70543
+ await this.publisher.healStatuses().catch((e) => this.log(`healing the timeline index: ${e.message}`));
69667
70544
  this.deliverer?.startQueue?.();
69668
70545
  await this.publisher.publishProfile();
69669
70546
  await this.store.flush?.();
@@ -69775,7 +70652,8 @@ var BrowserAgent = class _BrowserAgent {
69775
70652
  // The relay finds the account by the front's own key for it, which for a
69776
70653
  // mail-door account is the full address, not the bare handle.
69777
70654
  handle: doorKeyOf(config.gateway?.url) || config.handle,
69778
- sessionFetch: session.fetch
70655
+ sessionFetch: session.fetch,
70656
+ onGone: () => this.publisher.publishCollections({ followers: true })
69779
70657
  });
69780
70658
  this.publisher = new Publisher({
69781
70659
  config: this.store.getConfig(),
@@ -69788,12 +70666,14 @@ var BrowserAgent = class _BrowserAgent {
69788
70666
  // Who a post names, resolved — the same lookup the DeviceAgent
69789
70667
  // gives its publisher. Without it no mention from the browser ever
69790
70668
  // resolved: a direct message went to nobody, a mention notified no one.
69791
- resolveMention: (h) => resolveHandle(this, h)
70669
+ resolveMention: (h) => resolveHandle(this, h),
70670
+ resolveActor: (u) => this.intake.fetchAP(u)
69792
70671
  });
69793
70672
  this.atproto = new BrowserAtproto({ store: this.store, actorId: this.urls.actor, log: this.log });
69794
70673
  this.publisher.atproto = this.atproto;
69795
70674
  this.fediaccts = new BrowserFediAccounts({ store: this.store, actorId: this.urls.actor, log: this.log });
69796
70675
  this.lease = new Lease({ url: this.urls.state + "lease.json", fetchImpl: podFetch, log: this.log });
70676
+ this.c2s = new C2S({ agent: this, log: this.log });
69797
70677
  this.intake = new Intake({
69798
70678
  config: this.store.getConfig(),
69799
70679
  urls: this.urls,
@@ -69803,7 +70683,8 @@ var BrowserAgent = class _BrowserAgent {
69803
70683
  publisher: this.publisher,
69804
70684
  log: this.log,
69805
70685
  push: true,
69806
- lease: this.lease
70686
+ lease: this.lease,
70687
+ ownerPost: (a, o) => this.c2s.dispatch(a, o)
69807
70688
  });
69808
70689
  this.masto = new MastoApi({
69809
70690
  agent: this,
@@ -69863,7 +70744,8 @@ var BrowserAgent = class _BrowserAgent {
69863
70744
  atproto: this.atproto?.status() || null,
69864
70745
  podRequests: this.remote?.stats?.() || null,
69865
70746
  update: null,
69866
- inboxCooldownFor: 0
70747
+ inboxCooldownFor: 0,
70748
+ stateSkipped: this.store?.lastSkipped || []
69867
70749
  };
69868
70750
  }
69869
70751
  // Rotate the signing key (POST /rotate-key). Mint a fresh keypair, replace the