fedipod-server 0.13.2 → 0.14.1

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.
@@ -55946,6 +55946,17 @@ async function mentionsFor(publisher, content, inReplyTo) {
55946
55946
  }
55947
55947
  return mentions;
55948
55948
  }
55949
+ function assertDirectAddressed(content, mentions) {
55950
+ const wanted = [...new Set(mentionsIn(content))];
55951
+ const missing = wanted.filter((h) => !mentions.some((m) => m.handle === h));
55952
+ let why = null;
55953
+ if (!wanted.length) why = "a direct message names who it is for \u2014 mention them as @name@host";
55954
+ else if (missing.length) why = `could not find ${missing.map((h) => "@" + h).join(", ")} \u2014 the direct message was not sent`;
55955
+ if (!why) return;
55956
+ const e = new Error(why);
55957
+ e.code = "unaddressed";
55958
+ throw e;
55959
+ }
55949
55960
  async function publishNote(publisher, content, { inReplyTo, attachments, visibility = "public", spoilerText = null } = {}) {
55950
55961
  const { urls } = publisher;
55951
55962
  const priv = visibility === "private" || visibility === "direct";
@@ -55956,6 +55967,7 @@ async function publishNote(publisher, content, { inReplyTo, attachments, visibil
55956
55967
  const published = (/* @__PURE__ */ new Date()).toISOString();
55957
55968
  const slug = published.slice(0, 10) + "-" + node_crypto_default.randomBytes(4).toString("hex");
55958
55969
  const mentions = await publisher._mentionsFor(content, inReplyTo);
55970
+ if (visibility === "direct") assertDirectAddressed(content, mentions);
55959
55971
  const note = noteDoc({
55960
55972
  urls,
55961
55973
  slug,
@@ -56157,6 +56169,7 @@ async function publishQuestion(publisher, content, {
56157
56169
  const published = (/* @__PURE__ */ new Date()).toISOString();
56158
56170
  const slug = published.slice(0, 10) + "-" + node_crypto_default.randomBytes(4).toString("hex");
56159
56171
  const mentions = await publisher._mentionsFor(content, inReplyTo);
56172
+ if (visibility === "direct") assertDirectAddressed(content, mentions);
56160
56173
  const poll = {
56161
56174
  multiple: !!multiple,
56162
56175
  expiresAt: expiresAt || null,
@@ -57380,7 +57393,7 @@ async function onUndo(intake, activity, actor, { trusted = false } = {}) {
57380
57393
  await intake.republish({ followers: true });
57381
57394
  intake.log(`unfollowed by ${actor}`);
57382
57395
  }
57383
- async function onCreate(intake, activity, actor) {
57396
+ async function onCreate(intake, activity, actor, { trusted = false } = {}) {
57384
57397
  const objectId = typeof activity.object === "string" ? activity.object : activity.object?.id;
57385
57398
  if (!objectId) return "Create without object id";
57386
57399
  if (intake.store.isBlocked(objectId)) return `blocked domain (${objectId})`;
@@ -57389,7 +57402,8 @@ async function onCreate(intake, activity, actor) {
57389
57402
  if (!intake.concernsUs(envelope, actor)) return `not addressed to us (${objectId})`;
57390
57403
  const ingested = intake.store.getStatuses().some((x) => x.noteId === objectId && (x.kind === "timeline" || x.kind === "mention"));
57391
57404
  if (!ingested) {
57392
- const rejected = await intake.ingestNote(objectId, actor);
57405
+ const inline = trusted && typeof activity.object === "object" && activity.object?.id === objectId ? activity.object : null;
57406
+ const rejected = await intake.ingestNote(objectId, actor, { inline });
57393
57407
  if (rejected) return rejected;
57394
57408
  }
57395
57409
  if (intake.config.kind === "group") await intake.amplify(objectId, { activity });
@@ -57471,9 +57485,9 @@ async function onUpdate(intake, activity, actor) {
57471
57485
  const note = await intake.fetchAP(objectId);
57472
57486
  if (!note) throw new Error(`cannot refetch ${objectId} \u2014 will retry`);
57473
57487
  if (note.id !== objectId || !isContentType(note.type)) return `object not verifiable content (${objectId}, ${note.type})`;
57474
- const { attachmentsOf: attachmentsOf2, titledContent: titledContent2 } = await Promise.resolve().then(() => (init_wire(), wire_exports));
57488
+ const { attachmentsOf: attachmentsOf3, titledContent: titledContent2 } = await Promise.resolve().then(() => (init_wire(), wire_exports));
57475
57489
  const content = titledContent2(note);
57476
- const attachments = attachmentsOf2(note);
57490
+ const attachments = attachmentsOf3(note);
57477
57491
  const freshPoll = pollOf(note);
57478
57492
  const freshEmojis = emojisOf(note);
57479
57493
  intake.store.updateStatus(objectId, {
@@ -57603,12 +57617,12 @@ function referencesOurObject(intake, activity) {
57603
57617
  if (refs.some((r) => r.startsWith(intake.urls.notes))) return true;
57604
57618
  return intake.config.kind === "group" && refs.some((r) => intake.store.getStatuses().some((s) => s.noteId === r));
57605
57619
  }
57606
- async function ingestNote(intake, objectId, actor, { via } = {}) {
57607
- const note = await intake.fetchAP(objectId);
57620
+ async function ingestNote(intake, objectId, actor, { via, inline = null } = {}) {
57621
+ const note = inline || await intake.fetchAP(objectId);
57608
57622
  if (!note) return `object fetch failed (${objectId})`;
57609
57623
  if (note.id !== objectId || !isContentType(note.type)) return `object not verifiable content (${objectId}, ${note.type})`;
57610
- const { attachmentsOf: attachmentsOf2, titledContent: titledContent2 } = await Promise.resolve().then(() => (init_wire(), wire_exports));
57611
- const attachments = attachmentsOf2(note);
57624
+ const { attachmentsOf: attachmentsOf3, titledContent: titledContent2 } = await Promise.resolve().then(() => (init_wire(), wire_exports));
57625
+ const attachments = attachmentsOf3(note);
57612
57626
  const content = titledContent2(note);
57613
57627
  const author = authorOf(note, actor);
57614
57628
  if (!author) return `object names an author its origin does not vouch for (${objectId})`;
@@ -57652,7 +57666,9 @@ async function ingestNote(intake, objectId, actor, { via } = {}) {
57652
57666
  ...attachments.length ? { attachments } : {},
57653
57667
  ...via ? { via } : {}
57654
57668
  });
57655
- if (!followed || note.inReplyTo && String(note.inReplyTo).startsWith(intake.urls.notes)) {
57669
+ const namesUs = mentions.some((m) => m.href === intake.urls.actor);
57670
+ const replyToOurs = !!note.inReplyTo && String(note.inReplyTo).startsWith(intake.urls.notes);
57671
+ if (!followed || replyToOurs || direct || namesUs) {
57656
57672
  intake.store.addNotification({ type: "mention", actor: author, noteId: note.id });
57657
57673
  }
57658
57674
  if (note.inReplyTo && String(note.inReplyTo).startsWith(intake.urls.notes)) {
@@ -63128,7 +63144,7 @@ var Intake = class {
63128
63144
  case "Undo":
63129
63145
  return this.onUndo(activity, actor, { trusted });
63130
63146
  case "Create":
63131
- return this.onCreate(activity, actor);
63147
+ return this.onCreate(activity, actor, { trusted });
63132
63148
  case "Accept":
63133
63149
  return this.onAccept(activity, actor, { trusted });
63134
63150
  case "Like":
@@ -64107,7 +64123,7 @@ __export(social_exports, {
64107
64123
  });
64108
64124
  init_node_crypto();
64109
64125
  init_wire();
64110
- async function lookupWebFinger(acct) {
64126
+ async function lookupWebFinger(acct, { fetchImpl = null } = {}) {
64111
64127
  const clean = String(acct || "").replace(/^acct:/, "");
64112
64128
  const at = clean.lastIndexOf("@");
64113
64129
  if (at < 1) return null;
@@ -64115,9 +64131,8 @@ async function lookupWebFinger(acct) {
64115
64131
  if (!host || /[/\\?#]/.test(host)) return null;
64116
64132
  const { safeFetch: safeFetch2, readCapped: readCapped3 } = await Promise.resolve().then(() => (init_safefetch(), safefetch_exports));
64117
64133
  const url = `https://${host}/.well-known/webfinger?resource=${encodeURIComponent(`acct:${clean}`)}`;
64118
- const res = await safeFetch2(url, {
64119
- headers: { accept: "application/jrd+json, application/json" }
64120
- }).catch(() => null);
64134
+ const headers = { accept: "application/jrd+json, application/json" };
64135
+ const res = await (fetchImpl ? fetchImpl(url, { headers }) : safeFetch2(url, { headers })).catch(() => null);
64121
64136
  if (!res || res.status >= 400) return null;
64122
64137
  let jrd2;
64123
64138
  try {
@@ -64153,13 +64168,15 @@ async function resolveHandle(agent2, handle7) {
64153
64168
  const clean = String(handle7 || "").replace(/^@/, "");
64154
64169
  if (!/^[^@]+@[^@]+$/.test(clean)) throw new Error("handle must look like user@host");
64155
64170
  if (agent2.store.isBlocked("https://" + clean.split("@")[1] + "/")) throw new Error("domain is blocked");
64156
- const jrd2 = await lookupWebFinger("acct:" + clean);
64171
+ const remote = agent2.intake?.deliverer?.signedFetch ? (u, i) => agent2.intake.deliverer.signedFetch(u, i) : null;
64172
+ const lookup2 = (a) => lookupWebFinger(a, { fetchImpl: remote });
64173
+ const jrd2 = await lookup2("acct:" + clean);
64157
64174
  const self2 = selfLink(jrd2);
64158
64175
  if (!self2?.href) throw new Error(`webfinger found no actor for ${clean}`);
64159
64176
  const doc = await agent2.intake.fetchAP(self2.href);
64160
64177
  if (!doc?.id) throw new Error(`actor document unusable for ${clean}`);
64161
64178
  if (agent2.store.isBlocked(doc.id)) throw new Error("actor is blocked");
64162
- await confirmDelegation(clean.slice(clean.lastIndexOf("@") + 1), doc);
64179
+ await confirmDelegation(clean.slice(clean.lastIndexOf("@") + 1), doc, lookup2);
64163
64180
  await cacheCounts(agent2, doc);
64164
64181
  return doc;
64165
64182
  }
@@ -64528,6 +64545,16 @@ var postUrl = (uri) => {
64528
64545
  const m = String(uri).match(/^at:\/\/([^/]+)\/[^/]+\/(.+)$/);
64529
64546
  return m ? `https://bsky.app/profile/${m[1]}/post/${m[2]}` : null;
64530
64547
  };
64548
+ function attachmentsOf2(post) {
64549
+ const e = post?.embed || {};
64550
+ const images = [...e.images || [], ...e.media?.images || []].filter((i) => i?.fullsize).map((i) => ({ url: i.fullsize, mediaType: "image/jpeg", description: i.alt || "" }));
64551
+ const card = e.external?.thumb || e.media?.external?.thumb;
64552
+ if (card) {
64553
+ const ext = e.external || e.media.external;
64554
+ images.push({ url: card, mediaType: "image/jpeg", description: ext.title || ext.uri || "" });
64555
+ }
64556
+ return images;
64557
+ }
64531
64558
  var BskyFeed = class {
64532
64559
  constructor({ store, atproto, log: log2 = console.log, onNotification = null }) {
64533
64560
  Object.assign(this, { store, atproto, log: log2, onNotification });
@@ -64583,14 +64610,27 @@ var BskyFeed = class {
64583
64610
  return url;
64584
64611
  }
64585
64612
  // One Bluesky post into the statuses index. Returns its noteId, new or not.
64613
+ // A post seen before is completed, not duplicated: a reply first met as a
64614
+ // bare notification record learns its parent and its pictures from the
64615
+ // full view when that arrives.
64586
64616
  _mirrorPost(post, { via = null } = {}) {
64587
64617
  const noteId = post.uri;
64588
- const existing = this.store.getStatuses().some((s) => s.noteId === noteId);
64589
- if (existing) return { noteId, added: false };
64618
+ const inReplyTo = post.record?.reply?.parent?.uri || null;
64619
+ const attachments = attachmentsOf2(post);
64620
+ const existing = this.store.getStatuses().find((s) => s.noteId === noteId);
64621
+ if (existing) {
64622
+ const patch = {
64623
+ ...inReplyTo && !existing.inReplyTo ? { inReplyTo } : {},
64624
+ ...attachments.length && !existing.attachments?.length ? { attachments } : {}
64625
+ };
64626
+ if (Object.keys(patch).length && typeof this.store.updateStatus === "function") {
64627
+ this.store.updateStatus(noteId, patch);
64628
+ }
64629
+ return { noteId, added: false };
64630
+ }
64590
64631
  const actor = this._rememberAuthor(post.author);
64591
64632
  if (this.store.isBlocked(actor)) return { noteId, added: false };
64592
64633
  const text = post.record?.text || "";
64593
- const images = post.embed?.images || [];
64594
64634
  this.store.addStatus({
64595
64635
  noteId,
64596
64636
  actor,
@@ -64600,12 +64640,46 @@ var BskyFeed = class {
64600
64640
  ...post.cid ? { cid: post.cid } : {},
64601
64641
  link: postUrl(noteId),
64602
64642
  ...via ? { via } : {},
64603
- ...images.length ? {
64604
- attachments: images.map((i) => ({ url: i.fullsize, mediaType: "image/jpeg", description: i.alt || "" }))
64605
- } : {}
64643
+ ...inReplyTo ? { inReplyTo } : {},
64644
+ ...attachments.length ? { attachments } : {}
64606
64645
  });
64607
64646
  return { noteId, added: true };
64608
64647
  }
64648
+ // The conversation under one post, asked for when a post is opened: the
64649
+ // ancestors it hangs from and the replies beneath it, each mirrored with
64650
+ // the reply link the thread view is built from. View cache only, like the
64651
+ // timeline. Returns how many posts were new; a thread Bluesky will not
64652
+ // give (gone, blocked, rate-limited) adds none and is logged, not thrown.
64653
+ async mirrorThread(uri, { depth = 6 } = {}) {
64654
+ if (!this.atproto?.connected()) return 0;
64655
+ if (this.quietUntil && Date.now() < this.quietUntil) return 0;
64656
+ let out;
64657
+ try {
64658
+ out = await this.atproto.xrpc("app.bsky.feed.getPostThread", { params: { uri, depth, parentHeight: 40 } });
64659
+ } catch (e) {
64660
+ if (e.status === 429 || e.status >= 500) this._backOff(e.status, null);
64661
+ else this.log(`bskyfeed: thread ${uri}: ${e.message}`);
64662
+ return 0;
64663
+ }
64664
+ const self2 = this.atproto.read()?.did;
64665
+ const ownMirrors = new Set(this.store.getStatuses().map((s) => s.atproto?.uri).filter(Boolean));
64666
+ let added = 0;
64667
+ const take2 = (node) => {
64668
+ const post = node?.post;
64669
+ if (!post?.uri) return;
64670
+ if (post.author?.did === self2 && ownMirrors.has(post.uri)) return;
64671
+ if (this._mirrorPost(post).added) added++;
64672
+ };
64673
+ for (let up = out?.thread?.parent; up; up = up.parent) take2(up);
64674
+ const walk = (node, left) => {
64675
+ take2(node);
64676
+ if (left <= 0) return;
64677
+ for (const r of node?.replies || []) walk(r, left - 1);
64678
+ };
64679
+ walk(out?.thread, depth);
64680
+ if (added) this.log(`bskyfeed: +${added} from the thread under ${uri}`);
64681
+ return added;
64682
+ }
64609
64683
  async sweep() {
64610
64684
  if (!this.atproto?.connected()) return;
64611
64685
  if (this.quietUntil && Date.now() < this.quietUntil) return;
@@ -64644,8 +64718,13 @@ var BskyFeed = class {
64644
64718
  this.store.addNotification({ type: "follow", actor, bsky: true });
64645
64719
  await this.onNotification?.(n, { actor });
64646
64720
  } else if (n.reason === "mention" || n.reason === "reply") {
64647
- const record = { uri: n.uri, cid: n.cid, author: n.author, record: n.record, indexedAt: n.indexedAt };
64648
- this._mirrorPost(record);
64721
+ let view = null;
64722
+ try {
64723
+ const got = await this.atproto.xrpc("app.bsky.feed.getPosts", { params: { uris: n.uri } });
64724
+ view = (got?.posts || [])[0] || null;
64725
+ } catch {
64726
+ }
64727
+ this._mirrorPost(view || { uri: n.uri, cid: n.cid, author: n.author, record: n.record, indexedAt: n.indexedAt });
64649
64728
  this.store.addNotification({ type: "mention", actor, noteId: n.uri, bsky: true });
64650
64729
  await this.onNotification?.(n, { actor });
64651
64730
  }
@@ -65896,10 +65975,16 @@ async function handle6(api, ctx) {
65896
65975
  api.store.setScheduled(sched);
65897
65976
  return send(200, api.scheduledJson(entry));
65898
65977
  }
65899
- const note = await api.agent.publisher.publishNote(
65900
- body.status,
65901
- { inReplyTo, attachments, visibility, spoilerText }
65902
- );
65978
+ let note;
65979
+ try {
65980
+ note = await api.agent.publisher.publishNote(
65981
+ body.status,
65982
+ { inReplyTo, attachments, visibility, spoilerText }
65983
+ );
65984
+ } catch (e) {
65985
+ if (e.code === "unaddressed") return send(422, { error: e.message });
65986
+ throw e;
65987
+ }
65903
65988
  const s = api.store.getStatuses().find((x) => x.noteId === note.id);
65904
65989
  return send(200, api.status(s));
65905
65990
  }
@@ -65978,6 +66063,10 @@ async function handle6(api, ctx) {
65978
66063
  const mContext = /^\/api\/v1\/statuses\/([a-f0-9]+)\/context$/.exec(pathname);
65979
66064
  if (mContext) {
65980
66065
  const noteUrl = api.lookup(mContext[1]).s?.noteId;
66066
+ const opened = noteUrl && api.store.getStatuses().find((x) => x.noteId === noteUrl);
66067
+ if (opened?.kind === "bsky" && typeof api.agent?.bskyfeed?.mirrorThread === "function") {
66068
+ await api.agent.bskyfeed.mirrorThread(noteUrl);
66069
+ }
65981
66070
  const all = api.store.getStatuses();
65982
66071
  const byId = new Map(all.map((s) => [s.noteId, s]));
65983
66072
  const ancestors = [];
@@ -66413,8 +66502,8 @@ var TagFeed = class {
66413
66502
  await this.intake.fetchAP(author).catch(() => {
66414
66503
  });
66415
66504
  }
66416
- const { attachmentsOf: attachmentsOf2, titledContent: titledContent2 } = await Promise.resolve().then(() => (init_wire(), wire_exports));
66417
- const attachments = attachmentsOf2(note);
66505
+ const { attachmentsOf: attachmentsOf3, titledContent: titledContent2 } = await Promise.resolve().then(() => (init_wire(), wire_exports));
66506
+ const attachments = attachmentsOf3(note);
66418
66507
  this.store.addStatus({
66419
66508
  noteId,
66420
66509
  actor: author,
@@ -69677,7 +69766,11 @@ var BrowserAgent = class _BrowserAgent {
69677
69766
  deliverer: this.deliverer,
69678
69767
  publicKeyPem: keys.rsaPublicPem,
69679
69768
  assertionKey: null,
69680
- log: this.log
69769
+ log: this.log,
69770
+ // Who a post names, resolved — the same lookup the installed agent
69771
+ // gives its publisher. Without it no mention from the browser ever
69772
+ // resolved: a direct message went to nobody, a mention notified no one.
69773
+ resolveMention: (h) => resolveHandle(this, h)
69681
69774
  });
69682
69775
  this.atproto = new BrowserAtproto({ store: this.store, actorId: this.urls.actor, log: this.log });
69683
69776
  this.publisher.atproto = this.atproto;