@fedify/relay 2.0.0-pr.479.1922 → 2.0.0-pr.490.2
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/mod.cjs +304 -138
- package/dist/mod.d.cts +28 -18
- package/dist/mod.d.ts +29 -18
- package/dist/mod.js +300 -136
- package/package.json +7 -4
package/dist/mod.cjs
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
|
|
2
|
+
const { Temporal } = require("@js-temporal/polyfill");
|
|
3
|
+
|
|
1
4
|
//#region rolldown:runtime
|
|
2
5
|
var __create = Object.create;
|
|
3
6
|
var __defProp = Object.defineProperty;
|
|
@@ -24,8 +27,175 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
27
|
const __fedify_fedify = __toESM(require("@fedify/fedify"));
|
|
25
28
|
const __fedify_fedify_vocab = __toESM(require("@fedify/fedify/vocab"));
|
|
26
29
|
|
|
27
|
-
//#region src/
|
|
28
|
-
|
|
30
|
+
//#region src/litepub.ts
|
|
31
|
+
/**
|
|
32
|
+
* A LitePub-compatible ActivityPub relay implementation.
|
|
33
|
+
* This relay follows LitePub's relay protocol and extensions for
|
|
34
|
+
* enhanced federation capabilities.
|
|
35
|
+
*
|
|
36
|
+
* @since 2.0.0
|
|
37
|
+
*/
|
|
38
|
+
var LitePubRelay = class {
|
|
39
|
+
#federationBuilder;
|
|
40
|
+
#options;
|
|
41
|
+
#federation;
|
|
42
|
+
constructor(options, relayBuilder$1) {
|
|
43
|
+
this.#options = options;
|
|
44
|
+
this.#federationBuilder = relayBuilder$1;
|
|
45
|
+
}
|
|
46
|
+
async fetch(request) {
|
|
47
|
+
if (this.#federation == null) {
|
|
48
|
+
this.#federation = await this.#federationBuilder.build(this.#options);
|
|
49
|
+
this.setupInboxListeners();
|
|
50
|
+
}
|
|
51
|
+
return await this.#federation.fetch(request, { contextData: this.#options });
|
|
52
|
+
}
|
|
53
|
+
setupInboxListeners() {
|
|
54
|
+
if (this.#federation != null) this.#federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(__fedify_fedify.Follow, async (ctx, follow) => {
|
|
55
|
+
if (follow.id == null || follow.objectId == null) return;
|
|
56
|
+
const parsed = ctx.parseUri(follow.objectId);
|
|
57
|
+
const isPublicFollow = follow.objectId.href === "https://www.w3.org/ns/activitystreams#Public";
|
|
58
|
+
if (!isPublicFollow && parsed?.type !== "actor") return;
|
|
59
|
+
const relayActorUri = ctx.getActorUri(RELAY_SERVER_ACTOR);
|
|
60
|
+
const follower = await follow.getActor(ctx);
|
|
61
|
+
if (follower == null || follower.id == null || follower.preferredUsername == null || follower.inboxId == null) return;
|
|
62
|
+
const existingFollow = await ctx.data.kv.get(["follower", follower.id.href]);
|
|
63
|
+
if (existingFollow?.state === "pending") return;
|
|
64
|
+
let subscriptionApproved = false;
|
|
65
|
+
if (this.#options.subscriptionHandler) subscriptionApproved = await this.#options.subscriptionHandler(ctx, follower);
|
|
66
|
+
if (subscriptionApproved) {
|
|
67
|
+
await ctx.data.kv.set(["follower", follower.id.href], {
|
|
68
|
+
"actor": await follower.toJsonLd(),
|
|
69
|
+
"state": "pending"
|
|
70
|
+
});
|
|
71
|
+
await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, follower, new __fedify_fedify.Accept({
|
|
72
|
+
id: new URL(`#accepts`, relayActorUri),
|
|
73
|
+
actor: relayActorUri,
|
|
74
|
+
object: follow
|
|
75
|
+
}));
|
|
76
|
+
await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, follower, new __fedify_fedify.Follow({
|
|
77
|
+
actor: relayActorUri,
|
|
78
|
+
object: follower.id,
|
|
79
|
+
to: follower.id
|
|
80
|
+
}));
|
|
81
|
+
} else await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, follower, new __fedify_fedify.Reject({
|
|
82
|
+
id: new URL(`#rejects`, relayActorUri),
|
|
83
|
+
actor: relayActorUri,
|
|
84
|
+
object: follow
|
|
85
|
+
}));
|
|
86
|
+
}).on(__fedify_fedify.Accept, async (ctx, accept) => {
|
|
87
|
+
const follow = await accept.getObject({
|
|
88
|
+
crossOrigin: "trust",
|
|
89
|
+
...ctx
|
|
90
|
+
});
|
|
91
|
+
if (!(follow instanceof __fedify_fedify.Follow)) return;
|
|
92
|
+
const relayActorId = follow.actorId;
|
|
93
|
+
if (relayActorId == null) return;
|
|
94
|
+
const followerActor = await accept.getActor();
|
|
95
|
+
if (!(0, __fedify_fedify.isActor)(followerActor) || !followerActor.id) return;
|
|
96
|
+
const parsed = ctx.parseUri(relayActorId);
|
|
97
|
+
if (parsed == null || parsed.type !== "actor") return;
|
|
98
|
+
const followerData = await ctx.data.kv.get(["follower", followerActor.id.href]);
|
|
99
|
+
if (followerData == null) return;
|
|
100
|
+
const updatedFollowerData = {
|
|
101
|
+
...followerData,
|
|
102
|
+
state: "accepted"
|
|
103
|
+
};
|
|
104
|
+
await ctx.data.kv.set(["follower", followerActor.id.href], updatedFollowerData);
|
|
105
|
+
const followers = await ctx.data.kv.get(["followers"]) ?? [];
|
|
106
|
+
followers.push(followerActor.id.href);
|
|
107
|
+
await ctx.data.kv.set(["followers"], followers);
|
|
108
|
+
}).on(__fedify_fedify.Undo, async (ctx, undo) => {
|
|
109
|
+
const activity = await undo.getObject({
|
|
110
|
+
crossOrigin: "trust",
|
|
111
|
+
...ctx
|
|
112
|
+
});
|
|
113
|
+
if (activity instanceof __fedify_fedify.Follow) {
|
|
114
|
+
if (activity.id == null || activity.actorId == null) return;
|
|
115
|
+
const followers = await ctx.data.kv.get(["followers"]) ?? [];
|
|
116
|
+
const updatedFollowers = followers.filter((id) => id !== activity.actorId?.href);
|
|
117
|
+
await ctx.data.kv.set(["followers"], updatedFollowers);
|
|
118
|
+
await ctx.data.kv.delete(["follower", activity.actorId?.href]);
|
|
119
|
+
} else console.warn("Unsupported object type ({type}) for Undo activity: {object}", {
|
|
120
|
+
type: activity?.constructor.name,
|
|
121
|
+
object: activity
|
|
122
|
+
});
|
|
123
|
+
}).on(__fedify_fedify.Create, async (ctx, create) => {
|
|
124
|
+
const sender = await create.getActor(ctx);
|
|
125
|
+
const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
|
|
126
|
+
const announce = new __fedify_fedify.Announce({
|
|
127
|
+
id: new URL(`/announce#${crypto.randomUUID()}`, ctx.origin),
|
|
128
|
+
actor: ctx.getActorUri(RELAY_SERVER_ACTOR),
|
|
129
|
+
object: create.objectId,
|
|
130
|
+
to: __fedify_fedify.PUBLIC_COLLECTION,
|
|
131
|
+
published: Temporal.Now.instant()
|
|
132
|
+
});
|
|
133
|
+
await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", announce, {
|
|
134
|
+
excludeBaseUris,
|
|
135
|
+
preferSharedInbox: true
|
|
136
|
+
});
|
|
137
|
+
}).on(__fedify_fedify.Update, async (ctx, update) => {
|
|
138
|
+
const sender = await update.getActor(ctx);
|
|
139
|
+
const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
|
|
140
|
+
const announce = new __fedify_fedify.Announce({
|
|
141
|
+
id: new URL(`/announce#${crypto.randomUUID()}`, ctx.origin),
|
|
142
|
+
actor: ctx.getActorUri(RELAY_SERVER_ACTOR),
|
|
143
|
+
object: update.objectId,
|
|
144
|
+
to: __fedify_fedify.PUBLIC_COLLECTION,
|
|
145
|
+
published: Temporal.Now.instant()
|
|
146
|
+
});
|
|
147
|
+
await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", announce, {
|
|
148
|
+
excludeBaseUris,
|
|
149
|
+
preferSharedInbox: true
|
|
150
|
+
});
|
|
151
|
+
}).on(__fedify_fedify.Move, async (ctx, move) => {
|
|
152
|
+
const sender = await move.getActor(ctx);
|
|
153
|
+
const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
|
|
154
|
+
const announce = new __fedify_fedify.Announce({
|
|
155
|
+
id: new URL(`/announce#${crypto.randomUUID()}`, ctx.origin),
|
|
156
|
+
actor: ctx.getActorUri(RELAY_SERVER_ACTOR),
|
|
157
|
+
object: move.objectId,
|
|
158
|
+
to: __fedify_fedify.PUBLIC_COLLECTION,
|
|
159
|
+
published: Temporal.Now.instant()
|
|
160
|
+
});
|
|
161
|
+
await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", announce, {
|
|
162
|
+
excludeBaseUris,
|
|
163
|
+
preferSharedInbox: true
|
|
164
|
+
});
|
|
165
|
+
}).on(__fedify_fedify.Delete, async (ctx, deleteActivity) => {
|
|
166
|
+
const sender = await deleteActivity.getActor(ctx);
|
|
167
|
+
const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
|
|
168
|
+
const announce = new __fedify_fedify.Announce({
|
|
169
|
+
id: new URL(`/announce#${crypto.randomUUID()}`, ctx.origin),
|
|
170
|
+
actor: ctx.getActorUri(RELAY_SERVER_ACTOR),
|
|
171
|
+
object: deleteActivity.objectId,
|
|
172
|
+
to: __fedify_fedify.PUBLIC_COLLECTION,
|
|
173
|
+
published: Temporal.Now.instant()
|
|
174
|
+
});
|
|
175
|
+
await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", announce, {
|
|
176
|
+
excludeBaseUris,
|
|
177
|
+
preferSharedInbox: true
|
|
178
|
+
});
|
|
179
|
+
}).on(__fedify_fedify.Announce, async (ctx, announceActivity) => {
|
|
180
|
+
const sender = await announceActivity.getActor(ctx);
|
|
181
|
+
const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
|
|
182
|
+
const announce = new __fedify_fedify.Announce({
|
|
183
|
+
id: new URL(`/announce#${crypto.randomUUID()}`, ctx.origin),
|
|
184
|
+
actor: ctx.getActorUri(RELAY_SERVER_ACTOR),
|
|
185
|
+
object: announceActivity.objectId,
|
|
186
|
+
to: __fedify_fedify.PUBLIC_COLLECTION,
|
|
187
|
+
published: Temporal.Now.instant()
|
|
188
|
+
});
|
|
189
|
+
await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", announce, {
|
|
190
|
+
excludeBaseUris,
|
|
191
|
+
preferSharedInbox: true
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
//#endregion
|
|
198
|
+
//#region src/mastodon.ts
|
|
29
199
|
/**
|
|
30
200
|
* A Mastodon-compatible ActivityPub relay implementation.
|
|
31
201
|
* This relay follows Mastodon's relay protocol for maximum compatibility
|
|
@@ -34,125 +204,65 @@ const RELAY_SERVER_ACTOR = "relay";
|
|
|
34
204
|
* @since 2.0.0
|
|
35
205
|
*/
|
|
36
206
|
var MastodonRelay = class {
|
|
37
|
-
#
|
|
207
|
+
#federationBuilder;
|
|
38
208
|
#options;
|
|
39
|
-
#
|
|
40
|
-
constructor(options) {
|
|
209
|
+
#federation;
|
|
210
|
+
constructor(options, relayBuilder$1) {
|
|
41
211
|
this.#options = options;
|
|
42
|
-
this.#
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
preferredUsername: identifier,
|
|
54
|
-
name: "ActivityPub Relay",
|
|
55
|
-
summary: "Mastodon-compatible ActivityPub relay server",
|
|
56
|
-
inbox: ctx.getInboxUri(),
|
|
57
|
-
followers: ctx.getFollowersUri(identifier),
|
|
58
|
-
url: ctx.getActorUri(identifier),
|
|
59
|
-
publicKey: keys[0].cryptographicKey,
|
|
60
|
-
assertionMethods: keys.map((k) => k.multikey)
|
|
61
|
-
});
|
|
62
|
-
}).setKeyPairsDispatcher(async (_ctx, identifier) => {
|
|
63
|
-
if (identifier !== RELAY_SERVER_ACTOR) return [];
|
|
64
|
-
const rsaPairJson = await options.kv.get([
|
|
65
|
-
"keypair",
|
|
66
|
-
"rsa",
|
|
67
|
-
identifier
|
|
68
|
-
]);
|
|
69
|
-
const ed25519PairJson = await options.kv.get([
|
|
70
|
-
"keypair",
|
|
71
|
-
"ed25519",
|
|
72
|
-
identifier
|
|
73
|
-
]);
|
|
74
|
-
if (rsaPairJson == null || ed25519PairJson == null) {
|
|
75
|
-
const rsaPair$1 = await (0, __fedify_fedify.generateCryptoKeyPair)("RSASSA-PKCS1-v1_5");
|
|
76
|
-
const ed25519Pair$1 = await (0, __fedify_fedify.generateCryptoKeyPair)("Ed25519");
|
|
77
|
-
await options.kv.set([
|
|
78
|
-
"keypair",
|
|
79
|
-
"rsa",
|
|
80
|
-
identifier
|
|
81
|
-
], {
|
|
82
|
-
privateKey: await (0, __fedify_fedify.exportJwk)(rsaPair$1.privateKey),
|
|
83
|
-
publicKey: await (0, __fedify_fedify.exportJwk)(rsaPair$1.publicKey)
|
|
84
|
-
});
|
|
85
|
-
await options.kv.set([
|
|
86
|
-
"keypair",
|
|
87
|
-
"ed25519",
|
|
88
|
-
identifier
|
|
89
|
-
], {
|
|
90
|
-
privateKey: await (0, __fedify_fedify.exportJwk)(ed25519Pair$1.privateKey),
|
|
91
|
-
publicKey: await (0, __fedify_fedify.exportJwk)(ed25519Pair$1.publicKey)
|
|
92
|
-
});
|
|
93
|
-
return [rsaPair$1, ed25519Pair$1];
|
|
94
|
-
}
|
|
95
|
-
const rsaPair = {
|
|
96
|
-
privateKey: await (0, __fedify_fedify.importJwk)(rsaPairJson.privateKey, "private"),
|
|
97
|
-
publicKey: await (0, __fedify_fedify.importJwk)(rsaPairJson.publicKey, "public")
|
|
98
|
-
};
|
|
99
|
-
const ed25519Pair = {
|
|
100
|
-
privateKey: await (0, __fedify_fedify.importJwk)(ed25519PairJson.privateKey, "private"),
|
|
101
|
-
publicKey: await (0, __fedify_fedify.importJwk)(ed25519PairJson.publicKey, "public")
|
|
102
|
-
};
|
|
103
|
-
return [rsaPair, ed25519Pair];
|
|
104
|
-
});
|
|
105
|
-
this.#federation.setFollowersDispatcher("/users/{identifier}/followers", async (_ctx, identifier) => {
|
|
106
|
-
if (identifier !== RELAY_SERVER_ACTOR) return null;
|
|
107
|
-
const activityIds = await options.kv.get(["followers"]) ?? [];
|
|
108
|
-
const actors = [];
|
|
109
|
-
for (const activityId of activityIds) {
|
|
110
|
-
const actorJson = await options.kv.get(["follower", activityId]);
|
|
111
|
-
const actor = await __fedify_fedify_vocab.Object.fromJsonLd(actorJson);
|
|
112
|
-
if (!(0, __fedify_fedify_vocab.isActor)(actor)) continue;
|
|
113
|
-
actors.push(actor);
|
|
114
|
-
}
|
|
115
|
-
return { items: actors };
|
|
116
|
-
});
|
|
117
|
-
this.#federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(__fedify_fedify_vocab.Follow, async (ctx, follow) => {
|
|
212
|
+
this.#federationBuilder = relayBuilder$1;
|
|
213
|
+
}
|
|
214
|
+
async fetch(request) {
|
|
215
|
+
if (this.#federation == null) {
|
|
216
|
+
this.#federation = await this.#federationBuilder.build(this.#options);
|
|
217
|
+
this.setupInboxListeners();
|
|
218
|
+
}
|
|
219
|
+
return await this.#federation.fetch(request, { contextData: this.#options });
|
|
220
|
+
}
|
|
221
|
+
setupInboxListeners() {
|
|
222
|
+
if (this.#federation != null) this.#federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(__fedify_fedify.Follow, async (ctx, follow) => {
|
|
118
223
|
if (follow.id == null || follow.objectId == null) return;
|
|
119
224
|
const parsed = ctx.parseUri(follow.objectId);
|
|
120
225
|
const isPublicFollow = follow.objectId.href === "https://www.w3.org/ns/activitystreams#Public";
|
|
121
226
|
if (!isPublicFollow && parsed?.type !== "actor") return;
|
|
122
227
|
const relayActorUri = ctx.getActorUri(RELAY_SERVER_ACTOR);
|
|
123
|
-
const
|
|
124
|
-
if (
|
|
228
|
+
const follower = await follow.getActor(ctx);
|
|
229
|
+
if (follower == null || follower.id == null || follower.preferredUsername == null || follower.inboxId == null) return;
|
|
125
230
|
let approved = false;
|
|
126
|
-
if (this.#subscriptionHandler) approved = await this.#subscriptionHandler(ctx,
|
|
231
|
+
if (this.#options.subscriptionHandler) approved = await this.#options.subscriptionHandler(ctx, follower);
|
|
127
232
|
if (approved) {
|
|
128
|
-
const followers = await
|
|
129
|
-
followers.push(
|
|
130
|
-
await
|
|
131
|
-
await
|
|
132
|
-
|
|
233
|
+
const followers = await ctx.data.kv.get(["followers"]) ?? [];
|
|
234
|
+
followers.push(follower.id.href);
|
|
235
|
+
await ctx.data.kv.set(["followers"], followers);
|
|
236
|
+
await ctx.data.kv.set(["follower", follower.id.href], {
|
|
237
|
+
"actor": await follower.toJsonLd(),
|
|
238
|
+
"state": "accepted"
|
|
239
|
+
});
|
|
240
|
+
await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, follower, new __fedify_fedify.Accept({
|
|
133
241
|
id: new URL(`#accepts`, relayActorUri),
|
|
134
242
|
actor: relayActorUri,
|
|
135
243
|
object: follow
|
|
136
244
|
}));
|
|
137
|
-
} else await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR },
|
|
245
|
+
} else await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, follower, new __fedify_fedify.Reject({
|
|
138
246
|
id: new URL(`#rejects`, relayActorUri),
|
|
139
247
|
actor: relayActorUri,
|
|
140
248
|
object: follow
|
|
141
249
|
}));
|
|
142
|
-
}).on(
|
|
143
|
-
const activity = await undo.getObject(
|
|
144
|
-
|
|
250
|
+
}).on(__fedify_fedify.Undo, async (ctx, undo) => {
|
|
251
|
+
const activity = await undo.getObject({
|
|
252
|
+
crossOrigin: "trust",
|
|
253
|
+
...ctx
|
|
254
|
+
});
|
|
255
|
+
if (activity instanceof __fedify_fedify.Follow) {
|
|
145
256
|
if (activity.id == null || activity.actorId == null) return;
|
|
146
|
-
const
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
await
|
|
150
|
-
options.kv.delete(["follower", activityId]);
|
|
257
|
+
const followers = await ctx.data.kv.get(["followers"]) ?? [];
|
|
258
|
+
const updatedFollowers = followers.filter((id) => id !== activity.actorId?.href);
|
|
259
|
+
await ctx.data.kv.set(["followers"], updatedFollowers);
|
|
260
|
+
await ctx.data.kv.delete(["follower", activity.actorId?.href]);
|
|
151
261
|
} else console.warn("Unsupported object type ({type}) for Undo activity: {object}", {
|
|
152
262
|
type: activity?.constructor.name,
|
|
153
263
|
object: activity
|
|
154
264
|
});
|
|
155
|
-
}).on(
|
|
265
|
+
}).on(__fedify_fedify.Create, async (ctx, create) => {
|
|
156
266
|
const sender = await create.getActor(ctx);
|
|
157
267
|
const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
|
|
158
268
|
await ctx.forwardActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", {
|
|
@@ -160,7 +270,7 @@ var MastodonRelay = class {
|
|
|
160
270
|
excludeBaseUris,
|
|
161
271
|
preferSharedInbox: true
|
|
162
272
|
});
|
|
163
|
-
}).on(
|
|
273
|
+
}).on(__fedify_fedify.Delete, async (ctx, deleteActivity) => {
|
|
164
274
|
const sender = await deleteActivity.getActor(ctx);
|
|
165
275
|
const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
|
|
166
276
|
await ctx.forwardActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", {
|
|
@@ -168,16 +278,16 @@ var MastodonRelay = class {
|
|
|
168
278
|
excludeBaseUris,
|
|
169
279
|
preferSharedInbox: true
|
|
170
280
|
});
|
|
171
|
-
}).on(
|
|
172
|
-
const sender = await
|
|
281
|
+
}).on(__fedify_fedify.Move, async (ctx, move) => {
|
|
282
|
+
const sender = await move.getActor(ctx);
|
|
173
283
|
const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
|
|
174
284
|
await ctx.forwardActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", {
|
|
175
285
|
skipIfUnsigned: true,
|
|
176
286
|
excludeBaseUris,
|
|
177
287
|
preferSharedInbox: true
|
|
178
288
|
});
|
|
179
|
-
}).on(
|
|
180
|
-
const sender = await
|
|
289
|
+
}).on(__fedify_fedify.Update, async (ctx, update) => {
|
|
290
|
+
const sender = await update.getActor(ctx);
|
|
181
291
|
const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
|
|
182
292
|
await ctx.forwardActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", {
|
|
183
293
|
skipIfUnsigned: true,
|
|
@@ -186,44 +296,100 @@ var MastodonRelay = class {
|
|
|
186
296
|
});
|
|
187
297
|
});
|
|
188
298
|
}
|
|
189
|
-
get domain() {
|
|
190
|
-
return this.#options.domain || "localhost";
|
|
191
|
-
}
|
|
192
|
-
fetch(request) {
|
|
193
|
-
return this.#federation.fetch(request, { contextData: void 0 });
|
|
194
|
-
}
|
|
195
|
-
setSubscriptionHandler(handler) {
|
|
196
|
-
this.#subscriptionHandler = handler;
|
|
197
|
-
return this;
|
|
198
|
-
}
|
|
199
299
|
};
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
300
|
+
|
|
301
|
+
//#endregion
|
|
302
|
+
//#region src/relay.ts
|
|
303
|
+
const RELAY_SERVER_ACTOR = "relay";
|
|
304
|
+
const relayBuilder = (0, __fedify_fedify.createFederationBuilder)();
|
|
305
|
+
relayBuilder.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => {
|
|
306
|
+
if (identifier !== RELAY_SERVER_ACTOR) return null;
|
|
307
|
+
const keys = await ctx.getActorKeyPairs(identifier);
|
|
308
|
+
return new __fedify_fedify_vocab.Application({
|
|
309
|
+
id: ctx.getActorUri(identifier),
|
|
310
|
+
preferredUsername: identifier,
|
|
311
|
+
name: "ActivityPub Relay",
|
|
312
|
+
inbox: ctx.getInboxUri(),
|
|
313
|
+
followers: ctx.getFollowersUri(identifier),
|
|
314
|
+
following: ctx.getFollowingUri(identifier),
|
|
315
|
+
url: ctx.getActorUri(identifier),
|
|
316
|
+
publicKey: keys[0].cryptographicKey,
|
|
317
|
+
assertionMethods: keys.map((k) => k.multikey)
|
|
318
|
+
});
|
|
319
|
+
}).setKeyPairsDispatcher(async (ctx, identifier) => {
|
|
320
|
+
if (identifier !== RELAY_SERVER_ACTOR) return [];
|
|
321
|
+
const rsaPairJson = await ctx.data.kv.get([
|
|
322
|
+
"keypair",
|
|
323
|
+
"rsa",
|
|
324
|
+
identifier
|
|
325
|
+
]);
|
|
326
|
+
const ed25519PairJson = await ctx.data.kv.get([
|
|
327
|
+
"keypair",
|
|
328
|
+
"ed25519",
|
|
329
|
+
identifier
|
|
330
|
+
]);
|
|
331
|
+
if (rsaPairJson == null || ed25519PairJson == null) {
|
|
332
|
+
const rsaPair$1 = await (0, __fedify_fedify.generateCryptoKeyPair)("RSASSA-PKCS1-v1_5");
|
|
333
|
+
const ed25519Pair$1 = await (0, __fedify_fedify.generateCryptoKeyPair)("Ed25519");
|
|
334
|
+
await ctx.data.kv.set([
|
|
335
|
+
"keypair",
|
|
336
|
+
"rsa",
|
|
337
|
+
identifier
|
|
338
|
+
], {
|
|
339
|
+
privateKey: await (0, __fedify_fedify.exportJwk)(rsaPair$1.privateKey),
|
|
340
|
+
publicKey: await (0, __fedify_fedify.exportJwk)(rsaPair$1.publicKey)
|
|
341
|
+
});
|
|
342
|
+
await ctx.data.kv.set([
|
|
343
|
+
"keypair",
|
|
344
|
+
"ed25519",
|
|
345
|
+
identifier
|
|
346
|
+
], {
|
|
347
|
+
privateKey: await (0, __fedify_fedify.exportJwk)(ed25519Pair$1.privateKey),
|
|
348
|
+
publicKey: await (0, __fedify_fedify.exportJwk)(ed25519Pair$1.publicKey)
|
|
349
|
+
});
|
|
350
|
+
return [rsaPair$1, ed25519Pair$1];
|
|
217
351
|
}
|
|
218
|
-
|
|
219
|
-
|
|
352
|
+
const rsaPair = {
|
|
353
|
+
privateKey: await (0, __fedify_fedify.importJwk)(rsaPairJson.privateKey, "private"),
|
|
354
|
+
publicKey: await (0, __fedify_fedify.importJwk)(rsaPairJson.publicKey, "public")
|
|
355
|
+
};
|
|
356
|
+
const ed25519Pair = {
|
|
357
|
+
privateKey: await (0, __fedify_fedify.importJwk)(ed25519PairJson.privateKey, "private"),
|
|
358
|
+
publicKey: await (0, __fedify_fedify.importJwk)(ed25519PairJson.publicKey, "public")
|
|
359
|
+
};
|
|
360
|
+
return [rsaPair, ed25519Pair];
|
|
361
|
+
});
|
|
362
|
+
async function getFollowerActors(ctx) {
|
|
363
|
+
const followers = await ctx.data.kv.get(["followers"]) ?? [];
|
|
364
|
+
const actors = [];
|
|
365
|
+
for (const followerId of followers) {
|
|
366
|
+
const follower = await ctx.data.kv.get(["follower", followerId]);
|
|
367
|
+
if (!follower) continue;
|
|
368
|
+
const actor = await __fedify_fedify_vocab.Object.fromJsonLd(follower.actor);
|
|
369
|
+
if (!(0, __fedify_fedify_vocab.isActor)(actor)) continue;
|
|
370
|
+
actors.push(actor);
|
|
220
371
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
372
|
+
return actors;
|
|
373
|
+
}
|
|
374
|
+
relayBuilder.setFollowersDispatcher("/users/{identifier}/followers", async (ctx, identifier) => {
|
|
375
|
+
if (identifier !== RELAY_SERVER_ACTOR) return null;
|
|
376
|
+
const actors = await getFollowerActors(ctx);
|
|
377
|
+
return { items: actors };
|
|
378
|
+
});
|
|
379
|
+
relayBuilder.setFollowingDispatcher("/users/{identifier}/following", async (ctx, identifier) => {
|
|
380
|
+
if (identifier !== RELAY_SERVER_ACTOR) return null;
|
|
381
|
+
const actors = await getFollowerActors(ctx);
|
|
382
|
+
return { items: actors };
|
|
383
|
+
});
|
|
384
|
+
function createRelay(type, options) {
|
|
385
|
+
switch (type) {
|
|
386
|
+
case "mastodon": return new MastodonRelay(options, relayBuilder);
|
|
387
|
+
case "litepub": return new LitePubRelay(options, relayBuilder);
|
|
224
388
|
}
|
|
225
|
-
}
|
|
389
|
+
}
|
|
226
390
|
|
|
227
391
|
//#endregion
|
|
228
392
|
exports.LitePubRelay = LitePubRelay;
|
|
229
|
-
exports.MastodonRelay = MastodonRelay;
|
|
393
|
+
exports.MastodonRelay = MastodonRelay;
|
|
394
|
+
exports.RELAY_SERVER_ACTOR = RELAY_SERVER_ACTOR;
|
|
395
|
+
exports.createRelay = createRelay;
|
package/dist/mod.d.cts
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
|
-
import { Context,
|
|
1
|
+
import { Context, FederationBuilder, KvStore, MessageQueue } from "@fedify/fedify";
|
|
2
2
|
import { Actor } from "@fedify/fedify/vocab";
|
|
3
3
|
import { AuthenticatedDocumentLoaderFactory, DocumentLoaderFactory } from "@fedify/vocab-runtime";
|
|
4
|
+
import { FederationBuilder as FederationBuilder$1 } from "@fedify/fedify/federation";
|
|
4
5
|
|
|
5
6
|
//#region src/relay.d.ts
|
|
6
|
-
|
|
7
|
+
declare const RELAY_SERVER_ACTOR = "relay";
|
|
8
|
+
/**
|
|
9
|
+
* Supported relay types.
|
|
10
|
+
*/
|
|
11
|
+
type RelayType = "mastodon" | "litepub";
|
|
12
|
+
/**
|
|
13
|
+
* Common interface for all relay implementations.
|
|
14
|
+
*/
|
|
15
|
+
interface Relay {
|
|
16
|
+
fetch(request: Request): Promise<Response>;
|
|
17
|
+
}
|
|
7
18
|
/**
|
|
8
19
|
* Handler for subscription requests (Follow/Undo activities).
|
|
9
20
|
*/
|
|
10
|
-
type SubscriptionRequestHandler = (ctx: Context<
|
|
21
|
+
type SubscriptionRequestHandler = (ctx: Context<RelayOptions>, clientActor: Actor) => Promise<boolean>;
|
|
11
22
|
/**
|
|
12
23
|
* Configuration options for the ActivityPub relay.
|
|
13
24
|
*/
|
|
@@ -16,17 +27,16 @@ interface RelayOptions {
|
|
|
16
27
|
domain?: string;
|
|
17
28
|
documentLoaderFactory?: DocumentLoaderFactory;
|
|
18
29
|
authenticatedDocumentLoaderFactory?: AuthenticatedDocumentLoaderFactory;
|
|
19
|
-
federation?: Federation<void>;
|
|
20
30
|
queue?: MessageQueue;
|
|
31
|
+
subscriptionHandler?: SubscriptionRequestHandler;
|
|
21
32
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
interface Relay {
|
|
26
|
-
readonly domain: string;
|
|
27
|
-
fetch(request: Request): Promise<Response>;
|
|
28
|
-
setSubscriptionHandler(handler: SubscriptionRequestHandler): this;
|
|
33
|
+
interface RelayFollower {
|
|
34
|
+
readonly actor: unknown;
|
|
35
|
+
readonly state: string;
|
|
29
36
|
}
|
|
37
|
+
declare function createRelay(type: RelayType, options: RelayOptions): Relay;
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/mastodon.d.ts
|
|
30
40
|
/**
|
|
31
41
|
* A Mastodon-compatible ActivityPub relay implementation.
|
|
32
42
|
* This relay follows Mastodon's relay protocol for maximum compatibility
|
|
@@ -36,11 +46,12 @@ interface Relay {
|
|
|
36
46
|
*/
|
|
37
47
|
declare class MastodonRelay implements Relay {
|
|
38
48
|
#private;
|
|
39
|
-
constructor(options: RelayOptions);
|
|
40
|
-
get domain(): string;
|
|
49
|
+
constructor(options: RelayOptions, relayBuilder: FederationBuilder$1<RelayOptions>);
|
|
41
50
|
fetch(request: Request): Promise<Response>;
|
|
42
|
-
|
|
51
|
+
setupInboxListeners(): void;
|
|
43
52
|
}
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/litepub.d.ts
|
|
44
55
|
/**
|
|
45
56
|
* A LitePub-compatible ActivityPub relay implementation.
|
|
46
57
|
* This relay follows LitePub's relay protocol and extensions for
|
|
@@ -50,10 +61,9 @@ declare class MastodonRelay implements Relay {
|
|
|
50
61
|
*/
|
|
51
62
|
declare class LitePubRelay implements Relay {
|
|
52
63
|
#private;
|
|
53
|
-
constructor(options: RelayOptions);
|
|
54
|
-
get domain(): string;
|
|
64
|
+
constructor(options: RelayOptions, relayBuilder: FederationBuilder<RelayOptions>);
|
|
55
65
|
fetch(request: Request): Promise<Response>;
|
|
56
|
-
|
|
66
|
+
setupInboxListeners(): void;
|
|
57
67
|
}
|
|
58
68
|
//#endregion
|
|
59
|
-
export { LitePubRelay, MastodonRelay,
|
|
69
|
+
export { LitePubRelay, MastodonRelay, RELAY_SERVER_ACTOR, RelayFollower, RelayOptions, SubscriptionRequestHandler, createRelay };
|
package/dist/mod.d.ts
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Temporal } from "@js-temporal/polyfill";
|
|
2
|
+
import { Context, FederationBuilder, KvStore, MessageQueue } from "@fedify/fedify";
|
|
2
3
|
import { Actor } from "@fedify/fedify/vocab";
|
|
3
4
|
import { AuthenticatedDocumentLoaderFactory, DocumentLoaderFactory } from "@fedify/vocab-runtime";
|
|
5
|
+
import { FederationBuilder as FederationBuilder$1 } from "@fedify/fedify/federation";
|
|
4
6
|
|
|
5
7
|
//#region src/relay.d.ts
|
|
6
|
-
|
|
8
|
+
declare const RELAY_SERVER_ACTOR = "relay";
|
|
9
|
+
/**
|
|
10
|
+
* Supported relay types.
|
|
11
|
+
*/
|
|
12
|
+
type RelayType = "mastodon" | "litepub";
|
|
13
|
+
/**
|
|
14
|
+
* Common interface for all relay implementations.
|
|
15
|
+
*/
|
|
16
|
+
interface Relay {
|
|
17
|
+
fetch(request: Request): Promise<Response>;
|
|
18
|
+
}
|
|
7
19
|
/**
|
|
8
20
|
* Handler for subscription requests (Follow/Undo activities).
|
|
9
21
|
*/
|
|
10
|
-
type SubscriptionRequestHandler = (ctx: Context<
|
|
22
|
+
type SubscriptionRequestHandler = (ctx: Context<RelayOptions>, clientActor: Actor) => Promise<boolean>;
|
|
11
23
|
/**
|
|
12
24
|
* Configuration options for the ActivityPub relay.
|
|
13
25
|
*/
|
|
@@ -16,17 +28,16 @@ interface RelayOptions {
|
|
|
16
28
|
domain?: string;
|
|
17
29
|
documentLoaderFactory?: DocumentLoaderFactory;
|
|
18
30
|
authenticatedDocumentLoaderFactory?: AuthenticatedDocumentLoaderFactory;
|
|
19
|
-
federation?: Federation<void>;
|
|
20
31
|
queue?: MessageQueue;
|
|
32
|
+
subscriptionHandler?: SubscriptionRequestHandler;
|
|
21
33
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
interface Relay {
|
|
26
|
-
readonly domain: string;
|
|
27
|
-
fetch(request: Request): Promise<Response>;
|
|
28
|
-
setSubscriptionHandler(handler: SubscriptionRequestHandler): this;
|
|
34
|
+
interface RelayFollower {
|
|
35
|
+
readonly actor: unknown;
|
|
36
|
+
readonly state: string;
|
|
29
37
|
}
|
|
38
|
+
declare function createRelay(type: RelayType, options: RelayOptions): Relay;
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/mastodon.d.ts
|
|
30
41
|
/**
|
|
31
42
|
* A Mastodon-compatible ActivityPub relay implementation.
|
|
32
43
|
* This relay follows Mastodon's relay protocol for maximum compatibility
|
|
@@ -36,11 +47,12 @@ interface Relay {
|
|
|
36
47
|
*/
|
|
37
48
|
declare class MastodonRelay implements Relay {
|
|
38
49
|
#private;
|
|
39
|
-
constructor(options: RelayOptions);
|
|
40
|
-
get domain(): string;
|
|
50
|
+
constructor(options: RelayOptions, relayBuilder: FederationBuilder$1<RelayOptions>);
|
|
41
51
|
fetch(request: Request): Promise<Response>;
|
|
42
|
-
|
|
52
|
+
setupInboxListeners(): void;
|
|
43
53
|
}
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/litepub.d.ts
|
|
44
56
|
/**
|
|
45
57
|
* A LitePub-compatible ActivityPub relay implementation.
|
|
46
58
|
* This relay follows LitePub's relay protocol and extensions for
|
|
@@ -50,10 +62,9 @@ declare class MastodonRelay implements Relay {
|
|
|
50
62
|
*/
|
|
51
63
|
declare class LitePubRelay implements Relay {
|
|
52
64
|
#private;
|
|
53
|
-
constructor(options: RelayOptions);
|
|
54
|
-
get domain(): string;
|
|
65
|
+
constructor(options: RelayOptions, relayBuilder: FederationBuilder<RelayOptions>);
|
|
55
66
|
fetch(request: Request): Promise<Response>;
|
|
56
|
-
|
|
67
|
+
setupInboxListeners(): void;
|
|
57
68
|
}
|
|
58
69
|
//#endregion
|
|
59
|
-
export { LitePubRelay, MastodonRelay,
|
|
70
|
+
export { LitePubRelay, MastodonRelay, RELAY_SERVER_ACTOR, RelayFollower, RelayOptions, SubscriptionRequestHandler, createRelay };
|
package/dist/mod.js
CHANGED
|
@@ -1,8 +1,178 @@
|
|
|
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
|
-
|
|
5
|
-
|
|
2
|
+
import { Temporal } from "@js-temporal/polyfill";
|
|
3
|
+
|
|
4
|
+
import { Accept, Announce, Create, Delete, Follow, Move, PUBLIC_COLLECTION, Reject, Undo, Update, createFederationBuilder, exportJwk, generateCryptoKeyPair, importJwk, isActor } from "@fedify/fedify";
|
|
5
|
+
import { Application, Object as Object$1, isActor as isActor$1 } from "@fedify/fedify/vocab";
|
|
6
|
+
|
|
7
|
+
//#region src/litepub.ts
|
|
8
|
+
/**
|
|
9
|
+
* A LitePub-compatible ActivityPub relay implementation.
|
|
10
|
+
* This relay follows LitePub's relay protocol and extensions for
|
|
11
|
+
* enhanced federation capabilities.
|
|
12
|
+
*
|
|
13
|
+
* @since 2.0.0
|
|
14
|
+
*/
|
|
15
|
+
var LitePubRelay = class {
|
|
16
|
+
#federationBuilder;
|
|
17
|
+
#options;
|
|
18
|
+
#federation;
|
|
19
|
+
constructor(options, relayBuilder$1) {
|
|
20
|
+
this.#options = options;
|
|
21
|
+
this.#federationBuilder = relayBuilder$1;
|
|
22
|
+
}
|
|
23
|
+
async fetch(request) {
|
|
24
|
+
if (this.#federation == null) {
|
|
25
|
+
this.#federation = await this.#federationBuilder.build(this.#options);
|
|
26
|
+
this.setupInboxListeners();
|
|
27
|
+
}
|
|
28
|
+
return await this.#federation.fetch(request, { contextData: this.#options });
|
|
29
|
+
}
|
|
30
|
+
setupInboxListeners() {
|
|
31
|
+
if (this.#federation != null) this.#federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Follow, async (ctx, follow) => {
|
|
32
|
+
if (follow.id == null || follow.objectId == null) return;
|
|
33
|
+
const parsed = ctx.parseUri(follow.objectId);
|
|
34
|
+
const isPublicFollow = follow.objectId.href === "https://www.w3.org/ns/activitystreams#Public";
|
|
35
|
+
if (!isPublicFollow && parsed?.type !== "actor") return;
|
|
36
|
+
const relayActorUri = ctx.getActorUri(RELAY_SERVER_ACTOR);
|
|
37
|
+
const follower = await follow.getActor(ctx);
|
|
38
|
+
if (follower == null || follower.id == null || follower.preferredUsername == null || follower.inboxId == null) return;
|
|
39
|
+
const existingFollow = await ctx.data.kv.get(["follower", follower.id.href]);
|
|
40
|
+
if (existingFollow?.state === "pending") return;
|
|
41
|
+
let subscriptionApproved = false;
|
|
42
|
+
if (this.#options.subscriptionHandler) subscriptionApproved = await this.#options.subscriptionHandler(ctx, follower);
|
|
43
|
+
if (subscriptionApproved) {
|
|
44
|
+
await ctx.data.kv.set(["follower", follower.id.href], {
|
|
45
|
+
"actor": await follower.toJsonLd(),
|
|
46
|
+
"state": "pending"
|
|
47
|
+
});
|
|
48
|
+
await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, follower, new Accept({
|
|
49
|
+
id: new URL(`#accepts`, relayActorUri),
|
|
50
|
+
actor: relayActorUri,
|
|
51
|
+
object: follow
|
|
52
|
+
}));
|
|
53
|
+
await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, follower, new Follow({
|
|
54
|
+
actor: relayActorUri,
|
|
55
|
+
object: follower.id,
|
|
56
|
+
to: follower.id
|
|
57
|
+
}));
|
|
58
|
+
} else await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, follower, new Reject({
|
|
59
|
+
id: new URL(`#rejects`, relayActorUri),
|
|
60
|
+
actor: relayActorUri,
|
|
61
|
+
object: follow
|
|
62
|
+
}));
|
|
63
|
+
}).on(Accept, async (ctx, accept) => {
|
|
64
|
+
const follow = await accept.getObject({
|
|
65
|
+
crossOrigin: "trust",
|
|
66
|
+
...ctx
|
|
67
|
+
});
|
|
68
|
+
if (!(follow instanceof Follow)) return;
|
|
69
|
+
const relayActorId = follow.actorId;
|
|
70
|
+
if (relayActorId == null) return;
|
|
71
|
+
const followerActor = await accept.getActor();
|
|
72
|
+
if (!isActor(followerActor) || !followerActor.id) return;
|
|
73
|
+
const parsed = ctx.parseUri(relayActorId);
|
|
74
|
+
if (parsed == null || parsed.type !== "actor") return;
|
|
75
|
+
const followerData = await ctx.data.kv.get(["follower", followerActor.id.href]);
|
|
76
|
+
if (followerData == null) return;
|
|
77
|
+
const updatedFollowerData = {
|
|
78
|
+
...followerData,
|
|
79
|
+
state: "accepted"
|
|
80
|
+
};
|
|
81
|
+
await ctx.data.kv.set(["follower", followerActor.id.href], updatedFollowerData);
|
|
82
|
+
const followers = await ctx.data.kv.get(["followers"]) ?? [];
|
|
83
|
+
followers.push(followerActor.id.href);
|
|
84
|
+
await ctx.data.kv.set(["followers"], followers);
|
|
85
|
+
}).on(Undo, async (ctx, undo) => {
|
|
86
|
+
const activity = await undo.getObject({
|
|
87
|
+
crossOrigin: "trust",
|
|
88
|
+
...ctx
|
|
89
|
+
});
|
|
90
|
+
if (activity instanceof Follow) {
|
|
91
|
+
if (activity.id == null || activity.actorId == null) return;
|
|
92
|
+
const followers = await ctx.data.kv.get(["followers"]) ?? [];
|
|
93
|
+
const updatedFollowers = followers.filter((id) => id !== activity.actorId?.href);
|
|
94
|
+
await ctx.data.kv.set(["followers"], updatedFollowers);
|
|
95
|
+
await ctx.data.kv.delete(["follower", activity.actorId?.href]);
|
|
96
|
+
} else console.warn("Unsupported object type ({type}) for Undo activity: {object}", {
|
|
97
|
+
type: activity?.constructor.name,
|
|
98
|
+
object: activity
|
|
99
|
+
});
|
|
100
|
+
}).on(Create, async (ctx, create) => {
|
|
101
|
+
const sender = await create.getActor(ctx);
|
|
102
|
+
const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
|
|
103
|
+
const announce = new Announce({
|
|
104
|
+
id: new URL(`/announce#${crypto.randomUUID()}`, ctx.origin),
|
|
105
|
+
actor: ctx.getActorUri(RELAY_SERVER_ACTOR),
|
|
106
|
+
object: create.objectId,
|
|
107
|
+
to: PUBLIC_COLLECTION,
|
|
108
|
+
published: Temporal.Now.instant()
|
|
109
|
+
});
|
|
110
|
+
await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", announce, {
|
|
111
|
+
excludeBaseUris,
|
|
112
|
+
preferSharedInbox: true
|
|
113
|
+
});
|
|
114
|
+
}).on(Update, async (ctx, update) => {
|
|
115
|
+
const sender = await update.getActor(ctx);
|
|
116
|
+
const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
|
|
117
|
+
const announce = new Announce({
|
|
118
|
+
id: new URL(`/announce#${crypto.randomUUID()}`, ctx.origin),
|
|
119
|
+
actor: ctx.getActorUri(RELAY_SERVER_ACTOR),
|
|
120
|
+
object: update.objectId,
|
|
121
|
+
to: PUBLIC_COLLECTION,
|
|
122
|
+
published: Temporal.Now.instant()
|
|
123
|
+
});
|
|
124
|
+
await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", announce, {
|
|
125
|
+
excludeBaseUris,
|
|
126
|
+
preferSharedInbox: true
|
|
127
|
+
});
|
|
128
|
+
}).on(Move, async (ctx, move) => {
|
|
129
|
+
const sender = await move.getActor(ctx);
|
|
130
|
+
const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
|
|
131
|
+
const announce = new Announce({
|
|
132
|
+
id: new URL(`/announce#${crypto.randomUUID()}`, ctx.origin),
|
|
133
|
+
actor: ctx.getActorUri(RELAY_SERVER_ACTOR),
|
|
134
|
+
object: move.objectId,
|
|
135
|
+
to: PUBLIC_COLLECTION,
|
|
136
|
+
published: Temporal.Now.instant()
|
|
137
|
+
});
|
|
138
|
+
await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", announce, {
|
|
139
|
+
excludeBaseUris,
|
|
140
|
+
preferSharedInbox: true
|
|
141
|
+
});
|
|
142
|
+
}).on(Delete, async (ctx, deleteActivity) => {
|
|
143
|
+
const sender = await deleteActivity.getActor(ctx);
|
|
144
|
+
const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
|
|
145
|
+
const announce = new Announce({
|
|
146
|
+
id: new URL(`/announce#${crypto.randomUUID()}`, ctx.origin),
|
|
147
|
+
actor: ctx.getActorUri(RELAY_SERVER_ACTOR),
|
|
148
|
+
object: deleteActivity.objectId,
|
|
149
|
+
to: PUBLIC_COLLECTION,
|
|
150
|
+
published: Temporal.Now.instant()
|
|
151
|
+
});
|
|
152
|
+
await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", announce, {
|
|
153
|
+
excludeBaseUris,
|
|
154
|
+
preferSharedInbox: true
|
|
155
|
+
});
|
|
156
|
+
}).on(Announce, async (ctx, announceActivity) => {
|
|
157
|
+
const sender = await announceActivity.getActor(ctx);
|
|
158
|
+
const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
|
|
159
|
+
const announce = new Announce({
|
|
160
|
+
id: new URL(`/announce#${crypto.randomUUID()}`, ctx.origin),
|
|
161
|
+
actor: ctx.getActorUri(RELAY_SERVER_ACTOR),
|
|
162
|
+
object: announceActivity.objectId,
|
|
163
|
+
to: PUBLIC_COLLECTION,
|
|
164
|
+
published: Temporal.Now.instant()
|
|
165
|
+
});
|
|
166
|
+
await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", announce, {
|
|
167
|
+
excludeBaseUris,
|
|
168
|
+
preferSharedInbox: true
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
//#endregion
|
|
175
|
+
//#region src/mastodon.ts
|
|
6
176
|
/**
|
|
7
177
|
* A Mastodon-compatible ActivityPub relay implementation.
|
|
8
178
|
* This relay follows Mastodon's relay protocol for maximum compatibility
|
|
@@ -11,120 +181,60 @@ const RELAY_SERVER_ACTOR = "relay";
|
|
|
11
181
|
* @since 2.0.0
|
|
12
182
|
*/
|
|
13
183
|
var MastodonRelay = class {
|
|
14
|
-
#
|
|
184
|
+
#federationBuilder;
|
|
15
185
|
#options;
|
|
16
|
-
#
|
|
17
|
-
constructor(options) {
|
|
186
|
+
#federation;
|
|
187
|
+
constructor(options, relayBuilder$1) {
|
|
18
188
|
this.#options = options;
|
|
19
|
-
this.#
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
preferredUsername: identifier,
|
|
31
|
-
name: "ActivityPub Relay",
|
|
32
|
-
summary: "Mastodon-compatible ActivityPub relay server",
|
|
33
|
-
inbox: ctx.getInboxUri(),
|
|
34
|
-
followers: ctx.getFollowersUri(identifier),
|
|
35
|
-
url: ctx.getActorUri(identifier),
|
|
36
|
-
publicKey: keys[0].cryptographicKey,
|
|
37
|
-
assertionMethods: keys.map((k) => k.multikey)
|
|
38
|
-
});
|
|
39
|
-
}).setKeyPairsDispatcher(async (_ctx, identifier) => {
|
|
40
|
-
if (identifier !== RELAY_SERVER_ACTOR) return [];
|
|
41
|
-
const rsaPairJson = await options.kv.get([
|
|
42
|
-
"keypair",
|
|
43
|
-
"rsa",
|
|
44
|
-
identifier
|
|
45
|
-
]);
|
|
46
|
-
const ed25519PairJson = await options.kv.get([
|
|
47
|
-
"keypair",
|
|
48
|
-
"ed25519",
|
|
49
|
-
identifier
|
|
50
|
-
]);
|
|
51
|
-
if (rsaPairJson == null || ed25519PairJson == null) {
|
|
52
|
-
const rsaPair$1 = await generateCryptoKeyPair("RSASSA-PKCS1-v1_5");
|
|
53
|
-
const ed25519Pair$1 = await generateCryptoKeyPair("Ed25519");
|
|
54
|
-
await options.kv.set([
|
|
55
|
-
"keypair",
|
|
56
|
-
"rsa",
|
|
57
|
-
identifier
|
|
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 };
|
|
93
|
-
});
|
|
94
|
-
this.#federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Follow, async (ctx, follow) => {
|
|
189
|
+
this.#federationBuilder = relayBuilder$1;
|
|
190
|
+
}
|
|
191
|
+
async fetch(request) {
|
|
192
|
+
if (this.#federation == null) {
|
|
193
|
+
this.#federation = await this.#federationBuilder.build(this.#options);
|
|
194
|
+
this.setupInboxListeners();
|
|
195
|
+
}
|
|
196
|
+
return await this.#federation.fetch(request, { contextData: this.#options });
|
|
197
|
+
}
|
|
198
|
+
setupInboxListeners() {
|
|
199
|
+
if (this.#federation != null) this.#federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Follow, async (ctx, follow) => {
|
|
95
200
|
if (follow.id == null || follow.objectId == null) return;
|
|
96
201
|
const parsed = ctx.parseUri(follow.objectId);
|
|
97
202
|
const isPublicFollow = follow.objectId.href === "https://www.w3.org/ns/activitystreams#Public";
|
|
98
203
|
if (!isPublicFollow && parsed?.type !== "actor") return;
|
|
99
204
|
const relayActorUri = ctx.getActorUri(RELAY_SERVER_ACTOR);
|
|
100
|
-
const
|
|
101
|
-
if (
|
|
205
|
+
const follower = await follow.getActor(ctx);
|
|
206
|
+
if (follower == null || follower.id == null || follower.preferredUsername == null || follower.inboxId == null) return;
|
|
102
207
|
let approved = false;
|
|
103
|
-
if (this.#subscriptionHandler) approved = await this.#subscriptionHandler(ctx,
|
|
208
|
+
if (this.#options.subscriptionHandler) approved = await this.#options.subscriptionHandler(ctx, follower);
|
|
104
209
|
if (approved) {
|
|
105
|
-
const followers = await
|
|
106
|
-
followers.push(
|
|
107
|
-
await
|
|
108
|
-
await
|
|
109
|
-
|
|
210
|
+
const followers = await ctx.data.kv.get(["followers"]) ?? [];
|
|
211
|
+
followers.push(follower.id.href);
|
|
212
|
+
await ctx.data.kv.set(["followers"], followers);
|
|
213
|
+
await ctx.data.kv.set(["follower", follower.id.href], {
|
|
214
|
+
"actor": await follower.toJsonLd(),
|
|
215
|
+
"state": "accepted"
|
|
216
|
+
});
|
|
217
|
+
await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, follower, new Accept({
|
|
110
218
|
id: new URL(`#accepts`, relayActorUri),
|
|
111
219
|
actor: relayActorUri,
|
|
112
220
|
object: follow
|
|
113
221
|
}));
|
|
114
|
-
} else await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR },
|
|
222
|
+
} else await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, follower, new Reject({
|
|
115
223
|
id: new URL(`#rejects`, relayActorUri),
|
|
116
224
|
actor: relayActorUri,
|
|
117
225
|
object: follow
|
|
118
226
|
}));
|
|
119
227
|
}).on(Undo, async (ctx, undo) => {
|
|
120
|
-
const activity = await undo.getObject(
|
|
228
|
+
const activity = await undo.getObject({
|
|
229
|
+
crossOrigin: "trust",
|
|
230
|
+
...ctx
|
|
231
|
+
});
|
|
121
232
|
if (activity instanceof Follow) {
|
|
122
233
|
if (activity.id == null || activity.actorId == null) return;
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
await
|
|
127
|
-
options.kv.delete(["follower", activityId]);
|
|
234
|
+
const followers = await ctx.data.kv.get(["followers"]) ?? [];
|
|
235
|
+
const updatedFollowers = followers.filter((id) => id !== activity.actorId?.href);
|
|
236
|
+
await ctx.data.kv.set(["followers"], updatedFollowers);
|
|
237
|
+
await ctx.data.kv.delete(["follower", activity.actorId?.href]);
|
|
128
238
|
} else console.warn("Unsupported object type ({type}) for Undo activity: {object}", {
|
|
129
239
|
type: activity?.constructor.name,
|
|
130
240
|
object: activity
|
|
@@ -145,16 +255,16 @@ var MastodonRelay = class {
|
|
|
145
255
|
excludeBaseUris,
|
|
146
256
|
preferSharedInbox: true
|
|
147
257
|
});
|
|
148
|
-
}).on(Move, async (ctx,
|
|
149
|
-
const sender = await
|
|
258
|
+
}).on(Move, async (ctx, move) => {
|
|
259
|
+
const sender = await move.getActor(ctx);
|
|
150
260
|
const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
|
|
151
261
|
await ctx.forwardActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", {
|
|
152
262
|
skipIfUnsigned: true,
|
|
153
263
|
excludeBaseUris,
|
|
154
264
|
preferSharedInbox: true
|
|
155
265
|
});
|
|
156
|
-
}).on(Update, async (ctx,
|
|
157
|
-
const sender = await
|
|
266
|
+
}).on(Update, async (ctx, update) => {
|
|
267
|
+
const sender = await update.getActor(ctx);
|
|
158
268
|
const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
|
|
159
269
|
await ctx.forwardActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", {
|
|
160
270
|
skipIfUnsigned: true,
|
|
@@ -163,43 +273,97 @@ var MastodonRelay = class {
|
|
|
163
273
|
});
|
|
164
274
|
});
|
|
165
275
|
}
|
|
166
|
-
get domain() {
|
|
167
|
-
return this.#options.domain || "localhost";
|
|
168
|
-
}
|
|
169
|
-
fetch(request) {
|
|
170
|
-
return this.#federation.fetch(request, { contextData: void 0 });
|
|
171
|
-
}
|
|
172
|
-
setSubscriptionHandler(handler) {
|
|
173
|
-
this.#subscriptionHandler = handler;
|
|
174
|
-
return this;
|
|
175
|
-
}
|
|
176
276
|
};
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
277
|
+
|
|
278
|
+
//#endregion
|
|
279
|
+
//#region src/relay.ts
|
|
280
|
+
const RELAY_SERVER_ACTOR = "relay";
|
|
281
|
+
const relayBuilder = createFederationBuilder();
|
|
282
|
+
relayBuilder.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => {
|
|
283
|
+
if (identifier !== RELAY_SERVER_ACTOR) return null;
|
|
284
|
+
const keys = await ctx.getActorKeyPairs(identifier);
|
|
285
|
+
return new Application({
|
|
286
|
+
id: ctx.getActorUri(identifier),
|
|
287
|
+
preferredUsername: identifier,
|
|
288
|
+
name: "ActivityPub Relay",
|
|
289
|
+
inbox: ctx.getInboxUri(),
|
|
290
|
+
followers: ctx.getFollowersUri(identifier),
|
|
291
|
+
following: ctx.getFollowingUri(identifier),
|
|
292
|
+
url: ctx.getActorUri(identifier),
|
|
293
|
+
publicKey: keys[0].cryptographicKey,
|
|
294
|
+
assertionMethods: keys.map((k) => k.multikey)
|
|
295
|
+
});
|
|
296
|
+
}).setKeyPairsDispatcher(async (ctx, identifier) => {
|
|
297
|
+
if (identifier !== RELAY_SERVER_ACTOR) return [];
|
|
298
|
+
const rsaPairJson = await ctx.data.kv.get([
|
|
299
|
+
"keypair",
|
|
300
|
+
"rsa",
|
|
301
|
+
identifier
|
|
302
|
+
]);
|
|
303
|
+
const ed25519PairJson = await ctx.data.kv.get([
|
|
304
|
+
"keypair",
|
|
305
|
+
"ed25519",
|
|
306
|
+
identifier
|
|
307
|
+
]);
|
|
308
|
+
if (rsaPairJson == null || ed25519PairJson == null) {
|
|
309
|
+
const rsaPair$1 = await generateCryptoKeyPair("RSASSA-PKCS1-v1_5");
|
|
310
|
+
const ed25519Pair$1 = await generateCryptoKeyPair("Ed25519");
|
|
311
|
+
await ctx.data.kv.set([
|
|
312
|
+
"keypair",
|
|
313
|
+
"rsa",
|
|
314
|
+
identifier
|
|
315
|
+
], {
|
|
316
|
+
privateKey: await exportJwk(rsaPair$1.privateKey),
|
|
317
|
+
publicKey: await exportJwk(rsaPair$1.publicKey)
|
|
318
|
+
});
|
|
319
|
+
await ctx.data.kv.set([
|
|
320
|
+
"keypair",
|
|
321
|
+
"ed25519",
|
|
322
|
+
identifier
|
|
323
|
+
], {
|
|
324
|
+
privateKey: await exportJwk(ed25519Pair$1.privateKey),
|
|
325
|
+
publicKey: await exportJwk(ed25519Pair$1.publicKey)
|
|
326
|
+
});
|
|
327
|
+
return [rsaPair$1, ed25519Pair$1];
|
|
194
328
|
}
|
|
195
|
-
|
|
196
|
-
|
|
329
|
+
const rsaPair = {
|
|
330
|
+
privateKey: await importJwk(rsaPairJson.privateKey, "private"),
|
|
331
|
+
publicKey: await importJwk(rsaPairJson.publicKey, "public")
|
|
332
|
+
};
|
|
333
|
+
const ed25519Pair = {
|
|
334
|
+
privateKey: await importJwk(ed25519PairJson.privateKey, "private"),
|
|
335
|
+
publicKey: await importJwk(ed25519PairJson.publicKey, "public")
|
|
336
|
+
};
|
|
337
|
+
return [rsaPair, ed25519Pair];
|
|
338
|
+
});
|
|
339
|
+
async function getFollowerActors(ctx) {
|
|
340
|
+
const followers = await ctx.data.kv.get(["followers"]) ?? [];
|
|
341
|
+
const actors = [];
|
|
342
|
+
for (const followerId of followers) {
|
|
343
|
+
const follower = await ctx.data.kv.get(["follower", followerId]);
|
|
344
|
+
if (!follower) continue;
|
|
345
|
+
const actor = await Object$1.fromJsonLd(follower.actor);
|
|
346
|
+
if (!isActor$1(actor)) continue;
|
|
347
|
+
actors.push(actor);
|
|
197
348
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
349
|
+
return actors;
|
|
350
|
+
}
|
|
351
|
+
relayBuilder.setFollowersDispatcher("/users/{identifier}/followers", async (ctx, identifier) => {
|
|
352
|
+
if (identifier !== RELAY_SERVER_ACTOR) return null;
|
|
353
|
+
const actors = await getFollowerActors(ctx);
|
|
354
|
+
return { items: actors };
|
|
355
|
+
});
|
|
356
|
+
relayBuilder.setFollowingDispatcher("/users/{identifier}/following", async (ctx, identifier) => {
|
|
357
|
+
if (identifier !== RELAY_SERVER_ACTOR) return null;
|
|
358
|
+
const actors = await getFollowerActors(ctx);
|
|
359
|
+
return { items: actors };
|
|
360
|
+
});
|
|
361
|
+
function createRelay(type, options) {
|
|
362
|
+
switch (type) {
|
|
363
|
+
case "mastodon": return new MastodonRelay(options, relayBuilder);
|
|
364
|
+
case "litepub": return new LitePubRelay(options, relayBuilder);
|
|
201
365
|
}
|
|
202
|
-
}
|
|
366
|
+
}
|
|
203
367
|
|
|
204
368
|
//#endregion
|
|
205
|
-
export { LitePubRelay, MastodonRelay };
|
|
369
|
+
export { LitePubRelay, MastodonRelay, RELAY_SERVER_ACTOR, createRelay };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fedify/relay",
|
|
3
|
-
"version": "2.0.0-pr.
|
|
3
|
+
"version": "2.0.0-pr.490.2+99a396d5",
|
|
4
4
|
"description": "ActivityPub relay support for Fedify",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Fedify",
|
|
@@ -47,14 +47,17 @@
|
|
|
47
47
|
"dist/",
|
|
48
48
|
"package.json"
|
|
49
49
|
],
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@js-temporal/polyfill": "^0.5.1"
|
|
52
|
+
},
|
|
50
53
|
"peerDependencies": {
|
|
51
|
-
"@fedify/fedify": "^2.0.0-pr.
|
|
54
|
+
"@fedify/fedify": "^2.0.0-pr.490.2+99a396d5"
|
|
52
55
|
},
|
|
53
56
|
"devDependencies": {
|
|
54
57
|
"tsdown": "^0.12.9",
|
|
55
58
|
"typescript": "^5.9.3",
|
|
56
|
-
"@fedify/testing": "^2.0.0-pr.
|
|
57
|
-
"@fedify/vocab-runtime": "^2.0.0-pr.
|
|
59
|
+
"@fedify/testing": "^2.0.0-pr.490.2+99a396d5",
|
|
60
|
+
"@fedify/vocab-runtime": "^2.0.0-pr.490.2+99a396d5"
|
|
58
61
|
},
|
|
59
62
|
"scripts": {
|
|
60
63
|
"build": "deno task codegen && tsdown",
|