Skip to content

Commit 0b00165

Browse files
authored
Merge pull request #490 from sij411/feat/relay
refactor(relay): implement factory pattern with Mastodon and LitePub relay support
2 parents b8f2138 + 46b317c commit 0b00165

File tree

18 files changed

+2636
-2019
lines changed

18 files changed

+2636
-2019
lines changed

CHANGES.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ To be released.
138138
### @fedify/relay
139139

140140
- Created ActivityPub relay integration as the *@fedify/relay* package.
141-
[[#359], [#459], [#471] by Jiwon Kwon]
141+
[[#359], [#459], [#471], [#490] by Jiwon Kwon]
142142

143143
- Added `Relay` interface defining the common contract for relay
144144
implementations.
@@ -149,10 +149,13 @@ To be released.
149149
- Added `SubscriptionRequestHandler` type for custom subscription approval
150150
logic.
151151
- Added `RelayOptions` interface for relay configuration.
152+
- Added `RelayType` type alias to document the type-safe parameter
153+
- Added `createRelay()` factory function as a key public API
152154

153155
[#359]: https://github.com/fedify-dev/fedify/issues/359
154156
[#459]: https://github.com/fedify-dev/fedify/pull/459
155157
[#471]: https://github.com/fedify-dev/fedify/pull/471
158+
[#490]: https://github.com/fedify-dev/fedify/pull/490
156159

157160
### @fedify/vocab-tools
158161

deno.lock

Lines changed: 199 additions & 133 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/relay/README.md

Lines changed: 79 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,16 @@ Key features:
4646

4747
### LitePub-style relay
4848

49-
*LitePub relay support is planned for a future release.*
50-
5149
The LitePub-style relay protocol uses bidirectional following relationships
5250
and wraps activities in `Announce` activities for distribution.
5351

52+
Key features:
53+
54+
- Reciprocal following between relay and subscribers
55+
- Activities wrapped in `Announce` for distribution
56+
- Two-phase subscription (pending → accepted)
57+
- Enhanced federation capabilities
58+
5459

5560
Installation
5661
------------
@@ -83,43 +88,56 @@ bun add @fedify/relay
8388
Usage
8489
-----
8590

86-
### Creating a Mastodon-style relay
91+
### Creating a relay
8792

88-
Here's a simple example of creating a Mastodon-compatible relay server:
93+
Here's a simple example of creating a relay server using the factory function:
8994

9095
~~~~ typescript
91-
import { MastodonRelay } from "@fedify/relay";
96+
import { createRelay } from "@fedify/relay";
9297
import { MemoryKvStore } from "@fedify/fedify";
9398

94-
const relay = new MastodonRelay({
99+
// Create a Mastodon-style relay
100+
const relay = createRelay("mastodon", {
95101
kv: new MemoryKvStore(),
96102
domain: "relay.example.com",
97-
});
98-
99-
// Optional: Set a custom subscription handler to approve/reject subscriptions
100-
relay.setSubscriptionHandler(async (ctx, actor) => {
101-
// Implement your approval logic here
102-
// Return true to approve, false to reject
103-
const domain = new URL(actor.id!).hostname;
104-
const blockedDomains = ["spam.example", "blocked.example"];
105-
return !blockedDomains.includes(domain);
103+
// Optional: Set a custom subscription handler to approve/reject subscriptions
104+
subscriptionHandler: async (ctx, actor) => {
105+
// Implement your approval logic here
106+
// Return true to approve, false to reject
107+
const domain = new URL(actor.id!).hostname;
108+
const blockedDomains = ["spam.example", "blocked.example"];
109+
return !blockedDomains.includes(domain);
110+
},
106111
});
107112

108113
// Serve the relay
109114
Deno.serve((request) => relay.fetch(request));
110115
~~~~
111116

117+
You can also create a LitePub-style relay by changing the type:
118+
119+
~~~~ typescript
120+
const relay = createRelay("litepub", {
121+
kv: new MemoryKvStore(),
122+
domain: "relay.example.com",
123+
});
124+
~~~~
125+
112126
### Subscription handling
113127

114128
By default, the relay automatically rejects all subscription requests.
115-
You can customize this behavior by setting a subscription handler:
129+
You can customize this behavior by providing a subscription handler in the options:
116130

117131
~~~~ typescript
118-
relay.setSubscriptionHandler(async (ctx, actor) => {
119-
// Example: Only allow subscriptions from specific domains
120-
const domain = new URL(actor.id!).hostname;
121-
const allowedDomains = ["mastodon.social", "fosstodon.org"];
122-
return allowedDomains.includes(domain);
132+
const relay = createRelay("mastodon", {
133+
kv: new MemoryKvStore(),
134+
domain: "relay.example.com",
135+
subscriptionHandler: async (ctx, actor) => {
136+
// Example: Only allow subscriptions from specific domains
137+
const domain = new URL(actor.id!).hostname;
138+
const allowedDomains = ["mastodon.social", "fosstodon.org"];
139+
return allowedDomains.includes(domain);
140+
},
123141
});
124142
~~~~
125143

@@ -131,11 +149,11 @@ example with Hono:
131149

132150
~~~~ typescript
133151
import { Hono } from "hono";
134-
import { MastodonRelay } from "@fedify/relay";
152+
import { createRelay } from "@fedify/relay";
135153
import { MemoryKvStore } from "@fedify/fedify";
136154

137155
const app = new Hono();
138-
const relay = new MastodonRelay({
156+
const relay = createRelay("mastodon", {
139157
kv: new MemoryKvStore(),
140158
domain: "relay.example.com",
141159
});
@@ -191,38 +209,61 @@ details.
191209
API reference
192210
-------------
193211

194-
### `MastodonRelay`
195-
196-
A Mastodon-compatible ActivityPub relay implementation.
212+
### `createRelay()`
197213

198-
#### Constructor
214+
Factory function to create a relay instance.
199215

200216
~~~~ typescript
201-
new MastodonRelay(options: RelayOptions)
217+
function createRelay(
218+
type: "mastodon" | "litepub",
219+
options: RelayOptions
220+
): BaseRelay
202221
~~~~
203222

204-
#### Properties
223+
**Parameters:**
224+
225+
- `type`: The type of relay to create (`"mastodon"` or `"litepub"`)
226+
- `options`: Configuration options for the relay
205227

206-
- `domain`: The relay's domain name (read-only)
228+
**Returns:** A relay instance (`MastodonRelay` or `LitePubRelay`)
229+
230+
### `BaseRelay`
231+
232+
Abstract base class for relay implementations.
207233

208234
#### Methods
209235

210236
- `fetch(request: Request): Promise<Response>`: Handle incoming HTTP requests
211-
- `setSubscriptionHandler(handler: SubscriptionRequestHandler): this`:
212-
Set a custom handler for subscription approval/rejection
237+
238+
### `MastodonRelay`
239+
240+
A Mastodon-compatible ActivityPub relay implementation that extends `BaseRelay`.
241+
242+
- Uses direct activity forwarding
243+
- Immediate subscription approval
244+
- Compatible with standard ActivityPub implementations
245+
246+
### `LitePubRelay`
247+
248+
A LitePub-compatible ActivityPub relay implementation that extends `BaseRelay`.
249+
250+
- Uses bidirectional following
251+
- Activities wrapped in `Announce`
252+
- Two-phase subscription (pendingaccepted)
213253

214254
### `RelayOptions`
215255

216256
Configuration options for the relay:
217257

218258
- `kv: KvStore` (required): Keyvalue store for persisting relay data
219259
- `domain?: string`: Relay's domain name (defaults to `"localhost"`)
260+
- `name?: string`: Relay's display name (defaults to `"ActivityPub Relay"`)
261+
- `subscriptionHandler?: SubscriptionRequestHandler`: Custom handler for
262+
subscription approval/rejection
220263
- `documentLoaderFactory?: DocumentLoaderFactory`: Custom document loader
221264
factory
222265
- `authenticatedDocumentLoaderFactory?: AuthenticatedDocumentLoaderFactory`:
223266
Custom authenticated document loader factory
224-
- `federation?: Federation<void>`: Custom Federation instance (for advanced
225-
use cases)
226267
- `queue?: MessageQueue`: Message queue for background activity processing
227268

228269
### `SubscriptionRequestHandler`
@@ -231,17 +272,17 @@ A function that determines whether to approve a subscription request:
231272

232273
~~~~ typescript
233274
type SubscriptionRequestHandler = (
234-
ctx: Context<void>,
275+
ctx: Context<RelayOptions>,
235276
clientActor: Actor,
236277
) => Promise<boolean>
237278
~~~~
238279

239-
Parameters:
280+
**Parameters:**
240281

241-
- `ctx`: The Fedify context object
282+
- `ctx`: The Fedify context object with relay options
242283
- `clientActor`: The actor requesting to subscribe
243284

244-
Returns:
285+
**Returns:**
245286

246287
- `true` to approve the subscription
247288
- `false` to reject the subscription

packages/relay/package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
],
1111
"author": {
1212
"name": "Jiwon Kwon",
13-
"email": "jiwonkwon@duck.com"
13+
"email": "work@kwonjiwon.org"
1414
},
1515
"homepage": "https://fedify.dev/",
1616
"repository": {
@@ -47,6 +47,10 @@
4747
"dist/",
4848
"package.json"
4949
],
50+
"dependencies": {
51+
"@js-temporal/polyfill": "catalog:",
52+
"@logtape/logtape": "catalog:"
53+
},
5054
"peerDependencies": {
5155
"@fedify/fedify": "workspace:^"
5256
},
@@ -61,7 +65,6 @@
6165
"prepack": "deno task codegen && tsdown",
6266
"prepublish": "deno task codegen && tsdown",
6367
"test": "deno task codegen && tsdown && node --test",
64-
"test:bun": "deno task codegen && tsdown && bun test --timeout 60000",
65-
"test:cfworkers": "deno task codegen && wrangler deploy --dry-run --outdir src/cfworkers && node --import=tsx src/cfworkers/client.ts"
68+
"test:bun": "deno task codegen && tsdown && bun test --timeout 60000"
6669
}
6770
}

packages/relay/src/base.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { Federation, FederationBuilder } from "@fedify/fedify";
2+
import type { RelayOptions } from "./types.ts";
3+
4+
/**
5+
* Abstract base class for relay implementations.
6+
* Provides common infrastructure for both Mastodon and LitePub relays.
7+
*
8+
* @since 2.0.0
9+
*/
10+
export abstract class BaseRelay {
11+
protected federationBuilder: FederationBuilder<RelayOptions>;
12+
protected options: RelayOptions;
13+
protected federation?: Federation<RelayOptions>;
14+
15+
constructor(
16+
options: RelayOptions,
17+
relayBuilder: FederationBuilder<RelayOptions>,
18+
) {
19+
this.options = options;
20+
this.federationBuilder = relayBuilder;
21+
}
22+
23+
async fetch(request: Request): Promise<Response> {
24+
if (this.federation == null) {
25+
this.federation = await this.federationBuilder.build(this.options);
26+
this.setupInboxListeners();
27+
}
28+
29+
return await this.federation.fetch(request, {
30+
contextData: this.options,
31+
});
32+
}
33+
34+
/**
35+
* Set up inbox listeners for handling ActivityPub activities.
36+
* Each relay type implements this method with protocol-specific logic.
37+
*/
38+
protected abstract setupInboxListeners(): void;
39+
}

0 commit comments

Comments
 (0)