fedipod-server 0.13.1 → 0.14.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.
- package/lib/client/masto/accounts.mjs +6 -1
- package/lib/client/masto/statuses.mjs +16 -2
- package/lib/connections/bskyfeed.mjs +79 -8
- package/lib/core/intake/notes.mjs +7 -1
- package/lib/core/publisher/notes.mjs +17 -0
- package/lib/core/publisher/questions.mjs +2 -0
- package/lib/core/store.mjs +30 -1
- package/lib/gateway/front-core.mjs +3 -1
- package/package.json +1 -1
- package/web/app/dist/sw.js +141 -21
- package/web/app/dist/sw.js.map +3 -3
- package/web/app/site/sw.js +61 -7
|
@@ -145,8 +145,13 @@ export async function handle(api, ctx) {
|
|
|
145
145
|
if (acct === cfg?.handle || acct === `${cfg?.handle}@${api.host}`) {
|
|
146
146
|
return send(200, api.selfAccount());
|
|
147
147
|
}
|
|
148
|
+
// By host with its port, as the account is shown; and by hostname alone,
|
|
149
|
+
// for a client that drops the port from an address it was given.
|
|
148
150
|
const hit = Object.entries(api.store.getActors()).find(([u, a]) => {
|
|
149
|
-
try {
|
|
151
|
+
try {
|
|
152
|
+
const at = new URL(u);
|
|
153
|
+
return `${a.preferredUsername}@${at.host}` === acct || `${a.preferredUsername}@${at.hostname}` === acct;
|
|
154
|
+
} catch { return false; }
|
|
150
155
|
});
|
|
151
156
|
return hit ? send(200, api.account(hit[0])) : send(404, { error: 'Record not found' });
|
|
152
157
|
}
|
|
@@ -95,8 +95,16 @@ export async function handle(api, ctx) {
|
|
|
95
95
|
api.store.setScheduled(sched);
|
|
96
96
|
return send(200, api.scheduledJson(entry));
|
|
97
97
|
}
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
let note;
|
|
99
|
+
try {
|
|
100
|
+
note = await api.agent.publisher.publishNote(body.status,
|
|
101
|
+
{ inReplyTo, attachments, visibility, spoilerText });
|
|
102
|
+
} catch (e) {
|
|
103
|
+
// A direct message nobody could be found for: the sender's mistake to
|
|
104
|
+
// fix, said in the client, not a fault of the server.
|
|
105
|
+
if (e.code === 'unaddressed') return send(422, { error: e.message });
|
|
106
|
+
throw e;
|
|
107
|
+
}
|
|
100
108
|
const s = api.store.getStatuses().find(x => x.noteId === note.id);
|
|
101
109
|
return send(200, api.status(s));
|
|
102
110
|
}
|
|
@@ -182,6 +190,12 @@ export async function handle(api, ctx) {
|
|
|
182
190
|
const mContext = /^\/api\/v1\/statuses\/([a-f0-9]+)\/context$/.exec(pathname);
|
|
183
191
|
if (mContext) {
|
|
184
192
|
const noteUrl = api.lookup(mContext[1]).s?.noteId;
|
|
193
|
+
// A Bluesky post's conversation lives on Bluesky: ask for it now, so the
|
|
194
|
+
// replies (and their pictures) are in the mirror before the thread is built.
|
|
195
|
+
const opened = noteUrl && api.store.getStatuses().find(x => x.noteId === noteUrl);
|
|
196
|
+
if (opened?.kind === 'bsky' && typeof api.agent?.bskyfeed?.mirrorThread === 'function') {
|
|
197
|
+
await api.agent.bskyfeed.mirrorThread(noteUrl);
|
|
198
|
+
}
|
|
185
199
|
const all = api.store.getStatuses();
|
|
186
200
|
const byId = new Map(all.map(s => [s.noteId, s]));
|
|
187
201
|
const ancestors = [];
|
|
@@ -18,6 +18,22 @@ export const postUrl = (uri) => {
|
|
|
18
18
|
return m ? `https://bsky.app/profile/${m[1]}/post/${m[2]}` : null;
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
// Every place a post view keeps pictures: a plain image post, the media half
|
|
22
|
+
// of a quote-with-media, and a link card's thumbnail. Bluesky's view types
|
|
23
|
+
// differ by shape, not by a flag, so each is read where it lives.
|
|
24
|
+
export function attachmentsOf(post) {
|
|
25
|
+
const e = post?.embed || {};
|
|
26
|
+
const images = [...(e.images || []), ...(e.media?.images || [])]
|
|
27
|
+
.filter(i => i?.fullsize)
|
|
28
|
+
.map(i => ({ url: i.fullsize, mediaType: 'image/jpeg', description: i.alt || '' }));
|
|
29
|
+
const card = e.external?.thumb || e.media?.external?.thumb;
|
|
30
|
+
if (card) {
|
|
31
|
+
const ext = e.external || e.media.external;
|
|
32
|
+
images.push({ url: card, mediaType: 'image/jpeg', description: ext.title || ext.uri || '' });
|
|
33
|
+
}
|
|
34
|
+
return images;
|
|
35
|
+
}
|
|
36
|
+
|
|
21
37
|
export class BskyFeed {
|
|
22
38
|
constructor({ store, atproto, log = console.log, onNotification = null }) {
|
|
23
39
|
Object.assign(this, { store, atproto, log, onNotification });
|
|
@@ -73,14 +89,27 @@ export class BskyFeed {
|
|
|
73
89
|
}
|
|
74
90
|
|
|
75
91
|
// One Bluesky post into the statuses index. Returns its noteId, new or not.
|
|
92
|
+
// A post seen before is completed, not duplicated: a reply first met as a
|
|
93
|
+
// bare notification record learns its parent and its pictures from the
|
|
94
|
+
// full view when that arrives.
|
|
76
95
|
_mirrorPost(post, { via = null } = {}) {
|
|
77
96
|
const noteId = post.uri;
|
|
78
|
-
const
|
|
79
|
-
|
|
97
|
+
const inReplyTo = post.record?.reply?.parent?.uri || null;
|
|
98
|
+
const attachments = attachmentsOf(post);
|
|
99
|
+
const existing = this.store.getStatuses().find(s => s.noteId === noteId);
|
|
100
|
+
if (existing) {
|
|
101
|
+
const patch = {
|
|
102
|
+
...(inReplyTo && !existing.inReplyTo ? { inReplyTo } : {}),
|
|
103
|
+
...(attachments.length && !existing.attachments?.length ? { attachments } : {}),
|
|
104
|
+
};
|
|
105
|
+
if (Object.keys(patch).length && typeof this.store.updateStatus === 'function') {
|
|
106
|
+
this.store.updateStatus(noteId, patch);
|
|
107
|
+
}
|
|
108
|
+
return { noteId, added: false };
|
|
109
|
+
}
|
|
80
110
|
const actor = this._rememberAuthor(post.author);
|
|
81
111
|
if (this.store.isBlocked(actor)) return { noteId, added: false };
|
|
82
112
|
const text = post.record?.text || '';
|
|
83
|
-
const images = post.embed?.images || [];
|
|
84
113
|
this.store.addStatus({
|
|
85
114
|
noteId, actor,
|
|
86
115
|
content: `<p>${text.replace(/[&<>]/g, c => ({ '&': '&', '<': '<', '>': '>' }[c]))}</p>`,
|
|
@@ -89,13 +118,48 @@ export class BskyFeed {
|
|
|
89
118
|
...(post.cid ? { cid: post.cid } : {}),
|
|
90
119
|
link: postUrl(noteId),
|
|
91
120
|
...(via ? { via } : {}),
|
|
92
|
-
...(
|
|
93
|
-
|
|
94
|
-
} : {}),
|
|
121
|
+
...(inReplyTo ? { inReplyTo } : {}),
|
|
122
|
+
...(attachments.length ? { attachments } : {}),
|
|
95
123
|
});
|
|
96
124
|
return { noteId, added: true };
|
|
97
125
|
}
|
|
98
126
|
|
|
127
|
+
// The conversation under one post, asked for when a post is opened: the
|
|
128
|
+
// ancestors it hangs from and the replies beneath it, each mirrored with
|
|
129
|
+
// the reply link the thread view is built from. View cache only, like the
|
|
130
|
+
// timeline. Returns how many posts were new; a thread Bluesky will not
|
|
131
|
+
// give (gone, blocked, rate-limited) adds none and is logged, not thrown.
|
|
132
|
+
async mirrorThread(uri, { depth = 6 } = {}) {
|
|
133
|
+
if (!this.atproto?.connected()) return 0;
|
|
134
|
+
if (this.quietUntil && Date.now() < this.quietUntil) return 0;
|
|
135
|
+
let out;
|
|
136
|
+
try {
|
|
137
|
+
out = await this.atproto.xrpc('app.bsky.feed.getPostThread', { params: { uri, depth, parentHeight: 40 } });
|
|
138
|
+
} catch (e) {
|
|
139
|
+
if (e.status === 429 || e.status >= 500) this._backOff(e.status, null);
|
|
140
|
+
else this.log(`bskyfeed: thread ${uri}: ${e.message}`);
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
const self = this.atproto.read()?.did;
|
|
144
|
+
const ownMirrors = new Set(this.store.getStatuses().map(s => s.atproto?.uri).filter(Boolean));
|
|
145
|
+
let added = 0;
|
|
146
|
+
const take = (node) => {
|
|
147
|
+
const post = node?.post;
|
|
148
|
+
if (!post?.uri) return; // notFoundPost, blockedPost
|
|
149
|
+
if (post.author?.did === self && ownMirrors.has(post.uri)) return;
|
|
150
|
+
if (this._mirrorPost(post).added) added++;
|
|
151
|
+
};
|
|
152
|
+
for (let up = out?.thread?.parent; up; up = up.parent) take(up);
|
|
153
|
+
const walk = (node, left) => {
|
|
154
|
+
take(node);
|
|
155
|
+
if (left <= 0) return;
|
|
156
|
+
for (const r of node?.replies || []) walk(r, left - 1);
|
|
157
|
+
};
|
|
158
|
+
walk(out?.thread, depth);
|
|
159
|
+
if (added) this.log(`bskyfeed: +${added} from the thread under ${uri}`);
|
|
160
|
+
return added;
|
|
161
|
+
}
|
|
162
|
+
|
|
99
163
|
async sweep() {
|
|
100
164
|
if (!this.atproto?.connected()) return;
|
|
101
165
|
if (this.quietUntil && Date.now() < this.quietUntil) return;
|
|
@@ -142,8 +206,15 @@ export class BskyFeed {
|
|
|
142
206
|
this.store.addNotification({ type: 'follow', actor, bsky: true });
|
|
143
207
|
await this.onNotification?.(n, { actor });
|
|
144
208
|
} else if (n.reason === 'mention' || n.reason === 'reply') {
|
|
145
|
-
|
|
146
|
-
|
|
209
|
+
// The notification carries the bare record — no picture URLs and
|
|
210
|
+
// no view. The post view has both; the record is the fallback when
|
|
211
|
+
// Bluesky will not give the view.
|
|
212
|
+
let view = null;
|
|
213
|
+
try {
|
|
214
|
+
const got = await this.atproto.xrpc('app.bsky.feed.getPosts', { params: { uris: n.uri } });
|
|
215
|
+
view = (got?.posts || [])[0] || null;
|
|
216
|
+
} catch { /* the bare record still stands */ }
|
|
217
|
+
this._mirrorPost(view || { uri: n.uri, cid: n.cid, author: n.author, record: n.record, indexedAt: n.indexedAt });
|
|
147
218
|
this.store.addNotification({ type: 'mention', actor, noteId: n.uri, bsky: true });
|
|
148
219
|
await this.onNotification?.(n, { actor });
|
|
149
220
|
}
|
|
@@ -213,7 +213,13 @@ export async function ingestNote(intake, objectId, actor, { via } = {}) {
|
|
|
213
213
|
...(attachments.length ? { attachments } : {}),
|
|
214
214
|
...(via ? { via } : {}),
|
|
215
215
|
});
|
|
216
|
-
|
|
216
|
+
// A notification for anything addressed to this person: a stranger's
|
|
217
|
+
// post, a reply to one of ours, a direct message, or a post that names
|
|
218
|
+
// us — whoever sent it. A direct message from someone followed used to
|
|
219
|
+
// raise nothing and sat unseen in the private mentions.
|
|
220
|
+
const namesUs = mentions.some((m) => m.href === intake.urls.actor);
|
|
221
|
+
const replyToOurs = !!note.inReplyTo && String(note.inReplyTo).startsWith(intake.urls.notes);
|
|
222
|
+
if (!followed || replyToOurs || direct || namesUs) {
|
|
217
223
|
intake.store.addNotification({ type: 'mention', actor: author, noteId: note.id });
|
|
218
224
|
}
|
|
219
225
|
if (note.inReplyTo && String(note.inReplyTo).startsWith(intake.urls.notes)) {
|
|
@@ -83,6 +83,22 @@ export async function mentionsFor(publisher, content, inReplyTo) {
|
|
|
83
83
|
return mentions;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
// A direct post goes only to the people it names, so one that names nobody
|
|
87
|
+
// the agent can find would be kept in the private container and delivered
|
|
88
|
+
// to no one, with nothing said. Refused instead, before anything is written,
|
|
89
|
+
// with the handles that did not resolve — that is what the sender can act on.
|
|
90
|
+
export function assertDirectAddressed(content, mentions) {
|
|
91
|
+
const wanted = [...new Set(wire.mentionsIn(content))];
|
|
92
|
+
const missing = wanted.filter((h) => !mentions.some((m) => m.handle === h));
|
|
93
|
+
let why = null;
|
|
94
|
+
if (!wanted.length) why = 'a direct message names who it is for — mention them as @name@host';
|
|
95
|
+
else if (missing.length) why = `could not find ${missing.map((h) => '@' + h).join(', ')} — the direct message was not sent`;
|
|
96
|
+
if (!why) return;
|
|
97
|
+
const e = new Error(why);
|
|
98
|
+
e.code = 'unaddressed';
|
|
99
|
+
throw e;
|
|
100
|
+
}
|
|
101
|
+
|
|
86
102
|
export async function publishNote(publisher, content, { inReplyTo, attachments, visibility = 'public', spoilerText = null } = {}) {
|
|
87
103
|
const { urls } = publisher;
|
|
88
104
|
const priv = visibility === 'private' || visibility === 'direct';
|
|
@@ -93,6 +109,7 @@ export async function publishNote(publisher, content, { inReplyTo, attachments,
|
|
|
93
109
|
const published = new Date().toISOString();
|
|
94
110
|
const slug = published.slice(0, 10) + '-' + crypto.randomBytes(4).toString('hex');
|
|
95
111
|
const mentions = await publisher._mentionsFor(content, inReplyTo);
|
|
112
|
+
if (visibility === 'direct') assertDirectAddressed(content, mentions);
|
|
96
113
|
const note = wire.noteDoc({ urls, slug, content, published, inReplyTo, attachments, mentions,
|
|
97
114
|
visibility, summary: spoilerText, container: priv ? urls.privateNotes : urls.notes });
|
|
98
115
|
|
|
@@ -9,6 +9,7 @@ import crypto from 'node:crypto';
|
|
|
9
9
|
import * as wire from '../wire.mjs';
|
|
10
10
|
import * as polls from '../polls.mjs';
|
|
11
11
|
import * as podNotes from '../../pod/notes.mjs';
|
|
12
|
+
import { assertDirectAddressed } from './notes.mjs';
|
|
12
13
|
|
|
13
14
|
// How long a poll gathers votes before its Question is rewritten. Every vote
|
|
14
15
|
// changes a number other servers re-read, and rewriting per vote would make a
|
|
@@ -52,6 +53,7 @@ export async function publishQuestion(publisher, content, { options = [], multip
|
|
|
52
53
|
const published = new Date().toISOString();
|
|
53
54
|
const slug = published.slice(0, 10) + '-' + crypto.randomBytes(4).toString('hex');
|
|
54
55
|
const mentions = await publisher._mentionsFor(content, inReplyTo);
|
|
56
|
+
if (visibility === 'direct') assertDirectAddressed(content, mentions);
|
|
55
57
|
const poll = {
|
|
56
58
|
multiple: !!multiple,
|
|
57
59
|
expiresAt: expiresAt || null,
|
package/lib/core/store.mjs
CHANGED
|
@@ -502,5 +502,34 @@ export class PodStore {
|
|
|
502
502
|
this.cache.set('ids.json', ids);
|
|
503
503
|
return id;
|
|
504
504
|
}
|
|
505
|
-
|
|
505
|
+
// The map first, for ids minted under an older scheme. Then, because the
|
|
506
|
+
// id IS the hash of the url, whatever this store knows is scanned for the
|
|
507
|
+
// url that hashes to it — actors, posts, contacts, requests, media. The
|
|
508
|
+
// browser build's worker is stopped whenever it idles, and the in-memory
|
|
509
|
+
// map went with it: a client clicking an account it had just been shown
|
|
510
|
+
// reached a fresh worker that held the actor and could not name it.
|
|
511
|
+
urlFor(id) {
|
|
512
|
+
const ids = this.getIds();
|
|
513
|
+
if (ids[id]) return ids[id];
|
|
514
|
+
if (!/^[a-f0-9]{16}$/u.test(String(id))) return null;
|
|
515
|
+
const hash = (u) => crypto.createHash('sha256').update(u).digest('hex').slice(0, 16);
|
|
516
|
+
const seen = new Set();
|
|
517
|
+
const candidates = function* (store) {
|
|
518
|
+
for (const u of Object.keys(store.getActors())) yield u;
|
|
519
|
+
for (const st of store.getStatuses()) {
|
|
520
|
+
yield st.noteId; yield st.actor;
|
|
521
|
+
for (const a of st.attachments || []) if (a?.url) yield a.url;
|
|
522
|
+
}
|
|
523
|
+
const c = store.getContacts();
|
|
524
|
+
for (const f of [...c.followers, ...c.following]) if (f?.actor) yield f.actor;
|
|
525
|
+
for (const r of store.getRequests()) if (r?.actor) yield r.actor;
|
|
526
|
+
for (const u of Object.keys(store.getMedia())) yield u;
|
|
527
|
+
};
|
|
528
|
+
for (const u of candidates(this)) {
|
|
529
|
+
if (typeof u !== 'string' || seen.has(u)) continue;
|
|
530
|
+
seen.add(u);
|
|
531
|
+
if (hash(u) === id) { ids[id] = u; this.cache.set('ids.json', ids); return u; }
|
|
532
|
+
}
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
506
535
|
}
|
|
@@ -588,8 +588,10 @@ async function route(request, ctx) {
|
|
|
588
588
|
if (up.rest === 'ap/inbox/' || up.rest === 'ap/inbox') {
|
|
589
589
|
if (request.method !== 'POST') return { status: 405, headers: {}, body: '' };
|
|
590
590
|
const policy = await policyFor(rec, ctx.fetchImpl || fetch);
|
|
591
|
-
const { status } = await handleDelivery(request, identFor(rec, policy),
|
|
591
|
+
const { status, reason } = await handleDelivery(request, identFor(rec, policy),
|
|
592
592
|
{ podPut: (u, b, ct) => ctx.podPut(up.handle, u, b, ct), fetchImpl: ctx.fetchImpl });
|
|
593
|
+
// One line per delivery, so "did it arrive at the door" has an answer.
|
|
594
|
+
console.log(`door @${up.handle}: delivery → ${status} (${reason})`);
|
|
593
595
|
return { status, headers: {}, body: '' };
|
|
594
596
|
}
|
|
595
597
|
|
package/package.json
CHANGED
package/web/app/dist/sw.js
CHANGED
|
@@ -34555,8 +34555,40 @@ var PodStore = class {
|
|
|
34555
34555
|
this.cache.set("ids.json", ids);
|
|
34556
34556
|
return id;
|
|
34557
34557
|
}
|
|
34558
|
+
// The map first, for ids minted under an older scheme. Then, because the
|
|
34559
|
+
// id IS the hash of the url, whatever this store knows is scanned for the
|
|
34560
|
+
// url that hashes to it — actors, posts, contacts, requests, media. The
|
|
34561
|
+
// browser build's worker is stopped whenever it idles, and the in-memory
|
|
34562
|
+
// map went with it: a client clicking an account it had just been shown
|
|
34563
|
+
// reached a fresh worker that held the actor and could not name it.
|
|
34558
34564
|
urlFor(id) {
|
|
34559
|
-
|
|
34565
|
+
const ids = this.getIds();
|
|
34566
|
+
if (ids[id]) return ids[id];
|
|
34567
|
+
if (!/^[a-f0-9]{16}$/u.test(String(id))) return null;
|
|
34568
|
+
const hash = (u) => node_crypto_default.createHash("sha256").update(u).digest("hex").slice(0, 16);
|
|
34569
|
+
const seen = /* @__PURE__ */ new Set();
|
|
34570
|
+
const candidates = function* (store) {
|
|
34571
|
+
for (const u of Object.keys(store.getActors())) yield u;
|
|
34572
|
+
for (const st2 of store.getStatuses()) {
|
|
34573
|
+
yield st2.noteId;
|
|
34574
|
+
yield st2.actor;
|
|
34575
|
+
for (const a of st2.attachments || []) if (a?.url) yield a.url;
|
|
34576
|
+
}
|
|
34577
|
+
const c = store.getContacts();
|
|
34578
|
+
for (const f of [...c.followers, ...c.following]) if (f?.actor) yield f.actor;
|
|
34579
|
+
for (const r of store.getRequests()) if (r?.actor) yield r.actor;
|
|
34580
|
+
for (const u of Object.keys(store.getMedia())) yield u;
|
|
34581
|
+
};
|
|
34582
|
+
for (const u of candidates(this)) {
|
|
34583
|
+
if (typeof u !== "string" || seen.has(u)) continue;
|
|
34584
|
+
seen.add(u);
|
|
34585
|
+
if (hash(u) === id) {
|
|
34586
|
+
ids[id] = u;
|
|
34587
|
+
this.cache.set("ids.json", ids);
|
|
34588
|
+
return u;
|
|
34589
|
+
}
|
|
34590
|
+
}
|
|
34591
|
+
return null;
|
|
34560
34592
|
}
|
|
34561
34593
|
};
|
|
34562
34594
|
|
|
@@ -55914,6 +55946,17 @@ async function mentionsFor(publisher, content, inReplyTo) {
|
|
|
55914
55946
|
}
|
|
55915
55947
|
return mentions;
|
|
55916
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
|
+
}
|
|
55917
55960
|
async function publishNote(publisher, content, { inReplyTo, attachments, visibility = "public", spoilerText = null } = {}) {
|
|
55918
55961
|
const { urls } = publisher;
|
|
55919
55962
|
const priv = visibility === "private" || visibility === "direct";
|
|
@@ -55924,6 +55967,7 @@ async function publishNote(publisher, content, { inReplyTo, attachments, visibil
|
|
|
55924
55967
|
const published = (/* @__PURE__ */ new Date()).toISOString();
|
|
55925
55968
|
const slug = published.slice(0, 10) + "-" + node_crypto_default.randomBytes(4).toString("hex");
|
|
55926
55969
|
const mentions = await publisher._mentionsFor(content, inReplyTo);
|
|
55970
|
+
if (visibility === "direct") assertDirectAddressed(content, mentions);
|
|
55927
55971
|
const note = noteDoc({
|
|
55928
55972
|
urls,
|
|
55929
55973
|
slug,
|
|
@@ -56125,6 +56169,7 @@ async function publishQuestion(publisher, content, {
|
|
|
56125
56169
|
const published = (/* @__PURE__ */ new Date()).toISOString();
|
|
56126
56170
|
const slug = published.slice(0, 10) + "-" + node_crypto_default.randomBytes(4).toString("hex");
|
|
56127
56171
|
const mentions = await publisher._mentionsFor(content, inReplyTo);
|
|
56172
|
+
if (visibility === "direct") assertDirectAddressed(content, mentions);
|
|
56128
56173
|
const poll = {
|
|
56129
56174
|
multiple: !!multiple,
|
|
56130
56175
|
expiresAt: expiresAt || null,
|
|
@@ -57439,9 +57484,9 @@ async function onUpdate(intake, activity, actor) {
|
|
|
57439
57484
|
const note = await intake.fetchAP(objectId);
|
|
57440
57485
|
if (!note) throw new Error(`cannot refetch ${objectId} \u2014 will retry`);
|
|
57441
57486
|
if (note.id !== objectId || !isContentType(note.type)) return `object not verifiable content (${objectId}, ${note.type})`;
|
|
57442
|
-
const { attachmentsOf:
|
|
57487
|
+
const { attachmentsOf: attachmentsOf3, titledContent: titledContent2 } = await Promise.resolve().then(() => (init_wire(), wire_exports));
|
|
57443
57488
|
const content = titledContent2(note);
|
|
57444
|
-
const attachments =
|
|
57489
|
+
const attachments = attachmentsOf3(note);
|
|
57445
57490
|
const freshPoll = pollOf(note);
|
|
57446
57491
|
const freshEmojis = emojisOf(note);
|
|
57447
57492
|
intake.store.updateStatus(objectId, {
|
|
@@ -57575,8 +57620,8 @@ async function ingestNote(intake, objectId, actor, { via } = {}) {
|
|
|
57575
57620
|
const note = await intake.fetchAP(objectId);
|
|
57576
57621
|
if (!note) return `object fetch failed (${objectId})`;
|
|
57577
57622
|
if (note.id !== objectId || !isContentType(note.type)) return `object not verifiable content (${objectId}, ${note.type})`;
|
|
57578
|
-
const { attachmentsOf:
|
|
57579
|
-
const attachments =
|
|
57623
|
+
const { attachmentsOf: attachmentsOf3, titledContent: titledContent2 } = await Promise.resolve().then(() => (init_wire(), wire_exports));
|
|
57624
|
+
const attachments = attachmentsOf3(note);
|
|
57580
57625
|
const content = titledContent2(note);
|
|
57581
57626
|
const author = authorOf(note, actor);
|
|
57582
57627
|
if (!author) return `object names an author its origin does not vouch for (${objectId})`;
|
|
@@ -57620,7 +57665,9 @@ async function ingestNote(intake, objectId, actor, { via } = {}) {
|
|
|
57620
57665
|
...attachments.length ? { attachments } : {},
|
|
57621
57666
|
...via ? { via } : {}
|
|
57622
57667
|
});
|
|
57623
|
-
|
|
57668
|
+
const namesUs = mentions.some((m) => m.href === intake.urls.actor);
|
|
57669
|
+
const replyToOurs = !!note.inReplyTo && String(note.inReplyTo).startsWith(intake.urls.notes);
|
|
57670
|
+
if (!followed || replyToOurs || direct || namesUs) {
|
|
57624
57671
|
intake.store.addNotification({ type: "mention", actor: author, noteId: note.id });
|
|
57625
57672
|
}
|
|
57626
57673
|
if (note.inReplyTo && String(note.inReplyTo).startsWith(intake.urls.notes)) {
|
|
@@ -64496,6 +64543,16 @@ var postUrl = (uri) => {
|
|
|
64496
64543
|
const m = String(uri).match(/^at:\/\/([^/]+)\/[^/]+\/(.+)$/);
|
|
64497
64544
|
return m ? `https://bsky.app/profile/${m[1]}/post/${m[2]}` : null;
|
|
64498
64545
|
};
|
|
64546
|
+
function attachmentsOf2(post) {
|
|
64547
|
+
const e = post?.embed || {};
|
|
64548
|
+
const images = [...e.images || [], ...e.media?.images || []].filter((i) => i?.fullsize).map((i) => ({ url: i.fullsize, mediaType: "image/jpeg", description: i.alt || "" }));
|
|
64549
|
+
const card = e.external?.thumb || e.media?.external?.thumb;
|
|
64550
|
+
if (card) {
|
|
64551
|
+
const ext = e.external || e.media.external;
|
|
64552
|
+
images.push({ url: card, mediaType: "image/jpeg", description: ext.title || ext.uri || "" });
|
|
64553
|
+
}
|
|
64554
|
+
return images;
|
|
64555
|
+
}
|
|
64499
64556
|
var BskyFeed = class {
|
|
64500
64557
|
constructor({ store, atproto, log: log2 = console.log, onNotification = null }) {
|
|
64501
64558
|
Object.assign(this, { store, atproto, log: log2, onNotification });
|
|
@@ -64551,14 +64608,27 @@ var BskyFeed = class {
|
|
|
64551
64608
|
return url;
|
|
64552
64609
|
}
|
|
64553
64610
|
// One Bluesky post into the statuses index. Returns its noteId, new or not.
|
|
64611
|
+
// A post seen before is completed, not duplicated: a reply first met as a
|
|
64612
|
+
// bare notification record learns its parent and its pictures from the
|
|
64613
|
+
// full view when that arrives.
|
|
64554
64614
|
_mirrorPost(post, { via = null } = {}) {
|
|
64555
64615
|
const noteId = post.uri;
|
|
64556
|
-
const
|
|
64557
|
-
|
|
64616
|
+
const inReplyTo = post.record?.reply?.parent?.uri || null;
|
|
64617
|
+
const attachments = attachmentsOf2(post);
|
|
64618
|
+
const existing = this.store.getStatuses().find((s) => s.noteId === noteId);
|
|
64619
|
+
if (existing) {
|
|
64620
|
+
const patch = {
|
|
64621
|
+
...inReplyTo && !existing.inReplyTo ? { inReplyTo } : {},
|
|
64622
|
+
...attachments.length && !existing.attachments?.length ? { attachments } : {}
|
|
64623
|
+
};
|
|
64624
|
+
if (Object.keys(patch).length && typeof this.store.updateStatus === "function") {
|
|
64625
|
+
this.store.updateStatus(noteId, patch);
|
|
64626
|
+
}
|
|
64627
|
+
return { noteId, added: false };
|
|
64628
|
+
}
|
|
64558
64629
|
const actor = this._rememberAuthor(post.author);
|
|
64559
64630
|
if (this.store.isBlocked(actor)) return { noteId, added: false };
|
|
64560
64631
|
const text = post.record?.text || "";
|
|
64561
|
-
const images = post.embed?.images || [];
|
|
64562
64632
|
this.store.addStatus({
|
|
64563
64633
|
noteId,
|
|
64564
64634
|
actor,
|
|
@@ -64568,12 +64638,46 @@ var BskyFeed = class {
|
|
|
64568
64638
|
...post.cid ? { cid: post.cid } : {},
|
|
64569
64639
|
link: postUrl(noteId),
|
|
64570
64640
|
...via ? { via } : {},
|
|
64571
|
-
...
|
|
64572
|
-
|
|
64573
|
-
} : {}
|
|
64641
|
+
...inReplyTo ? { inReplyTo } : {},
|
|
64642
|
+
...attachments.length ? { attachments } : {}
|
|
64574
64643
|
});
|
|
64575
64644
|
return { noteId, added: true };
|
|
64576
64645
|
}
|
|
64646
|
+
// The conversation under one post, asked for when a post is opened: the
|
|
64647
|
+
// ancestors it hangs from and the replies beneath it, each mirrored with
|
|
64648
|
+
// the reply link the thread view is built from. View cache only, like the
|
|
64649
|
+
// timeline. Returns how many posts were new; a thread Bluesky will not
|
|
64650
|
+
// give (gone, blocked, rate-limited) adds none and is logged, not thrown.
|
|
64651
|
+
async mirrorThread(uri, { depth = 6 } = {}) {
|
|
64652
|
+
if (!this.atproto?.connected()) return 0;
|
|
64653
|
+
if (this.quietUntil && Date.now() < this.quietUntil) return 0;
|
|
64654
|
+
let out;
|
|
64655
|
+
try {
|
|
64656
|
+
out = await this.atproto.xrpc("app.bsky.feed.getPostThread", { params: { uri, depth, parentHeight: 40 } });
|
|
64657
|
+
} catch (e) {
|
|
64658
|
+
if (e.status === 429 || e.status >= 500) this._backOff(e.status, null);
|
|
64659
|
+
else this.log(`bskyfeed: thread ${uri}: ${e.message}`);
|
|
64660
|
+
return 0;
|
|
64661
|
+
}
|
|
64662
|
+
const self2 = this.atproto.read()?.did;
|
|
64663
|
+
const ownMirrors = new Set(this.store.getStatuses().map((s) => s.atproto?.uri).filter(Boolean));
|
|
64664
|
+
let added = 0;
|
|
64665
|
+
const take2 = (node) => {
|
|
64666
|
+
const post = node?.post;
|
|
64667
|
+
if (!post?.uri) return;
|
|
64668
|
+
if (post.author?.did === self2 && ownMirrors.has(post.uri)) return;
|
|
64669
|
+
if (this._mirrorPost(post).added) added++;
|
|
64670
|
+
};
|
|
64671
|
+
for (let up = out?.thread?.parent; up; up = up.parent) take2(up);
|
|
64672
|
+
const walk = (node, left) => {
|
|
64673
|
+
take2(node);
|
|
64674
|
+
if (left <= 0) return;
|
|
64675
|
+
for (const r of node?.replies || []) walk(r, left - 1);
|
|
64676
|
+
};
|
|
64677
|
+
walk(out?.thread, depth);
|
|
64678
|
+
if (added) this.log(`bskyfeed: +${added} from the thread under ${uri}`);
|
|
64679
|
+
return added;
|
|
64680
|
+
}
|
|
64577
64681
|
async sweep() {
|
|
64578
64682
|
if (!this.atproto?.connected()) return;
|
|
64579
64683
|
if (this.quietUntil && Date.now() < this.quietUntil) return;
|
|
@@ -64612,8 +64716,13 @@ var BskyFeed = class {
|
|
|
64612
64716
|
this.store.addNotification({ type: "follow", actor, bsky: true });
|
|
64613
64717
|
await this.onNotification?.(n, { actor });
|
|
64614
64718
|
} else if (n.reason === "mention" || n.reason === "reply") {
|
|
64615
|
-
|
|
64616
|
-
|
|
64719
|
+
let view = null;
|
|
64720
|
+
try {
|
|
64721
|
+
const got = await this.atproto.xrpc("app.bsky.feed.getPosts", { params: { uris: n.uri } });
|
|
64722
|
+
view = (got?.posts || [])[0] || null;
|
|
64723
|
+
} catch {
|
|
64724
|
+
}
|
|
64725
|
+
this._mirrorPost(view || { uri: n.uri, cid: n.cid, author: n.author, record: n.record, indexedAt: n.indexedAt });
|
|
64617
64726
|
this.store.addNotification({ type: "mention", actor, noteId: n.uri, bsky: true });
|
|
64618
64727
|
await this.onNotification?.(n, { actor });
|
|
64619
64728
|
}
|
|
@@ -65431,7 +65540,8 @@ async function handle4(api, ctx) {
|
|
|
65431
65540
|
}
|
|
65432
65541
|
const hit = Object.entries(api.store.getActors()).find(([u, a]) => {
|
|
65433
65542
|
try {
|
|
65434
|
-
|
|
65543
|
+
const at = new URL(u);
|
|
65544
|
+
return `${a.preferredUsername}@${at.host}` === acct || `${a.preferredUsername}@${at.hostname}` === acct;
|
|
65435
65545
|
} catch {
|
|
65436
65546
|
return false;
|
|
65437
65547
|
}
|
|
@@ -65863,10 +65973,16 @@ async function handle6(api, ctx) {
|
|
|
65863
65973
|
api.store.setScheduled(sched);
|
|
65864
65974
|
return send(200, api.scheduledJson(entry));
|
|
65865
65975
|
}
|
|
65866
|
-
|
|
65867
|
-
|
|
65868
|
-
|
|
65869
|
-
|
|
65976
|
+
let note;
|
|
65977
|
+
try {
|
|
65978
|
+
note = await api.agent.publisher.publishNote(
|
|
65979
|
+
body.status,
|
|
65980
|
+
{ inReplyTo, attachments, visibility, spoilerText }
|
|
65981
|
+
);
|
|
65982
|
+
} catch (e) {
|
|
65983
|
+
if (e.code === "unaddressed") return send(422, { error: e.message });
|
|
65984
|
+
throw e;
|
|
65985
|
+
}
|
|
65870
65986
|
const s = api.store.getStatuses().find((x) => x.noteId === note.id);
|
|
65871
65987
|
return send(200, api.status(s));
|
|
65872
65988
|
}
|
|
@@ -65945,6 +66061,10 @@ async function handle6(api, ctx) {
|
|
|
65945
66061
|
const mContext = /^\/api\/v1\/statuses\/([a-f0-9]+)\/context$/.exec(pathname);
|
|
65946
66062
|
if (mContext) {
|
|
65947
66063
|
const noteUrl = api.lookup(mContext[1]).s?.noteId;
|
|
66064
|
+
const opened = noteUrl && api.store.getStatuses().find((x) => x.noteId === noteUrl);
|
|
66065
|
+
if (opened?.kind === "bsky" && typeof api.agent?.bskyfeed?.mirrorThread === "function") {
|
|
66066
|
+
await api.agent.bskyfeed.mirrorThread(noteUrl);
|
|
66067
|
+
}
|
|
65948
66068
|
const all = api.store.getStatuses();
|
|
65949
66069
|
const byId = new Map(all.map((s) => [s.noteId, s]));
|
|
65950
66070
|
const ancestors = [];
|
|
@@ -66380,8 +66500,8 @@ var TagFeed = class {
|
|
|
66380
66500
|
await this.intake.fetchAP(author).catch(() => {
|
|
66381
66501
|
});
|
|
66382
66502
|
}
|
|
66383
|
-
const { attachmentsOf:
|
|
66384
|
-
const attachments =
|
|
66503
|
+
const { attachmentsOf: attachmentsOf3, titledContent: titledContent2 } = await Promise.resolve().then(() => (init_wire(), wire_exports));
|
|
66504
|
+
const attachments = attachmentsOf3(note);
|
|
66385
66505
|
this.store.addStatus({
|
|
66386
66506
|
noteId,
|
|
66387
66507
|
actor: author,
|