@fedify/relay 2.0.0-dev.1908 → 2.0.0-dev.206
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/LICENSE +1 -1
- package/README.md +159 -53
- package/dist/litepub.test.d.ts +3 -0
- package/dist/litepub.test.js +701 -0
- package/dist/mastodon.test.d.ts +3 -0
- package/dist/mastodon.test.js +664 -0
- package/dist/mod.cjs +403 -179
- package/dist/mod.d.cts +123 -31
- package/dist/mod.d.ts +124 -31
- package/dist/mod.js +403 -179
- package/dist/types-ZeHCmEIv.js +28333 -0
- package/package.json +13 -11
package/dist/mod.js
CHANGED
|
@@ -1,179 +1,296 @@
|
|
|
1
|
-
import { createFederation, exportJwk, generateCryptoKeyPair, importJwk } from "@fedify/fedify";
|
|
2
|
-
import { Accept, Create, Delete, Follow, Move, Object as Object$1, Reject, Service, Undo, Update, isActor } from "@fedify/fedify/vocab";
|
|
3
1
|
|
|
4
|
-
|
|
2
|
+
import { Temporal } from "@js-temporal/polyfill";
|
|
3
|
+
|
|
4
|
+
import { createFederationBuilder, exportJwk, generateCryptoKeyPair, importJwk } from "@fedify/fedify";
|
|
5
|
+
import { Accept, Announce, Application, Create, Delete, Follow, Move, Object as Object$1, PUBLIC_COLLECTION, Reject, Undo, Update, isActor } from "@fedify/vocab";
|
|
6
|
+
import { getLogger } from "@logtape/logtape";
|
|
7
|
+
|
|
8
|
+
//#region src/types.ts
|
|
5
9
|
const RELAY_SERVER_ACTOR = "relay";
|
|
6
10
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* with Mastodon instances.
|
|
11
|
+
* Type predicate to check if a value is valid RelayFollowerData from KV store.
|
|
12
|
+
* Validates the storage format (JSON-LD), not the deserialized Actor instance.
|
|
10
13
|
*
|
|
11
|
-
* @
|
|
14
|
+
* @param value The value to check
|
|
15
|
+
* @returns true if the value is a RelayFollowerData
|
|
16
|
+
* @internal
|
|
12
17
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
], {
|
|
59
|
-
privateKey: await exportJwk(rsaPair$1.privateKey),
|
|
60
|
-
publicKey: await exportJwk(rsaPair$1.publicKey)
|
|
61
|
-
});
|
|
62
|
-
await options.kv.set([
|
|
63
|
-
"keypair",
|
|
64
|
-
"ed25519",
|
|
65
|
-
identifier
|
|
66
|
-
], {
|
|
67
|
-
privateKey: await exportJwk(ed25519Pair$1.privateKey),
|
|
68
|
-
publicKey: await exportJwk(ed25519Pair$1.publicKey)
|
|
69
|
-
});
|
|
70
|
-
return [rsaPair$1, ed25519Pair$1];
|
|
71
|
-
}
|
|
72
|
-
const rsaPair = {
|
|
73
|
-
privateKey: await importJwk(rsaPairJson.privateKey, "private"),
|
|
74
|
-
publicKey: await importJwk(rsaPairJson.publicKey, "public")
|
|
75
|
-
};
|
|
76
|
-
const ed25519Pair = {
|
|
77
|
-
privateKey: await importJwk(ed25519PairJson.privateKey, "private"),
|
|
78
|
-
publicKey: await importJwk(ed25519PairJson.publicKey, "public")
|
|
79
|
-
};
|
|
80
|
-
return [rsaPair, ed25519Pair];
|
|
81
|
-
});
|
|
82
|
-
this.#federation.setFollowersDispatcher("/users/{identifier}/followers", async (_ctx, identifier) => {
|
|
83
|
-
if (identifier !== RELAY_SERVER_ACTOR) return null;
|
|
84
|
-
const activityIds = await options.kv.get(["followers"]) ?? [];
|
|
85
|
-
const actors = [];
|
|
86
|
-
for (const activityId of activityIds) {
|
|
87
|
-
const actorJson = await options.kv.get(["follower", activityId]);
|
|
88
|
-
const actor = await Object$1.fromJsonLd(actorJson);
|
|
89
|
-
if (!isActor(actor)) continue;
|
|
90
|
-
actors.push(actor);
|
|
91
|
-
}
|
|
92
|
-
return { items: actors };
|
|
18
|
+
function isRelayFollowerData(value) {
|
|
19
|
+
if (!value || typeof value !== "object") return false;
|
|
20
|
+
const obj = value;
|
|
21
|
+
return "actor" in obj && "state" in obj && typeof obj.state === "string" && (obj.state === "pending" || obj.state === "accepted");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/builder.ts
|
|
26
|
+
const relayBuilder = createFederationBuilder();
|
|
27
|
+
relayBuilder.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => {
|
|
28
|
+
if (identifier !== RELAY_SERVER_ACTOR) return null;
|
|
29
|
+
const keys = await ctx.getActorKeyPairs(identifier);
|
|
30
|
+
return new Application({
|
|
31
|
+
id: ctx.getActorUri(identifier),
|
|
32
|
+
preferredUsername: identifier,
|
|
33
|
+
name: ctx.data.name ?? "ActivityPub Relay",
|
|
34
|
+
inbox: ctx.getInboxUri(),
|
|
35
|
+
followers: ctx.getFollowersUri(identifier),
|
|
36
|
+
following: ctx.getFollowingUri(identifier),
|
|
37
|
+
url: ctx.getActorUri(identifier),
|
|
38
|
+
publicKey: keys[0].cryptographicKey,
|
|
39
|
+
assertionMethods: keys.map((k) => k.multikey)
|
|
40
|
+
});
|
|
41
|
+
}).setKeyPairsDispatcher(async (ctx, identifier) => {
|
|
42
|
+
if (identifier !== RELAY_SERVER_ACTOR) return [];
|
|
43
|
+
const rsaPairJson = await ctx.data.kv.get([
|
|
44
|
+
"keypair",
|
|
45
|
+
"rsa",
|
|
46
|
+
identifier
|
|
47
|
+
]);
|
|
48
|
+
const ed25519PairJson = await ctx.data.kv.get([
|
|
49
|
+
"keypair",
|
|
50
|
+
"ed25519",
|
|
51
|
+
identifier
|
|
52
|
+
]);
|
|
53
|
+
if (rsaPairJson == null || ed25519PairJson == null) {
|
|
54
|
+
const rsaPair$1 = await generateCryptoKeyPair("RSASSA-PKCS1-v1_5");
|
|
55
|
+
const ed25519Pair$1 = await generateCryptoKeyPair("Ed25519");
|
|
56
|
+
await ctx.data.kv.set([
|
|
57
|
+
"keypair",
|
|
58
|
+
"rsa",
|
|
59
|
+
identifier
|
|
60
|
+
], {
|
|
61
|
+
privateKey: await exportJwk(rsaPair$1.privateKey),
|
|
62
|
+
publicKey: await exportJwk(rsaPair$1.publicKey)
|
|
93
63
|
});
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (recipient == null || recipient.id == null || recipient.preferredUsername == null || recipient.inboxId == null) return;
|
|
102
|
-
let approved = false;
|
|
103
|
-
if (this.#subscriptionHandler) approved = await this.#subscriptionHandler(ctx, recipient);
|
|
104
|
-
if (approved) {
|
|
105
|
-
const followers = await options.kv.get(["followers"]) ?? [];
|
|
106
|
-
followers.push(follow.id.href);
|
|
107
|
-
await options.kv.set(["followers"], followers);
|
|
108
|
-
await options.kv.set(["follower", follow.id.href], await recipient.toJsonLd());
|
|
109
|
-
await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, recipient, new Accept({
|
|
110
|
-
id: new URL(`#accepts`, relayActorUri),
|
|
111
|
-
actor: relayActorUri,
|
|
112
|
-
object: follow
|
|
113
|
-
}));
|
|
114
|
-
} else await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, recipient, new Reject({
|
|
115
|
-
id: new URL(`#rejects`, relayActorUri),
|
|
116
|
-
actor: relayActorUri,
|
|
117
|
-
object: follow
|
|
118
|
-
}));
|
|
119
|
-
}).on(Undo, async (ctx, undo) => {
|
|
120
|
-
const activity = await undo.getObject(ctx);
|
|
121
|
-
if (activity instanceof Follow) {
|
|
122
|
-
if (activity.id == null || activity.actorId == null) return;
|
|
123
|
-
const activityId = activity.id.href;
|
|
124
|
-
const followers = await options.kv.get(["followers"]) ?? [];
|
|
125
|
-
const updatedFollowers = followers.filter((id) => id !== activityId);
|
|
126
|
-
await options.kv.set(["followers"], updatedFollowers);
|
|
127
|
-
options.kv.delete(["follower", activityId]);
|
|
128
|
-
} else console.warn("Unsupported object type ({type}) for Undo activity: {object}", {
|
|
129
|
-
type: activity?.constructor.name,
|
|
130
|
-
object: activity
|
|
131
|
-
});
|
|
132
|
-
}).on(Create, async (ctx, create) => {
|
|
133
|
-
const sender = await create.getActor(ctx);
|
|
134
|
-
const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
|
|
135
|
-
await ctx.forwardActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", {
|
|
136
|
-
skipIfUnsigned: true,
|
|
137
|
-
excludeBaseUris,
|
|
138
|
-
preferSharedInbox: true
|
|
139
|
-
});
|
|
140
|
-
}).on(Delete, async (ctx, deleteActivity) => {
|
|
141
|
-
const sender = await deleteActivity.getActor(ctx);
|
|
142
|
-
const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
|
|
143
|
-
await ctx.forwardActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", {
|
|
144
|
-
skipIfUnsigned: true,
|
|
145
|
-
excludeBaseUris,
|
|
146
|
-
preferSharedInbox: true
|
|
147
|
-
});
|
|
148
|
-
}).on(Move, async (ctx, deleteActivity) => {
|
|
149
|
-
const sender = await deleteActivity.getActor(ctx);
|
|
150
|
-
const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
|
|
151
|
-
await ctx.forwardActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", {
|
|
152
|
-
skipIfUnsigned: true,
|
|
153
|
-
excludeBaseUris,
|
|
154
|
-
preferSharedInbox: true
|
|
155
|
-
});
|
|
156
|
-
}).on(Update, async (ctx, deleteActivity) => {
|
|
157
|
-
const sender = await deleteActivity.getActor(ctx);
|
|
158
|
-
const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
|
|
159
|
-
await ctx.forwardActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", {
|
|
160
|
-
skipIfUnsigned: true,
|
|
161
|
-
excludeBaseUris,
|
|
162
|
-
preferSharedInbox: true
|
|
163
|
-
});
|
|
64
|
+
await ctx.data.kv.set([
|
|
65
|
+
"keypair",
|
|
66
|
+
"ed25519",
|
|
67
|
+
identifier
|
|
68
|
+
], {
|
|
69
|
+
privateKey: await exportJwk(ed25519Pair$1.privateKey),
|
|
70
|
+
publicKey: await exportJwk(ed25519Pair$1.publicKey)
|
|
164
71
|
});
|
|
72
|
+
return [rsaPair$1, ed25519Pair$1];
|
|
73
|
+
}
|
|
74
|
+
const rsaPair = {
|
|
75
|
+
privateKey: await importJwk(rsaPairJson.privateKey, "private"),
|
|
76
|
+
publicKey: await importJwk(rsaPairJson.publicKey, "public")
|
|
77
|
+
};
|
|
78
|
+
const ed25519Pair = {
|
|
79
|
+
privateKey: await importJwk(ed25519PairJson.privateKey, "private"),
|
|
80
|
+
publicKey: await importJwk(ed25519PairJson.publicKey, "public")
|
|
81
|
+
};
|
|
82
|
+
return [rsaPair, ed25519Pair];
|
|
83
|
+
});
|
|
84
|
+
async function getFollowerActors(ctx) {
|
|
85
|
+
const actors = [];
|
|
86
|
+
for await (const { value } of ctx.data.kv.list(["follower"])) {
|
|
87
|
+
if (!isRelayFollowerData(value)) continue;
|
|
88
|
+
if (value.state !== "accepted") continue;
|
|
89
|
+
const actor = await Object$1.fromJsonLd(value.actor);
|
|
90
|
+
if (!isActor(actor)) continue;
|
|
91
|
+
actors.push(actor);
|
|
92
|
+
}
|
|
93
|
+
return actors;
|
|
94
|
+
}
|
|
95
|
+
async function dispatchRelayActors(ctx, identifier) {
|
|
96
|
+
if (identifier !== RELAY_SERVER_ACTOR) return null;
|
|
97
|
+
const actors = await getFollowerActors(ctx);
|
|
98
|
+
return { items: actors };
|
|
99
|
+
}
|
|
100
|
+
relayBuilder.setFollowersDispatcher("/users/{identifier}/followers", dispatchRelayActors);
|
|
101
|
+
relayBuilder.setFollowingDispatcher("/users/{identifier}/following", dispatchRelayActors);
|
|
102
|
+
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region src/base.ts
|
|
105
|
+
/**
|
|
106
|
+
* Abstract base class for relay implementations.
|
|
107
|
+
* Provides common infrastructure for both Mastodon and LitePub relays.
|
|
108
|
+
*
|
|
109
|
+
* @internal
|
|
110
|
+
*/
|
|
111
|
+
var BaseRelay = class {
|
|
112
|
+
federationBuilder;
|
|
113
|
+
options;
|
|
114
|
+
federation;
|
|
115
|
+
constructor(options, relayBuilder$1) {
|
|
116
|
+
this.options = options;
|
|
117
|
+
this.federationBuilder = relayBuilder$1;
|
|
118
|
+
}
|
|
119
|
+
async fetch(request) {
|
|
120
|
+
return await (await this.#getFederation()).fetch(request, { contextData: this.options });
|
|
165
121
|
}
|
|
166
|
-
|
|
167
|
-
|
|
122
|
+
/**
|
|
123
|
+
* Helper method to parse and validate follower data from storage.
|
|
124
|
+
* Deserializes JSON-LD actor data and validates it.
|
|
125
|
+
*
|
|
126
|
+
* @param actorId The actor ID of the follower
|
|
127
|
+
* @param data Raw data from KV store
|
|
128
|
+
* @returns RelayFollower object if valid, null otherwise
|
|
129
|
+
* @internal
|
|
130
|
+
*/
|
|
131
|
+
async parseFollowerData(actorId, data) {
|
|
132
|
+
if (!isRelayFollowerData(data)) return null;
|
|
133
|
+
const actor = await Object$1.fromJsonLd(data.actor);
|
|
134
|
+
if (!isActor(actor)) return null;
|
|
135
|
+
return {
|
|
136
|
+
actorId,
|
|
137
|
+
actor,
|
|
138
|
+
state: data.state
|
|
139
|
+
};
|
|
168
140
|
}
|
|
169
|
-
|
|
170
|
-
|
|
141
|
+
/**
|
|
142
|
+
* Lists all followers of the relay.
|
|
143
|
+
*
|
|
144
|
+
* @returns An async iterator of follower entries
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```ts
|
|
148
|
+
* import { createRelay } from "@fedify/relay";
|
|
149
|
+
* import { MemoryKvStore } from "@fedify/fedify";
|
|
150
|
+
*
|
|
151
|
+
* const relay = createRelay("mastodon", {
|
|
152
|
+
* kv: new MemoryKvStore(),
|
|
153
|
+
* origin: "https://relay.example.com",
|
|
154
|
+
* subscriptionHandler: async (ctx, actor) => true,
|
|
155
|
+
* });
|
|
156
|
+
*
|
|
157
|
+
* for await (const follower of relay.listFollowers()) {
|
|
158
|
+
* console.log(`Follower: ${follower.actorId}`);
|
|
159
|
+
* console.log(`State: ${follower.state}`);
|
|
160
|
+
* console.log(`Actor: ${follower.actor.name}`);
|
|
161
|
+
* }
|
|
162
|
+
* ```
|
|
163
|
+
*
|
|
164
|
+
* @since 2.0.0
|
|
165
|
+
*/
|
|
166
|
+
async *listFollowers() {
|
|
167
|
+
for await (const entry of this.options.kv.list(["follower"])) {
|
|
168
|
+
const actorId = entry.key[1];
|
|
169
|
+
if (typeof actorId !== "string") continue;
|
|
170
|
+
const follower = await this.parseFollowerData(actorId, entry.value);
|
|
171
|
+
if (follower) yield follower;
|
|
172
|
+
}
|
|
171
173
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
174
|
+
/**
|
|
175
|
+
* Gets a specific follower by actor ID.
|
|
176
|
+
*
|
|
177
|
+
* @param actorId The actor ID (URL) of the follower to retrieve
|
|
178
|
+
* @returns The follower entry if found, null otherwise
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* ```ts
|
|
182
|
+
* import { createRelay } from "@fedify/relay";
|
|
183
|
+
* import { MemoryKvStore } from "@fedify/fedify";
|
|
184
|
+
*
|
|
185
|
+
* const relay = createRelay("mastodon", {
|
|
186
|
+
* kv: new MemoryKvStore(),
|
|
187
|
+
* origin: "https://relay.example.com",
|
|
188
|
+
* subscriptionHandler: async (ctx, actor) => true,
|
|
189
|
+
* });
|
|
190
|
+
*
|
|
191
|
+
* const follower = await relay.getFollower(
|
|
192
|
+
* "https://mastodon.example.com/users/alice"
|
|
193
|
+
* );
|
|
194
|
+
* if (follower) {
|
|
195
|
+
* console.log(`State: ${follower.state}`);
|
|
196
|
+
* console.log(`Actor: ${follower.actor.preferredUsername}`);
|
|
197
|
+
* }
|
|
198
|
+
* ```
|
|
199
|
+
*
|
|
200
|
+
* @since 2.0.0
|
|
201
|
+
*/
|
|
202
|
+
async getFollower(actorId) {
|
|
203
|
+
const followerData = await this.options.kv.get(["follower", actorId]);
|
|
204
|
+
return await this.parseFollowerData(actorId, followerData);
|
|
205
|
+
}
|
|
206
|
+
async #getFederation() {
|
|
207
|
+
if (this.federation == null) {
|
|
208
|
+
this.federation = await this.federationBuilder.build(this.options);
|
|
209
|
+
this.setupInboxListeners();
|
|
210
|
+
}
|
|
211
|
+
return this.federation;
|
|
212
|
+
}
|
|
213
|
+
async #createContext() {
|
|
214
|
+
const context = (await this.#getFederation()).createContext(new URL(this.options.origin), this.options);
|
|
215
|
+
return context;
|
|
216
|
+
}
|
|
217
|
+
async getActorUri() {
|
|
218
|
+
const context = await this.#createContext();
|
|
219
|
+
return context.getActorUri(RELAY_SERVER_ACTOR);
|
|
220
|
+
}
|
|
221
|
+
async getSharedInboxUri() {
|
|
222
|
+
const context = await this.#createContext();
|
|
223
|
+
return context.getInboxUri();
|
|
175
224
|
}
|
|
176
225
|
};
|
|
226
|
+
|
|
227
|
+
//#endregion
|
|
228
|
+
//#region src/follow.ts
|
|
229
|
+
/**
|
|
230
|
+
* Validate Follow activity and return follower actor if valid.
|
|
231
|
+
* This validation is common to both Mastodon and LitePub relay protocols.
|
|
232
|
+
*
|
|
233
|
+
* @param ctx The federation context
|
|
234
|
+
* @param follow The Follow activity to validate
|
|
235
|
+
* @returns The follower Actor if valid, null otherwise
|
|
236
|
+
*/
|
|
237
|
+
async function validateFollowActivity(ctx, follow) {
|
|
238
|
+
if (follow.id == null || follow.objectId == null) return null;
|
|
239
|
+
const parsed = ctx.parseUri(follow.objectId);
|
|
240
|
+
const isPublicFollow = follow.objectId.href === "https://www.w3.org/ns/activitystreams#Public";
|
|
241
|
+
if (!isPublicFollow && parsed?.type !== "actor") return null;
|
|
242
|
+
const follower = await follow.getActor(ctx);
|
|
243
|
+
if (follower == null || follower.id == null || follower.preferredUsername == null || follower.inboxId == null) return null;
|
|
244
|
+
return follower;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Send Accept or Reject response for a Follow activity.
|
|
248
|
+
* This is common to both Mastodon and LitePub relay protocols.
|
|
249
|
+
*
|
|
250
|
+
* @param ctx The federation context
|
|
251
|
+
* @param follow The Follow activity being responded to
|
|
252
|
+
* @param follower The actor who sent the Follow
|
|
253
|
+
* @param approved Whether the follow was approved
|
|
254
|
+
*/
|
|
255
|
+
async function sendFollowResponse(ctx, follow, follower, approved) {
|
|
256
|
+
const relayActorUri = ctx.getActorUri(RELAY_SERVER_ACTOR);
|
|
257
|
+
const Activity = approved ? Accept : Reject;
|
|
258
|
+
const action = approved ? "accepts" : "rejects";
|
|
259
|
+
await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, follower, new Activity({
|
|
260
|
+
id: new URL(`#${action}`, relayActorUri),
|
|
261
|
+
actor: relayActorUri,
|
|
262
|
+
object: follow
|
|
263
|
+
}));
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Handle Undo activity for Follow.
|
|
267
|
+
* This logic is identical for both Mastodon and LitePub relay protocols.
|
|
268
|
+
*
|
|
269
|
+
* @param ctx The federation context
|
|
270
|
+
* @param undo The Undo activity to handle
|
|
271
|
+
* @param logger The logger instance to use for warnings
|
|
272
|
+
*/
|
|
273
|
+
async function handleUndoFollow(ctx, undo, logger$2) {
|
|
274
|
+
const activity = await undo.getObject({
|
|
275
|
+
crossOrigin: "trust",
|
|
276
|
+
...ctx
|
|
277
|
+
});
|
|
278
|
+
if (activity instanceof Follow) {
|
|
279
|
+
if (activity.id == null || activity.actorId == null) return;
|
|
280
|
+
await ctx.data.kv.delete(["follower", activity.actorId.href]);
|
|
281
|
+
} else logger$2.warn("Unsupported object type ({type}) for Undo activity: {object}", {
|
|
282
|
+
type: activity?.constructor.name,
|
|
283
|
+
object: activity
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
//#endregion
|
|
288
|
+
//#region src/litepub.ts
|
|
289
|
+
const logger$1 = getLogger([
|
|
290
|
+
"fedify",
|
|
291
|
+
"relay",
|
|
292
|
+
"litepub"
|
|
293
|
+
]);
|
|
177
294
|
/**
|
|
178
295
|
* A LitePub-compatible ActivityPub relay implementation.
|
|
179
296
|
* This relay follows LitePub's relay protocol and extensions for
|
|
@@ -181,25 +298,132 @@ var MastodonRelay = class {
|
|
|
181
298
|
*
|
|
182
299
|
* @since 2.0.0
|
|
183
300
|
*/
|
|
184
|
-
var LitePubRelay = class {
|
|
185
|
-
#
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
301
|
+
var LitePubRelay = class extends BaseRelay {
|
|
302
|
+
async #announceToFollowers(ctx, activity) {
|
|
303
|
+
const sender = await activity.getActor(ctx);
|
|
304
|
+
const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
|
|
305
|
+
const announce = new Announce({
|
|
306
|
+
id: new URL(`/announce#${crypto.randomUUID()}`, ctx.origin),
|
|
307
|
+
actor: ctx.getActorUri(RELAY_SERVER_ACTOR),
|
|
308
|
+
object: activity.objectId,
|
|
309
|
+
to: PUBLIC_COLLECTION,
|
|
310
|
+
published: Temporal.Now.instant()
|
|
311
|
+
});
|
|
312
|
+
await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", announce, {
|
|
313
|
+
excludeBaseUris,
|
|
314
|
+
preferSharedInbox: true
|
|
315
|
+
});
|
|
191
316
|
}
|
|
192
|
-
|
|
193
|
-
|
|
317
|
+
setupInboxListeners() {
|
|
318
|
+
if (this.federation != null) this.federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Follow, async (ctx, follow) => {
|
|
319
|
+
const follower = await validateFollowActivity(ctx, follow);
|
|
320
|
+
if (!follower || !follower.id) return;
|
|
321
|
+
const existingFollow = await ctx.data.kv.get(["follower", follower.id.href]);
|
|
322
|
+
if (existingFollow?.state === "pending") return;
|
|
323
|
+
const approved = await this.options.subscriptionHandler(ctx, follower);
|
|
324
|
+
if (approved) {
|
|
325
|
+
await ctx.data.kv.set(["follower", follower.id.href], {
|
|
326
|
+
actor: await follower.toJsonLd(),
|
|
327
|
+
state: "pending"
|
|
328
|
+
});
|
|
329
|
+
await sendFollowResponse(ctx, follow, follower, approved);
|
|
330
|
+
const relayActorUri = ctx.getActorUri(RELAY_SERVER_ACTOR);
|
|
331
|
+
await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, follower, new Follow({
|
|
332
|
+
actor: relayActorUri,
|
|
333
|
+
object: follower.id,
|
|
334
|
+
to: follower.id
|
|
335
|
+
}));
|
|
336
|
+
} else await sendFollowResponse(ctx, follow, follower, approved);
|
|
337
|
+
}).on(Accept, async (ctx, accept) => {
|
|
338
|
+
const follow = await accept.getObject({
|
|
339
|
+
crossOrigin: "trust",
|
|
340
|
+
...ctx
|
|
341
|
+
});
|
|
342
|
+
if (!(follow instanceof Follow)) return;
|
|
343
|
+
const relayActorId = follow.actorId;
|
|
344
|
+
if (relayActorId == null) return;
|
|
345
|
+
const followerActor = await accept.getActor();
|
|
346
|
+
if (!isActor(followerActor) || !followerActor.id) return;
|
|
347
|
+
const parsed = ctx.parseUri(relayActorId);
|
|
348
|
+
if (parsed == null || parsed.type !== "actor") return;
|
|
349
|
+
const followerData = await ctx.data.kv.get(["follower", followerActor.id.href]);
|
|
350
|
+
if (followerData == null) return;
|
|
351
|
+
const updatedFollowerData = {
|
|
352
|
+
...followerData,
|
|
353
|
+
state: "accepted"
|
|
354
|
+
};
|
|
355
|
+
await ctx.data.kv.set(["follower", followerActor.id.href], updatedFollowerData);
|
|
356
|
+
}).on(Undo, async (ctx, undo) => await handleUndoFollow(ctx, undo, logger$1)).on(Create, async (ctx, create) => await this.#announceToFollowers(ctx, create)).on(Update, async (ctx, update) => await this.#announceToFollowers(ctx, update)).on(Move, async (ctx, move) => await this.#announceToFollowers(ctx, move)).on(Delete, async (ctx, deleteActivity) => await this.#announceToFollowers(ctx, deleteActivity)).on(Announce, async (ctx, announce) => await this.#announceToFollowers(ctx, announce));
|
|
194
357
|
}
|
|
195
|
-
|
|
196
|
-
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
//#endregion
|
|
361
|
+
//#region src/mastodon.ts
|
|
362
|
+
const logger = getLogger([
|
|
363
|
+
"fedify",
|
|
364
|
+
"relay",
|
|
365
|
+
"mastodon"
|
|
366
|
+
]);
|
|
367
|
+
/**
|
|
368
|
+
* A Mastodon-compatible ActivityPub relay implementation.
|
|
369
|
+
* This relay follows Mastodon's relay protocol for compatibility
|
|
370
|
+
* with Mastodon instances.
|
|
371
|
+
*
|
|
372
|
+
* @since 2.0.0
|
|
373
|
+
*/
|
|
374
|
+
var MastodonRelay = class extends BaseRelay {
|
|
375
|
+
async #forwardToFollowers(ctx, activity) {
|
|
376
|
+
const sender = await activity.getActor(ctx);
|
|
377
|
+
const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
|
|
378
|
+
await ctx.forwardActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", {
|
|
379
|
+
skipIfUnsigned: true,
|
|
380
|
+
excludeBaseUris,
|
|
381
|
+
preferSharedInbox: true
|
|
382
|
+
});
|
|
197
383
|
}
|
|
198
|
-
|
|
199
|
-
this
|
|
200
|
-
|
|
384
|
+
setupInboxListeners() {
|
|
385
|
+
if (this.federation != null) this.federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Follow, async (ctx, follow) => {
|
|
386
|
+
const follower = await validateFollowActivity(ctx, follow);
|
|
387
|
+
if (!follower || !follower.id) return;
|
|
388
|
+
const approved = await this.options.subscriptionHandler(ctx, follower);
|
|
389
|
+
if (approved) await ctx.data.kv.set(["follower", follower.id.href], {
|
|
390
|
+
actor: await follower.toJsonLd(),
|
|
391
|
+
state: "accepted"
|
|
392
|
+
});
|
|
393
|
+
await sendFollowResponse(ctx, follow, follower, approved);
|
|
394
|
+
}).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));
|
|
201
395
|
}
|
|
202
396
|
};
|
|
203
397
|
|
|
204
398
|
//#endregion
|
|
205
|
-
|
|
399
|
+
//#region src/factory.ts
|
|
400
|
+
/**
|
|
401
|
+
* Factory function to create a relay instance.
|
|
402
|
+
*
|
|
403
|
+
* @param type The type of relay to create ("mastodon" or "litepub")
|
|
404
|
+
* @param options Configuration options for the relay
|
|
405
|
+
* @returns A relay instance
|
|
406
|
+
*
|
|
407
|
+
* @example
|
|
408
|
+
* ```ts
|
|
409
|
+
* import { createRelay } from "@fedify/relay";
|
|
410
|
+
* import { MemoryKvStore } from "@fedify/fedify";
|
|
411
|
+
*
|
|
412
|
+
* const relay = createRelay("mastodon", {
|
|
413
|
+
* kv: new MemoryKvStore(),
|
|
414
|
+
* origin: "https://relay.example.com",
|
|
415
|
+
* subscriptionHandler: async (ctx, actor) => true,
|
|
416
|
+
* });
|
|
417
|
+
* ```
|
|
418
|
+
*
|
|
419
|
+
* @since 2.0.0
|
|
420
|
+
*/
|
|
421
|
+
function createRelay(type, options) {
|
|
422
|
+
switch (type) {
|
|
423
|
+
case "mastodon": return new MastodonRelay(options, relayBuilder);
|
|
424
|
+
case "litepub": return new LitePubRelay(options, relayBuilder);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
//#endregion
|
|
429
|
+
export { RELAY_SERVER_ACTOR, createRelay };
|