Expand description

Utilities for using this library with axum web framework

§HTTP endpoints

The next step is to allow other servers to fetch our actors and objects. For this we need to create an HTTP route, most commonly at the same path where the actor or object can be viewed in a web browser. On this path there should be another route which responds to requests with header Accept: application/activity+json and serves the JSON data. This needs to be done for all actors and objects. Note that only local items should be served in this way.


#[tokio::main]
async fn main() -> Result<(), Error> {
    let data = FederationConfig::builder()
        .domain("example.com")
        .app_data(DbConnection)
        .build().await?;
        
    let app = axum::Router::new()
        .route("/user/:name", get(http_get_user))
        .layer(FederationMiddleware::new(data));

    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    tracing::debug!("listening on {}", addr);
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await?;
    Ok(())
}

async fn http_get_user(
    header_map: HeaderMap,
    Path(name): Path<String>,
    data: Data<DbConnection>,
) -> impl IntoResponse {
    let accept = header_map.get("accept").map(|v| v.to_str().unwrap());
    if accept == Some(FEDERATION_CONTENT_TYPE) {
        let db_user = data.read_local_user(&name).await.unwrap();
        let json_user = db_user.into_json(&data).await.unwrap();
        FederationJson(WithContext::new_default(json_user)).into_response()
    }
    else {
        generate_user_html(name, data).await
    }
}

There are a couple of things going on here. Like before we are constructing the federation config with our domain and application data. We pass this to a middleware to make it available in request handlers, then listening on a port with the axum webserver.

The http_get_user method allows retrieving a user profile from /user/:name. It checks the accept header, and compares it to the one used by Activitypub (application/activity+json). If it matches, the user is read from database and converted to Activitypub json format. The context field is added (WithContext for json-ld compliance), and it is converted to a JSON response with header content-type: application/activity+json using FederationJson. It can now be retrieved with the command curl -H 'Accept: application/activity+json' ... introduced earlier, or with ObjectId.

If the accept header doesn’t match, it renders the user profile as HTML for viewing in a web browser.

We also need to implement a webfinger endpoint, which can resolve a handle like @nutomic@lemmy.ml into an ID like https://lemmy.ml/u/nutomic that can be used by Activitypub. Webfinger is not part of the ActivityPub standard, but the fact that Mastodon requires it makes it de-facto mandatory. It is defined in RFC 7033. Implementing it basically means handling requests of the formhttps://mastodon.social/.well-known/webfinger?resource=acct:LemmyDev@mastodon.social.

To do this we can implement the following HTTP handler which must be bound to path .well-known/webfinger.


#[derive(Deserialize)]
struct WebfingerQuery {
    resource: String,
}

async fn webfinger(
    Query(query): Query<WebfingerQuery>,
    data: Data<DbConnection>,
) -> Result<Json<Webfinger>, Error> {
    let name = extract_webfinger_name(&query.resource, &data)?;
    let db_user = data.read_local_user(name).await?;
    Ok(Json(build_webfinger_response(query.resource, db_user.federation_id)))
}

Modules§

  • Handles incoming activities, verifying HTTP signatures and other checks
  • Wrapper struct to respond with application/activity+json in axum handlers