@fedify/relay 2.4.0-pr.934.40 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mod.d.cts CHANGED
@@ -1,7 +1,7 @@
1
1
  /// <reference lib="esnext.temporal" />
2
2
  import { Context, KvStore, MessageQueue } from "@fedify/fedify";
3
3
  import { Actor } from "@fedify/vocab";
4
- //#region ../vocab-runtime/dist/docloader-xRGn1azD.d.ts
4
+ //#region ../vocab-runtime/dist/docloader-CYwqh5Df.d.ts
5
5
  /**
6
6
  * Options for making `User-Agent` string.
7
7
  * @see {@link getUserAgent}
@@ -54,6 +54,13 @@ interface DocumentLoaderOptions {
54
54
  * @since 1.8.0
55
55
  */
56
56
  signal?: AbortSignal;
57
+ /**
58
+ * Whether to lower error-level logs for recoverable document loading
59
+ * failures to warning-level logs. The loader still throws the error.
60
+ * @default `false`
61
+ * @since 2.4.0
62
+ */
63
+ suppressError?: boolean;
57
64
  }
58
65
  /**
59
66
  * A JSON-LD document loader that fetches documents from the Web.
@@ -94,6 +101,28 @@ interface DocumentLoaderFactoryOptions {
94
101
  * @since 2.2.0
95
102
  */
96
103
  maxRedirection?: number;
104
+ /**
105
+ * The timeout in milliseconds for each call of the created document
106
+ * loader. The timeout is shared by all the steps of a call, including
107
+ * URL validation, every HTTP redirect and alternate document link it
108
+ * follows, retries, and reading the response body; it does not restart
109
+ * for each of them. It does not interrupt synchronous work such as
110
+ * parsing a document that has already been received.
111
+ *
112
+ * When a call times out, the loader throws a {@link FetchError} without
113
+ * a {@link FetchError.response}, whose `cause` is a `DOMException` named
114
+ * `"TimeoutError"`. An `AbortSignal` passed through
115
+ * {@link DocumentLoaderOptions.signal} still cancels a call; in that case
116
+ * the loader throws the signal's reason as before.
117
+ *
118
+ * Fractional values are rounded up. Set it to `null` to turn off the
119
+ * timeout.
120
+ * @default `10000` (10 seconds)
121
+ * @throws {RangeError} If the value is not a positive finite number or is
122
+ * greater than 2,147,483,647 (about 24.8 days).
123
+ * @since 2.4.0
124
+ */
125
+ timeout?: number | null;
97
126
  }
98
127
  /**
99
128
  * A factory function that creates an authenticated {@link DocumentLoader} for
@@ -114,6 +143,7 @@ type AuthenticatedDocumentLoaderFactory = (identity: {
114
143
  * @param url The URL of the document to load.
115
144
  * @param response The response to get the document from.
116
145
  * @param fetch The function to fetch the document.
146
+ * @param options The options for loading the document.
117
147
  * @returns The loaded remote document.
118
148
  * @throws {FetchError} If the response is not OK.
119
149
  * @internal
@@ -126,6 +156,12 @@ declare const RELAY_SERVER_ACTOR = "relay";
126
156
  */
127
157
  type RelayType = "mastodon" | "litepub";
128
158
  /**
159
+ * A follower's subscription state.
160
+ *
161
+ * @internal
162
+ */
163
+ type RelayFollowerState = "pending" | "accepted";
164
+ /**
129
165
  * Handler for subscription requests (Follow/Undo activities).
130
166
  */
131
167
  type SubscriptionRequestHandler = (ctx: Context<RelayOptions>, clientActor: Actor) => Promise<boolean>;
@@ -182,7 +218,7 @@ interface RelayFollower {
182
218
  /** The validated Actor object. */
183
219
  readonly actor: Actor;
184
220
  /** The follower's state. */
185
- readonly state: "pending" | "accepted";
221
+ readonly state: RelayFollowerState;
186
222
  }
187
223
  /**
188
224
  * Public interface for ActivityPub relay implementations.
package/dist/mod.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /// <reference lib="esnext.temporal" />
2
2
  import { Context, KvStore, MessageQueue } from "@fedify/fedify";
3
3
  import { Actor } from "@fedify/vocab";
4
- //#region ../vocab-runtime/dist/docloader-xRGn1azD.d.ts
4
+ //#region ../vocab-runtime/dist/docloader-CYwqh5Df.d.ts
5
5
  /**
6
6
  * Options for making `User-Agent` string.
7
7
  * @see {@link getUserAgent}
@@ -54,6 +54,13 @@ interface DocumentLoaderOptions {
54
54
  * @since 1.8.0
55
55
  */
56
56
  signal?: AbortSignal;
57
+ /**
58
+ * Whether to lower error-level logs for recoverable document loading
59
+ * failures to warning-level logs. The loader still throws the error.
60
+ * @default `false`
61
+ * @since 2.4.0
62
+ */
63
+ suppressError?: boolean;
57
64
  }
58
65
  /**
59
66
  * A JSON-LD document loader that fetches documents from the Web.
@@ -94,6 +101,28 @@ interface DocumentLoaderFactoryOptions {
94
101
  * @since 2.2.0
95
102
  */
96
103
  maxRedirection?: number;
104
+ /**
105
+ * The timeout in milliseconds for each call of the created document
106
+ * loader. The timeout is shared by all the steps of a call, including
107
+ * URL validation, every HTTP redirect and alternate document link it
108
+ * follows, retries, and reading the response body; it does not restart
109
+ * for each of them. It does not interrupt synchronous work such as
110
+ * parsing a document that has already been received.
111
+ *
112
+ * When a call times out, the loader throws a {@link FetchError} without
113
+ * a {@link FetchError.response}, whose `cause` is a `DOMException` named
114
+ * `"TimeoutError"`. An `AbortSignal` passed through
115
+ * {@link DocumentLoaderOptions.signal} still cancels a call; in that case
116
+ * the loader throws the signal's reason as before.
117
+ *
118
+ * Fractional values are rounded up. Set it to `null` to turn off the
119
+ * timeout.
120
+ * @default `10000` (10 seconds)
121
+ * @throws {RangeError} If the value is not a positive finite number or is
122
+ * greater than 2,147,483,647 (about 24.8 days).
123
+ * @since 2.4.0
124
+ */
125
+ timeout?: number | null;
97
126
  }
98
127
  /**
99
128
  * A factory function that creates an authenticated {@link DocumentLoader} for
@@ -114,6 +143,7 @@ type AuthenticatedDocumentLoaderFactory = (identity: {
114
143
  * @param url The URL of the document to load.
115
144
  * @param response The response to get the document from.
116
145
  * @param fetch The function to fetch the document.
146
+ * @param options The options for loading the document.
117
147
  * @returns The loaded remote document.
118
148
  * @throws {FetchError} If the response is not OK.
119
149
  * @internal
@@ -126,6 +156,12 @@ declare const RELAY_SERVER_ACTOR = "relay";
126
156
  */
127
157
  type RelayType = "mastodon" | "litepub";
128
158
  /**
159
+ * A follower's subscription state.
160
+ *
161
+ * @internal
162
+ */
163
+ type RelayFollowerState = "pending" | "accepted";
164
+ /**
129
165
  * Handler for subscription requests (Follow/Undo activities).
130
166
  */
131
167
  type SubscriptionRequestHandler = (ctx: Context<RelayOptions>, clientActor: Actor) => Promise<boolean>;
@@ -182,7 +218,7 @@ interface RelayFollower {
182
218
  /** The validated Actor object. */
183
219
  readonly actor: Actor;
184
220
  /** The follower's state. */
185
- readonly state: "pending" | "accepted";
221
+ readonly state: RelayFollowerState;
186
222
  }
187
223
  /**
188
224
  * 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
- if (!isRelayFollowerData(value)) continue;
82
- if (value.state !== "accepted") continue;
83
- const actor = await Object$1.fromJsonLd(value.actor);
84
- if (!isActor(actor)) continue;
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 this.parseFollowerData(actorId, entry.value);
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
- const followerData = await this.options.kv.get(["follower", actorId]);
196
- return await this.parseFollowerData(actorId, followerData);
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
- async #announceToFollowers(ctx, activity) {
289
- const sender = await activity.getActor(ctx);
290
- const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
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
- if (this.federation != null) this.federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Follow, async (ctx, follow) => {
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 == null) return;
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
- }).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));
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
- async #forwardToFollowers(ctx, activity) {
360
- const sender = await activity.getActor(ctx);
361
- const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
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