fedipod-server 0.17.0 → 0.18.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/c2s.mjs +95 -61
- package/lib/core/contexts/anno.json +126 -0
- package/lib/core/contexts/index.mjs +4 -0
- package/lib/core/contexts/map.json +2 -1
- package/lib/core/intake/index.mjs +32 -5
- package/lib/core/publisher/index.mjs +15 -1
- package/lib/core/publisher/notes.mjs +84 -2
- package/lib/core/publisher/questions.mjs +1 -0
- package/lib/core/publisher/restore.mjs +27 -2
- package/lib/core/social.mjs +1 -0
- package/lib/core/store.mjs +4 -0
- package/lib/core/wire.mjs +16 -12
- package/lib/gateway/front-core.mjs +43 -1
- package/lib/gateway/gateway-core.mjs +40 -0
- package/lib/pod/actor.mjs +2 -2
- package/lib/pod/transport.mjs +9 -3
- package/package.json +1 -1
- package/run-agent.mjs +9 -0
- package/web/admin/index.html +2 -0
- package/web/admin/upkeep.js +9 -1
- package/web/app/README.md +1 -1
- package/web/app/agent.mjs +10 -0
- package/web/app/boot.mjs +49 -10
- package/web/app/dist/boot.js +47 -7
- package/web/app/dist/boot.js.map +2 -2
- package/web/app/dist/sw.js +1325 -661
- package/web/app/dist/sw.js.map +4 -4
- package/web/app/index.html +16 -6
- package/web/app/site/admin/index.html +2 -0
- package/web/app/site/admin/upkeep.js +9 -1
- package/web/app/site/boot.js +89 -37
- package/web/app/site/index.html +16 -6
- package/web/app/site/sw.js +1326 -662
- package/web/front/#new-account.html# +0 -43
- package/web/front/new-account.html~ +0 -50
package/lib/client/c2s.mjs
CHANGED
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
// GETs on the actor and outbox are redirects: the pod's documents are the
|
|
9
9
|
// canonical ones, and a second renderer here would only drift from them.
|
|
10
10
|
//
|
|
11
|
+
// `dispatch` takes an activity and answers with { status, body, headers }; it
|
|
12
|
+
// touches no request or response. The HTTP handler below is one caller. The
|
|
13
|
+
// other is the inbox drain, handing over an activity the Gateway took at the
|
|
14
|
+
// outbox door on the owner's behalf and stamped as theirs.
|
|
15
|
+
//
|
|
11
16
|
// The inbox is the exception, and has to be. Deliveries land in a container on
|
|
12
17
|
// the pod which the drain empties as it handles each item, so reading that
|
|
13
18
|
// container tells the owner only what has not been dealt with yet. What was
|
|
@@ -18,6 +23,7 @@
|
|
|
18
23
|
import * as social from '../core/social.mjs';
|
|
19
24
|
import * as wire from '../core/wire.mjs';
|
|
20
25
|
import { readLenient } from '../core/as2.mjs';
|
|
26
|
+
import { safeSlug } from '../core/publisher/notes.mjs';
|
|
21
27
|
|
|
22
28
|
const MAX_BODY = 512 * 1024; // same ceiling the inbox drain enforces
|
|
23
29
|
|
|
@@ -154,7 +160,10 @@ export class C2S {
|
|
|
154
160
|
async handle(req, res, pathname, url) { // eslint-disable-line no-unused-vars
|
|
155
161
|
if (pathname !== '/ap/outbox' && pathname !== '/ap/actor' && pathname !== '/ap/inbox') return false;
|
|
156
162
|
if (req.method === 'OPTIONS') {
|
|
157
|
-
|
|
163
|
+
// Accept-Post is what a client such as dokieli reads to choose a format;
|
|
164
|
+
// naming JSON only is what makes it send JSON-LD rather than HTML.
|
|
165
|
+
res.writeHead(204, pathname === '/ap/inbox' ? { allow: 'GET, OPTIONS' }
|
|
166
|
+
: { allow: 'GET, POST, OPTIONS', 'accept-post': 'application/ld+json, application/activity+json' });
|
|
158
167
|
res.end(); return true;
|
|
159
168
|
}
|
|
160
169
|
if (!this.agent.configured() || !this.urls) {
|
|
@@ -196,9 +205,16 @@ export class C2S {
|
|
|
196
205
|
// takes decisions from it and the publisher builds the document that is
|
|
197
206
|
// actually posted, so nothing a client sent is republished verbatim and a
|
|
198
207
|
// term it aliased still means what it says.
|
|
199
|
-
|
|
208
|
+
// `raw` is the document as the client wrote it. The graph is what
|
|
209
|
+
// decisions are taken from; the bytes are what an object that is stored
|
|
210
|
+
// as sent (publishObject) is stored from — a read against our contexts
|
|
211
|
+
// rewrites a stranger's terms to full IRIs, which is right for reading and
|
|
212
|
+
// wrong for keeping.
|
|
213
|
+
let activity; let raw = null;
|
|
200
214
|
try {
|
|
201
|
-
const
|
|
215
|
+
const body = await readBody(req);
|
|
216
|
+
try { raw = JSON.parse(body); } catch { raw = null; }
|
|
217
|
+
const read = await readLenient(body);
|
|
202
218
|
activity = read.view ?? read.doc;
|
|
203
219
|
} catch (e) {
|
|
204
220
|
return this.send(res, 400, { error: `unreadable body: ${e.message}` });
|
|
@@ -206,34 +222,47 @@ export class C2S {
|
|
|
206
222
|
if (!activity || typeof activity !== 'object' || Array.isArray(activity) || !activity.type) {
|
|
207
223
|
return this.send(res, 400, { error: 'a typed ActivityStreams object is required' });
|
|
208
224
|
}
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
try {
|
|
216
|
-
return await this.dispatch(res, activity);
|
|
217
|
-
} catch (e) {
|
|
218
|
-
this.log(`c2s ${activity.type}: ${e.message}`);
|
|
219
|
-
return this.send(res, 422, { error: e.message || String(e) });
|
|
220
|
-
}
|
|
225
|
+
// The name the client asks for its new document (LDP's Slug), taken when
|
|
226
|
+
// it is plain and free — it is what lets the client know the address of
|
|
227
|
+
// what it made before anything answers.
|
|
228
|
+
const slug = safeSlug(req.headers.slug) || null;
|
|
229
|
+
const r = await this.dispatch(activity, { slug, raw });
|
|
230
|
+
return this.send(res, r.status, r.body, r.headers);
|
|
221
231
|
}
|
|
222
232
|
|
|
223
233
|
// Addressing → the facade's four visibilities, inverting the table the
|
|
224
|
-
// composer writes (wire.
|
|
225
|
-
//
|
|
234
|
+
// composer writes (wire.addressing). Nothing stated is a public post — what
|
|
235
|
+
// every client means by a post with no audience chosen, and what a client
|
|
236
|
+
// that never addresses (dokieli's annotations) needs.
|
|
226
237
|
visibilityOf(activity, object) {
|
|
227
238
|
const to = arr(activity.to ?? object?.to).map(idOf);
|
|
228
239
|
const cc = arr(activity.cc ?? object?.cc).map(idOf);
|
|
229
|
-
if (!to.length && !cc.length) return
|
|
240
|
+
if (!to.length && !cc.length) return 'public';
|
|
230
241
|
if (to.includes(wire.PUBLIC)) return 'public';
|
|
231
242
|
if (cc.includes(wire.PUBLIC)) return 'unlisted';
|
|
232
243
|
if (to.includes(this.urls.followers)) return 'private';
|
|
233
244
|
return 'direct';
|
|
234
245
|
}
|
|
235
246
|
|
|
236
|
-
async dispatch(
|
|
247
|
+
async dispatch(activity, { slug = null, raw = null } = {}) {
|
|
248
|
+
const reply = (status, body, headers = {}) => ({ status, body, headers });
|
|
249
|
+
if (!activity || typeof activity !== 'object' || Array.isArray(activity) || !activity.type) {
|
|
250
|
+
return reply(400, { error: 'a typed ActivityStreams object is required' });
|
|
251
|
+
}
|
|
252
|
+
// A bare object arrives without an activity around it; the server supplies
|
|
253
|
+
// the Create (§6.2.1), carrying the object's own addressing up onto it.
|
|
254
|
+
if (!ACTIVITY_TYPES.has(activity.type)) {
|
|
255
|
+
activity = { type: 'Create', object: activity, to: activity.to, cc: activity.cc };
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
return await this._dispatch(activity, { slug, raw, reply });
|
|
259
|
+
} catch (e) {
|
|
260
|
+
this.log(`c2s ${activity?.type}: ${e.message}`);
|
|
261
|
+
return reply(422, { error: e.message || String(e) });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async _dispatch(activity, { slug, raw, reply }) {
|
|
237
266
|
const agent = this.agent;
|
|
238
267
|
const object = typeof activity.object === 'object' && activity.object !== null
|
|
239
268
|
? activity.object : null;
|
|
@@ -242,18 +271,22 @@ export class C2S {
|
|
|
242
271
|
switch (activity.type) {
|
|
243
272
|
case 'Create': {
|
|
244
273
|
const makes = object?.type || 'Note';
|
|
245
|
-
if (!object
|
|
246
|
-
return this.send(res, 422, { error: 'only a Note or a Question (or a bare Note) can be created here' });
|
|
247
|
-
}
|
|
274
|
+
if (!object) return reply(422, { error: 'a Create carries the object it creates' });
|
|
248
275
|
const visibility = this.visibilityOf(activity, object);
|
|
249
|
-
|
|
250
|
-
|
|
276
|
+
// Not a Note and not a poll: stored as sent, under this actor, and
|
|
277
|
+
// the Create around it delivered — a Web Annotation, for one.
|
|
278
|
+
if (makes !== 'Note' && makes !== 'Question') {
|
|
279
|
+
const asSent = raw && typeof raw === 'object' && !Array.isArray(raw)
|
|
280
|
+
? (ACTIVITY_TYPES.has(raw.type) ? (raw.object && typeof raw.object === 'object' ? raw.object : null) : raw)
|
|
281
|
+
: null;
|
|
282
|
+
const made = await agent.publisher.publishObject(asSent || object, { visibility, slug });
|
|
283
|
+
return reply(201, { id: made.createId, object: made.id }, { location: made.createId });
|
|
251
284
|
}
|
|
252
285
|
// `source.content` is the client's plain text when it sends one; bare
|
|
253
286
|
// `content` is TREATED as plain text and escaped — markup survives as
|
|
254
287
|
// visible characters rather than as markup. Documented v1 limit.
|
|
255
288
|
const text = String(object.source?.content ?? object.content ?? '');
|
|
256
|
-
if (!text.trim()) return
|
|
289
|
+
if (!text.trim()) return reply(422, { error: 'the note has no content' });
|
|
257
290
|
|
|
258
291
|
// A Question is a poll: the choices are in oneOf (pick one) or anyOf
|
|
259
292
|
// (pick several), each naming itself, and endTime is when it shuts.
|
|
@@ -270,11 +303,11 @@ export class C2S {
|
|
|
270
303
|
visibility,
|
|
271
304
|
spoilerText: object.summary || null,
|
|
272
305
|
});
|
|
273
|
-
return
|
|
306
|
+
return reply(201,
|
|
274
307
|
{ id: wire.createActivityId(question.id), object: question.id },
|
|
275
308
|
{ location: wire.createActivityId(question.id) });
|
|
276
309
|
} catch (e) {
|
|
277
|
-
return
|
|
310
|
+
return reply(422, { error: e.message });
|
|
278
311
|
}
|
|
279
312
|
}
|
|
280
313
|
|
|
@@ -287,22 +320,23 @@ export class C2S {
|
|
|
287
320
|
attachments,
|
|
288
321
|
visibility,
|
|
289
322
|
spoilerText: object.summary || null,
|
|
323
|
+
slug,
|
|
290
324
|
});
|
|
291
|
-
return
|
|
325
|
+
return reply(201, { id: wire.createActivityId(note.id), object: note.id },
|
|
292
326
|
{ location: wire.createActivityId(note.id) });
|
|
293
327
|
}
|
|
294
328
|
|
|
295
329
|
case 'Update': {
|
|
296
330
|
if (objectId === this.urls.actor) {
|
|
297
|
-
return
|
|
331
|
+
return reply(422, { error: 'edit the profile on the admin surface; actor updates are not taken here' });
|
|
298
332
|
}
|
|
299
333
|
const s = this.byIri(objectId);
|
|
300
|
-
if (!s) return
|
|
334
|
+
if (!s) return reply(404, { error: 'no such note here' });
|
|
301
335
|
if (s.actor !== this.urls.actor || s.kind !== 'post') {
|
|
302
|
-
return
|
|
336
|
+
return reply(403, { error: 'not your note' });
|
|
303
337
|
}
|
|
304
338
|
const text = String(object?.source?.content ?? object?.content ?? '');
|
|
305
|
-
if (!text.trim()) return
|
|
339
|
+
if (!text.trim()) return reply(422, { error: 'the edit has no content' });
|
|
306
340
|
const attachments = object?.attachment !== undefined
|
|
307
341
|
? arr(object.attachment).map((a) => ({
|
|
308
342
|
url: a?.url, mediaType: a?.mediaType,
|
|
@@ -312,52 +346,52 @@ export class C2S {
|
|
|
312
346
|
await agent.publisher.updateNote(s, {
|
|
313
347
|
content: text, spoilerText: object?.summary || null, attachments,
|
|
314
348
|
});
|
|
315
|
-
return
|
|
349
|
+
return reply(200, { ok: true, object: s.noteId });
|
|
316
350
|
}
|
|
317
351
|
|
|
318
352
|
case 'Delete': {
|
|
319
353
|
if (objectId === this.urls.actor) {
|
|
320
|
-
return
|
|
354
|
+
return reply(422, { error: 'retiring the actor is done on the admin surface, where it asks twice' });
|
|
321
355
|
}
|
|
322
356
|
const s = this.byIri(objectId);
|
|
323
|
-
if (!s) return
|
|
357
|
+
if (!s) return reply(404, { error: 'no such note here' });
|
|
324
358
|
if (s.actor !== this.urls.actor || s.kind !== 'post') {
|
|
325
|
-
return
|
|
359
|
+
return reply(403, { error: 'not your note' });
|
|
326
360
|
}
|
|
327
361
|
const r = await social.deleteNote(agent, s);
|
|
328
|
-
if (!r.ok) return
|
|
329
|
-
return
|
|
362
|
+
if (!r.ok) return reply(502, { error: r.error, stillPublished: r.stillPublished });
|
|
363
|
+
return reply(200, { ok: true });
|
|
330
364
|
}
|
|
331
365
|
|
|
332
366
|
case 'Follow': {
|
|
333
|
-
if (!objectId) return
|
|
367
|
+
if (!objectId) return reply(400, { error: 'whom? object must name an actor' });
|
|
334
368
|
// An acct: form or bare handle resolves through WebFinger; an https
|
|
335
369
|
// IRI is fetched directly.
|
|
336
370
|
if (/^acct:|^@|^[^/@]+@[^/@]+$/.test(objectId) && !/^https?:/.test(objectId)) {
|
|
337
371
|
const r = await social.followHandle(agent, objectId.replace(/^acct:/, ''));
|
|
338
372
|
const rec = this.store.getContacts().following.find((f) => f.actor === r.actor);
|
|
339
|
-
return
|
|
373
|
+
return reply(201, { id: rec?.followActivity?.id, object: r.actor },
|
|
340
374
|
rec?.followActivity?.id ? { location: rec.followActivity.id } : {});
|
|
341
375
|
}
|
|
342
376
|
const doc = await social.followActor(agent, objectId);
|
|
343
377
|
const rec = this.store.getContacts().following.find((f) => f.actor === doc.id);
|
|
344
|
-
return
|
|
378
|
+
return reply(201, { id: rec?.followActivity?.id, object: doc.id },
|
|
345
379
|
rec?.followActivity?.id ? { location: rec.followActivity.id } : {});
|
|
346
380
|
}
|
|
347
381
|
|
|
348
382
|
case 'Like': {
|
|
349
383
|
const s = this.byIri(objectId);
|
|
350
|
-
if (!s) return
|
|
384
|
+
if (!s) return reply(422, { error: 'that note is not held here — like what the timeline holds' });
|
|
351
385
|
const updated = await social.favourite(agent, s);
|
|
352
|
-
return
|
|
386
|
+
return reply(201, { id: updated.likeActivity?.id, object: s.noteId },
|
|
353
387
|
updated.likeActivity?.id ? { location: updated.likeActivity.id } : {});
|
|
354
388
|
}
|
|
355
389
|
|
|
356
390
|
case 'Announce': {
|
|
357
391
|
const s = this.byIri(objectId);
|
|
358
|
-
if (!s) return
|
|
392
|
+
if (!s) return reply(422, { error: 'that note is not held here — boost what the timeline holds' });
|
|
359
393
|
const updated = await social.reblog(agent, s);
|
|
360
|
-
return
|
|
394
|
+
return reply(201, { id: updated.announceActivity?.id, object: s.noteId },
|
|
361
395
|
updated.announceActivity?.id ? { location: updated.announceActivity.id } : {});
|
|
362
396
|
}
|
|
363
397
|
|
|
@@ -368,37 +402,37 @@ export class C2S {
|
|
|
368
402
|
const innerId = idOf(activity.object);
|
|
369
403
|
if (inner?.type === 'Block') {
|
|
370
404
|
const target = idOf(inner.object);
|
|
371
|
-
if (!target) return
|
|
405
|
+
if (!target) return reply(400, { error: 'unblock whom?' });
|
|
372
406
|
await social.unblockActor(agent, target);
|
|
373
|
-
return
|
|
407
|
+
return reply(200, { ok: true, object: target });
|
|
374
408
|
}
|
|
375
409
|
const statuses = this.store.getStatuses();
|
|
376
410
|
let s = innerId ? statuses.find((x) => x.likeActivity?.id === innerId) : null;
|
|
377
411
|
if (!s && inner?.type === 'Like') s = this.byIri(idOf(inner.object));
|
|
378
412
|
if (s?.favourited) {
|
|
379
413
|
const updated = await social.unfavourite(agent, s);
|
|
380
|
-
return
|
|
414
|
+
return reply(200, { ok: true, object: updated.noteId });
|
|
381
415
|
}
|
|
382
416
|
s = innerId ? statuses.find((x) => x.announceActivity?.id === innerId) : null;
|
|
383
417
|
if (!s && inner?.type === 'Announce') s = this.byIri(idOf(inner.object));
|
|
384
418
|
if (s?.reblogged) {
|
|
385
419
|
const updated = await social.unreblog(agent, s);
|
|
386
|
-
return
|
|
420
|
+
return reply(200, { ok: true, object: updated.noteId });
|
|
387
421
|
}
|
|
388
422
|
const following = this.store.getContacts().following;
|
|
389
423
|
const rec = following.find((f) => f.followActivity?.id === innerId)
|
|
390
424
|
|| (inner?.type === 'Follow' ? following.find((f) => f.actor === idOf(inner.object)) : null);
|
|
391
425
|
if (rec) {
|
|
392
426
|
await social.unfollowActor(agent, rec.actor);
|
|
393
|
-
return
|
|
427
|
+
return reply(200, { ok: true, object: rec.actor });
|
|
394
428
|
}
|
|
395
|
-
return
|
|
429
|
+
return reply(422, { error: 'nothing here matches what that Undo names' });
|
|
396
430
|
}
|
|
397
431
|
|
|
398
432
|
case 'Block': {
|
|
399
|
-
if (!objectId) return
|
|
433
|
+
if (!objectId) return reply(400, { error: 'block whom? object must name an actor' });
|
|
400
434
|
await social.blockActor(agent, objectId);
|
|
401
|
-
return
|
|
435
|
+
return reply(200, { ok: true, object: objectId });
|
|
402
436
|
}
|
|
403
437
|
|
|
404
438
|
case 'Add':
|
|
@@ -406,12 +440,12 @@ export class C2S {
|
|
|
406
440
|
// The one collection a client may edit is the pins (§7.6/§7.9 in the
|
|
407
441
|
// other direction): target must be the featured collection.
|
|
408
442
|
if (idOf(activity.target) !== this.urls.featured) {
|
|
409
|
-
return
|
|
443
|
+
return reply(422, { error: 'the featured collection is the one Add/Remove edits here' });
|
|
410
444
|
}
|
|
411
445
|
const s = this.byIri(objectId);
|
|
412
|
-
if (!s) return
|
|
446
|
+
if (!s) return reply(404, { error: 'no such note here' });
|
|
413
447
|
const updated = await social.pinStatus(agent, s, activity.type === 'Add');
|
|
414
|
-
return
|
|
448
|
+
return reply(200, { ok: true, object: updated.noteId, pinned: !!updated.pinned });
|
|
415
449
|
}
|
|
416
450
|
|
|
417
451
|
case 'Accept':
|
|
@@ -419,19 +453,19 @@ export class C2S {
|
|
|
419
453
|
// Answering a held follow request: the object is the Follow (or the
|
|
420
454
|
// requester). Which request is meant comes from the Follow's actor.
|
|
421
455
|
const requester = object?.actor ? idOf(object.actor) : objectId;
|
|
422
|
-
if (!requester) return
|
|
456
|
+
if (!requester) return reply(400, { error: 'whose request? object must name the Follow or its actor' });
|
|
423
457
|
const r = activity.type === 'Accept'
|
|
424
458
|
? await social.admitRequest(agent, requester).catch((e) => ({ error: e.message }))
|
|
425
459
|
: await social.refuseRequest(agent, requester).catch((e) => ({ error: e.message }));
|
|
426
|
-
if (r.error) return
|
|
427
|
-
return
|
|
460
|
+
if (r.error) return reply(404, { error: r.error });
|
|
461
|
+
return reply(200, { ok: true, object: requester });
|
|
428
462
|
}
|
|
429
463
|
|
|
430
464
|
case 'Move':
|
|
431
|
-
return
|
|
465
|
+
return reply(422, { error: 'moving the account is done on the admin surface, where it asks twice' });
|
|
432
466
|
|
|
433
467
|
default:
|
|
434
|
-
return
|
|
468
|
+
return reply(422, { error: `no handler for ${activity.type}` });
|
|
435
469
|
}
|
|
436
470
|
}
|
|
437
471
|
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
{
|
|
2
|
+
"@context": {
|
|
3
|
+
"oa": "http://www.w3.org/ns/oa#",
|
|
4
|
+
"dc": "http://purl.org/dc/elements/1.1/",
|
|
5
|
+
"dcterms": "http://purl.org/dc/terms/",
|
|
6
|
+
"dctypes": "http://purl.org/dc/dcmitype/",
|
|
7
|
+
"foaf": "http://xmlns.com/foaf/0.1/",
|
|
8
|
+
"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
|
|
9
|
+
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
|
|
10
|
+
"skos": "http://www.w3.org/2004/02/skos/core#",
|
|
11
|
+
"xsd": "http://www.w3.org/2001/XMLSchema#",
|
|
12
|
+
"iana": "http://www.iana.org/assignments/relation/",
|
|
13
|
+
"owl": "http://www.w3.org/2002/07/owl#",
|
|
14
|
+
"as": "http://www.w3.org/ns/activitystreams#",
|
|
15
|
+
"schema": "http://schema.org/",
|
|
16
|
+
|
|
17
|
+
"id": {"@type": "@id", "@id": "@id"},
|
|
18
|
+
"type": {"@type": "@id", "@id": "@type"},
|
|
19
|
+
|
|
20
|
+
"Annotation": "oa:Annotation",
|
|
21
|
+
"Dataset": "dctypes:Dataset",
|
|
22
|
+
"Image": "dctypes:StillImage",
|
|
23
|
+
"Video": "dctypes:MovingImage",
|
|
24
|
+
"Audio": "dctypes:Sound",
|
|
25
|
+
"Text": "dctypes:Text",
|
|
26
|
+
"TextualBody": "oa:TextualBody",
|
|
27
|
+
"ResourceSelection": "oa:ResourceSelection",
|
|
28
|
+
"SpecificResource": "oa:SpecificResource",
|
|
29
|
+
"FragmentSelector": "oa:FragmentSelector",
|
|
30
|
+
"CssSelector": "oa:CssSelector",
|
|
31
|
+
"XPathSelector": "oa:XPathSelector",
|
|
32
|
+
"TextQuoteSelector": "oa:TextQuoteSelector",
|
|
33
|
+
"TextPositionSelector": "oa:TextPositionSelector",
|
|
34
|
+
"DataPositionSelector": "oa:DataPositionSelector",
|
|
35
|
+
"SvgSelector": "oa:SvgSelector",
|
|
36
|
+
"RangeSelector": "oa:RangeSelector",
|
|
37
|
+
"TimeState": "oa:TimeState",
|
|
38
|
+
"HttpRequestState": "oa:HttpRequestState",
|
|
39
|
+
"CssStylesheet": "oa:CssStyle",
|
|
40
|
+
"Choice": "oa:Choice",
|
|
41
|
+
"Person": "foaf:Person",
|
|
42
|
+
"Software": "as:Application",
|
|
43
|
+
"Organization": "foaf:Organization",
|
|
44
|
+
"AnnotationCollection": "as:OrderedCollection",
|
|
45
|
+
"AnnotationPage": "as:OrderedCollectionPage",
|
|
46
|
+
"Audience": "schema:Audience",
|
|
47
|
+
|
|
48
|
+
"Motivation": "oa:Motivation",
|
|
49
|
+
"bookmarking": "oa:bookmarking",
|
|
50
|
+
"classifying": "oa:classifying",
|
|
51
|
+
"commenting": "oa:commenting",
|
|
52
|
+
"describing": "oa:describing",
|
|
53
|
+
"editing": "oa:editing",
|
|
54
|
+
"highlighting": "oa:highlighting",
|
|
55
|
+
"identifying": "oa:identifying",
|
|
56
|
+
"linking": "oa:linking",
|
|
57
|
+
"moderating": "oa:moderating",
|
|
58
|
+
"questioning": "oa:questioning",
|
|
59
|
+
"replying": "oa:replying",
|
|
60
|
+
"reviewing": "oa:reviewing",
|
|
61
|
+
"assessing": "oa:assessing",
|
|
62
|
+
"tagging": "oa:tagging",
|
|
63
|
+
|
|
64
|
+
"auto": "oa:autoDirection",
|
|
65
|
+
"ltr": "oa:ltrDirection",
|
|
66
|
+
"rtl": "oa:rtlDirection",
|
|
67
|
+
|
|
68
|
+
"body": {"@type": "@id", "@id": "oa:hasBody"},
|
|
69
|
+
"target": {"@type": "@id", "@id": "oa:hasTarget"},
|
|
70
|
+
"source": {"@type": "@id", "@id": "oa:hasSource"},
|
|
71
|
+
"selector": {"@type": "@id", "@id": "oa:hasSelector"},
|
|
72
|
+
"state": {"@type": "@id", "@id": "oa:hasState"},
|
|
73
|
+
"scope": {"@type": "@id", "@id": "oa:hasScope"},
|
|
74
|
+
"refinedBy": {"@type": "@id", "@id": "oa:refinedBy"},
|
|
75
|
+
"startSelector": {"@type": "@id", "@id": "oa:hasStartSelector"},
|
|
76
|
+
"endSelector": {"@type": "@id", "@id": "oa:hasEndSelector"},
|
|
77
|
+
"renderedVia": {"@type": "@id", "@id": "oa:renderedVia"},
|
|
78
|
+
"creator": {"@type": "@id", "@id": "dcterms:creator"},
|
|
79
|
+
"generator": {"@type": "@id", "@id": "as:generator"},
|
|
80
|
+
"rights": {"@type": "@id", "@id": "dcterms:rights"},
|
|
81
|
+
"homepage": {"@type": "@id", "@id": "foaf:homepage"},
|
|
82
|
+
"via": {"@type": "@id", "@id": "oa:via"},
|
|
83
|
+
"canonical": {"@type": "@id", "@id": "oa:canonical"},
|
|
84
|
+
"stylesheet": {"@type": "@id", "@id": "oa:styledBy"},
|
|
85
|
+
"cached": {"@type": "@id", "@id": "oa:cachedSource"},
|
|
86
|
+
"conformsTo": {"@type": "@id", "@id": "dcterms:conformsTo"},
|
|
87
|
+
"items": {"@type": "@id", "@id": "as:items", "@container": "@list"},
|
|
88
|
+
"partOf": {"@type": "@id", "@id": "as:partOf"},
|
|
89
|
+
"first": {"@type": "@id", "@id": "as:first"},
|
|
90
|
+
"last": {"@type": "@id", "@id": "as:last"},
|
|
91
|
+
"next": {"@type": "@id", "@id": "as:next"},
|
|
92
|
+
"prev": {"@type": "@id", "@id": "as:prev"},
|
|
93
|
+
"audience": {"@type": "@id", "@id": "schema:audience"},
|
|
94
|
+
"motivation": {"@type": "@vocab", "@id": "oa:motivatedBy"},
|
|
95
|
+
"purpose": {"@type": "@vocab", "@id": "oa:hasPurpose"},
|
|
96
|
+
"textDirection": {"@type": "@vocab", "@id": "oa:textDirection"},
|
|
97
|
+
|
|
98
|
+
"accessibility": "schema:accessibilityFeature",
|
|
99
|
+
"bodyValue": "oa:bodyValue",
|
|
100
|
+
"format": "dc:format",
|
|
101
|
+
"language": "dc:language",
|
|
102
|
+
"processingLanguage": "oa:processingLanguage",
|
|
103
|
+
"value": "rdf:value",
|
|
104
|
+
"exact": "oa:exact",
|
|
105
|
+
"prefix": "oa:prefix",
|
|
106
|
+
"suffix": "oa:suffix",
|
|
107
|
+
"styleClass": "oa:styleClass",
|
|
108
|
+
"name": "foaf:name",
|
|
109
|
+
"email": "foaf:mbox",
|
|
110
|
+
"email_sha1": "foaf:mbox_sha1sum",
|
|
111
|
+
"nickname": "foaf:nick",
|
|
112
|
+
"label": "rdfs:label",
|
|
113
|
+
|
|
114
|
+
"created": {"@id": "dcterms:created", "@type": "xsd:dateTime"},
|
|
115
|
+
"modified": {"@id": "dcterms:modified", "@type": "xsd:dateTime"},
|
|
116
|
+
"generated": {"@id": "dcterms:issued", "@type": "xsd:dateTime"},
|
|
117
|
+
"sourceDate": {"@id": "oa:sourceDate", "@type": "xsd:dateTime"},
|
|
118
|
+
"sourceDateStart": {"@id": "oa:sourceDateStart", "@type": "xsd:dateTime"},
|
|
119
|
+
"sourceDateEnd": {"@id": "oa:sourceDateEnd", "@type": "xsd:dateTime"},
|
|
120
|
+
|
|
121
|
+
"start": {"@id": "oa:start", "@type": "xsd:nonNegativeInteger"},
|
|
122
|
+
"end": {"@id": "oa:end", "@type": "xsd:nonNegativeInteger"},
|
|
123
|
+
"total": {"@id": "as:totalItems", "@type": "xsd:nonNegativeInteger"},
|
|
124
|
+
"startIndex": {"@id": "as:startIndex", "@type": "xsd:nonNegativeInteger"}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -25,6 +25,9 @@ import ctx10 from './fep-5711.json' with { type: 'json' };
|
|
|
25
25
|
import ctx11 from './join-lemmy.json' with { type: 'json' };
|
|
26
26
|
import ctx12 from './joinmastodon.json' with { type: 'json' };
|
|
27
27
|
import ctx13 from './miscellany.json' with { type: 'json' };
|
|
28
|
+
// Not from fedify: the W3C Web Annotation context, which a client such as
|
|
29
|
+
// dokieli names on the annotations it posts to the outbox.
|
|
30
|
+
import ctx14 from './anno.json' with { type: 'json' };
|
|
28
31
|
|
|
29
32
|
/** URL → the context document itself. Nothing outside this map is ever resolved. */
|
|
30
33
|
export const CONTEXTS = {
|
|
@@ -42,4 +45,5 @@ export const CONTEXTS = {
|
|
|
42
45
|
'https://join-lemmy.org/context.json': ctx11,
|
|
43
46
|
'http://joinmastodon.org/ns': ctx12,
|
|
44
47
|
'https://purl.archive.org/miscellany': ctx13,
|
|
48
|
+
'http://www.w3.org/ns/anno.jsonld': ctx14,
|
|
45
49
|
};
|
|
@@ -12,5 +12,6 @@
|
|
|
12
12
|
"https://w3id.org/fep/5711": "fep-5711.json",
|
|
13
13
|
"https://join-lemmy.org/context.json": "join-lemmy.json",
|
|
14
14
|
"http://joinmastodon.org/ns": "joinmastodon.json",
|
|
15
|
-
"https://purl.archive.org/miscellany": "miscellany.json"
|
|
15
|
+
"https://purl.archive.org/miscellany": "miscellany.json",
|
|
16
|
+
"http://www.w3.org/ns/anno.jsonld": "anno.json"
|
|
16
17
|
}
|
|
@@ -59,8 +59,10 @@ const MAX_ITEM_ATTEMPTS = 5;
|
|
|
59
59
|
const MAX_ITEMS_PER_DRAIN = 50;
|
|
60
60
|
|
|
61
61
|
export class Intake {
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
// `ownerPost(activity, { raw, slug })` is the client-to-server dispatcher,
|
|
63
|
+
// for an item the Gateway's outbox door took on the owner's behalf.
|
|
64
|
+
constructor({ config, urls, remote, store, deliverer, publisher, log = console.log, lease = null, archive = null, push = true, pollSeconds = null, ownerPost = null }) {
|
|
65
|
+
Object.assign(this, { config, urls, remote, store, deliverer, publisher, log, lease, archive, push, pollSeconds, ownerPost });
|
|
64
66
|
this.serial = Date.now();
|
|
65
67
|
this.stopped = false;
|
|
66
68
|
// (attempt counts are kept in pod state — see _bumpAttempt)
|
|
@@ -429,9 +431,14 @@ export class Intake {
|
|
|
429
431
|
// receipt reads as null, which is exactly today's unverified behavior.
|
|
430
432
|
const receipt = activity ? await this._readReceipt(url) : null;
|
|
431
433
|
if (activity && this.gatewaySecret()) this._bumpGatewayStat(!!receipt?.verified);
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
434
|
+
// The owner's own post, taken at the outbox door: not mail to read
|
|
435
|
+
// but a write to make. Never archived or forwarded as if received.
|
|
436
|
+
const owned = activity && this.isOwnerPost(receipt);
|
|
437
|
+
const rejection = !activity ? 'unparsable JSON'
|
|
438
|
+
: owned ? await this.ownerPostFrom(activity, raw, receipt)
|
|
439
|
+
: await this.handle(activity, receipt);
|
|
440
|
+
if (!rejection && raw && !owned) await this._archive(url, raw, activity);
|
|
441
|
+
if (!rejection && !owned) await this._maybeForward(activity); // §7.1.2, only what we accepted
|
|
435
442
|
if (rejection) {
|
|
436
443
|
this.store.addDeadLetter({
|
|
437
444
|
inboxUrl: url, reason: rejection, activity: trimActivity(activity),
|
|
@@ -594,6 +601,26 @@ export class Intake {
|
|
|
594
601
|
gatewaySecret(...a) { return verify.gatewaySecret(this, ...a); }
|
|
595
602
|
_bumpGatewayStat(...a) { return verify.bumpGatewayStat(this, ...a); }
|
|
596
603
|
_readReceipt(...a) { return verify.readReceipt(this, ...a); }
|
|
604
|
+
|
|
605
|
+
// A receipt the door stamped `c2s` for THIS actor, and only a receipt whose
|
|
606
|
+
// HMAC verified (readReceipt returns nothing else). A stranger appending an
|
|
607
|
+
// item that claims to be ours has no such receipt and is read as mail.
|
|
608
|
+
isOwnerPost(receipt) {
|
|
609
|
+
return !!(this.ownerPost && receipt?.verified && receipt.method === 'c2s' && receipt.actor === this.urls.actor);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Publish what the owner posted at the door. The bytes as the client wrote
|
|
613
|
+
// them go to the dispatcher (an object stored as sent must be the client's
|
|
614
|
+
// document, not our reading of it); the graph is what it decides from.
|
|
615
|
+
// Returns null when published, else the reason it was not — a dead letter.
|
|
616
|
+
async ownerPostFrom(activity, raw, receipt) {
|
|
617
|
+
let asSent = null;
|
|
618
|
+
try { asSent = raw ? JSON.parse(raw) : null; } catch { asSent = null; }
|
|
619
|
+
const r = await this.ownerPost(activity, { raw: asSent, slug: receipt.slug || null });
|
|
620
|
+
if (!r || r.status >= 300) return `owner post refused (${r?.status || '?'}): ${r?.body?.error || ''}`;
|
|
621
|
+
this.log(`owner post from the outbox door published: ${r.body?.object || r.body?.id || activity.type}`);
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
597
624
|
receiptVouchesFor(...a) { return verify.receiptVouchesFor(this, ...a); }
|
|
598
625
|
isGone(...a) { return verify.isGone(this, ...a); }
|
|
599
626
|
|
|
@@ -76,6 +76,15 @@ export class Publisher {
|
|
|
76
76
|
// republish button. Without it, an actor lost from the pod would match the
|
|
77
77
|
// digest, be skipped, and leave the agent reporting success while nobody can
|
|
78
78
|
// resolve it.
|
|
79
|
+
// The Gateway's outbox door for this account, when a Gateway is attached:
|
|
80
|
+
// beside its inbox door, or under the fronted actor.
|
|
81
|
+
gatewayOutbox() {
|
|
82
|
+
const gw = this.config.gateway;
|
|
83
|
+
if (!(gw && gw.url && gw.mode && gw.mode !== 'off')) return null;
|
|
84
|
+
if (gw.frontActor) return String(gw.frontActor).replace(/ap\/actor\/?$/u, 'ap/outbox');
|
|
85
|
+
return String(gw.url).replace(/ap\/inbox\/?$/u, 'ap/outbox');
|
|
86
|
+
}
|
|
87
|
+
|
|
79
88
|
async publishProfile({ force = false } = {}) {
|
|
80
89
|
const { urls } = this;
|
|
81
90
|
const host = new URL(urls.base).host;
|
|
@@ -105,7 +114,8 @@ export class Publisher {
|
|
|
105
114
|
inbox: gwActive ? gw.url : null,
|
|
106
115
|
// The agent's own outbox endpoint, where it is reachable: a client
|
|
107
116
|
// following the actor must arrive somewhere that will take a write.
|
|
108
|
-
|
|
117
|
+
// Otherwise the Gateway's outbox door, when one is attached.
|
|
118
|
+
outbox: this.clientOrigin ? `${this.clientOrigin}ap/outbox` : this.gatewayOutbox(),
|
|
109
119
|
// How a client-to-server client finds the way in with nothing configured
|
|
110
120
|
// by hand. Advertised only where the surface is publicly reachable.
|
|
111
121
|
oauthAuthorize: this.clientOrigin ? `${this.clientOrigin}oauth/authorize` : null,
|
|
@@ -163,6 +173,8 @@ export class Publisher {
|
|
|
163
173
|
actorUrl: urls.actor,
|
|
164
174
|
accountName: `@${this.config.handle}@${host}`,
|
|
165
175
|
kind: this.config.kind,
|
|
176
|
+
// Where a Solid client posts: dokieli reads `as:outbox` off the WebID.
|
|
177
|
+
outbox: this.clientOrigin ? `${this.clientOrigin}ap/outbox` : this.gatewayOutbox(),
|
|
166
178
|
});
|
|
167
179
|
if (wrote) this.log('WebID profile now lists the actor as a foaf:account');
|
|
168
180
|
} catch (e) {
|
|
@@ -400,6 +412,7 @@ export class Publisher {
|
|
|
400
412
|
reconcileFollowers(...a) { return restore.reconcileFollowers(this, ...a); }
|
|
401
413
|
reconcileOutbox(...a) { return restore.reconcileOutbox(this, ...a); }
|
|
402
414
|
rebuildStatuses(...a) { return restore.rebuildStatuses(this, ...a); }
|
|
415
|
+
healStatuses(...a) { return restore.healStatuses(this, ...a); }
|
|
403
416
|
|
|
404
417
|
// notes.mjs
|
|
405
418
|
ensureMediaContainer(...a) { return notes.ensureMediaContainer(this, ...a); }
|
|
@@ -408,6 +421,7 @@ export class Publisher {
|
|
|
408
421
|
privateReady(...a) { return notes.privateReady(this, ...a); }
|
|
409
422
|
_mentionsFor(...a) { return notes.mentionsFor(this, ...a); }
|
|
410
423
|
publishNote(...a) { return notes.publishNote(this, ...a); }
|
|
424
|
+
publishObject(...a) { return notes.publishObject(this, ...a); }
|
|
411
425
|
updateNote(...a) { return notes.updateNote(this, ...a); }
|
|
412
426
|
|
|
413
427
|
// questions.mjs
|