Skip to content

Commit

Permalink
NodeInfo 2.1
Browse files Browse the repository at this point in the history
Close #1
  • Loading branch information
dahlia committed Mar 9, 2024
1 parent 1fa631c commit 13e59ce
Show file tree
Hide file tree
Showing 22 changed files with 901 additions and 30 deletions.
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
}
},
"cSpell.words": [
"activitypub",
"bccs",
"btos",
"cfworker",
Expand All @@ -37,10 +38,13 @@
"docloader",
"fedify",
"fediverse",
"halfyear",
"httpsig",
"jsonld",
"langstr",
"Lemmy",
"Misskey",
"nodeinfo",
"phensley",
"Pixelfed",
"rels",
Expand Down
11 changes: 11 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ Version 0.2.0

To be released.

- Implemented [NodeInfo] 2.1 protocol. [[#1]]

- Now `Federation.handle()` accepts requests for */.well-known/nodeinfo*.
- Added `Federation.setNodeInfoDispatcher()` method.
- Added `Context.getNodeInfoUri()` method.
- Added `NodeInfo` interface.
- Added `NodeInfoDispatcher` type.

[NodeInfo]: https://nodeinfo.diaspora.software/
[#1]: https://github.com/dahlia/fedify/issues/1


Version 0.1.0
-------------
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ The rough roadmap is to implement the following features out of the box:
- [HTTP Signatures]
- Middlewares for handling webhooks
- [ActivityPub] client
- [NodeInfo] protocol
- Special touch for interoperability with Mastodon and few other popular
fediverse software

Expand Down Expand Up @@ -53,6 +54,7 @@ resources:
[Activity Vocabulary]: https://www.w3.org/TR/activitystreams-vocabulary/
[WebFinger]: https://datatracker.ietf.org/doc/html/rfc7033
[HTTP Signatures]: https://tools.ietf.org/html/draft-cavage-http-signatures-12
[NodeInfo]: https://nodeinfo.diaspora.software/


Installation
Expand Down
1 change: 1 addition & 0 deletions docs/_includes/head.njk
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ a:hover code { text-decoration: underline; }
a:active code { background-color: transparent; }
pre, pre > code { font-size: var(--font-size); }
dl { margin-top: var(--typography-spacing-vertical); }
dd { margin-bottom: var(--typography-spacing-vertical); }
.callout {
border-left: 0.5rem solid var(--primary);
padding-left: 1.5rem;
Expand Down
2 changes: 2 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ The rough roadmap is to implement the following features out of the box:
- [HTTP Signatures]
- Middlewares for handling webhooks
- [ActivityPub] client
- [NodeInfo] protocol
- Special touch for interoperability with Mastodon and few other popular
fediverse software

Expand All @@ -46,3 +47,4 @@ resources:
[Activity Vocabulary]: https://www.w3.org/TR/activitystreams-vocabulary/
[WebFinger]: https://datatracker.ietf.org/doc/html/rfc7033
[HTTP Signatures]: https://tools.ietf.org/html/draft-cavage-http-signatures-12
[NodeInfo]: https://nodeinfo.diaspora.software/
1 change: 1 addition & 0 deletions docs/manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ framework.
- [Vocabulary](./manual/vocab.md)
- [Actor dispatcher](./manual/actor.md)
- [Inbox listeners](./manual/inbox.md)
- [NodeInfo](./manual/nodeinfo.md)

However, this manual is not a complete guide to the Fedify framework.
In particular, you probably want to look up the [API reference] times to times,
Expand Down
139 changes: 139 additions & 0 deletions docs/manual/nodeinfo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
---
parent: Manual
nav_order: 6
---

NodeInfo
========

According to the official [NodeInfo] website:

> NodeInfo is an effort to create a standardized way of exposing metadata
> about a server running one of the distributed social networks.
> The two key goals are being able to get better insights into the user base of
> distributed social networking and the ability to build tools that allow
> users to choose the best-fitting software and server for their needs.
In fact, many ActivityPub servers, including Mastodon and Misskey, provide
NodeInfo endpoints. Fedify provides a way to expose NodeInfo endpoints for
your server.

> [!NOTE]
> The version of NodeInfo that Fedify supports is 2.1.
[NodeInfo]: https://nodeinfo.diaspora.software/


Exposing NodeInfo endpoint
--------------------------

To expose a NodeInfo endpoint, you need to register a NodeInfo dispatcher with
`Federation.setNodeInfoDispatcher()` method. The following shows how to expose
a NodeInfo endpoint:

~~~~ typescript
import { Federation } from "jsr:@fedify/fedify";

const federation = new Federation({
// Omitted for brevity; see the related section for details.
});

federation.setNodeInfoDispatcher("/nodeinfo/2.1", async (ctx) => {
return {
software: {
name: "your-software-name", // Lowercase, digits, and hyphens only.
version: { major: 1, minor: 0, patch: 0 },
homepage: new URL("https://your-software.com/"),
}
protocols: ["activitypub"],
usage: {
// Usage statistics is hard-coded here for demonstration purposes.
// You should replace these with real statistics:
users: { total: 100, activeHalfyear: 50, activeMonth: 20 },
localPosts: 1000,
localComments: 2000,
}
}
});
~~~~

For details about the `NodeInfo` interface,
see the [next section](#nodeinfo-schema).

> [!TIP]
> You don't have to use */nodeinfo/2.1* as the path, but it is quite common
> among ActivityPub servers.
> [!NOTE]
> Whether or not you expose a NodeInfo endpoint, */.well-known/nodeinfo* is
> automatically handled by `Federation.handle()` method. In case you don't
> register a NodeInfo dispatcher, Fedify will respond with an empty `links`
> array, e.g.:
>
> ~~~~ json
> {
> "links": []
> }
> ~~~~
NodeInfo schema
---------------
The `NodeInfo` interface is defined as follows:
`software.name`
: *Required.* The canonical name of the server software. This must comply
with pattern `/^[a-z0-9-]+$/`.
`software.version`
: *Required.* The version of the server software. This must be a valid
[`SemVer`] object. For your information, a Semantic Versioning string
can be parsed into a [`SemVer`] object using [`parse()`] function.
`software.repository`
: The [`URL`] of the source code repository of the server software.
`software.homepage`
: The [`URL`] of the homepage of the server software.
`protocols`
: *Required and non-empty.* The protocols supported on the server. At least
one protocol must be supported. You usually put `["activitypub"]` here.
`services.inbound`
: The third party sites the server can retrieve messages from for combined
display with regular traffic.
`services.outbound`
: The third party sites the server can publish messages to on the behalf of
a user.
`openRegistrations`
: Whether the server allows open self-registration. Defaults to `false`.
`usage.users.total`
: The total amount of on the server registered users. This `number` has to
be an integer greater than or equal to zero.
`usage.users.activeHalfyear`
: The amount of users that signed in at least once in the last 180 days.
This `number` has to be an integer greater than or equal to zero.
`usage.users.activeMonth`
: The amount of users that signed in at least once in the last 30 days.
This `number` has to be an integer greater than or equal to zero.
`usage.localPosts`
: The amount of posts that were made by users that are registered on
the server. This `number` has to be an integer greater than or equal to
zero.
`usage.localComments`
: The amount of comments that were made by users that are registered on
the server. This `number` has to be an integer greater than or equal to
zero.
[`SemVer`]: https://jsr.io/@std/semver/doc/~/SemVer
[`parse()`]: https://jsr.io/@std/semver/doc/~/parse
[`URL`]: https://developer.mozilla.org/en-US/docs/Web/API/URL
6 changes: 6 additions & 0 deletions examples/blog/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"deno.enable": true,
"deno.lint": true,
"files.eol": "\n",
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true,
"git.openRepositoryInParentFolders": "never",
"[javascript]": {
"editor.defaultFormatter": "denoland.vscode-deno",
Expand All @@ -27,12 +30,15 @@
"editor.formatOnSave": true
},
"cSpell.words": [
"activitypub",
"codegen",
"deno",
"docloader",
"fedify",
"fediverse",
"halfyear",
"httpsig",
"nodeinfo",
"preact",
"unfollow",
"unfollowing",
Expand Down
40 changes: 40 additions & 0 deletions examples/blog/federation/mod.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Temporal } from "npm:@js-temporal/polyfill@^0.4.4";
import { parse } from "jsr:@std/semver@^0.218.2";
import { dirname, join } from "jsr:@std/path@^0.218.2";
import { Federation } from "fedify/federation";
import {
Accept,
Expand Down Expand Up @@ -270,6 +272,44 @@ federation
return "";
});

// Registers the NodeInfo dispatcher, which is responsible for providing
// the server information:
federation.setNodeInfoDispatcher("/nodeinfo/2.1", async (_ctx) => {
const rootDenoFile = join(
dirname(dirname(dirname(import.meta.dirname!))),
"deno.json",
);
const denoJson = JSON.parse(await Deno.readTextFile(rootDenoFile));
const { posts } = await getPosts(1);
const recentPost = posts.length > 0 ? posts[0] : null;
const now = Temporal.Now.instant();
return {
software: {
name: "fedify-example-blog",
version: parse(denoJson.version),
repository: new URL(
"https://github.com/dahlia/fedify/tree/main/examples/blog",
),
},
protocols: ["activitypub"],
usage: {
users: {
total: 1,
activeMonth: recentPost == null ||
recentPost.published < now.subtract({ hours: 24 * 30 })
? 0
: 1,
activeHalfyear: recentPost == null ||
recentPost.published < now.subtract({ hours: 24 * 30 * 6 })
? 0
: 1,
},
localComments: 0,
localPosts: Number(await countPosts()),
},
};
});

function getHref(link: Link | URL | string | null): string | null {
if (link == null) return null;
if (link instanceof Link) return link.href?.href ?? null;
Expand Down
13 changes: 11 additions & 2 deletions federation/callback.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import { NodeInfo } from "../nodeinfo/types.ts";
import { Actor } from "../vocab/actor.ts";
import { CryptographicKey } from "../vocab/mod.ts";
import { Activity } from "../vocab/mod.ts";
import { Activity, CryptographicKey } from "../vocab/mod.ts";
import { PageItems } from "./collection.ts";
import { RequestContext } from "./context.ts";

/**
* A callback that dispatches a {@link NodeInfo} object.
*
* @typeParam TContextData The context data to pass to the {@link Context}.
*/
export type NodeInfoDispatcher<TContextData> = (
context: RequestContext<TContextData>,
) => NodeInfo | Promise<NodeInfo>;

/**
* A callback that dispatches an {@link Actor} object.
*
Expand Down
13 changes: 13 additions & 0 deletions federation/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,44 +16,57 @@ export interface Context<TContextData> {
*/
readonly documentLoader: DocumentLoader;

/**
* Builds the URI of the NodeInfo document.
* @returns The NodeInfo URI.
* @throws {RouterError} If no NodeInfo dispatcher is available.
*/
getNodeInfoUri(): URL;

/**
* Builds the URI of an actor with the given handle.
* @param handle The actor's handle.
* @returns The actor's URI.
* @throws {RouterError} If no actor dispatcher is available.
*/
getActorUri(handle: string): URL;

/**
* Builds the URI of an actor's outbox with the given handle.
* @param handle The actor's handle.
* @returns The actor's outbox URI.
* @throws {RouterError} If no outbox dispatcher is available.
*/
getOutboxUri(handle: string): URL;

/**
* Builds the URI of the shared inbox.
* @returns The shared inbox URI.
* @throws {RouterError} If no inbox listener is available.
*/
getInboxUri(): URL;

/**
* Builds the URI of an actor's inbox with the given handle.
* @param handle The actor's handle.
* @returns The actor's inbox URI.
* @throws {RouterError} If no inbox listener is available.
*/
getInboxUri(handle: string): URL;

/**
* Builds the URI of an actor's following collection with the given handle.
* @param handle The actor's handle.
* @returns The actor's following collection URI.
* @throws {RouterError} If no following collection is available.
*/
getFollowingUri(handle: string): URL;

/**
* Builds the URI of an actor's followers collection with the given handle.
* @param handle The actor's handle.
* @returns The actor's followers collection URI.
* @throws {RouterError} If no followers collection is available.
*/
getFollowersUri(handle: string): URL;

Expand Down
Loading

0 comments on commit 13e59ce

Please sign in to comment.