fedipod-server 0.17.0 → 0.19.0

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