11import type { Federation , FederationBuilder } from "@fedify/fedify" ;
2- import type { RelayOptions } from "./types.ts" ;
2+ import { isActor , Object as APObject } from "@fedify/fedify/vocab" ;
3+ import {
4+ isRelayFollowerData ,
5+ type Relay ,
6+ type RelayFollower ,
7+ type RelayOptions ,
8+ } from "./types.ts" ;
39
410/**
511 * Abstract base class for relay implementations.
612 * Provides common infrastructure for both Mastodon and LitePub relays.
713 *
8- * @since 2.0.0
14+ * @internal
915 */
10- export abstract class BaseRelay {
16+ export abstract class BaseRelay implements Relay {
1117 protected federationBuilder : FederationBuilder < RelayOptions > ;
1218 protected options : RelayOptions ;
1319 protected federation ?: Federation < RelayOptions > ;
@@ -31,6 +37,99 @@ export abstract class BaseRelay {
3137 } ) ;
3238 }
3339
40+ /**
41+ * Helper method to parse and validate follower data from storage.
42+ * Deserializes JSON-LD actor data and validates it.
43+ *
44+ * @param actorId The actor ID of the follower
45+ * @param data Raw data from KV store
46+ * @returns RelayFollower object if valid, null otherwise
47+ * @internal
48+ */
49+ private async parseFollowerData (
50+ actorId : string ,
51+ data : unknown ,
52+ ) : Promise < RelayFollower | null > {
53+ if ( ! isRelayFollowerData ( data ) ) return null ;
54+
55+ const actor = await APObject . fromJsonLd ( data . actor ) ;
56+ if ( ! isActor ( actor ) ) return null ;
57+
58+ return {
59+ actorId,
60+ actor,
61+ state : data . state ,
62+ } ;
63+ }
64+
65+ /**
66+ * Lists all followers of the relay.
67+ *
68+ * @returns An async iterator of follower entries
69+ *
70+ * @example
71+ * ```ts
72+ * import { createRelay } from "@fedify/relay";
73+ * import { MemoryKvStore } from "@fedify/fedify";
74+ *
75+ * const relay = createRelay("mastodon", {
76+ * kv: new MemoryKvStore(),
77+ * domain: "relay.example.com",
78+ * subscriptionHandler: async (ctx, actor) => true,
79+ * });
80+ *
81+ * for await (const follower of relay.listFollowers()) {
82+ * console.log(`Follower: ${follower.actorId}`);
83+ * console.log(`State: ${follower.state}`);
84+ * console.log(`Actor: ${follower.actor.name}`);
85+ * }
86+ * ```
87+ *
88+ * @since 2.0.0
89+ */
90+ async * listFollowers ( ) : AsyncIterableIterator < RelayFollower > {
91+ for await ( const entry of this . options . kv . list ( [ "follower" ] ) ) {
92+ const actorId = entry . key [ 1 ] ;
93+ if ( typeof actorId !== "string" ) continue ;
94+
95+ const follower = await this . parseFollowerData ( actorId , entry . value ) ;
96+ if ( follower ) yield follower ;
97+ }
98+ }
99+
100+ /**
101+ * Gets a specific follower by actor ID.
102+ *
103+ * @param actorId The actor ID (URL) of the follower to retrieve
104+ * @returns The follower entry if found, null otherwise
105+ *
106+ * @example
107+ * ```ts
108+ * import { createRelay } from "@fedify/relay";
109+ * import { MemoryKvStore } from "@fedify/fedify";
110+ *
111+ * const relay = createRelay("mastodon", {
112+ * kv: new MemoryKvStore(),
113+ * domain: "relay.example.com",
114+ * subscriptionHandler: async (ctx, actor) => true,
115+ * });
116+ *
117+ * const follower = await relay.getFollower(
118+ * "https://mastodon.example.com/users/alice"
119+ * );
120+ * if (follower) {
121+ * console.log(`State: ${follower.state}`);
122+ * console.log(`Actor: ${follower.actor.preferredUsername}`);
123+ * }
124+ * ```
125+ *
126+ * @since 2.0.0
127+ */
128+ async getFollower ( actorId : string ) : Promise < RelayFollower | null > {
129+ const followerData = await this . options . kv . get ( [ "follower" , actorId ] ) ;
130+ return await this . parseFollowerData ( actorId , followerData ) ;
131+ }
132+
34133 /**
35134 * Set up inbox listeners for handling ActivityPub activities.
36135 * Each relay type implements this method with protocol-specific logic.
0 commit comments