@fedify/relay 2.4.0-dev.1792 → 2.4.0-dev.1797
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/dist/litepub.test.js +107 -6
- package/dist/mastodon.test.js +58 -5
- package/dist/mod.cjs +141 -126
- package/dist/mod.d.cts +125 -3
- package/dist/mod.d.ts +125 -3
- package/dist/mod.js +141 -126
- package/dist/{types-CZvP2qE2.js → types-BDWJ2_j0.js} +1 -0
- package/package.json +4 -4
package/dist/litepub.test.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import "temporal-polyfill";
|
|
2
2
|
import "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
|
-
import { t as isRelayFollowerData } from "./types-
|
|
4
|
+
import { t as isRelayFollowerData } from "./types-BDWJ2_j0.js";
|
|
5
5
|
import { MemoryKvStore, signRequest } from "@fedify/fedify";
|
|
6
6
|
import { createRelay } from "@fedify/relay";
|
|
7
7
|
import { Accept, Announce, Create, Delete, Follow, Move, Note, Person, Undo, Update } from "@fedify/vocab";
|
|
8
8
|
import { exportSpki, getDocumentLoader } from "@fedify/vocab-runtime";
|
|
9
|
-
import { ok, strictEqual } from "node:assert";
|
|
9
|
+
import { deepStrictEqual, ok, strictEqual } from "node:assert";
|
|
10
10
|
import test, { describe } from "node:test";
|
|
11
11
|
//#region src/litepub.test.ts
|
|
12
12
|
const mockDocumentLoader = async (url) => {
|
|
@@ -288,7 +288,58 @@ describe("LitePubRelay", () => {
|
|
|
288
288
|
await relay.fetch(request);
|
|
289
289
|
strictEqual(await kv.get(["follower", "https://remote.example.com/users/alice"]), void 0);
|
|
290
290
|
});
|
|
291
|
-
test("
|
|
291
|
+
test("replaces malformed follower data on Follow", async () => {
|
|
292
|
+
const followerId = "https://remote.example.com/users/alice";
|
|
293
|
+
const malformedRows = [
|
|
294
|
+
{ state: "pending" },
|
|
295
|
+
{
|
|
296
|
+
actor: null,
|
|
297
|
+
state: "pending"
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
actor: {},
|
|
301
|
+
state: "pending"
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
actor: await new Person({ id: new URL("https://remote.example.com/users/bob") }).toJsonLd(),
|
|
305
|
+
state: "pending"
|
|
306
|
+
}
|
|
307
|
+
];
|
|
308
|
+
for (const malformedRow of malformedRows) {
|
|
309
|
+
const kv = new MemoryKvStore();
|
|
310
|
+
await kv.set(["follower", followerId], malformedRow);
|
|
311
|
+
let handlerCallCount = 0;
|
|
312
|
+
const relay = createRelay("litepub", {
|
|
313
|
+
kv,
|
|
314
|
+
origin: "https://relay.example.com",
|
|
315
|
+
documentLoaderFactory: () => mockDocumentLoader,
|
|
316
|
+
authenticatedDocumentLoaderFactory: () => mockDocumentLoader,
|
|
317
|
+
subscriptionHandler: () => {
|
|
318
|
+
handlerCallCount++;
|
|
319
|
+
return Promise.resolve(true);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
strictEqual(await relay.getFollower(followerId), null);
|
|
323
|
+
const followActivity = new Follow({
|
|
324
|
+
id: new URL("https://remote.example.com/activities/follow/1"),
|
|
325
|
+
actor: new URL(followerId),
|
|
326
|
+
object: new URL("https://relay.example.com/users/relay")
|
|
327
|
+
});
|
|
328
|
+
let request = new Request("https://relay.example.com/inbox", {
|
|
329
|
+
method: "POST",
|
|
330
|
+
headers: { "Content-Type": "application/activity+json" },
|
|
331
|
+
body: JSON.stringify(await followActivity.toJsonLd({ contextLoader: mockDocumentLoader }))
|
|
332
|
+
});
|
|
333
|
+
request = await signRequest(request, rsaKeyPair.privateKey, rsaPublicKey.id);
|
|
334
|
+
await relay.fetch(request);
|
|
335
|
+
strictEqual(handlerCallCount, 1);
|
|
336
|
+
const follower = await relay.getFollower(followerId);
|
|
337
|
+
ok(follower);
|
|
338
|
+
strictEqual(follower.state, "pending");
|
|
339
|
+
strictEqual(follower.actor.id?.href, followerId);
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
for (const state of ["pending", "accepted"]) test(`ignores duplicate Follow activity from ${state} follower`, async () => {
|
|
292
343
|
const kv = new MemoryKvStore();
|
|
293
344
|
let handlerCallCount = 0;
|
|
294
345
|
const relay = createRelay("litepub", {
|
|
@@ -308,7 +359,7 @@ describe("LitePubRelay", () => {
|
|
|
308
359
|
});
|
|
309
360
|
await kv.set(["follower", "https://remote.example.com/users/alice"], {
|
|
310
361
|
actor: await follower.toJsonLd(),
|
|
311
|
-
state
|
|
362
|
+
state
|
|
312
363
|
});
|
|
313
364
|
const followActivity = new Follow({
|
|
314
365
|
id: new URL("https://remote.example.com/activities/follow/1"),
|
|
@@ -323,6 +374,9 @@ describe("LitePubRelay", () => {
|
|
|
323
374
|
request = await signRequest(request, rsaKeyPair.privateKey, rsaPublicKey.id);
|
|
324
375
|
await relay.fetch(request);
|
|
325
376
|
strictEqual(handlerCallCount, 0);
|
|
377
|
+
const followerData = await kv.get(["follower", "https://remote.example.com/users/alice"]);
|
|
378
|
+
ok(isRelayFollowerData(followerData));
|
|
379
|
+
strictEqual(followerData.state, state);
|
|
326
380
|
});
|
|
327
381
|
test("handles Accept activity completing reciprocal follow", async () => {
|
|
328
382
|
const kv = new MemoryKvStore();
|
|
@@ -363,7 +417,54 @@ describe("LitePubRelay", () => {
|
|
|
363
417
|
ok(isRelayFollowerData(followerData));
|
|
364
418
|
strictEqual(followerData.state, "accepted");
|
|
365
419
|
});
|
|
366
|
-
test("
|
|
420
|
+
test("ignores Accept activity for invalid follower data", async () => {
|
|
421
|
+
const followerId = "https://remote.example.com/users/alice";
|
|
422
|
+
const invalidRows = [
|
|
423
|
+
{ state: "pending" },
|
|
424
|
+
{
|
|
425
|
+
actor: null,
|
|
426
|
+
state: "pending"
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
actor: {},
|
|
430
|
+
state: "pending"
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
actor: await new Person({ id: new URL("https://remote.example.com/users/bob") }).toJsonLd(),
|
|
434
|
+
state: "pending"
|
|
435
|
+
}
|
|
436
|
+
];
|
|
437
|
+
for (const invalidRow of invalidRows) {
|
|
438
|
+
const kv = new MemoryKvStore();
|
|
439
|
+
await kv.set(["follower", followerId], invalidRow);
|
|
440
|
+
const relay = createRelay("litepub", {
|
|
441
|
+
kv,
|
|
442
|
+
origin: "https://relay.example.com",
|
|
443
|
+
documentLoaderFactory: () => mockDocumentLoader,
|
|
444
|
+
authenticatedDocumentLoaderFactory: () => mockDocumentLoader,
|
|
445
|
+
subscriptionHandler: () => Promise.resolve(true)
|
|
446
|
+
});
|
|
447
|
+
const relayFollow = new Follow({
|
|
448
|
+
id: new URL("https://relay.example.com/activities/follow/1"),
|
|
449
|
+
actor: new URL("https://relay.example.com/users/relay"),
|
|
450
|
+
object: new URL(followerId)
|
|
451
|
+
});
|
|
452
|
+
const acceptActivity = new Accept({
|
|
453
|
+
id: new URL("https://remote.example.com/activities/accept/1"),
|
|
454
|
+
actor: new URL(followerId),
|
|
455
|
+
object: relayFollow
|
|
456
|
+
});
|
|
457
|
+
let request = new Request("https://relay.example.com/inbox", {
|
|
458
|
+
method: "POST",
|
|
459
|
+
headers: { "Content-Type": "application/activity+json" },
|
|
460
|
+
body: JSON.stringify(await acceptActivity.toJsonLd({ contextLoader: mockDocumentLoader }))
|
|
461
|
+
});
|
|
462
|
+
request = await signRequest(request, rsaKeyPair.privateKey, rsaPublicKey.id);
|
|
463
|
+
await relay.fetch(request);
|
|
464
|
+
deepStrictEqual(await kv.get(["follower", followerId]), invalidRow);
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
for (const state of ["pending", "accepted"]) test(`handles Undo Follow activity for ${state} follower`, async () => {
|
|
367
468
|
const kv = new MemoryKvStore();
|
|
368
469
|
const followerId = "https://remote.example.com/users/alice";
|
|
369
470
|
const follower = new Person({
|
|
@@ -373,7 +474,7 @@ describe("LitePubRelay", () => {
|
|
|
373
474
|
});
|
|
374
475
|
await kv.set(["follower", followerId], {
|
|
375
476
|
actor: await follower.toJsonLd(),
|
|
376
|
-
state
|
|
477
|
+
state
|
|
377
478
|
});
|
|
378
479
|
const relay = createRelay("litepub", {
|
|
379
480
|
kv,
|
package/dist/mastodon.test.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import "temporal-polyfill";
|
|
2
2
|
import "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
|
-
import { t as isRelayFollowerData } from "./types-
|
|
5
|
-
import { MemoryKvStore, signRequest } from "@fedify/fedify";
|
|
4
|
+
import { t as isRelayFollowerData } from "./types-BDWJ2_j0.js";
|
|
5
|
+
import { MemoryKvStore, signJsonLd, signRequest } from "@fedify/fedify";
|
|
6
6
|
import { createRelay } from "@fedify/relay";
|
|
7
|
-
import { Create, Delete, Follow, Move, Note, Person, Undo, Update } from "@fedify/vocab";
|
|
7
|
+
import { Announce, Create, Delete, Follow, Move, Note, Person, Undo, Update } from "@fedify/vocab";
|
|
8
8
|
import { exportSpki, getDocumentLoader } from "@fedify/vocab-runtime";
|
|
9
|
-
import { ok, strictEqual } from "node:assert";
|
|
9
|
+
import { deepStrictEqual, ok, strictEqual } from "node:assert";
|
|
10
10
|
import test, { describe } from "node:test";
|
|
11
11
|
//#region src/mastodon.test.ts
|
|
12
12
|
const mockDocumentLoader = async (url) => {
|
|
@@ -254,7 +254,9 @@ describe("MastodonRelay", () => {
|
|
|
254
254
|
await relay.fetch(request);
|
|
255
255
|
strictEqual(handlerCalled, true);
|
|
256
256
|
ok(handlerActor);
|
|
257
|
-
|
|
257
|
+
const followerData = await kv.get(["follower", "https://remote.example.com/users/alice"]);
|
|
258
|
+
ok(isRelayFollowerData(followerData));
|
|
259
|
+
strictEqual(followerData.state, "accepted");
|
|
258
260
|
});
|
|
259
261
|
test("handles Follow activity with subscription rejection", async () => {
|
|
260
262
|
const kv = new MemoryKvStore();
|
|
@@ -433,6 +435,57 @@ describe("MastodonRelay", () => {
|
|
|
433
435
|
const response = await relay.fetch(request);
|
|
434
436
|
ok(response.status === 200 || response.status === 202);
|
|
435
437
|
});
|
|
438
|
+
test("handles Announce activity forwarding", async () => {
|
|
439
|
+
const kv = new MemoryKvStore();
|
|
440
|
+
const follower = new Person({
|
|
441
|
+
id: new URL("https://follower.example.com/users/bob"),
|
|
442
|
+
preferredUsername: "bob",
|
|
443
|
+
inbox: new URL("https://follower.example.com/users/bob/inbox")
|
|
444
|
+
});
|
|
445
|
+
await kv.set(["follower", follower.id.href], {
|
|
446
|
+
actor: await follower.toJsonLd(),
|
|
447
|
+
state: "accepted"
|
|
448
|
+
});
|
|
449
|
+
const relay = createRelay("mastodon", {
|
|
450
|
+
kv,
|
|
451
|
+
origin: "https://relay.example.com",
|
|
452
|
+
documentLoaderFactory: () => mockDocumentLoader,
|
|
453
|
+
authenticatedDocumentLoaderFactory: () => mockDocumentLoader,
|
|
454
|
+
subscriptionHandler: () => Promise.resolve(true)
|
|
455
|
+
});
|
|
456
|
+
const signedAnnounce = await signJsonLd(await new Announce({
|
|
457
|
+
id: new URL("https://remote.example.com/activities/announce/1"),
|
|
458
|
+
actor: new URL("https://remote.example.com/users/alice"),
|
|
459
|
+
object: new URL("https://remote.example.com/notes/1")
|
|
460
|
+
}).toJsonLd({ contextLoader: mockDocumentLoader }), rsaKeyPair.privateKey, rsaPublicKey.id, { contextLoader: mockDocumentLoader });
|
|
461
|
+
let request = new Request("https://relay.example.com/inbox", {
|
|
462
|
+
method: "POST",
|
|
463
|
+
headers: { "Content-Type": "application/activity+json" },
|
|
464
|
+
body: JSON.stringify(signedAnnounce)
|
|
465
|
+
});
|
|
466
|
+
request = await signRequest(request, rsaKeyPair.privateKey, rsaPublicKey.id);
|
|
467
|
+
const originalFetch = globalThis.fetch;
|
|
468
|
+
let deliveryMethod;
|
|
469
|
+
let deliveredActivity;
|
|
470
|
+
globalThis.fetch = (async (input, init) => {
|
|
471
|
+
const outboundRequest = input instanceof Request ? input : new Request(input, init);
|
|
472
|
+
if (outboundRequest.url === "https://follower.example.com/users/bob/inbox") {
|
|
473
|
+
deliveryMethod = outboundRequest.method;
|
|
474
|
+
deliveredActivity = await outboundRequest.json();
|
|
475
|
+
return new Response(null, { status: 202 });
|
|
476
|
+
}
|
|
477
|
+
return originalFetch(input, init);
|
|
478
|
+
});
|
|
479
|
+
try {
|
|
480
|
+
const response = await relay.fetch(request);
|
|
481
|
+
ok(response.status === 200 || response.status === 202, `Unexpected inbox response status: ${response.status}`);
|
|
482
|
+
} finally {
|
|
483
|
+
globalThis.fetch = originalFetch;
|
|
484
|
+
}
|
|
485
|
+
ok(deliveredActivity, "Expected Announce delivery to the follower inbox");
|
|
486
|
+
strictEqual(deliveryMethod, "POST");
|
|
487
|
+
deepStrictEqual(deliveredActivity, signedAnnounce);
|
|
488
|
+
});
|
|
436
489
|
test("ignores Follow activity without required fields", async () => {
|
|
437
490
|
const kv = new MemoryKvStore();
|
|
438
491
|
const relay = createRelay("mastodon", {
|
package/dist/mod.cjs
CHANGED
|
@@ -18,6 +18,28 @@ function isRelayFollowerData(value) {
|
|
|
18
18
|
const obj = value;
|
|
19
19
|
return "actor" in obj && "state" in obj && typeof obj.state === "string" && (obj.state === "pending" || obj.state === "accepted");
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Parses and semantically validates follower data from storage.
|
|
23
|
+
*
|
|
24
|
+
* @param actorId The actor ID used as the follower's storage key.
|
|
25
|
+
* @param value The stored follower data.
|
|
26
|
+
* @returns The parsed follower, or `null` if the row is invalid.
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
async function parseRelayFollowerData(actorId, value) {
|
|
30
|
+
if (!isRelayFollowerData(value)) return null;
|
|
31
|
+
try {
|
|
32
|
+
const actor = await _fedify_vocab.Object.fromJsonLd(value.actor);
|
|
33
|
+
if (!(0, _fedify_vocab.isActor)(actor) || actor.id?.href !== actorId) return null;
|
|
34
|
+
return {
|
|
35
|
+
actorId,
|
|
36
|
+
actor,
|
|
37
|
+
state: value.state
|
|
38
|
+
};
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
21
43
|
//#endregion
|
|
22
44
|
//#region src/builder.ts
|
|
23
45
|
const relayBuilder = (0, _fedify_fedify.createFederationBuilder)();
|
|
@@ -78,12 +100,12 @@ relayBuilder.setActorDispatcher("/users/{identifier}", async (ctx, identifier) =
|
|
|
78
100
|
});
|
|
79
101
|
async function getFollowerActors(ctx) {
|
|
80
102
|
const actors = [];
|
|
81
|
-
for await (const { value } of ctx.data.kv.list(["follower"])) {
|
|
82
|
-
|
|
83
|
-
if (
|
|
84
|
-
const
|
|
85
|
-
if (
|
|
86
|
-
actors.push(actor);
|
|
103
|
+
for await (const { key, value } of ctx.data.kv.list(["follower"])) {
|
|
104
|
+
const actorId = key[1];
|
|
105
|
+
if (typeof actorId !== "string") continue;
|
|
106
|
+
const follower = await parseRelayFollowerData(actorId, value);
|
|
107
|
+
if (follower?.state !== "accepted") continue;
|
|
108
|
+
actors.push(follower.actor);
|
|
87
109
|
}
|
|
88
110
|
return actors;
|
|
89
111
|
}
|
|
@@ -3658,6 +3680,64 @@ const Temporal$2 = /*@__PURE__*/ Object.defineProperties({}, {
|
|
|
3658
3680
|
const Temporal$1 = NativeTemporal || Temporal$2;
|
|
3659
3681
|
NativeTemporal && Date.prototype.toTemporalInstant;
|
|
3660
3682
|
//#endregion
|
|
3683
|
+
//#region src/follow.ts
|
|
3684
|
+
/**
|
|
3685
|
+
* Validate Follow activity and return follower actor if valid.
|
|
3686
|
+
* This validation is common to both Mastodon and LitePub relay protocols.
|
|
3687
|
+
*
|
|
3688
|
+
* @param ctx The federation context
|
|
3689
|
+
* @param follow The Follow activity to validate
|
|
3690
|
+
* @returns The follower Actor if valid, null otherwise
|
|
3691
|
+
*/
|
|
3692
|
+
async function validateFollowActivity(ctx, follow) {
|
|
3693
|
+
if (follow.id == null || follow.objectId == null) return null;
|
|
3694
|
+
const parsed = ctx.parseUri(follow.objectId);
|
|
3695
|
+
if (!(follow.objectId.href === "https://www.w3.org/ns/activitystreams#Public") && parsed?.type !== "actor") return null;
|
|
3696
|
+
const follower = await follow.getActor(ctx);
|
|
3697
|
+
if (follower == null || follower.id == null || follower.preferredUsername == null || follower.inboxId == null) return null;
|
|
3698
|
+
return follower;
|
|
3699
|
+
}
|
|
3700
|
+
/**
|
|
3701
|
+
* Send Accept or Reject response for a Follow activity.
|
|
3702
|
+
* This is common to both Mastodon and LitePub relay protocols.
|
|
3703
|
+
*
|
|
3704
|
+
* @param ctx The federation context
|
|
3705
|
+
* @param follow The Follow activity being responded to
|
|
3706
|
+
* @param follower The actor who sent the Follow
|
|
3707
|
+
* @param approved Whether the follow was approved
|
|
3708
|
+
*/
|
|
3709
|
+
async function sendFollowResponse(ctx, follow, follower, approved) {
|
|
3710
|
+
const relayActorUri = ctx.getActorUri(RELAY_SERVER_ACTOR);
|
|
3711
|
+
const Activity = approved ? _fedify_vocab.Accept : _fedify_vocab.Reject;
|
|
3712
|
+
const action = approved ? "accepts" : "rejects";
|
|
3713
|
+
await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, follower, new Activity({
|
|
3714
|
+
id: new URL(`#${action}`, relayActorUri),
|
|
3715
|
+
actor: relayActorUri,
|
|
3716
|
+
object: follow
|
|
3717
|
+
}));
|
|
3718
|
+
}
|
|
3719
|
+
/**
|
|
3720
|
+
* Handle Undo activity for Follow.
|
|
3721
|
+
* This logic is identical for both Mastodon and LitePub relay protocols.
|
|
3722
|
+
*
|
|
3723
|
+
* @param ctx The federation context
|
|
3724
|
+
* @param undo The Undo activity to handle
|
|
3725
|
+
* @param logger The logger instance to use for warnings
|
|
3726
|
+
*/
|
|
3727
|
+
async function handleUndoFollow(ctx, undo, logger) {
|
|
3728
|
+
const activity = await undo.getObject({
|
|
3729
|
+
crossOrigin: "trust",
|
|
3730
|
+
...ctx
|
|
3731
|
+
});
|
|
3732
|
+
if (activity instanceof _fedify_vocab.Follow) {
|
|
3733
|
+
if (activity.id == null || activity.actorId == null) return;
|
|
3734
|
+
await ctx.data.kv.delete(["follower", activity.actorId.href]);
|
|
3735
|
+
} else logger.warn("Unsupported object type ({type}) for Undo activity: {object}", {
|
|
3736
|
+
type: activity?.constructor.name,
|
|
3737
|
+
object: activity
|
|
3738
|
+
});
|
|
3739
|
+
}
|
|
3740
|
+
//#endregion
|
|
3661
3741
|
//#region src/base.ts
|
|
3662
3742
|
/**
|
|
3663
3743
|
* Abstract base class for relay implementations.
|
|
@@ -3677,25 +3757,6 @@ var BaseRelay = class {
|
|
|
3677
3757
|
return await (await this.#getFederation()).fetch(request, { contextData: this.options });
|
|
3678
3758
|
}
|
|
3679
3759
|
/**
|
|
3680
|
-
* Helper method to parse and validate follower data from storage.
|
|
3681
|
-
* Deserializes JSON-LD actor data and validates it.
|
|
3682
|
-
*
|
|
3683
|
-
* @param actorId The actor ID of the follower
|
|
3684
|
-
* @param data Raw data from KV store
|
|
3685
|
-
* @returns RelayFollower object if valid, null otherwise
|
|
3686
|
-
* @internal
|
|
3687
|
-
*/
|
|
3688
|
-
async parseFollowerData(actorId, data) {
|
|
3689
|
-
if (!isRelayFollowerData(data)) return null;
|
|
3690
|
-
const actor = await _fedify_vocab.Object.fromJsonLd(data.actor);
|
|
3691
|
-
if (!(0, _fedify_vocab.isActor)(actor)) return null;
|
|
3692
|
-
return {
|
|
3693
|
-
actorId,
|
|
3694
|
-
actor,
|
|
3695
|
-
state: data.state
|
|
3696
|
-
};
|
|
3697
|
-
}
|
|
3698
|
-
/**
|
|
3699
3760
|
* Lists all followers of the relay.
|
|
3700
3761
|
*
|
|
3701
3762
|
* @returns An async iterator of follower entries
|
|
@@ -3724,7 +3785,7 @@ var BaseRelay = class {
|
|
|
3724
3785
|
for await (const entry of this.options.kv.list(["follower"])) {
|
|
3725
3786
|
const actorId = entry.key[1];
|
|
3726
3787
|
if (typeof actorId !== "string") continue;
|
|
3727
|
-
const follower = await
|
|
3788
|
+
const follower = await parseRelayFollowerData(actorId, entry.value);
|
|
3728
3789
|
if (follower) yield follower;
|
|
3729
3790
|
}
|
|
3730
3791
|
}
|
|
@@ -3757,8 +3818,35 @@ var BaseRelay = class {
|
|
|
3757
3818
|
* @since 2.0.0
|
|
3758
3819
|
*/
|
|
3759
3820
|
async getFollower(actorId) {
|
|
3760
|
-
|
|
3761
|
-
|
|
3821
|
+
return await parseRelayFollowerData(actorId, await this.options.kv.get(["follower", actorId]));
|
|
3822
|
+
}
|
|
3823
|
+
shouldSkipFollow(_ctx, _follower) {
|
|
3824
|
+
return Promise.resolve(false);
|
|
3825
|
+
}
|
|
3826
|
+
afterFollowApproved(_ctx, _follower) {
|
|
3827
|
+
return Promise.resolve();
|
|
3828
|
+
}
|
|
3829
|
+
async #handleFollow(ctx, follow) {
|
|
3830
|
+
const follower = await validateFollowActivity(ctx, follow);
|
|
3831
|
+
if (follower?.id == null || await this.shouldSkipFollow(ctx, follower)) return;
|
|
3832
|
+
const approved = await this.options.subscriptionHandler(ctx, follower);
|
|
3833
|
+
if (approved) await ctx.data.kv.set(["follower", follower.id.href], {
|
|
3834
|
+
actor: await follower.toJsonLd(),
|
|
3835
|
+
state: this.initialFollowerState
|
|
3836
|
+
});
|
|
3837
|
+
await sendFollowResponse(ctx, follow, follower, approved);
|
|
3838
|
+
if (approved) await this.afterFollowApproved(ctx, follower);
|
|
3839
|
+
}
|
|
3840
|
+
async #relayActivity(ctx, activity) {
|
|
3841
|
+
const senderId = activity.actorId;
|
|
3842
|
+
const excludeBaseUris = senderId == null ? [] : [senderId];
|
|
3843
|
+
await this.deliverActivity(ctx, activity, excludeBaseUris);
|
|
3844
|
+
}
|
|
3845
|
+
setupInboxListeners() {
|
|
3846
|
+
if (this.federation == null) throw new Error("Federation must be initialized before inbox listeners");
|
|
3847
|
+
const listeners = this.federation.setInboxListeners("/users/{identifier}/inbox", "/inbox");
|
|
3848
|
+
listeners.on(_fedify_vocab.Follow, async (ctx, follow) => await this.#handleFollow(ctx, follow)).on(_fedify_vocab.Undo, async (ctx, undo) => await handleUndoFollow(ctx, undo, this.logger)).on(_fedify_vocab.Create, async (ctx, create) => await this.#relayActivity(ctx, create)).on(_fedify_vocab.Delete, async (ctx, deleteActivity) => await this.#relayActivity(ctx, deleteActivity)).on(_fedify_vocab.Move, async (ctx, move) => await this.#relayActivity(ctx, move)).on(_fedify_vocab.Update, async (ctx, update) => await this.#relayActivity(ctx, update)).on(_fedify_vocab.Announce, async (ctx, announce) => await this.#relayActivity(ctx, announce));
|
|
3849
|
+
return listeners;
|
|
3762
3850
|
}
|
|
3763
3851
|
async #getFederation() {
|
|
3764
3852
|
if (this.federation == null) {
|
|
@@ -3778,64 +3866,6 @@ var BaseRelay = class {
|
|
|
3778
3866
|
}
|
|
3779
3867
|
};
|
|
3780
3868
|
//#endregion
|
|
3781
|
-
//#region src/follow.ts
|
|
3782
|
-
/**
|
|
3783
|
-
* Validate Follow activity and return follower actor if valid.
|
|
3784
|
-
* This validation is common to both Mastodon and LitePub relay protocols.
|
|
3785
|
-
*
|
|
3786
|
-
* @param ctx The federation context
|
|
3787
|
-
* @param follow The Follow activity to validate
|
|
3788
|
-
* @returns The follower Actor if valid, null otherwise
|
|
3789
|
-
*/
|
|
3790
|
-
async function validateFollowActivity(ctx, follow) {
|
|
3791
|
-
if (follow.id == null || follow.objectId == null) return null;
|
|
3792
|
-
const parsed = ctx.parseUri(follow.objectId);
|
|
3793
|
-
if (!(follow.objectId.href === "https://www.w3.org/ns/activitystreams#Public") && parsed?.type !== "actor") return null;
|
|
3794
|
-
const follower = await follow.getActor(ctx);
|
|
3795
|
-
if (follower == null || follower.id == null || follower.preferredUsername == null || follower.inboxId == null) return null;
|
|
3796
|
-
return follower;
|
|
3797
|
-
}
|
|
3798
|
-
/**
|
|
3799
|
-
* Send Accept or Reject response for a Follow activity.
|
|
3800
|
-
* This is common to both Mastodon and LitePub relay protocols.
|
|
3801
|
-
*
|
|
3802
|
-
* @param ctx The federation context
|
|
3803
|
-
* @param follow The Follow activity being responded to
|
|
3804
|
-
* @param follower The actor who sent the Follow
|
|
3805
|
-
* @param approved Whether the follow was approved
|
|
3806
|
-
*/
|
|
3807
|
-
async function sendFollowResponse(ctx, follow, follower, approved) {
|
|
3808
|
-
const relayActorUri = ctx.getActorUri(RELAY_SERVER_ACTOR);
|
|
3809
|
-
const Activity = approved ? _fedify_vocab.Accept : _fedify_vocab.Reject;
|
|
3810
|
-
const action = approved ? "accepts" : "rejects";
|
|
3811
|
-
await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, follower, new Activity({
|
|
3812
|
-
id: new URL(`#${action}`, relayActorUri),
|
|
3813
|
-
actor: relayActorUri,
|
|
3814
|
-
object: follow
|
|
3815
|
-
}));
|
|
3816
|
-
}
|
|
3817
|
-
/**
|
|
3818
|
-
* Handle Undo activity for Follow.
|
|
3819
|
-
* This logic is identical for both Mastodon and LitePub relay protocols.
|
|
3820
|
-
*
|
|
3821
|
-
* @param ctx The federation context
|
|
3822
|
-
* @param undo The Undo activity to handle
|
|
3823
|
-
* @param logger The logger instance to use for warnings
|
|
3824
|
-
*/
|
|
3825
|
-
async function handleUndoFollow(ctx, undo, logger) {
|
|
3826
|
-
const activity = await undo.getObject({
|
|
3827
|
-
crossOrigin: "trust",
|
|
3828
|
-
...ctx
|
|
3829
|
-
});
|
|
3830
|
-
if (activity instanceof _fedify_vocab.Follow) {
|
|
3831
|
-
if (activity.id == null || activity.actorId == null) return;
|
|
3832
|
-
await ctx.data.kv.delete(["follower", activity.actorId.href]);
|
|
3833
|
-
} else logger.warn("Unsupported object type ({type}) for Undo activity: {object}", {
|
|
3834
|
-
type: activity?.constructor.name,
|
|
3835
|
-
object: activity
|
|
3836
|
-
});
|
|
3837
|
-
}
|
|
3838
|
-
//#endregion
|
|
3839
3869
|
//#region src/litepub.ts
|
|
3840
3870
|
const Temporal = Temporal$1;
|
|
3841
3871
|
const logger$1 = (0, _logtape_logtape.getLogger)([
|
|
@@ -3851,9 +3881,23 @@ const logger$1 = (0, _logtape_logtape.getLogger)([
|
|
|
3851
3881
|
* @since 2.0.0
|
|
3852
3882
|
*/
|
|
3853
3883
|
var LitePubRelay = class extends BaseRelay {
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3884
|
+
initialFollowerState = "pending";
|
|
3885
|
+
logger = logger$1;
|
|
3886
|
+
async shouldSkipFollow(ctx, follower) {
|
|
3887
|
+
if (follower.id == null) return true;
|
|
3888
|
+
const existingFollow = await ctx.data.kv.get(["follower", follower.id.href]);
|
|
3889
|
+
return await parseRelayFollowerData(follower.id.href, existingFollow) != null;
|
|
3890
|
+
}
|
|
3891
|
+
async afterFollowApproved(ctx, follower) {
|
|
3892
|
+
if (follower.id == null) return;
|
|
3893
|
+
const relayActorUri = ctx.getActorUri(RELAY_SERVER_ACTOR);
|
|
3894
|
+
await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, follower, new _fedify_vocab.Follow({
|
|
3895
|
+
actor: relayActorUri,
|
|
3896
|
+
object: follower.id,
|
|
3897
|
+
to: follower.id
|
|
3898
|
+
}));
|
|
3899
|
+
}
|
|
3900
|
+
async deliverActivity(ctx, activity, excludeBaseUris) {
|
|
3857
3901
|
const announce = new _fedify_vocab.Announce({
|
|
3858
3902
|
id: new URL(`/announce#${crypto.randomUUID()}`, ctx.origin),
|
|
3859
3903
|
actor: ctx.getActorUri(RELAY_SERVER_ACTOR),
|
|
@@ -3867,25 +3911,7 @@ var LitePubRelay = class extends BaseRelay {
|
|
|
3867
3911
|
});
|
|
3868
3912
|
}
|
|
3869
3913
|
setupInboxListeners() {
|
|
3870
|
-
|
|
3871
|
-
const follower = await validateFollowActivity(ctx, follow);
|
|
3872
|
-
if (!follower || !follower.id) return;
|
|
3873
|
-
if ((await ctx.data.kv.get(["follower", follower.id.href]))?.state === "pending") return;
|
|
3874
|
-
const approved = await this.options.subscriptionHandler(ctx, follower);
|
|
3875
|
-
if (approved) {
|
|
3876
|
-
await ctx.data.kv.set(["follower", follower.id.href], {
|
|
3877
|
-
actor: await follower.toJsonLd(),
|
|
3878
|
-
state: "pending"
|
|
3879
|
-
});
|
|
3880
|
-
await sendFollowResponse(ctx, follow, follower, approved);
|
|
3881
|
-
const relayActorUri = ctx.getActorUri(RELAY_SERVER_ACTOR);
|
|
3882
|
-
await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, follower, new _fedify_vocab.Follow({
|
|
3883
|
-
actor: relayActorUri,
|
|
3884
|
-
object: follower.id,
|
|
3885
|
-
to: follower.id
|
|
3886
|
-
}));
|
|
3887
|
-
} else await sendFollowResponse(ctx, follow, follower, approved);
|
|
3888
|
-
}).on(_fedify_vocab.Accept, async (ctx, accept) => {
|
|
3914
|
+
return super.setupInboxListeners().on(_fedify_vocab.Accept, async (ctx, accept) => {
|
|
3889
3915
|
const follow = await accept.getObject({
|
|
3890
3916
|
crossOrigin: "trust",
|
|
3891
3917
|
...ctx
|
|
@@ -3893,18 +3919,19 @@ var LitePubRelay = class extends BaseRelay {
|
|
|
3893
3919
|
if (!(follow instanceof _fedify_vocab.Follow)) return;
|
|
3894
3920
|
const relayActorId = follow.actorId;
|
|
3895
3921
|
if (relayActorId == null) return;
|
|
3896
|
-
const followerActor = await accept.getActor();
|
|
3922
|
+
const followerActor = await accept.getActor(ctx);
|
|
3897
3923
|
if (!(0, _fedify_vocab.isActor)(followerActor) || !followerActor.id) return;
|
|
3898
3924
|
const parsed = ctx.parseUri(relayActorId);
|
|
3899
3925
|
if (parsed == null || parsed.type !== "actor") return;
|
|
3900
3926
|
const followerData = await ctx.data.kv.get(["follower", followerActor.id.href]);
|
|
3901
|
-
if (followerData
|
|
3927
|
+
if (!isRelayFollowerData(followerData)) return;
|
|
3928
|
+
if (await parseRelayFollowerData(followerActor.id.href, followerData) == null) return;
|
|
3902
3929
|
const updatedFollowerData = {
|
|
3903
3930
|
...followerData,
|
|
3904
3931
|
state: "accepted"
|
|
3905
3932
|
};
|
|
3906
3933
|
await ctx.data.kv.set(["follower", followerActor.id.href], updatedFollowerData);
|
|
3907
|
-
})
|
|
3934
|
+
});
|
|
3908
3935
|
}
|
|
3909
3936
|
};
|
|
3910
3937
|
//#endregion
|
|
@@ -3922,27 +3949,15 @@ const logger = (0, _logtape_logtape.getLogger)([
|
|
|
3922
3949
|
* @since 2.0.0
|
|
3923
3950
|
*/
|
|
3924
3951
|
var MastodonRelay = class extends BaseRelay {
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3952
|
+
initialFollowerState = "accepted";
|
|
3953
|
+
logger = logger;
|
|
3954
|
+
async deliverActivity(ctx, _activity, excludeBaseUris) {
|
|
3928
3955
|
await ctx.forwardActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", {
|
|
3929
3956
|
skipIfUnsigned: true,
|
|
3930
3957
|
excludeBaseUris,
|
|
3931
3958
|
preferSharedInbox: true
|
|
3932
3959
|
});
|
|
3933
3960
|
}
|
|
3934
|
-
setupInboxListeners() {
|
|
3935
|
-
if (this.federation != null) this.federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(_fedify_vocab.Follow, async (ctx, follow) => {
|
|
3936
|
-
const follower = await validateFollowActivity(ctx, follow);
|
|
3937
|
-
if (!follower || !follower.id) return;
|
|
3938
|
-
const approved = await this.options.subscriptionHandler(ctx, follower);
|
|
3939
|
-
if (approved) await ctx.data.kv.set(["follower", follower.id.href], {
|
|
3940
|
-
actor: await follower.toJsonLd(),
|
|
3941
|
-
state: "accepted"
|
|
3942
|
-
});
|
|
3943
|
-
await sendFollowResponse(ctx, follow, follower, approved);
|
|
3944
|
-
}).on(_fedify_vocab.Undo, async (ctx, undo) => await handleUndoFollow(ctx, undo, logger)).on(_fedify_vocab.Create, async (ctx, create) => await this.#forwardToFollowers(ctx, create)).on(_fedify_vocab.Delete, async (ctx, deleteActivity) => await this.#forwardToFollowers(ctx, deleteActivity)).on(_fedify_vocab.Move, async (ctx, move) => await this.#forwardToFollowers(ctx, move)).on(_fedify_vocab.Update, async (ctx, update) => await this.#forwardToFollowers(ctx, update)).on(_fedify_vocab.Announce, async (ctx, announce) => await this.#forwardToFollowers(ctx, announce));
|
|
3945
|
-
}
|
|
3946
3961
|
};
|
|
3947
3962
|
//#endregion
|
|
3948
3963
|
//#region src/factory.ts
|
package/dist/mod.d.cts
CHANGED
|
@@ -1,8 +1,124 @@
|
|
|
1
1
|
/// <reference lib="esnext.temporal" />
|
|
2
2
|
import { Context, KvStore, MessageQueue } from "@fedify/fedify";
|
|
3
3
|
import { Actor } from "@fedify/vocab";
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
//#region ../vocab-runtime/dist/docloader-BK627tQQ.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Options for making `User-Agent` string.
|
|
7
|
+
* @see {@link getUserAgent}
|
|
8
|
+
* @since 1.3.0
|
|
9
|
+
*/
|
|
10
|
+
interface GetUserAgentOptions {
|
|
11
|
+
/**
|
|
12
|
+
* An optional software name and version, e.g., `"Hollo/1.0.0"`.
|
|
13
|
+
*/
|
|
14
|
+
software?: string | null;
|
|
15
|
+
/**
|
|
16
|
+
* An optional URL to append to the user agent string.
|
|
17
|
+
* Usually the URL of the ActivityPub instance.
|
|
18
|
+
*/
|
|
19
|
+
url?: string | URL | null;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Gets the user agent string for the given application and URL.
|
|
23
|
+
* @param options The options for making the user agent string.
|
|
24
|
+
* @returns The user agent string.
|
|
25
|
+
* @since 1.3.0
|
|
26
|
+
*/
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/docloader.d.ts
|
|
29
|
+
/**
|
|
30
|
+
* A remote JSON-LD document and its context fetched by
|
|
31
|
+
* a {@link DocumentLoader}.
|
|
32
|
+
*/
|
|
33
|
+
interface RemoteDocument {
|
|
34
|
+
/**
|
|
35
|
+
* The URL of the context document.
|
|
36
|
+
*/
|
|
37
|
+
contextUrl: string | null;
|
|
38
|
+
/**
|
|
39
|
+
* The fetched JSON-LD document.
|
|
40
|
+
*/
|
|
41
|
+
document: unknown;
|
|
42
|
+
/**
|
|
43
|
+
* The URL of the fetched document.
|
|
44
|
+
*/
|
|
45
|
+
documentUrl: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Options for {@link DocumentLoader}.
|
|
49
|
+
* @since 1.8.0
|
|
50
|
+
*/
|
|
51
|
+
interface DocumentLoaderOptions {
|
|
52
|
+
/**
|
|
53
|
+
* An `AbortSignal` for cancellation.
|
|
54
|
+
* @since 1.8.0
|
|
55
|
+
*/
|
|
56
|
+
signal?: AbortSignal;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A JSON-LD document loader that fetches documents from the Web.
|
|
60
|
+
* @param url The URL of the document to load.
|
|
61
|
+
* @param options The options for the document loader.
|
|
62
|
+
* @returns The loaded remote document.
|
|
63
|
+
*/
|
|
64
|
+
type DocumentLoader = (url: string, options?: DocumentLoaderOptions) => Promise<RemoteDocument>;
|
|
65
|
+
/**
|
|
66
|
+
* A factory function that creates a {@link DocumentLoader} with options.
|
|
67
|
+
* @param options The options for the document loader.
|
|
68
|
+
* @returns The document loader.
|
|
69
|
+
* @since 1.4.0
|
|
70
|
+
*/
|
|
71
|
+
type DocumentLoaderFactory = (options?: DocumentLoaderFactoryOptions) => DocumentLoader;
|
|
72
|
+
/**
|
|
73
|
+
* Options for {@link DocumentLoaderFactory}.
|
|
74
|
+
* @see {@link DocumentLoaderFactory}
|
|
75
|
+
* @see {@link AuthenticatedDocumentLoaderFactory}
|
|
76
|
+
* @since 1.4.0
|
|
77
|
+
*/
|
|
78
|
+
interface DocumentLoaderFactoryOptions {
|
|
79
|
+
/**
|
|
80
|
+
* Whether to allow fetching private network addresses.
|
|
81
|
+
* Turned off by default.
|
|
82
|
+
* @default `false``
|
|
83
|
+
*/
|
|
84
|
+
allowPrivateAddress?: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Options for making `User-Agent` string.
|
|
87
|
+
* If a string is given, it is used as the `User-Agent` header value.
|
|
88
|
+
* If an object is given, it is passed to {@link getUserAgent} function.
|
|
89
|
+
*/
|
|
90
|
+
userAgent?: GetUserAgentOptions | string;
|
|
91
|
+
/**
|
|
92
|
+
* The maximum number of redirections to follow.
|
|
93
|
+
* @default `20`
|
|
94
|
+
* @since 2.2.0
|
|
95
|
+
*/
|
|
96
|
+
maxRedirection?: number;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* A factory function that creates an authenticated {@link DocumentLoader} for
|
|
100
|
+
* a given identity. This is used for fetching documents that require
|
|
101
|
+
* authentication.
|
|
102
|
+
* @param identity The identity to create the document loader for.
|
|
103
|
+
* The actor's key pair.
|
|
104
|
+
* @param options The options for the document loader.
|
|
105
|
+
* @returns The authenticated document loader.
|
|
106
|
+
* @since 0.4.0
|
|
107
|
+
*/
|
|
108
|
+
type AuthenticatedDocumentLoaderFactory = (identity: {
|
|
109
|
+
keyId: URL;
|
|
110
|
+
privateKey: CryptoKey;
|
|
111
|
+
}, options?: DocumentLoaderFactoryOptions) => DocumentLoader;
|
|
112
|
+
/**
|
|
113
|
+
* Gets a {@link RemoteDocument} from the given response.
|
|
114
|
+
* @param url The URL of the document to load.
|
|
115
|
+
* @param response The response to get the document from.
|
|
116
|
+
* @param fetch The function to fetch the document.
|
|
117
|
+
* @returns The loaded remote document.
|
|
118
|
+
* @throws {FetchError} If the response is not OK.
|
|
119
|
+
* @internal
|
|
120
|
+
*/
|
|
121
|
+
//#endregion
|
|
6
122
|
//#region src/types.d.ts
|
|
7
123
|
declare const RELAY_SERVER_ACTOR = "relay";
|
|
8
124
|
/**
|
|
@@ -10,6 +126,12 @@ declare const RELAY_SERVER_ACTOR = "relay";
|
|
|
10
126
|
*/
|
|
11
127
|
type RelayType = "mastodon" | "litepub";
|
|
12
128
|
/**
|
|
129
|
+
* A follower's subscription state.
|
|
130
|
+
*
|
|
131
|
+
* @internal
|
|
132
|
+
*/
|
|
133
|
+
type RelayFollowerState = "pending" | "accepted";
|
|
134
|
+
/**
|
|
13
135
|
* Handler for subscription requests (Follow/Undo activities).
|
|
14
136
|
*/
|
|
15
137
|
type SubscriptionRequestHandler = (ctx: Context<RelayOptions>, clientActor: Actor) => Promise<boolean>;
|
|
@@ -66,7 +188,7 @@ interface RelayFollower {
|
|
|
66
188
|
/** The validated Actor object. */
|
|
67
189
|
readonly actor: Actor;
|
|
68
190
|
/** The follower's state. */
|
|
69
|
-
readonly state:
|
|
191
|
+
readonly state: RelayFollowerState;
|
|
70
192
|
}
|
|
71
193
|
/**
|
|
72
194
|
* Public interface for ActivityPub relay implementations.
|
package/dist/mod.d.ts
CHANGED
|
@@ -1,8 +1,124 @@
|
|
|
1
1
|
/// <reference lib="esnext.temporal" />
|
|
2
2
|
import { Context, KvStore, MessageQueue } from "@fedify/fedify";
|
|
3
3
|
import { Actor } from "@fedify/vocab";
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
//#region ../vocab-runtime/dist/docloader-BK627tQQ.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Options for making `User-Agent` string.
|
|
7
|
+
* @see {@link getUserAgent}
|
|
8
|
+
* @since 1.3.0
|
|
9
|
+
*/
|
|
10
|
+
interface GetUserAgentOptions {
|
|
11
|
+
/**
|
|
12
|
+
* An optional software name and version, e.g., `"Hollo/1.0.0"`.
|
|
13
|
+
*/
|
|
14
|
+
software?: string | null;
|
|
15
|
+
/**
|
|
16
|
+
* An optional URL to append to the user agent string.
|
|
17
|
+
* Usually the URL of the ActivityPub instance.
|
|
18
|
+
*/
|
|
19
|
+
url?: string | URL | null;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Gets the user agent string for the given application and URL.
|
|
23
|
+
* @param options The options for making the user agent string.
|
|
24
|
+
* @returns The user agent string.
|
|
25
|
+
* @since 1.3.0
|
|
26
|
+
*/
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/docloader.d.ts
|
|
29
|
+
/**
|
|
30
|
+
* A remote JSON-LD document and its context fetched by
|
|
31
|
+
* a {@link DocumentLoader}.
|
|
32
|
+
*/
|
|
33
|
+
interface RemoteDocument {
|
|
34
|
+
/**
|
|
35
|
+
* The URL of the context document.
|
|
36
|
+
*/
|
|
37
|
+
contextUrl: string | null;
|
|
38
|
+
/**
|
|
39
|
+
* The fetched JSON-LD document.
|
|
40
|
+
*/
|
|
41
|
+
document: unknown;
|
|
42
|
+
/**
|
|
43
|
+
* The URL of the fetched document.
|
|
44
|
+
*/
|
|
45
|
+
documentUrl: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Options for {@link DocumentLoader}.
|
|
49
|
+
* @since 1.8.0
|
|
50
|
+
*/
|
|
51
|
+
interface DocumentLoaderOptions {
|
|
52
|
+
/**
|
|
53
|
+
* An `AbortSignal` for cancellation.
|
|
54
|
+
* @since 1.8.0
|
|
55
|
+
*/
|
|
56
|
+
signal?: AbortSignal;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A JSON-LD document loader that fetches documents from the Web.
|
|
60
|
+
* @param url The URL of the document to load.
|
|
61
|
+
* @param options The options for the document loader.
|
|
62
|
+
* @returns The loaded remote document.
|
|
63
|
+
*/
|
|
64
|
+
type DocumentLoader = (url: string, options?: DocumentLoaderOptions) => Promise<RemoteDocument>;
|
|
65
|
+
/**
|
|
66
|
+
* A factory function that creates a {@link DocumentLoader} with options.
|
|
67
|
+
* @param options The options for the document loader.
|
|
68
|
+
* @returns The document loader.
|
|
69
|
+
* @since 1.4.0
|
|
70
|
+
*/
|
|
71
|
+
type DocumentLoaderFactory = (options?: DocumentLoaderFactoryOptions) => DocumentLoader;
|
|
72
|
+
/**
|
|
73
|
+
* Options for {@link DocumentLoaderFactory}.
|
|
74
|
+
* @see {@link DocumentLoaderFactory}
|
|
75
|
+
* @see {@link AuthenticatedDocumentLoaderFactory}
|
|
76
|
+
* @since 1.4.0
|
|
77
|
+
*/
|
|
78
|
+
interface DocumentLoaderFactoryOptions {
|
|
79
|
+
/**
|
|
80
|
+
* Whether to allow fetching private network addresses.
|
|
81
|
+
* Turned off by default.
|
|
82
|
+
* @default `false``
|
|
83
|
+
*/
|
|
84
|
+
allowPrivateAddress?: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Options for making `User-Agent` string.
|
|
87
|
+
* If a string is given, it is used as the `User-Agent` header value.
|
|
88
|
+
* If an object is given, it is passed to {@link getUserAgent} function.
|
|
89
|
+
*/
|
|
90
|
+
userAgent?: GetUserAgentOptions | string;
|
|
91
|
+
/**
|
|
92
|
+
* The maximum number of redirections to follow.
|
|
93
|
+
* @default `20`
|
|
94
|
+
* @since 2.2.0
|
|
95
|
+
*/
|
|
96
|
+
maxRedirection?: number;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* A factory function that creates an authenticated {@link DocumentLoader} for
|
|
100
|
+
* a given identity. This is used for fetching documents that require
|
|
101
|
+
* authentication.
|
|
102
|
+
* @param identity The identity to create the document loader for.
|
|
103
|
+
* The actor's key pair.
|
|
104
|
+
* @param options The options for the document loader.
|
|
105
|
+
* @returns The authenticated document loader.
|
|
106
|
+
* @since 0.4.0
|
|
107
|
+
*/
|
|
108
|
+
type AuthenticatedDocumentLoaderFactory = (identity: {
|
|
109
|
+
keyId: URL;
|
|
110
|
+
privateKey: CryptoKey;
|
|
111
|
+
}, options?: DocumentLoaderFactoryOptions) => DocumentLoader;
|
|
112
|
+
/**
|
|
113
|
+
* Gets a {@link RemoteDocument} from the given response.
|
|
114
|
+
* @param url The URL of the document to load.
|
|
115
|
+
* @param response The response to get the document from.
|
|
116
|
+
* @param fetch The function to fetch the document.
|
|
117
|
+
* @returns The loaded remote document.
|
|
118
|
+
* @throws {FetchError} If the response is not OK.
|
|
119
|
+
* @internal
|
|
120
|
+
*/
|
|
121
|
+
//#endregion
|
|
6
122
|
//#region src/types.d.ts
|
|
7
123
|
declare const RELAY_SERVER_ACTOR = "relay";
|
|
8
124
|
/**
|
|
@@ -10,6 +126,12 @@ declare const RELAY_SERVER_ACTOR = "relay";
|
|
|
10
126
|
*/
|
|
11
127
|
type RelayType = "mastodon" | "litepub";
|
|
12
128
|
/**
|
|
129
|
+
* A follower's subscription state.
|
|
130
|
+
*
|
|
131
|
+
* @internal
|
|
132
|
+
*/
|
|
133
|
+
type RelayFollowerState = "pending" | "accepted";
|
|
134
|
+
/**
|
|
13
135
|
* Handler for subscription requests (Follow/Undo activities).
|
|
14
136
|
*/
|
|
15
137
|
type SubscriptionRequestHandler = (ctx: Context<RelayOptions>, clientActor: Actor) => Promise<boolean>;
|
|
@@ -66,7 +188,7 @@ interface RelayFollower {
|
|
|
66
188
|
/** The validated Actor object. */
|
|
67
189
|
readonly actor: Actor;
|
|
68
190
|
/** The follower's state. */
|
|
69
|
-
readonly state:
|
|
191
|
+
readonly state: RelayFollowerState;
|
|
70
192
|
}
|
|
71
193
|
/**
|
|
72
194
|
* Public interface for ActivityPub relay implementations.
|
package/dist/mod.js
CHANGED
|
@@ -17,6 +17,28 @@ function isRelayFollowerData(value) {
|
|
|
17
17
|
const obj = value;
|
|
18
18
|
return "actor" in obj && "state" in obj && typeof obj.state === "string" && (obj.state === "pending" || obj.state === "accepted");
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Parses and semantically validates follower data from storage.
|
|
22
|
+
*
|
|
23
|
+
* @param actorId The actor ID used as the follower's storage key.
|
|
24
|
+
* @param value The stored follower data.
|
|
25
|
+
* @returns The parsed follower, or `null` if the row is invalid.
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
async function parseRelayFollowerData(actorId, value) {
|
|
29
|
+
if (!isRelayFollowerData(value)) return null;
|
|
30
|
+
try {
|
|
31
|
+
const actor = await Object$1.fromJsonLd(value.actor);
|
|
32
|
+
if (!isActor(actor) || actor.id?.href !== actorId) return null;
|
|
33
|
+
return {
|
|
34
|
+
actorId,
|
|
35
|
+
actor,
|
|
36
|
+
state: value.state
|
|
37
|
+
};
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
20
42
|
//#endregion
|
|
21
43
|
//#region src/builder.ts
|
|
22
44
|
const relayBuilder = createFederationBuilder();
|
|
@@ -77,12 +99,12 @@ relayBuilder.setActorDispatcher("/users/{identifier}", async (ctx, identifier) =
|
|
|
77
99
|
});
|
|
78
100
|
async function getFollowerActors(ctx) {
|
|
79
101
|
const actors = [];
|
|
80
|
-
for await (const { value } of ctx.data.kv.list(["follower"])) {
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
const
|
|
84
|
-
if (
|
|
85
|
-
actors.push(actor);
|
|
102
|
+
for await (const { key, value } of ctx.data.kv.list(["follower"])) {
|
|
103
|
+
const actorId = key[1];
|
|
104
|
+
if (typeof actorId !== "string") continue;
|
|
105
|
+
const follower = await parseRelayFollowerData(actorId, value);
|
|
106
|
+
if (follower?.state !== "accepted") continue;
|
|
107
|
+
actors.push(follower.actor);
|
|
86
108
|
}
|
|
87
109
|
return actors;
|
|
88
110
|
}
|
|
@@ -93,6 +115,64 @@ async function dispatchRelayActors(ctx, identifier) {
|
|
|
93
115
|
relayBuilder.setFollowersDispatcher("/users/{identifier}/followers", dispatchRelayActors);
|
|
94
116
|
relayBuilder.setFollowingDispatcher("/users/{identifier}/following", dispatchRelayActors);
|
|
95
117
|
//#endregion
|
|
118
|
+
//#region src/follow.ts
|
|
119
|
+
/**
|
|
120
|
+
* Validate Follow activity and return follower actor if valid.
|
|
121
|
+
* This validation is common to both Mastodon and LitePub relay protocols.
|
|
122
|
+
*
|
|
123
|
+
* @param ctx The federation context
|
|
124
|
+
* @param follow The Follow activity to validate
|
|
125
|
+
* @returns The follower Actor if valid, null otherwise
|
|
126
|
+
*/
|
|
127
|
+
async function validateFollowActivity(ctx, follow) {
|
|
128
|
+
if (follow.id == null || follow.objectId == null) return null;
|
|
129
|
+
const parsed = ctx.parseUri(follow.objectId);
|
|
130
|
+
if (!(follow.objectId.href === "https://www.w3.org/ns/activitystreams#Public") && parsed?.type !== "actor") return null;
|
|
131
|
+
const follower = await follow.getActor(ctx);
|
|
132
|
+
if (follower == null || follower.id == null || follower.preferredUsername == null || follower.inboxId == null) return null;
|
|
133
|
+
return follower;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Send Accept or Reject response for a Follow activity.
|
|
137
|
+
* This is common to both Mastodon and LitePub relay protocols.
|
|
138
|
+
*
|
|
139
|
+
* @param ctx The federation context
|
|
140
|
+
* @param follow The Follow activity being responded to
|
|
141
|
+
* @param follower The actor who sent the Follow
|
|
142
|
+
* @param approved Whether the follow was approved
|
|
143
|
+
*/
|
|
144
|
+
async function sendFollowResponse(ctx, follow, follower, approved) {
|
|
145
|
+
const relayActorUri = ctx.getActorUri(RELAY_SERVER_ACTOR);
|
|
146
|
+
const Activity = approved ? Accept : Reject;
|
|
147
|
+
const action = approved ? "accepts" : "rejects";
|
|
148
|
+
await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, follower, new Activity({
|
|
149
|
+
id: new URL(`#${action}`, relayActorUri),
|
|
150
|
+
actor: relayActorUri,
|
|
151
|
+
object: follow
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Handle Undo activity for Follow.
|
|
156
|
+
* This logic is identical for both Mastodon and LitePub relay protocols.
|
|
157
|
+
*
|
|
158
|
+
* @param ctx The federation context
|
|
159
|
+
* @param undo The Undo activity to handle
|
|
160
|
+
* @param logger The logger instance to use for warnings
|
|
161
|
+
*/
|
|
162
|
+
async function handleUndoFollow(ctx, undo, logger) {
|
|
163
|
+
const activity = await undo.getObject({
|
|
164
|
+
crossOrigin: "trust",
|
|
165
|
+
...ctx
|
|
166
|
+
});
|
|
167
|
+
if (activity instanceof Follow) {
|
|
168
|
+
if (activity.id == null || activity.actorId == null) return;
|
|
169
|
+
await ctx.data.kv.delete(["follower", activity.actorId.href]);
|
|
170
|
+
} else logger.warn("Unsupported object type ({type}) for Undo activity: {object}", {
|
|
171
|
+
type: activity?.constructor.name,
|
|
172
|
+
object: activity
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
//#endregion
|
|
96
176
|
//#region src/base.ts
|
|
97
177
|
/**
|
|
98
178
|
* Abstract base class for relay implementations.
|
|
@@ -112,25 +192,6 @@ var BaseRelay = class {
|
|
|
112
192
|
return await (await this.#getFederation()).fetch(request, { contextData: this.options });
|
|
113
193
|
}
|
|
114
194
|
/**
|
|
115
|
-
* Helper method to parse and validate follower data from storage.
|
|
116
|
-
* Deserializes JSON-LD actor data and validates it.
|
|
117
|
-
*
|
|
118
|
-
* @param actorId The actor ID of the follower
|
|
119
|
-
* @param data Raw data from KV store
|
|
120
|
-
* @returns RelayFollower object if valid, null otherwise
|
|
121
|
-
* @internal
|
|
122
|
-
*/
|
|
123
|
-
async parseFollowerData(actorId, data) {
|
|
124
|
-
if (!isRelayFollowerData(data)) return null;
|
|
125
|
-
const actor = await Object$1.fromJsonLd(data.actor);
|
|
126
|
-
if (!isActor(actor)) return null;
|
|
127
|
-
return {
|
|
128
|
-
actorId,
|
|
129
|
-
actor,
|
|
130
|
-
state: data.state
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
195
|
* Lists all followers of the relay.
|
|
135
196
|
*
|
|
136
197
|
* @returns An async iterator of follower entries
|
|
@@ -159,7 +220,7 @@ var BaseRelay = class {
|
|
|
159
220
|
for await (const entry of this.options.kv.list(["follower"])) {
|
|
160
221
|
const actorId = entry.key[1];
|
|
161
222
|
if (typeof actorId !== "string") continue;
|
|
162
|
-
const follower = await
|
|
223
|
+
const follower = await parseRelayFollowerData(actorId, entry.value);
|
|
163
224
|
if (follower) yield follower;
|
|
164
225
|
}
|
|
165
226
|
}
|
|
@@ -192,8 +253,35 @@ var BaseRelay = class {
|
|
|
192
253
|
* @since 2.0.0
|
|
193
254
|
*/
|
|
194
255
|
async getFollower(actorId) {
|
|
195
|
-
|
|
196
|
-
|
|
256
|
+
return await parseRelayFollowerData(actorId, await this.options.kv.get(["follower", actorId]));
|
|
257
|
+
}
|
|
258
|
+
shouldSkipFollow(_ctx, _follower) {
|
|
259
|
+
return Promise.resolve(false);
|
|
260
|
+
}
|
|
261
|
+
afterFollowApproved(_ctx, _follower) {
|
|
262
|
+
return Promise.resolve();
|
|
263
|
+
}
|
|
264
|
+
async #handleFollow(ctx, follow) {
|
|
265
|
+
const follower = await validateFollowActivity(ctx, follow);
|
|
266
|
+
if (follower?.id == null || await this.shouldSkipFollow(ctx, follower)) return;
|
|
267
|
+
const approved = await this.options.subscriptionHandler(ctx, follower);
|
|
268
|
+
if (approved) await ctx.data.kv.set(["follower", follower.id.href], {
|
|
269
|
+
actor: await follower.toJsonLd(),
|
|
270
|
+
state: this.initialFollowerState
|
|
271
|
+
});
|
|
272
|
+
await sendFollowResponse(ctx, follow, follower, approved);
|
|
273
|
+
if (approved) await this.afterFollowApproved(ctx, follower);
|
|
274
|
+
}
|
|
275
|
+
async #relayActivity(ctx, activity) {
|
|
276
|
+
const senderId = activity.actorId;
|
|
277
|
+
const excludeBaseUris = senderId == null ? [] : [senderId];
|
|
278
|
+
await this.deliverActivity(ctx, activity, excludeBaseUris);
|
|
279
|
+
}
|
|
280
|
+
setupInboxListeners() {
|
|
281
|
+
if (this.federation == null) throw new Error("Federation must be initialized before inbox listeners");
|
|
282
|
+
const listeners = this.federation.setInboxListeners("/users/{identifier}/inbox", "/inbox");
|
|
283
|
+
listeners.on(Follow, async (ctx, follow) => await this.#handleFollow(ctx, follow)).on(Undo, async (ctx, undo) => await handleUndoFollow(ctx, undo, this.logger)).on(Create, async (ctx, create) => await this.#relayActivity(ctx, create)).on(Delete, async (ctx, deleteActivity) => await this.#relayActivity(ctx, deleteActivity)).on(Move, async (ctx, move) => await this.#relayActivity(ctx, move)).on(Update, async (ctx, update) => await this.#relayActivity(ctx, update)).on(Announce, async (ctx, announce) => await this.#relayActivity(ctx, announce));
|
|
284
|
+
return listeners;
|
|
197
285
|
}
|
|
198
286
|
async #getFederation() {
|
|
199
287
|
if (this.federation == null) {
|
|
@@ -213,64 +301,6 @@ var BaseRelay = class {
|
|
|
213
301
|
}
|
|
214
302
|
};
|
|
215
303
|
//#endregion
|
|
216
|
-
//#region src/follow.ts
|
|
217
|
-
/**
|
|
218
|
-
* Validate Follow activity and return follower actor if valid.
|
|
219
|
-
* This validation is common to both Mastodon and LitePub relay protocols.
|
|
220
|
-
*
|
|
221
|
-
* @param ctx The federation context
|
|
222
|
-
* @param follow The Follow activity to validate
|
|
223
|
-
* @returns The follower Actor if valid, null otherwise
|
|
224
|
-
*/
|
|
225
|
-
async function validateFollowActivity(ctx, follow) {
|
|
226
|
-
if (follow.id == null || follow.objectId == null) return null;
|
|
227
|
-
const parsed = ctx.parseUri(follow.objectId);
|
|
228
|
-
if (!(follow.objectId.href === "https://www.w3.org/ns/activitystreams#Public") && parsed?.type !== "actor") return null;
|
|
229
|
-
const follower = await follow.getActor(ctx);
|
|
230
|
-
if (follower == null || follower.id == null || follower.preferredUsername == null || follower.inboxId == null) return null;
|
|
231
|
-
return follower;
|
|
232
|
-
}
|
|
233
|
-
/**
|
|
234
|
-
* Send Accept or Reject response for a Follow activity.
|
|
235
|
-
* This is common to both Mastodon and LitePub relay protocols.
|
|
236
|
-
*
|
|
237
|
-
* @param ctx The federation context
|
|
238
|
-
* @param follow The Follow activity being responded to
|
|
239
|
-
* @param follower The actor who sent the Follow
|
|
240
|
-
* @param approved Whether the follow was approved
|
|
241
|
-
*/
|
|
242
|
-
async function sendFollowResponse(ctx, follow, follower, approved) {
|
|
243
|
-
const relayActorUri = ctx.getActorUri(RELAY_SERVER_ACTOR);
|
|
244
|
-
const Activity = approved ? Accept : Reject;
|
|
245
|
-
const action = approved ? "accepts" : "rejects";
|
|
246
|
-
await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, follower, new Activity({
|
|
247
|
-
id: new URL(`#${action}`, relayActorUri),
|
|
248
|
-
actor: relayActorUri,
|
|
249
|
-
object: follow
|
|
250
|
-
}));
|
|
251
|
-
}
|
|
252
|
-
/**
|
|
253
|
-
* Handle Undo activity for Follow.
|
|
254
|
-
* This logic is identical for both Mastodon and LitePub relay protocols.
|
|
255
|
-
*
|
|
256
|
-
* @param ctx The federation context
|
|
257
|
-
* @param undo The Undo activity to handle
|
|
258
|
-
* @param logger The logger instance to use for warnings
|
|
259
|
-
*/
|
|
260
|
-
async function handleUndoFollow(ctx, undo, logger) {
|
|
261
|
-
const activity = await undo.getObject({
|
|
262
|
-
crossOrigin: "trust",
|
|
263
|
-
...ctx
|
|
264
|
-
});
|
|
265
|
-
if (activity instanceof Follow) {
|
|
266
|
-
if (activity.id == null || activity.actorId == null) return;
|
|
267
|
-
await ctx.data.kv.delete(["follower", activity.actorId.href]);
|
|
268
|
-
} else logger.warn("Unsupported object type ({type}) for Undo activity: {object}", {
|
|
269
|
-
type: activity?.constructor.name,
|
|
270
|
-
object: activity
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
//#endregion
|
|
274
304
|
//#region src/litepub.ts
|
|
275
305
|
const logger$1 = getLogger([
|
|
276
306
|
"fedify",
|
|
@@ -285,9 +315,23 @@ const logger$1 = getLogger([
|
|
|
285
315
|
* @since 2.0.0
|
|
286
316
|
*/
|
|
287
317
|
var LitePubRelay = class extends BaseRelay {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
318
|
+
initialFollowerState = "pending";
|
|
319
|
+
logger = logger$1;
|
|
320
|
+
async shouldSkipFollow(ctx, follower) {
|
|
321
|
+
if (follower.id == null) return true;
|
|
322
|
+
const existingFollow = await ctx.data.kv.get(["follower", follower.id.href]);
|
|
323
|
+
return await parseRelayFollowerData(follower.id.href, existingFollow) != null;
|
|
324
|
+
}
|
|
325
|
+
async afterFollowApproved(ctx, follower) {
|
|
326
|
+
if (follower.id == null) return;
|
|
327
|
+
const relayActorUri = ctx.getActorUri(RELAY_SERVER_ACTOR);
|
|
328
|
+
await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, follower, new Follow({
|
|
329
|
+
actor: relayActorUri,
|
|
330
|
+
object: follower.id,
|
|
331
|
+
to: follower.id
|
|
332
|
+
}));
|
|
333
|
+
}
|
|
334
|
+
async deliverActivity(ctx, activity, excludeBaseUris) {
|
|
291
335
|
const announce = new Announce({
|
|
292
336
|
id: new URL(`/announce#${crypto.randomUUID()}`, ctx.origin),
|
|
293
337
|
actor: ctx.getActorUri(RELAY_SERVER_ACTOR),
|
|
@@ -301,25 +345,7 @@ var LitePubRelay = class extends BaseRelay {
|
|
|
301
345
|
});
|
|
302
346
|
}
|
|
303
347
|
setupInboxListeners() {
|
|
304
|
-
|
|
305
|
-
const follower = await validateFollowActivity(ctx, follow);
|
|
306
|
-
if (!follower || !follower.id) return;
|
|
307
|
-
if ((await ctx.data.kv.get(["follower", follower.id.href]))?.state === "pending") return;
|
|
308
|
-
const approved = await this.options.subscriptionHandler(ctx, follower);
|
|
309
|
-
if (approved) {
|
|
310
|
-
await ctx.data.kv.set(["follower", follower.id.href], {
|
|
311
|
-
actor: await follower.toJsonLd(),
|
|
312
|
-
state: "pending"
|
|
313
|
-
});
|
|
314
|
-
await sendFollowResponse(ctx, follow, follower, approved);
|
|
315
|
-
const relayActorUri = ctx.getActorUri(RELAY_SERVER_ACTOR);
|
|
316
|
-
await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, follower, new Follow({
|
|
317
|
-
actor: relayActorUri,
|
|
318
|
-
object: follower.id,
|
|
319
|
-
to: follower.id
|
|
320
|
-
}));
|
|
321
|
-
} else await sendFollowResponse(ctx, follow, follower, approved);
|
|
322
|
-
}).on(Accept, async (ctx, accept) => {
|
|
348
|
+
return super.setupInboxListeners().on(Accept, async (ctx, accept) => {
|
|
323
349
|
const follow = await accept.getObject({
|
|
324
350
|
crossOrigin: "trust",
|
|
325
351
|
...ctx
|
|
@@ -327,18 +353,19 @@ var LitePubRelay = class extends BaseRelay {
|
|
|
327
353
|
if (!(follow instanceof Follow)) return;
|
|
328
354
|
const relayActorId = follow.actorId;
|
|
329
355
|
if (relayActorId == null) return;
|
|
330
|
-
const followerActor = await accept.getActor();
|
|
356
|
+
const followerActor = await accept.getActor(ctx);
|
|
331
357
|
if (!isActor(followerActor) || !followerActor.id) return;
|
|
332
358
|
const parsed = ctx.parseUri(relayActorId);
|
|
333
359
|
if (parsed == null || parsed.type !== "actor") return;
|
|
334
360
|
const followerData = await ctx.data.kv.get(["follower", followerActor.id.href]);
|
|
335
|
-
if (followerData
|
|
361
|
+
if (!isRelayFollowerData(followerData)) return;
|
|
362
|
+
if (await parseRelayFollowerData(followerActor.id.href, followerData) == null) return;
|
|
336
363
|
const updatedFollowerData = {
|
|
337
364
|
...followerData,
|
|
338
365
|
state: "accepted"
|
|
339
366
|
};
|
|
340
367
|
await ctx.data.kv.set(["follower", followerActor.id.href], updatedFollowerData);
|
|
341
|
-
})
|
|
368
|
+
});
|
|
342
369
|
}
|
|
343
370
|
};
|
|
344
371
|
//#endregion
|
|
@@ -356,27 +383,15 @@ const logger = getLogger([
|
|
|
356
383
|
* @since 2.0.0
|
|
357
384
|
*/
|
|
358
385
|
var MastodonRelay = class extends BaseRelay {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
386
|
+
initialFollowerState = "accepted";
|
|
387
|
+
logger = logger;
|
|
388
|
+
async deliverActivity(ctx, _activity, excludeBaseUris) {
|
|
362
389
|
await ctx.forwardActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", {
|
|
363
390
|
skipIfUnsigned: true,
|
|
364
391
|
excludeBaseUris,
|
|
365
392
|
preferSharedInbox: true
|
|
366
393
|
});
|
|
367
394
|
}
|
|
368
|
-
setupInboxListeners() {
|
|
369
|
-
if (this.federation != null) this.federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Follow, async (ctx, follow) => {
|
|
370
|
-
const follower = await validateFollowActivity(ctx, follow);
|
|
371
|
-
if (!follower || !follower.id) return;
|
|
372
|
-
const approved = await this.options.subscriptionHandler(ctx, follower);
|
|
373
|
-
if (approved) await ctx.data.kv.set(["follower", follower.id.href], {
|
|
374
|
-
actor: await follower.toJsonLd(),
|
|
375
|
-
state: "accepted"
|
|
376
|
-
});
|
|
377
|
-
await sendFollowResponse(ctx, follow, follower, approved);
|
|
378
|
-
}).on(Undo, async (ctx, undo) => await handleUndoFollow(ctx, undo, logger)).on(Create, async (ctx, create) => await this.#forwardToFollowers(ctx, create)).on(Delete, async (ctx, deleteActivity) => await this.#forwardToFollowers(ctx, deleteActivity)).on(Move, async (ctx, move) => await this.#forwardToFollowers(ctx, move)).on(Update, async (ctx, update) => await this.#forwardToFollowers(ctx, update)).on(Announce, async (ctx, announce) => await this.#forwardToFollowers(ctx, announce));
|
|
379
|
-
}
|
|
380
395
|
};
|
|
381
396
|
//#endregion
|
|
382
397
|
//#region src/factory.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fedify/relay",
|
|
3
|
-
"version": "2.4.0-dev.
|
|
3
|
+
"version": "2.4.0-dev.1797+6c3f0b36",
|
|
4
4
|
"description": "ActivityPub relay support for Fedify",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Fedify",
|
|
@@ -47,14 +47,14 @@
|
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@logtape/logtape": "^2.2.0",
|
|
49
49
|
"temporal-polyfill": "^1.0.1",
|
|
50
|
-
"@fedify/fedify": "^2.4.0-dev.
|
|
51
|
-
"@fedify/vocab": "2.4.0-dev.
|
|
50
|
+
"@fedify/fedify": "^2.4.0-dev.1797+6c3f0b36",
|
|
51
|
+
"@fedify/vocab": "2.4.0-dev.1797+6c3f0b36"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"tsdown": "^0.22.0",
|
|
55
55
|
"typescript": "^6.0.0",
|
|
56
56
|
"urlpattern-polyfill": "^10.1.0",
|
|
57
|
-
"@fedify/vocab-runtime": "^2.4.0-dev.
|
|
57
|
+
"@fedify/vocab-runtime": "^2.4.0-dev.1797+6c3f0b36"
|
|
58
58
|
},
|
|
59
59
|
"scripts": {
|
|
60
60
|
"build:self": "tsdown",
|