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.
- package/lib/client/masto/statuses.mjs +16 -2
- package/lib/connections/bskyfeed.mjs +79 -8
- package/lib/core/intake/activities.mjs +10 -2
- package/lib/core/intake/index.mjs +1 -1
- package/lib/core/intake/notes.mjs +11 -3
- package/lib/core/publisher/notes.mjs +17 -0
- package/lib/core/publisher/questions.mjs +2 -0
- package/lib/core/social.mjs +17 -6
- package/lib/gateway/front-core.mjs +11 -2
- package/lib/pod/inbox.mjs +4 -1
- package/package.json +1 -1
- package/web/app/agent.mjs +5 -1
- package/web/app/dist/sw.js +124 -31
- package/web/app/dist/sw.js.map +3 -3
- package/web/app/site/sw.js +124 -31
package/web/app/dist/sw.js
CHANGED
|
@@ -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
|
|
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:
|
|
57488
|
+
const { attachmentsOf: attachmentsOf3, titledContent: titledContent2 } = await Promise.resolve().then(() => (init_wire(), wire_exports));
|
|
57475
57489
|
const content = titledContent2(note);
|
|
57476
|
-
const attachments =
|
|
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:
|
|
57611
|
-
const attachments =
|
|
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
|
-
|
|
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
|
|
64119
|
-
|
|
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
|
|
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
|
|
64589
|
-
|
|
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
|
-
...
|
|
64604
|
-
|
|
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
|
-
|
|
64648
|
-
|
|
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
|
-
|
|
65900
|
-
|
|
65901
|
-
|
|
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:
|
|
66417
|
-
const attachments =
|
|
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;
|