Dont use apub in type names

This commit is contained in:
Felix Ableitner 2023-03-16 21:41:29 +01:00
parent bd3f17a4df
commit 072353fc41
30 changed files with 258 additions and 276 deletions

2
Cargo.lock generated
View file

@ -4,7 +4,7 @@ version = 3
[[package]] [[package]]
name = "activitypub_federation" name = "activitypub_federation"
version = "0.4.0-rc2" version = "0.4.0-rc3"
dependencies = [ dependencies = [
"activitystreams-kinds", "activitystreams-kinds",
"actix-rt", "actix-rt",

View file

@ -73,7 +73,7 @@ pub struct DbUser {
pub display_name: String, pub display_name: String,
pub password_hash: Option<String>, pub password_hash: Option<String>,
pub email: Option<String>, pub email: Option<String>,
pub apub_id: Url, pub federation_id: Url,
pub inbox: Url, pub inbox: Url,
pub outbox: Url, pub outbox: Url,
pub local: bool, pub local: bool,
@ -83,8 +83,8 @@ pub struct DbUser {
} }
``` ```
Field names and other details of this type can be chosen freely according to your requirements. It only matters that the required data is being stored. Its important that this struct doesn't represent only local users who registered directly on our website, but also remote users that are registered on other instances and federated to us. The `local` column helps to easily distinguish both. It can also be distinguished from the domain of the `apub_id` URL, but that would be a much more expensive operation. All users have a `public_key`, but only local users have a `private_key`. On the other hand, `password_hash` and `email` are only present for local users. inbox` and `outbox` URLs need to be stored because each implementation is free to choose its own format for them, so they can't be regenerated on the fly. Field names and other details of this type can be chosen freely according to your requirements. It only matters that the required data is being stored. Its important that this struct doesn't represent only local users who registered directly on our website, but also remote users that are registered on other instances and federated to us. The `local` column helps to easily distinguish both. It can also be distinguished from the domain of the `federation_id` URL, but that would be a much more expensive operation. All users have a `public_key`, but only local users have a `private_key`. On the other hand, `password_hash` and `email` are only present for local users. inbox` and `outbox` URLs need to be stored because each implementation is free to choose its own format for them, so they can't be regenerated on the fly.
In larger projects it makes sense to split this data in two. One for data relevant to local users (`password_hash`, `email` etc.) and one for data that is shared by both local and federated users (`apub_id`, `public_key` etc). In larger projects it makes sense to split this data in two. One for data relevant to local users (`password_hash`, `email` etc.) and one for data that is shared by both local and federated users (`federation_id`, `public_key` etc).
Finally we need to implement the traits [ApubObject](crate::traits::ApubObject) and [Actor](crate::traits::Actor) for `DbUser`. These traits are used to convert between `Person` and `DbUser` types. [ApubObject::from_apub](crate::traits::ApubObject::from_apub) must store the received object in database, so that it can later be retrieved without network calls using [ApubObject::read_from_apub_id](crate::traits::ApubObject::read_from_apub_id). Refer to the documentation for more details. Finally we need to implement the traits [Object](crate::traits::Object) and [Actor](crate::traits::Actor) for `DbUser`. These traits are used to convert between `Person` and `DbUser` types. [Object::from_json](crate::traits::Object::from_json) must store the received object in database, so that it can later be retrieved without network calls using [Object::read_from_id](crate::traits::Object::read_from_id). Refer to the documentation for more details.

View file

@ -25,4 +25,4 @@ The most important fields are:
- `attributedTo`: ID of the user who created this post - `attributedTo`: ID of the user who created this post
- `to`, `cc`: Who the object is for. The special "public" URL indicates that everyone can view it. It also gets delivered to followers of the LemmyDev account. - `to`, `cc`: Who the object is for. The special "public" URL indicates that everyone can view it. It also gets delivered to followers of the LemmyDev account.
Just like for `Person` before, we need to implement a protocol type and a database type, then implement trait `ApubObject`. See the example for details. Just like for `Person` before, we need to implement a protocol type and a database type, then implement trait `Object`. See the example for details.

View file

@ -6,17 +6,17 @@ The next step is to allow other servers to fetch our actors and objects. For thi
# use std::net::SocketAddr; # use std::net::SocketAddr;
# use activitypub_federation::config::FederationConfig; # use activitypub_federation::config::FederationConfig;
# use activitypub_federation::protocol::context::WithContext; # use activitypub_federation::protocol::context::WithContext;
# use activitypub_federation::axum::json::ApubJson; # use activitypub_federation::axum::json::FederationJson;
# use anyhow::Error; # use anyhow::Error;
# use activitypub_federation::traits::tests::Person; # use activitypub_federation::traits::tests::Person;
# use activitypub_federation::config::Data; # use activitypub_federation::config::Data;
# use activitypub_federation::traits::tests::DbConnection; # use activitypub_federation::traits::tests::DbConnection;
# use axum::extract::Path; # use axum::extract::Path;
# use activitypub_federation::config::ApubMiddleware; # use activitypub_federation::config::FederationMiddleware;
# use axum::routing::get; # use axum::routing::get;
# use crate::activitypub_federation::traits::ApubObject; # use crate::activitypub_federation::traits::Object;
# use axum::headers::ContentType; # use axum::headers::ContentType;
# use activitypub_federation::APUB_JSON_CONTENT_TYPE; # use activitypub_federation::FEDERATION_CONTENT_TYPE;
# use axum::TypedHeader; # use axum::TypedHeader;
# use axum::response::IntoResponse; # use axum::response::IntoResponse;
# use http::HeaderMap; # use http::HeaderMap;
@ -31,7 +31,7 @@ async fn main() -> Result<(), Error> {
let app = axum::Router::new() let app = axum::Router::new()
.route("/user/:name", get(http_get_user)) .route("/user/:name", get(http_get_user))
.layer(ApubMiddleware::new(data)); .layer(FederationMiddleware::new(data));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
tracing::debug!("listening on {}", addr); tracing::debug!("listening on {}", addr);
@ -47,10 +47,10 @@ async fn http_get_user(
data: Data<DbConnection>, data: Data<DbConnection>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let accept = header_map.get("accept").map(|v| v.to_str().unwrap()); let accept = header_map.get("accept").map(|v| v.to_str().unwrap());
if accept == Some(APUB_JSON_CONTENT_TYPE) { if accept == Some(FEDERATION_CONTENT_TYPE) {
let db_user = data.read_local_user(name).await.unwrap(); let db_user = data.read_local_user(name).await.unwrap();
let apub_user = db_user.into_apub(&data).await.unwrap(); let json_user = db_user.into_json(&data).await.unwrap();
ApubJson(WithContext::new_default(apub_user)).into_response() FederationJson(WithContext::new_default(json_user)).into_response()
} }
else { else {
generate_user_html(name, data).await generate_user_html(name, data).await
@ -60,7 +60,7 @@ async fn http_get_user(
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. 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 `ApubJson`. It can now be retrieved with the command `curl -H 'Accept: application/activity+json' ...` introduced earlier, or with `ObjectId`. 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. If the `accept` header doesn't match, it renders the user profile as HTML for viewing in a web browser.
@ -89,6 +89,6 @@ async fn webfinger(
) -> Result<Json<Webfinger>, Error> { ) -> Result<Json<Webfinger>, Error> {
let name = extract_webfinger_name(&query.resource, &data)?; let name = extract_webfinger_name(&query.resource, &data)?;
let db_user = data.read_local_user(name).await?; let db_user = data.read_local_user(name).await?;
Ok(Json(build_webfinger_response(query.resource, db_user.apub_id))) Ok(Json(build_webfinger_response(query.resource, db_user.federation_id)))
} }
``` ```

View file

@ -21,7 +21,7 @@ assert!(user.is_ok());
}).unwrap() }).unwrap()
``` ```
`dereference` retrieves the object JSON at the given URL, and uses serde to convert it to `Person`. It then calls your method `ApubObject::from_apub` which inserts it in the database and returns a `DbUser` struct. `request_data` contains the federation config as well as a counter of outgoing HTTP requests. If this counter exceeds the configured maximum, further requests are aborted in order to avoid recursive fetching which could allow for a denial of service attack. `dereference` retrieves the object JSON at the given URL, and uses serde to convert it to `Person`. It then calls your method `Object::from_json` which inserts it in the database and returns a `DbUser` struct. `request_data` contains the federation config as well as a counter of outgoing HTTP requests. If this counter exceeds the configured maximum, further requests are aborted in order to avoid recursive fetching which could allow for a denial of service attack.
After dereferencing a remote object, it is stored in the local database and can be retrieved using [ObjectId::dereference_local](crate::fetch::object_id::ObjectId::dereference_local) without any network requests. This is important for performance reasons and for searching. After dereferencing a remote object, it is stored in the local database and can be retrieved using [ObjectId::dereference_local](crate::fetch::object_id::ObjectId::dereference_local) without any network requests. This is important for performance reasons and for searching.

View file

@ -21,7 +21,7 @@ To send an activity we need to initialize our previously defined struct, and pic
# let recipient = DB_USER.clone(); # let recipient = DB_USER.clone();
let activity = Follow { let activity = Follow {
actor: ObjectId::parse("https://lemmy.ml/u/nutomic")?, actor: ObjectId::parse("https://lemmy.ml/u/nutomic")?,
object: recipient.apub_id.clone().into(), object: recipient.federation_id.clone().into(),
kind: Default::default(), kind: Default::default(),
id: "https://lemmy.ml/activities/321".try_into()? id: "https://lemmy.ml/activities/321".try_into()?
}; };

View file

@ -5,7 +5,7 @@ It is sometimes necessary to fetch from a URL, but we don't know the exact type
```no_run ```no_run
# use activitypub_federation::traits::tests::{DbUser, DbPost}; # use activitypub_federation::traits::tests::{DbUser, DbPost};
# use activitypub_federation::fetch::object_id::ObjectId; # use activitypub_federation::fetch::object_id::ObjectId;
# use activitypub_federation::traits::ApubObject; # use activitypub_federation::traits::Object;
# use activitypub_federation::config::FederationConfig; # use activitypub_federation::config::FederationConfig;
# use serde::{Deserialize, Serialize}; # use serde::{Deserialize, Serialize};
# use activitypub_federation::traits::tests::DbConnection; # use activitypub_federation::traits::tests::DbConnection;
@ -20,43 +20,43 @@ pub enum SearchableDbObjects {
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
#[serde(untagged)] #[serde(untagged)]
pub enum SearchableApubObjects { pub enum SearchableObjects {
Person(Person), Person(Person),
Note(Note) Note(Note)
} }
#[async_trait::async_trait] #[async_trait::async_trait]
impl ApubObject for SearchableDbObjects { impl Object for SearchableDbObjects {
type DataType = DbConnection; type DataType = DbConnection;
type ApubType = SearchableApubObjects; type Kind = SearchableObjects;
type Error = anyhow::Error; type Error = anyhow::Error;
async fn read_from_apub_id( async fn read_from_id(
object_id: Url, object_id: Url,
data: &Data<Self::DataType>, data: &Data<Self::DataType>,
) -> Result<Option<Self>, Self::Error> { ) -> Result<Option<Self>, Self::Error> {
Ok(None) Ok(None)
} }
async fn into_apub( async fn into_json(
self, self,
data: &Data<Self::DataType>, data: &Data<Self::DataType>,
) -> Result<Self::ApubType, Self::Error> { ) -> Result<Self::Kind, Self::Error> {
unimplemented!(); unimplemented!();
} }
async fn verify(apub: &Self::ApubType, expected_domain: &Url, _data: &Data<Self::DataType>) -> Result<(), Self::Error> { async fn verify(json: &Self::Kind, expected_domain: &Url, _data: &Data<Self::DataType>) -> Result<(), Self::Error> {
Ok(()) Ok(())
} }
async fn from_apub( async fn from_json(
apub: Self::ApubType, json: Self::Kind,
data: &Data<Self::DataType>, data: &Data<Self::DataType>,
) -> Result<Self, Self::Error> { ) -> Result<Self, Self::Error> {
use SearchableDbObjects::*; use SearchableDbObjects::*;
match apub { match json {
SearchableApubObjects::Person(p) => Ok(User(DbUser::from_apub(p, data).await?)), SearchableObjects::Person(p) => Ok(User(DbUser::from_json(p, data).await?)),
SearchableApubObjects::Note(n) => Ok(Post(DbPost::from_apub(n, data).await?)), SearchableObjects::Note(n) => Ok(Post(DbPost::from_json(n, data).await?)),
} }
} }
} }

View file

@ -11,7 +11,7 @@ use activitypub_federation::{
fetch::object_id::ObjectId, fetch::object_id::ObjectId,
kinds::activity::CreateType, kinds::activity::CreateType,
protocol::{context::WithContext, helpers::deserialize_one_or_many}, protocol::{context::WithContext, helpers::deserialize_one_or_many},
traits::{ActivityHandler, ApubObject}, traits::{ActivityHandler, Object},
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url; use url::Url;
@ -63,7 +63,7 @@ impl ActivityHandler for CreatePost {
} }
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> { async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
DbPost::from_apub(self.object, data).await?; DbPost::from_json(self.object, data).await?;
Ok(()) Ok(())
} }
} }

View file

@ -6,12 +6,12 @@ use crate::{
use activitypub_federation::{ use activitypub_federation::{
axum::{ axum::{
inbox::{receive_activity, ActivityData}, inbox::{receive_activity, ActivityData},
json::ApubJson, json::FederationJson,
}, },
config::Data, config::Data,
fetch::webfinger::{build_webfinger_response, extract_webfinger_name, Webfinger}, fetch::webfinger::{build_webfinger_response, extract_webfinger_name, Webfinger},
protocol::context::WithContext, protocol::context::WithContext,
traits::ApubObject, traits::Object,
}; };
use axum::{ use axum::{
extract::{Path, Query}, extract::{Path, Query},
@ -32,10 +32,10 @@ impl IntoResponse for Error {
pub async fn http_get_user( pub async fn http_get_user(
Path(name): Path<String>, Path(name): Path<String>,
data: Data<DatabaseHandle>, data: Data<DatabaseHandle>,
) -> Result<ApubJson<WithContext<Person>>, Error> { ) -> Result<FederationJson<WithContext<Person>>, Error> {
let db_user = data.read_user(&name)?; let db_user = data.read_user(&name)?;
let apub_user = db_user.into_apub(&data).await?; let json_user = db_user.into_json(&data).await?;
Ok(ApubJson(WithContext::new_default(apub_user))) Ok(FederationJson(WithContext::new_default(json_user)))
} }
#[debug_handler] #[debug_handler]

View file

@ -4,7 +4,7 @@ use crate::{
objects::{person::DbUser, post::DbPost}, objects::{person::DbUser, post::DbPost},
utils::generate_object_id, utils::generate_object_id,
}; };
use activitypub_federation::config::{ApubMiddleware, FederationConfig}; use activitypub_federation::config::{FederationConfig, FederationMiddleware};
use axum::{ use axum::{
routing::{get, post}, routing::{get, post},
Router, Router,
@ -55,7 +55,7 @@ async fn main() -> Result<(), Error> {
.route("/:user", get(http_get_user)) .route("/:user", get(http_get_user))
.route("/:user/inbox", post(http_post_user_inbox)) .route("/:user/inbox", post(http_post_user_inbox))
.route("/.well-known/webfinger", get(webfinger)) .route("/.well-known/webfinger", get(webfinger))
.layer(ApubMiddleware::new(config)); .layer(FederationMiddleware::new(config));
let addr = BIND_ADDRESS let addr = BIND_ADDRESS
.to_socket_addrs()? .to_socket_addrs()?

View file

@ -5,7 +5,7 @@ use activitypub_federation::{
http_signatures::generate_actor_keypair, http_signatures::generate_actor_keypair,
kinds::actor::PersonType, kinds::actor::PersonType,
protocol::{public_key::PublicKey, verification::verify_domains_match}, protocol::{public_key::PublicKey, verification::verify_domains_match},
traits::{ActivityHandler, Actor, ApubObject}, traits::{ActivityHandler, Actor, Object},
}; };
use chrono::{Local, NaiveDateTime}; use chrono::{Local, NaiveDateTime};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -64,16 +64,16 @@ pub struct Person {
} }
#[async_trait::async_trait] #[async_trait::async_trait]
impl ApubObject for DbUser { impl Object for DbUser {
type DataType = DatabaseHandle; type DataType = DatabaseHandle;
type ApubType = Person; type Kind = Person;
type Error = Error; type Error = Error;
fn last_refreshed_at(&self) -> Option<NaiveDateTime> { fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
Some(self.last_refreshed_at) Some(self.last_refreshed_at)
} }
async fn read_from_apub_id( async fn read_from_id(
object_id: Url, object_id: Url,
data: &Data<Self::DataType>, data: &Data<Self::DataType>,
) -> Result<Option<Self>, Self::Error> { ) -> Result<Option<Self>, Self::Error> {
@ -85,7 +85,7 @@ impl ApubObject for DbUser {
Ok(res) Ok(res)
} }
async fn into_apub(self, _data: &Data<Self::DataType>) -> Result<Self::ApubType, Self::Error> { async fn into_json(self, _data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
Ok(Person { Ok(Person {
preferred_username: self.name.clone(), preferred_username: self.name.clone(),
kind: Default::default(), kind: Default::default(),
@ -96,23 +96,23 @@ impl ApubObject for DbUser {
} }
async fn verify( async fn verify(
apub: &Self::ApubType, json: &Self::Kind,
expected_domain: &Url, expected_domain: &Url,
_data: &Data<Self::DataType>, _data: &Data<Self::DataType>,
) -> Result<(), Self::Error> { ) -> Result<(), Self::Error> {
verify_domains_match(apub.id.inner(), expected_domain)?; verify_domains_match(json.id.inner(), expected_domain)?;
Ok(()) Ok(())
} }
async fn from_apub( async fn from_json(
apub: Self::ApubType, json: Self::Kind,
_data: &Data<Self::DataType>, _data: &Data<Self::DataType>,
) -> Result<Self, Self::Error> { ) -> Result<Self, Self::Error> {
Ok(DbUser { Ok(DbUser {
name: apub.preferred_username, name: json.preferred_username,
ap_id: apub.id, ap_id: json.id,
inbox: apub.inbox, inbox: json.inbox,
public_key: apub.public_key.public_key_pem, public_key: json.public_key.public_key_pem,
private_key: None, private_key: None,
last_refreshed_at: Local::now().naive_local(), last_refreshed_at: Local::now().naive_local(),
followers: vec![], followers: vec![],

View file

@ -10,7 +10,7 @@ use activitypub_federation::{
fetch::object_id::ObjectId, fetch::object_id::ObjectId,
kinds::{object::NoteType, public}, kinds::{object::NoteType, public},
protocol::{helpers::deserialize_one_or_many, verification::verify_domains_match}, protocol::{helpers::deserialize_one_or_many, verification::verify_domains_match},
traits::{Actor, ApubObject}, traits::{Actor, Object},
}; };
use activitystreams_kinds::link::MentionType; use activitystreams_kinds::link::MentionType;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -46,44 +46,41 @@ pub struct Mention {
} }
#[async_trait::async_trait] #[async_trait::async_trait]
impl ApubObject for DbPost { impl Object for DbPost {
type DataType = DatabaseHandle; type DataType = DatabaseHandle;
type ApubType = Note; type Kind = Note;
type Error = Error; type Error = Error;
async fn read_from_apub_id( async fn read_from_id(
_object_id: Url, _object_id: Url,
_data: &Data<Self::DataType>, _data: &Data<Self::DataType>,
) -> Result<Option<Self>, Self::Error> { ) -> Result<Option<Self>, Self::Error> {
Ok(None) Ok(None)
} }
async fn into_apub(self, _data: &Data<Self::DataType>) -> Result<Self::ApubType, Self::Error> { async fn into_json(self, _data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
unimplemented!() unimplemented!()
} }
async fn verify( async fn verify(
apub: &Self::ApubType, json: &Self::Kind,
expected_domain: &Url, expected_domain: &Url,
_data: &Data<Self::DataType>, _data: &Data<Self::DataType>,
) -> Result<(), Self::Error> { ) -> Result<(), Self::Error> {
verify_domains_match(apub.id.inner(), expected_domain)?; verify_domains_match(json.id.inner(), expected_domain)?;
Ok(()) Ok(())
} }
async fn from_apub( async fn from_json(json: Self::Kind, data: &Data<Self::DataType>) -> Result<Self, Self::Error> {
apub: Self::ApubType,
data: &Data<Self::DataType>,
) -> Result<Self, Self::Error> {
println!( println!(
"Received post with content {} and id {}", "Received post with content {} and id {}",
&apub.content, &apub.id &json.content, &json.id
); );
let creator = apub.attributed_to.dereference(data).await?; let creator = json.attributed_to.dereference(data).await?;
let post = DbPost { let post = DbPost {
text: apub.content, text: json.content,
ap_id: apub.id.clone(), ap_id: json.id.clone(),
creator: apub.attributed_to.clone(), creator: json.attributed_to.clone(),
local: false, local: false,
}; };
@ -97,7 +94,7 @@ impl ApubObject for DbPost {
attributed_to: data.local_user().ap_id, attributed_to: data.local_user().ap_id,
to: vec![public()], to: vec![public()],
content: format!("Hello {}", creator.name), content: format!("Hello {}", creator.name),
in_reply_to: Some(apub.id.clone()), in_reply_to: Some(json.id.clone()),
tag: vec![mention], tag: vec![mention],
}; };
CreatePost::send(note, creator.shared_inbox_or_inbox(), data).await?; CreatePost::send(note, creator.shared_inbox_or_inbox(), data).await?;

View file

@ -8,7 +8,7 @@ use activitypub_federation::{
fetch::object_id::ObjectId, fetch::object_id::ObjectId,
kinds::activity::CreateType, kinds::activity::CreateType,
protocol::helpers::deserialize_one_or_many, protocol::helpers::deserialize_one_or_many,
traits::{ActivityHandler, ApubObject}, traits::{ActivityHandler, Object},
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url; use url::Url;
@ -56,7 +56,7 @@ impl ActivityHandler for CreatePost {
} }
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> { async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
DbPost::from_apub(self.object, data).await?; DbPost::from_json(self.object, data).await?;
Ok(()) Ok(())
} }
} }

View file

@ -5,11 +5,11 @@ use crate::{
}; };
use activitypub_federation::{ use activitypub_federation::{
actix_web::inbox::receive_activity, actix_web::inbox::receive_activity,
config::{ApubMiddleware, Data, FederationConfig}, config::{Data, FederationConfig, FederationMiddleware},
fetch::webfinger::{build_webfinger_response, extract_webfinger_name}, fetch::webfinger::{build_webfinger_response, extract_webfinger_name},
protocol::context::WithContext, protocol::context::WithContext,
traits::ApubObject, traits::Object,
APUB_JSON_CONTENT_TYPE, FEDERATION_CONTENT_TYPE,
}; };
use actix_web::{web, web::Bytes, App, HttpRequest, HttpResponse, HttpServer}; use actix_web::{web, web::Bytes, App, HttpRequest, HttpResponse, HttpServer};
use anyhow::anyhow; use anyhow::anyhow;
@ -22,7 +22,7 @@ pub fn listen(config: &FederationConfig<DatabaseHandle>) -> Result<(), Error> {
let config = config.clone(); let config = config.clone();
let server = HttpServer::new(move || { let server = HttpServer::new(move || {
App::new() App::new()
.wrap(ApubMiddleware::new(config.clone())) .wrap(FederationMiddleware::new(config.clone()))
.route("/{user}", web::get().to(http_get_user)) .route("/{user}", web::get().to(http_get_user))
.route("/{user}/inbox", web::post().to(http_post_user_inbox)) .route("/{user}/inbox", web::post().to(http_post_user_inbox))
.route("/.well-known/webfinger", web::get().to(webfinger)) .route("/.well-known/webfinger", web::get().to(webfinger))
@ -40,10 +40,10 @@ pub async fn http_get_user(
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let db_user = data.local_user(); let db_user = data.local_user();
if user_name.into_inner() == db_user.name { if user_name.into_inner() == db_user.name {
let apub_user = db_user.into_apub(&data).await?; let json_user = db_user.into_json(&data).await?;
Ok(HttpResponse::Ok() Ok(HttpResponse::Ok()
.content_type(APUB_JSON_CONTENT_TYPE) .content_type(FEDERATION_CONTENT_TYPE)
.json(WithContext::new_default(apub_user))) .json(WithContext::new_default(json_user)))
} else { } else {
Err(anyhow!("Invalid user").into()) Err(anyhow!("Invalid user").into())
} }

View file

@ -6,12 +6,12 @@ use crate::{
use activitypub_federation::{ use activitypub_federation::{
axum::{ axum::{
inbox::{receive_activity, ActivityData}, inbox::{receive_activity, ActivityData},
json::ApubJson, json::FederationJson,
}, },
config::{ApubMiddleware, Data, FederationConfig}, config::{Data, FederationConfig, FederationMiddleware},
fetch::webfinger::{build_webfinger_response, extract_webfinger_name, Webfinger}, fetch::webfinger::{build_webfinger_response, extract_webfinger_name, Webfinger},
protocol::context::WithContext, protocol::context::WithContext,
traits::ApubObject, traits::Object,
}; };
use axum::{ use axum::{
extract::{Path, Query}, extract::{Path, Query},
@ -33,7 +33,7 @@ pub fn listen(config: &FederationConfig<DatabaseHandle>) -> Result<(), Error> {
.route("/:user/inbox", post(http_post_user_inbox)) .route("/:user/inbox", post(http_post_user_inbox))
.route("/:user", get(http_get_user)) .route("/:user", get(http_get_user))
.route("/.well-known/webfinger", get(webfinger)) .route("/.well-known/webfinger", get(webfinger))
.layer(ApubMiddleware::new(config)); .layer(FederationMiddleware::new(config));
let addr = hostname let addr = hostname
.to_socket_addrs()? .to_socket_addrs()?
@ -49,10 +49,10 @@ pub fn listen(config: &FederationConfig<DatabaseHandle>) -> Result<(), Error> {
async fn http_get_user( async fn http_get_user(
Path(name): Path<String>, Path(name): Path<String>,
data: Data<DatabaseHandle>, data: Data<DatabaseHandle>,
) -> Result<ApubJson<WithContext<Person>>, Error> { ) -> Result<FederationJson<WithContext<Person>>, Error> {
let db_user = data.read_user(&name)?; let db_user = data.read_user(&name)?;
let apub_user = db_user.into_apub(&data).await?; let json_user = db_user.into_json(&data).await?;
Ok(ApubJson(WithContext::new_default(apub_user))) Ok(FederationJson(WithContext::new_default(json_user)))
} }
#[debug_handler] #[debug_handler]

View file

@ -12,7 +12,7 @@ use activitypub_federation::{
http_signatures::generate_actor_keypair, http_signatures::generate_actor_keypair,
kinds::actor::PersonType, kinds::actor::PersonType,
protocol::{context::WithContext, public_key::PublicKey, verification::verify_domains_match}, protocol::{context::WithContext, public_key::PublicKey, verification::verify_domains_match},
traits::{ActivityHandler, Actor, ApubObject}, traits::{ActivityHandler, Actor, Object},
}; };
use chrono::{Local, NaiveDateTime}; use chrono::{Local, NaiveDateTime};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -92,7 +92,7 @@ impl DbUser {
pub async fn post(&self, post: DbPost, data: &Data<DatabaseHandle>) -> Result<(), Error> { pub async fn post(&self, post: DbPost, data: &Data<DatabaseHandle>) -> Result<(), Error> {
let id = generate_object_id(data.domain())?; let id = generate_object_id(data.domain())?;
let create = CreatePost::new(post.into_apub(data).await?, id.clone()); let create = CreatePost::new(post.into_json(data).await?, id.clone());
let mut inboxes = vec![]; let mut inboxes = vec![];
for f in self.followers.clone() { for f in self.followers.clone() {
let user: DbUser = ObjectId::from(f).dereference(data).await?; let user: DbUser = ObjectId::from(f).dereference(data).await?;
@ -119,16 +119,16 @@ impl DbUser {
} }
#[async_trait::async_trait] #[async_trait::async_trait]
impl ApubObject for DbUser { impl Object for DbUser {
type DataType = DatabaseHandle; type DataType = DatabaseHandle;
type ApubType = Person; type Kind = Person;
type Error = Error; type Error = Error;
fn last_refreshed_at(&self) -> Option<NaiveDateTime> { fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
Some(self.last_refreshed_at) Some(self.last_refreshed_at)
} }
async fn read_from_apub_id( async fn read_from_id(
object_id: Url, object_id: Url,
data: &Data<Self::DataType>, data: &Data<Self::DataType>,
) -> Result<Option<Self>, Self::Error> { ) -> Result<Option<Self>, Self::Error> {
@ -140,7 +140,7 @@ impl ApubObject for DbUser {
Ok(res) Ok(res)
} }
async fn into_apub(self, _data: &Data<Self::DataType>) -> Result<Self::ApubType, Self::Error> { async fn into_json(self, _data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
Ok(Person { Ok(Person {
preferred_username: self.name.clone(), preferred_username: self.name.clone(),
kind: Default::default(), kind: Default::default(),
@ -151,23 +151,20 @@ impl ApubObject for DbUser {
} }
async fn verify( async fn verify(
apub: &Self::ApubType, json: &Self::Kind,
expected_domain: &Url, expected_domain: &Url,
_data: &Data<Self::DataType>, _data: &Data<Self::DataType>,
) -> Result<(), Self::Error> { ) -> Result<(), Self::Error> {
verify_domains_match(apub.id.inner(), expected_domain)?; verify_domains_match(json.id.inner(), expected_domain)?;
Ok(()) Ok(())
} }
async fn from_apub( async fn from_json(json: Self::Kind, data: &Data<Self::DataType>) -> Result<Self, Self::Error> {
apub: Self::ApubType,
data: &Data<Self::DataType>,
) -> Result<Self, Self::Error> {
let user = DbUser { let user = DbUser {
name: apub.preferred_username, name: json.preferred_username,
ap_id: apub.id, ap_id: json.id,
inbox: apub.inbox, inbox: json.inbox,
public_key: apub.public_key.public_key_pem, public_key: json.public_key.public_key_pem,
private_key: None, private_key: None,
last_refreshed_at: Local::now().naive_local(), last_refreshed_at: Local::now().naive_local(),
followers: vec![], followers: vec![],

View file

@ -4,7 +4,7 @@ use activitypub_federation::{
fetch::object_id::ObjectId, fetch::object_id::ObjectId,
kinds::{object::NoteType, public}, kinds::{object::NoteType, public},
protocol::{helpers::deserialize_one_or_many, verification::verify_domains_match}, protocol::{helpers::deserialize_one_or_many, verification::verify_domains_match},
traits::ApubObject, traits::Object,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url; use url::Url;
@ -42,12 +42,12 @@ pub struct Note {
} }
#[async_trait::async_trait] #[async_trait::async_trait]
impl ApubObject for DbPost { impl Object for DbPost {
type DataType = DatabaseHandle; type DataType = DatabaseHandle;
type ApubType = Note; type Kind = Note;
type Error = Error; type Error = Error;
async fn read_from_apub_id( async fn read_from_id(
object_id: Url, object_id: Url,
data: &Data<Self::DataType>, data: &Data<Self::DataType>,
) -> Result<Option<Self>, Self::Error> { ) -> Result<Option<Self>, Self::Error> {
@ -59,7 +59,7 @@ impl ApubObject for DbPost {
Ok(res) Ok(res)
} }
async fn into_apub(self, data: &Data<Self::DataType>) -> Result<Self::ApubType, Self::Error> { async fn into_json(self, data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
let creator = self.creator.dereference_local(data).await?; let creator = self.creator.dereference_local(data).await?;
Ok(Note { Ok(Note {
kind: Default::default(), kind: Default::default(),
@ -71,22 +71,19 @@ impl ApubObject for DbPost {
} }
async fn verify( async fn verify(
apub: &Self::ApubType, json: &Self::Kind,
expected_domain: &Url, expected_domain: &Url,
_data: &Data<Self::DataType>, _data: &Data<Self::DataType>,
) -> Result<(), Self::Error> { ) -> Result<(), Self::Error> {
verify_domains_match(apub.id.inner(), expected_domain)?; verify_domains_match(json.id.inner(), expected_domain)?;
Ok(()) Ok(())
} }
async fn from_apub( async fn from_json(json: Self::Kind, data: &Data<Self::DataType>) -> Result<Self, Self::Error> {
apub: Self::ApubType,
data: &Data<Self::DataType>,
) -> Result<Self, Self::Error> {
let post = DbPost { let post = DbPost {
text: apub.content, text: json.content,
ap_id: apub.id, ap_id: json.id,
creator: apub.attributed_to, creator: json.attributed_to,
local: false, local: false,
}; };

View file

@ -8,7 +8,7 @@ use crate::{
http_signatures::sign_request, http_signatures::sign_request,
reqwest_shim::ResponseExt, reqwest_shim::ResponseExt,
traits::{ActivityHandler, Actor}, traits::{ActivityHandler, Actor},
APUB_JSON_CONTENT_TYPE, FEDERATION_CONTENT_TYPE,
}; };
use anyhow::anyhow; use anyhow::anyhow;
use background_jobs::{ use background_jobs::{
@ -203,7 +203,7 @@ fn generate_request_headers(inbox_url: &Url) -> HeaderMap {
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();
headers.insert( headers.insert(
HeaderName::from_static("content-type"), HeaderName::from_static("content-type"),
HeaderValue::from_static(APUB_JSON_CONTENT_TYPE), HeaderValue::from_static(FEDERATION_CONTENT_TYPE),
); );
headers.insert( headers.insert(
HeaderName::from_static("host"), HeaderName::from_static("host"),

View file

@ -5,7 +5,7 @@ use crate::{
error::Error, error::Error,
fetch::object_id::ObjectId, fetch::object_id::ObjectId,
http_signatures::{verify_inbox_hash, verify_signature}, http_signatures::{verify_inbox_hash, verify_signature},
traits::{ActivityHandler, Actor, ApubObject}, traits::{ActivityHandler, Actor, Object},
}; };
use actix_web::{web::Bytes, HttpRequest, HttpResponse}; use actix_web::{web::Bytes, HttpRequest, HttpResponse};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
@ -21,13 +21,13 @@ pub async fn receive_activity<Activity, ActorT, Datatype>(
) -> Result<HttpResponse, <Activity as ActivityHandler>::Error> ) -> Result<HttpResponse, <Activity as ActivityHandler>::Error>
where where
Activity: ActivityHandler<DataType = Datatype> + DeserializeOwned + Send + 'static, Activity: ActivityHandler<DataType = Datatype> + DeserializeOwned + Send + 'static,
ActorT: ApubObject<DataType = Datatype> + Actor + Send + 'static, ActorT: Object<DataType = Datatype> + Actor + Send + 'static,
for<'de2> <ActorT as ApubObject>::ApubType: serde::Deserialize<'de2>, for<'de2> <ActorT as Object>::Kind: serde::Deserialize<'de2>,
<Activity as ActivityHandler>::Error: From<anyhow::Error> <Activity as ActivityHandler>::Error: From<anyhow::Error>
+ From<Error> + From<Error>
+ From<<ActorT as ApubObject>::Error> + From<<ActorT as Object>::Error>
+ From<serde_json::Error>, + From<serde_json::Error>,
<ActorT as ApubObject>::Error: From<Error> + From<anyhow::Error>, <ActorT as Object>::Error: From<Error> + From<anyhow::Error>,
Datatype: Clone, Datatype: Clone,
{ {
verify_inbox_hash(request.headers().get("Digest"), &body)?; verify_inbox_hash(request.headers().get("Digest"), &body)?;

View file

@ -1,4 +1,4 @@
use crate::config::{ApubMiddleware, Data, FederationConfig}; use crate::config::{Data, FederationConfig, FederationMiddleware};
use actix_web::{ use actix_web::{
dev::{forward_ready, Payload, Service, ServiceRequest, ServiceResponse, Transform}, dev::{forward_ready, Payload, Service, ServiceRequest, ServiceResponse, Transform},
Error, Error,
@ -8,7 +8,7 @@ use actix_web::{
}; };
use std::future::{ready, Ready}; use std::future::{ready, Ready};
impl<S, B, T> Transform<S, ServiceRequest> for ApubMiddleware<T> impl<S, B, T> Transform<S, ServiceRequest> for FederationMiddleware<T>
where where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>, S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static, S::Future: 'static,
@ -17,12 +17,12 @@ where
{ {
type Response = ServiceResponse<B>; type Response = ServiceResponse<B>;
type Error = Error; type Error = Error;
type Transform = ApubService<S, T>; type Transform = FederationService<S, T>;
type InitError = (); type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>; type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future { fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(ApubService { ready(Ok(FederationService {
service, service,
config: self.0.clone(), config: self.0.clone(),
})) }))
@ -31,7 +31,7 @@ where
/// Passes [FederationConfig] to HTTP handlers, converting it to [Data] in the process /// Passes [FederationConfig] to HTTP handlers, converting it to [Data] in the process
#[doc(hidden)] #[doc(hidden)]
pub struct ApubService<S, T: Clone> pub struct FederationService<S, T: Clone>
where where
S: Service<ServiceRequest, Error = Error>, S: Service<ServiceRequest, Error = Error>,
S::Future: 'static, S::Future: 'static,
@ -41,7 +41,7 @@ where
config: FederationConfig<T>, config: FederationConfig<T>,
} }
impl<S, B, T> Service<ServiceRequest> for ApubService<S, T> impl<S, B, T> Service<ServiceRequest> for FederationService<S, T>
where where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>, S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static, S::Future: 'static,
@ -69,7 +69,7 @@ impl<T: Clone + 'static> FromRequest for Data<T> {
ready(match req.extensions().get::<FederationConfig<T>>() { ready(match req.extensions().get::<FederationConfig<T>>() {
Some(c) => Ok(c.to_request_data()), Some(c) => Ok(c.to_request_data()),
None => Err(actix_web::error::ErrorBadRequest( None => Err(actix_web::error::ErrorBadRequest(
"Missing extension, did you register ApubMiddleware?", "Missing extension, did you register FederationMiddleware?",
)), )),
}) })
} }

View file

@ -7,7 +7,7 @@ use crate::{
error::Error, error::Error,
fetch::object_id::ObjectId, fetch::object_id::ObjectId,
http_signatures::{verify_inbox_hash, verify_signature}, http_signatures::{verify_inbox_hash, verify_signature},
traits::{ActivityHandler, Actor, ApubObject}, traits::{ActivityHandler, Actor, Object},
}; };
use axum::{ use axum::{
async_trait, async_trait,
@ -27,13 +27,13 @@ pub async fn receive_activity<Activity, ActorT, Datatype>(
) -> Result<(), <Activity as ActivityHandler>::Error> ) -> Result<(), <Activity as ActivityHandler>::Error>
where where
Activity: ActivityHandler<DataType = Datatype> + DeserializeOwned + Send + 'static, Activity: ActivityHandler<DataType = Datatype> + DeserializeOwned + Send + 'static,
ActorT: ApubObject<DataType = Datatype> + Actor + Send + 'static, ActorT: Object<DataType = Datatype> + Actor + Send + 'static,
for<'de2> <ActorT as ApubObject>::ApubType: serde::Deserialize<'de2>, for<'de2> <ActorT as Object>::Kind: serde::Deserialize<'de2>,
<Activity as ActivityHandler>::Error: From<anyhow::Error> <Activity as ActivityHandler>::Error: From<anyhow::Error>
+ From<Error> + From<Error>
+ From<<ActorT as ApubObject>::Error> + From<<ActorT as Object>::Error>
+ From<serde_json::Error>, + From<serde_json::Error>,
<ActorT as ApubObject>::Error: From<Error> + From<anyhow::Error>, <ActorT as Object>::Error: From<Error> + From<anyhow::Error>,
Datatype: Clone, Datatype: Clone,
{ {
verify_inbox_hash(activity_data.headers.get("Digest"), &activity_data.body)?; verify_inbox_hash(activity_data.headers.get("Digest"), &activity_data.body)?;

View file

@ -3,34 +3,34 @@
//! ``` //! ```
//! # use anyhow::Error; //! # use anyhow::Error;
//! # use axum::extract::Path; //! # use axum::extract::Path;
//! # use activitypub_federation::axum::json::ApubJson; //! # use activitypub_federation::axum::json::FederationJson;
//! # use activitypub_federation::protocol::context::WithContext; //! # use activitypub_federation::protocol::context::WithContext;
//! # use activitypub_federation::config::Data; //! # use activitypub_federation::config::Data;
//! # use activitypub_federation::traits::ApubObject; //! # use activitypub_federation::traits::Object;
//! # use activitypub_federation::traits::tests::{DbConnection, DbUser, Person}; //! # use activitypub_federation::traits::tests::{DbConnection, DbUser, Person};
//! async fn http_get_user(Path(name): Path<String>, data: Data<DbConnection>) -> Result<ApubJson<WithContext<Person>>, Error> { //! async fn http_get_user(Path(name): Path<String>, data: Data<DbConnection>) -> Result<FederationJson<WithContext<Person>>, Error> {
//! let user: DbUser = data.read_local_user(name).await?; //! let user: DbUser = data.read_local_user(name).await?;
//! let person = user.into_apub(&data).await?; //! let person = user.into_json(&data).await?;
//! //!
//! Ok(ApubJson(WithContext::new_default(person))) //! Ok(FederationJson(WithContext::new_default(person)))
//! } //! }
//! ``` //! ```
use crate::APUB_JSON_CONTENT_TYPE; use crate::FEDERATION_CONTENT_TYPE;
use axum::response::IntoResponse; use axum::response::IntoResponse;
use http::header; use http::header;
use serde::Serialize; use serde::Serialize;
/// Wrapper struct to respond with `application/activity+json` in axum handlers /// Wrapper struct to respond with `application/activity+json` in axum handlers
#[derive(Debug, Clone, Copy, Default)] #[derive(Debug, Clone, Copy, Default)]
pub struct ApubJson<Json: Serialize>(pub Json); pub struct FederationJson<Json: Serialize>(pub Json);
impl<Json: Serialize> IntoResponse for ApubJson<Json> { impl<Json: Serialize> IntoResponse for FederationJson<Json> {
fn into_response(self) -> axum::response::Response { fn into_response(self) -> axum::response::Response {
let mut response = axum::response::Json(self.0).into_response(); let mut response = axum::response::Json(self.0).into_response();
response.headers_mut().insert( response.headers_mut().insert(
header::CONTENT_TYPE, header::CONTENT_TYPE,
APUB_JSON_CONTENT_TYPE FEDERATION_CONTENT_TYPE
.parse() .parse()
.expect("Parsing 'application/activity+json' should never fail"), .expect("Parsing 'application/activity+json' should never fail"),
); );

View file

@ -1,14 +1,14 @@
use crate::config::{ApubMiddleware, Data, FederationConfig}; use crate::config::{Data, FederationConfig, FederationMiddleware};
use axum::{async_trait, body::Body, extract::FromRequestParts, http::Request, response::Response}; use axum::{async_trait, body::Body, extract::FromRequestParts, http::Request, response::Response};
use http::{request::Parts, StatusCode}; use http::{request::Parts, StatusCode};
use std::task::{Context, Poll}; use std::task::{Context, Poll};
use tower::{Layer, Service}; use tower::{Layer, Service};
impl<S, T: Clone> Layer<S> for ApubMiddleware<T> { impl<S, T: Clone> Layer<S> for FederationMiddleware<T> {
type Service = ApubService<S, T>; type Service = FederationService<S, T>;
fn layer(&self, inner: S) -> Self::Service { fn layer(&self, inner: S) -> Self::Service {
ApubService { FederationService {
inner, inner,
config: self.0.clone(), config: self.0.clone(),
} }
@ -18,12 +18,12 @@ impl<S, T: Clone> Layer<S> for ApubMiddleware<T> {
/// Passes [FederationConfig] to HTTP handlers, converting it to [Data] in the process /// Passes [FederationConfig] to HTTP handlers, converting it to [Data] in the process
#[doc(hidden)] #[doc(hidden)]
#[derive(Clone)] #[derive(Clone)]
pub struct ApubService<S, T: Clone> { pub struct FederationService<S, T: Clone> {
inner: S, inner: S,
config: FederationConfig<T>, config: FederationConfig<T>,
} }
impl<S, T> Service<Request<Body>> for ApubService<S, T> impl<S, T> Service<Request<Body>> for FederationService<S, T>
where where
S: Service<Request<Body>, Response = Response> + Send + 'static, S: Service<Request<Body>, Response = Response> + Send + 'static,
S::Future: Send + 'static, S::Future: Send + 'static,
@ -56,7 +56,7 @@ where
Some(c) => Ok(c.to_request_data()), Some(c) => Ok(c.to_request_data()),
None => Err(( None => Err((
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
"Missing extension, did you register ApubMiddleware?", "Missing extension, did you register FederationMiddleware?",
)), )),
} }
} }

View file

@ -295,11 +295,11 @@ impl<T: Clone> Deref for Data<T> {
/// Middleware for HTTP handlers which provides access to [Data] /// Middleware for HTTP handlers which provides access to [Data]
#[derive(Clone)] #[derive(Clone)]
pub struct ApubMiddleware<T: Clone>(pub(crate) FederationConfig<T>); pub struct FederationMiddleware<T: Clone>(pub(crate) FederationConfig<T>);
impl<T: Clone> ApubMiddleware<T> { impl<T: Clone> FederationMiddleware<T> {
/// Construct a new middleware instance /// Construct a new middleware instance
pub fn new(config: FederationConfig<T>) -> Self { pub fn new(config: FederationConfig<T>) -> Self {
ApubMiddleware(config) FederationMiddleware(config)
} }
} }

View file

@ -1,4 +1,4 @@
use crate::{config::Data, error::Error, fetch::fetch_object_http, traits::ApubCollection}; use crate::{config::Data, error::Error, fetch::fetch_object_http, traits::Collection};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
fmt::{Debug, Display, Formatter}, fmt::{Debug, Display, Formatter},
@ -11,13 +11,13 @@ use url::Url;
#[serde(transparent)] #[serde(transparent)]
pub struct CollectionId<Kind>(Box<Url>, PhantomData<Kind>) pub struct CollectionId<Kind>(Box<Url>, PhantomData<Kind>)
where where
Kind: ApubCollection, Kind: Collection,
for<'de2> <Kind as ApubCollection>::ApubType: Deserialize<'de2>; for<'de2> <Kind as Collection>::Kind: Deserialize<'de2>;
impl<Kind> CollectionId<Kind> impl<Kind> CollectionId<Kind>
where where
Kind: ApubCollection, Kind: Collection,
for<'de2> <Kind as ApubCollection>::ApubType: Deserialize<'de2>, for<'de2> <Kind as Collection>::Kind: Deserialize<'de2>,
{ {
/// Construct a new CollectionId instance /// Construct a new CollectionId instance
pub fn parse<T>(url: T) -> Result<Self, url::ParseError> pub fn parse<T>(url: T) -> Result<Self, url::ParseError>
@ -34,23 +34,23 @@ where
/// any caching. /// any caching.
pub async fn dereference( pub async fn dereference(
&self, &self,
owner: &<Kind as ApubCollection>::Owner, owner: &<Kind as Collection>::Owner,
data: &Data<<Kind as ApubCollection>::DataType>, data: &Data<<Kind as Collection>::DataType>,
) -> Result<Kind, <Kind as ApubCollection>::Error> ) -> Result<Kind, <Kind as Collection>::Error>
where where
<Kind as ApubCollection>::Error: From<Error>, <Kind as Collection>::Error: From<Error>,
{ {
let apub = fetch_object_http(&self.0, data).await?; let json = fetch_object_http(&self.0, data).await?;
Kind::verify(&apub, &self.0, data).await?; Kind::verify(&json, &self.0, data).await?;
Kind::from_apub(apub, owner, data).await Kind::from_json(json, owner, data).await
} }
} }
/// Need to implement clone manually, to avoid requiring Kind to be Clone /// Need to implement clone manually, to avoid requiring Kind to be Clone
impl<Kind> Clone for CollectionId<Kind> impl<Kind> Clone for CollectionId<Kind>
where where
Kind: ApubCollection, Kind: Collection,
for<'de2> <Kind as ApubCollection>::ApubType: serde::Deserialize<'de2>, for<'de2> <Kind as Collection>::Kind: serde::Deserialize<'de2>,
{ {
fn clone(&self) -> Self { fn clone(&self) -> Self {
CollectionId(self.0.clone(), self.1) CollectionId(self.0.clone(), self.1)
@ -59,8 +59,8 @@ where
impl<Kind> Display for CollectionId<Kind> impl<Kind> Display for CollectionId<Kind>
where where
Kind: ApubCollection, Kind: Collection,
for<'de2> <Kind as ApubCollection>::ApubType: serde::Deserialize<'de2>, for<'de2> <Kind as Collection>::Kind: serde::Deserialize<'de2>,
{ {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0.as_str()) write!(f, "{}", self.0.as_str())
@ -69,8 +69,8 @@ where
impl<Kind> Debug for CollectionId<Kind> impl<Kind> Debug for CollectionId<Kind>
where where
Kind: ApubCollection, Kind: Collection,
for<'de2> <Kind as ApubCollection>::ApubType: serde::Deserialize<'de2>, for<'de2> <Kind as Collection>::Kind: serde::Deserialize<'de2>,
{ {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0.as_str()) write!(f, "{}", self.0.as_str())
@ -78,8 +78,8 @@ where
} }
impl<Kind> From<CollectionId<Kind>> for Url impl<Kind> From<CollectionId<Kind>> for Url
where where
Kind: ApubCollection, Kind: Collection,
for<'de2> <Kind as ApubCollection>::ApubType: serde::Deserialize<'de2>, for<'de2> <Kind as Collection>::Kind: serde::Deserialize<'de2>,
{ {
fn from(id: CollectionId<Kind>) -> Self { fn from(id: CollectionId<Kind>) -> Self {
*id.0 *id.0
@ -88,8 +88,8 @@ where
impl<Kind> From<Url> for CollectionId<Kind> impl<Kind> From<Url> for CollectionId<Kind>
where where
Kind: ApubCollection + Send + 'static, Kind: Collection + Send + 'static,
for<'de2> <Kind as ApubCollection>::ApubType: serde::Deserialize<'de2>, for<'de2> <Kind as Collection>::Kind: serde::Deserialize<'de2>,
{ {
fn from(url: Url) -> Self { fn from(url: Url) -> Self {
CollectionId(Box::new(url), PhantomData::<Kind>) CollectionId(Box::new(url), PhantomData::<Kind>)

View file

@ -2,7 +2,7 @@
//! //!
#![doc = include_str!("../../docs/07_fetching_data.md")] #![doc = include_str!("../../docs/07_fetching_data.md")]
use crate::{config::Data, error::Error, reqwest_shim::ResponseExt, APUB_JSON_CONTENT_TYPE}; use crate::{config::Data, error::Error, reqwest_shim::ResponseExt, FEDERATION_CONTENT_TYPE};
use http::StatusCode; use http::StatusCode;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
@ -44,7 +44,7 @@ pub async fn fetch_object_http<T: Clone, Kind: DeserializeOwned>(
let res = config let res = config
.client .client
.get(url.as_str()) .get(url.as_str())
.header("Accept", APUB_JSON_CONTENT_TYPE) .header("Accept", FEDERATION_CONTENT_TYPE)
.timeout(config.request_timeout) .timeout(config.request_timeout)
.send() .send()
.await .await

View file

@ -1,4 +1,4 @@
use crate::{config::Data, error::Error, fetch::fetch_object_http, traits::ApubObject}; use crate::{config::Data, error::Error, fetch::fetch_object_http, traits::Object};
use anyhow::anyhow; use anyhow::anyhow;
use chrono::{Duration as ChronoDuration, NaiveDateTime, Utc}; use chrono::{Duration as ChronoDuration, NaiveDateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -11,8 +11,8 @@ use url::Url;
impl<T> FromStr for ObjectId<T> impl<T> FromStr for ObjectId<T>
where where
T: ApubObject + Send + 'static, T: Object + Send + 'static,
for<'de2> <T as ApubObject>::ApubType: Deserialize<'de2>, for<'de2> <T as Object>::Kind: Deserialize<'de2>,
{ {
type Err = url::ParseError; type Err = url::ParseError;
@ -58,13 +58,13 @@ where
#[serde(transparent)] #[serde(transparent)]
pub struct ObjectId<Kind>(Box<Url>, PhantomData<Kind>) pub struct ObjectId<Kind>(Box<Url>, PhantomData<Kind>)
where where
Kind: ApubObject, Kind: Object,
for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>; for<'de2> <Kind as Object>::Kind: serde::Deserialize<'de2>;
impl<Kind> ObjectId<Kind> impl<Kind> ObjectId<Kind>
where where
Kind: ApubObject + Send + 'static, Kind: Object + Send + 'static,
for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>, for<'de2> <Kind as Object>::Kind: serde::Deserialize<'de2>,
{ {
/// Construct a new objectid instance /// Construct a new objectid instance
pub fn parse<T>(url: T) -> Result<Self, url::ParseError> pub fn parse<T>(url: T) -> Result<Self, url::ParseError>
@ -88,10 +88,10 @@ where
/// Fetches an activitypub object, either from local database (if possible), or over http. /// Fetches an activitypub object, either from local database (if possible), or over http.
pub async fn dereference( pub async fn dereference(
&self, &self,
data: &Data<<Kind as ApubObject>::DataType>, data: &Data<<Kind as Object>::DataType>,
) -> Result<Kind, <Kind as ApubObject>::Error> ) -> Result<Kind, <Kind as Object>::Error>
where where
<Kind as ApubObject>::Error: From<Error> + From<anyhow::Error>, <Kind as Object>::Error: From<Error> + From<anyhow::Error>,
{ {
let db_object = self.dereference_from_db(data).await?; let db_object = self.dereference_from_db(data).await?;
@ -123,10 +123,10 @@ where
/// the object is not found in the database. /// the object is not found in the database.
pub async fn dereference_local( pub async fn dereference_local(
&self, &self,
data: &Data<<Kind as ApubObject>::DataType>, data: &Data<<Kind as Object>::DataType>,
) -> Result<Kind, <Kind as ApubObject>::Error> ) -> Result<Kind, <Kind as Object>::Error>
where where
<Kind as ApubObject>::Error: From<Error>, <Kind as Object>::Error: From<Error>,
{ {
let object = self.dereference_from_db(data).await?; let object = self.dereference_from_db(data).await?;
object.ok_or_else(|| Error::NotFound.into()) object.ok_or_else(|| Error::NotFound.into())
@ -135,19 +135,19 @@ where
/// returning none means the object was not found in local db /// returning none means the object was not found in local db
async fn dereference_from_db( async fn dereference_from_db(
&self, &self,
data: &Data<<Kind as ApubObject>::DataType>, data: &Data<<Kind as Object>::DataType>,
) -> Result<Option<Kind>, <Kind as ApubObject>::Error> { ) -> Result<Option<Kind>, <Kind as Object>::Error> {
let id = self.0.clone(); let id = self.0.clone();
ApubObject::read_from_apub_id(*id, data).await Object::read_from_id(*id, data).await
} }
async fn dereference_from_http( async fn dereference_from_http(
&self, &self,
data: &Data<<Kind as ApubObject>::DataType>, data: &Data<<Kind as Object>::DataType>,
db_object: Option<Kind>, db_object: Option<Kind>,
) -> Result<Kind, <Kind as ApubObject>::Error> ) -> Result<Kind, <Kind as Object>::Error>
where where
<Kind as ApubObject>::Error: From<Error> + From<anyhow::Error>, <Kind as Object>::Error: From<Error> + From<anyhow::Error>,
{ {
let res = fetch_object_http(&self.0, data).await; let res = fetch_object_http(&self.0, data).await;
@ -161,15 +161,15 @@ where
let res2 = res?; let res2 = res?;
Kind::verify(&res2, self.inner(), data).await?; Kind::verify(&res2, self.inner(), data).await?;
Kind::from_apub(res2, data).await Kind::from_json(res2, data).await
} }
} }
/// Need to implement clone manually, to avoid requiring Kind to be Clone /// Need to implement clone manually, to avoid requiring Kind to be Clone
impl<Kind> Clone for ObjectId<Kind> impl<Kind> Clone for ObjectId<Kind>
where where
Kind: ApubObject, Kind: Object,
for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>, for<'de2> <Kind as Object>::Kind: serde::Deserialize<'de2>,
{ {
fn clone(&self) -> Self { fn clone(&self) -> Self {
ObjectId(self.0.clone(), self.1) ObjectId(self.0.clone(), self.1)
@ -195,8 +195,8 @@ fn should_refetch_object(last_refreshed: NaiveDateTime) -> bool {
impl<Kind> Display for ObjectId<Kind> impl<Kind> Display for ObjectId<Kind>
where where
Kind: ApubObject, Kind: Object,
for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>, for<'de2> <Kind as Object>::Kind: serde::Deserialize<'de2>,
{ {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0.as_str()) write!(f, "{}", self.0.as_str())
@ -205,8 +205,8 @@ where
impl<Kind> Debug for ObjectId<Kind> impl<Kind> Debug for ObjectId<Kind>
where where
Kind: ApubObject, Kind: Object,
for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>, for<'de2> <Kind as Object>::Kind: serde::Deserialize<'de2>,
{ {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0.as_str()) write!(f, "{}", self.0.as_str())
@ -215,8 +215,8 @@ where
impl<Kind> From<ObjectId<Kind>> for Url impl<Kind> From<ObjectId<Kind>> for Url
where where
Kind: ApubObject, Kind: Object,
for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>, for<'de2> <Kind as Object>::Kind: serde::Deserialize<'de2>,
{ {
fn from(id: ObjectId<Kind>) -> Self { fn from(id: ObjectId<Kind>) -> Self {
*id.0 *id.0
@ -225,8 +225,8 @@ where
impl<Kind> From<Url> for ObjectId<Kind> impl<Kind> From<Url> for ObjectId<Kind>
where where
Kind: ApubObject + Send + 'static, Kind: Object + Send + 'static,
for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>, for<'de2> <Kind as Object>::Kind: serde::Deserialize<'de2>,
{ {
fn from(url: Url) -> Self { fn from(url: Url) -> Self {
ObjectId(Box::new(url), PhantomData::<Kind>) ObjectId(Box::new(url), PhantomData::<Kind>)
@ -235,8 +235,8 @@ where
impl<Kind> PartialEq for ObjectId<Kind> impl<Kind> PartialEq for ObjectId<Kind>
where where
Kind: ApubObject, Kind: Object,
for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>, for<'de2> <Kind as Object>::Kind: serde::Deserialize<'de2>,
{ {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.0.eq(&other.0) && self.1 == other.1 self.0.eq(&other.0) && self.1 == other.1

View file

@ -2,8 +2,8 @@ use crate::{
config::Data, config::Data,
error::{Error, Error::WebfingerResolveFailed}, error::{Error, Error::WebfingerResolveFailed},
fetch::{fetch_object_http, object_id::ObjectId}, fetch::{fetch_object_http, object_id::ObjectId},
traits::{Actor, ApubObject}, traits::{Actor, Object},
APUB_JSON_CONTENT_TYPE, FEDERATION_CONTENT_TYPE,
}; };
use anyhow::anyhow; use anyhow::anyhow;
use itertools::Itertools; use itertools::Itertools;
@ -20,11 +20,11 @@ use url::Url;
pub async fn webfinger_resolve_actor<T: Clone, Kind>( pub async fn webfinger_resolve_actor<T: Clone, Kind>(
identifier: &str, identifier: &str,
data: &Data<T>, data: &Data<T>,
) -> Result<Kind, <Kind as ApubObject>::Error> ) -> Result<Kind, <Kind as Object>::Error>
where where
Kind: ApubObject + Actor + Send + 'static + ApubObject<DataType = T>, Kind: Object + Actor + Send + 'static + Object<DataType = T>,
for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>, for<'de2> <Kind as Object>::Kind: serde::Deserialize<'de2>,
<Kind as ApubObject>::Error: <Kind as Object>::Error:
From<crate::error::Error> + From<anyhow::Error> + From<url::ParseError> + Send + Sync, From<crate::error::Error> + From<anyhow::Error> + From<url::ParseError> + Send + Sync,
{ {
let (_, domain) = identifier let (_, domain) = identifier
@ -107,7 +107,7 @@ pub fn build_webfinger_response(subject: String, url: Url) -> Webfinger {
}, },
WebfingerLink { WebfingerLink {
rel: Some("self".to_string()), rel: Some("self".to_string()),
kind: Some(APUB_JSON_CONTENT_TYPE.to_string()), kind: Some(FEDERATION_CONTENT_TYPE.to_string()),
href: Some(url), href: Some(url),
properties: Default::default(), properties: Default::default(),
}, },

View file

@ -26,4 +26,4 @@ pub mod traits;
pub use activitystreams_kinds as kinds; pub use activitystreams_kinds as kinds;
/// Mime type for Activitypub data, used for `Accept` and `Content-Type` HTTP headers /// Mime type for Activitypub data, used for `Accept` and `Content-Type` HTTP headers
pub static APUB_JSON_CONTENT_TYPE: &str = "application/activity+json"; pub static FEDERATION_CONTENT_TYPE: &str = "application/activity+json";

View file

@ -18,7 +18,7 @@ use url::Url;
/// # use activitypub_federation::config::Data; /// # use activitypub_federation::config::Data;
/// # use activitypub_federation::fetch::object_id::ObjectId; /// # use activitypub_federation::fetch::object_id::ObjectId;
/// # use activitypub_federation::protocol::verification::verify_domains_match; /// # use activitypub_federation::protocol::verification::verify_domains_match;
/// # use activitypub_federation::traits::{Actor, ApubObject}; /// # use activitypub_federation::traits::{Actor, Object};
/// # use activitypub_federation::traits::tests::{DbConnection, DbUser}; /// # use activitypub_federation::traits::tests::{DbConnection, DbUser};
/// # /// #
/// /// How the post is read/written in the local database /// /// How the post is read/written in the local database
@ -43,18 +43,18 @@ use url::Url;
/// } /// }
/// ///
/// #[async_trait::async_trait] /// #[async_trait::async_trait]
/// impl ApubObject for DbPost { /// impl Object for DbPost {
/// type DataType = DbConnection; /// type DataType = DbConnection;
/// type ApubType = Note; /// type Kind = Note;
/// type Error = anyhow::Error; /// type Error = anyhow::Error;
/// ///
/// async fn read_from_apub_id(object_id: Url, data: &Data<Self::DataType>) -> Result<Option<Self>, Self::Error> { /// async fn read_from_id(object_id: Url, data: &Data<Self::DataType>) -> Result<Option<Self>, Self::Error> {
/// // Attempt to read object from local database. Return Ok(None) if not found. /// // Attempt to read object from local database. Return Ok(None) if not found.
/// let post: Option<DbPost> = data.read_post_from_apub_id(object_id).await?; /// let post: Option<DbPost> = data.read_post_from_json_id(object_id).await?;
/// Ok(post) /// Ok(post)
/// } /// }
/// ///
/// async fn into_apub(self, data: &Data<Self::DataType>) -> Result<Self::ApubType, Self::Error> { /// async fn into_json(self, data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
/// // Called when a local object gets sent out over Activitypub. Simply convert it to the /// // Called when a local object gets sent out over Activitypub. Simply convert it to the
/// // protocol struct /// // protocol struct
/// Ok(Note { /// Ok(Note {
@ -66,20 +66,20 @@ use url::Url;
/// }) /// })
/// } /// }
/// ///
/// async fn verify(apub: &Self::ApubType, expected_domain: &Url, data: &Data<Self::DataType>,) -> Result<(), Self::Error> { /// async fn verify(json: &Self::Kind, expected_domain: &Url, data: &Data<Self::DataType>,) -> Result<(), Self::Error> {
/// verify_domains_match(apub.id.inner(), expected_domain)?; /// verify_domains_match(json.id.inner(), expected_domain)?;
/// // additional application specific checks /// // additional application specific checks
/// Ok(()) /// Ok(())
/// } /// }
/// ///
/// async fn from_apub(apub: Self::ApubType, data: &Data<Self::DataType>) -> Result<Self, Self::Error> { /// async fn from_json(json: Self::Kind, data: &Data<Self::DataType>) -> Result<Self, Self::Error> {
/// // Called when a remote object gets received over Activitypub. Validate and insert it /// // Called when a remote object gets received over Activitypub. Validate and insert it
/// // into the database. /// // into the database.
/// ///
/// let post = DbPost { /// let post = DbPost {
/// text: apub.content, /// text: json.content,
/// ap_id: apub.id, /// ap_id: json.id,
/// creator: apub.attributed_to, /// creator: json.attributed_to,
/// local: false, /// local: false,
/// }; /// };
/// ///
@ -93,12 +93,12 @@ use url::Url;
/// ///
/// } /// }
#[async_trait] #[async_trait]
pub trait ApubObject: Sized { pub trait Object: Sized {
/// App data type passed to handlers. Must be identical to /// App data type passed to handlers. Must be identical to
/// [crate::config::FederationConfigBuilder::app_data] type. /// [crate::config::FederationConfigBuilder::app_data] type.
type DataType: Clone + Send + Sync; type DataType: Clone + Send + Sync;
/// The type of protocol struct which gets sent over network to federate this database struct. /// The type of protocol struct which gets sent over network to federate this database struct.
type ApubType; type Kind;
/// Error type returned by handler methods /// Error type returned by handler methods
type Error; type Error;
@ -118,7 +118,7 @@ pub trait ApubObject: Sized {
/// Try to read the object with given `id` from local database. /// Try to read the object with given `id` from local database.
/// ///
/// Should return `Ok(None)` if not found. /// Should return `Ok(None)` if not found.
async fn read_from_apub_id( async fn read_from_id(
object_id: Url, object_id: Url,
data: &Data<Self::DataType>, data: &Data<Self::DataType>,
) -> Result<Option<Self>, Self::Error>; ) -> Result<Option<Self>, Self::Error>;
@ -134,7 +134,7 @@ pub trait ApubObject: Sized {
/// ///
/// Called when a local object gets fetched by another instance over HTTP, or when an object /// Called when a local object gets fetched by another instance over HTTP, or when an object
/// gets sent in an activity. /// gets sent in an activity.
async fn into_apub(self, data: &Data<Self::DataType>) -> Result<Self::ApubType, Self::Error>; async fn into_json(self, data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error>;
/// Verifies that the received object is valid. /// Verifies that the received object is valid.
/// ///
@ -144,7 +144,7 @@ pub trait ApubObject: Sized {
/// It is necessary to use a separate method for this, because it might be used for activities /// It is necessary to use a separate method for this, because it might be used for activities
/// like `Delete/Note`, which shouldn't perform any database write for the inner `Note`. /// like `Delete/Note`, which shouldn't perform any database write for the inner `Note`.
async fn verify( async fn verify(
apub: &Self::ApubType, json: &Self::Kind,
expected_domain: &Url, expected_domain: &Url,
data: &Data<Self::DataType>, data: &Data<Self::DataType>,
) -> Result<(), Self::Error>; ) -> Result<(), Self::Error>;
@ -154,10 +154,7 @@ pub trait ApubObject: Sized {
/// Called when an object is received from HTTP fetch or as part of an activity. This method /// Called when an object is received from HTTP fetch or as part of an activity. This method
/// should write the received object to database. Note that there is no distinction between /// should write the received object to database. Note that there is no distinction between
/// create and update, so an `upsert` operation should be used. /// create and update, so an `upsert` operation should be used.
async fn from_apub( async fn from_json(json: Self::Kind, data: &Data<Self::DataType>) -> Result<Self, Self::Error>;
apub: Self::ApubType,
data: &Data<Self::DataType>,
) -> Result<Self, Self::Error>;
} }
/// Handler for receiving incoming activities. /// Handler for receiving incoming activities.
@ -227,12 +224,12 @@ pub trait ActivityHandler {
/// Called when an activity is received. /// Called when an activity is received.
/// ///
/// Should perform validation and possibly write action to the database. In case the activity /// Should perform validation and possibly write action to the database. In case the activity
/// has a nested `object` field, must call `object.from_apub` handler. /// has a nested `object` field, must call `object.from_json` handler.
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error>; async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error>;
} }
/// Trait to allow retrieving common Actor data. /// Trait to allow retrieving common Actor data.
pub trait Actor: ApubObject + Send + 'static { pub trait Actor: Object + Send + 'static {
/// `id` field of the actor /// `id` field of the actor
fn id(&self) -> Url; fn id(&self) -> Url;
@ -295,14 +292,14 @@ where
/// Trait for federating collections /// Trait for federating collections
#[async_trait] #[async_trait]
pub trait ApubCollection: Sized { pub trait Collection: Sized {
/// Actor or object that this collection belongs to /// Actor or object that this collection belongs to
type Owner; type Owner;
/// App data type passed to handlers. Must be identical to /// App data type passed to handlers. Must be identical to
/// [crate::config::FederationConfigBuilder::app_data] type. /// [crate::config::FederationConfigBuilder::app_data] type.
type DataType: Clone + Send + Sync; type DataType: Clone + Send + Sync;
/// The type of protocol struct which gets sent over network to federate this database struct. /// The type of protocol struct which gets sent over network to federate this database struct.
type ApubType: for<'de2> Deserialize<'de2>; type Kind: for<'de2> Deserialize<'de2>;
/// Error type returned by handler methods /// Error type returned by handler methods
type Error; type Error;
@ -310,14 +307,14 @@ pub trait ApubCollection: Sized {
async fn read_local( async fn read_local(
owner: &Self::Owner, owner: &Self::Owner,
data: &Data<Self::DataType>, data: &Data<Self::DataType>,
) -> Result<Self::ApubType, Self::Error>; ) -> Result<Self::Kind, Self::Error>;
/// Verifies that the received object is valid. /// Verifies that the received object is valid.
/// ///
/// You should check here that the domain of id matches `expected_domain`. Additionally you /// You should check here that the domain of id matches `expected_domain`. Additionally you
/// should perform any application specific checks. /// should perform any application specific checks.
async fn verify( async fn verify(
apub: &Self::ApubType, json: &Self::Kind,
expected_domain: &Url, expected_domain: &Url,
data: &Data<Self::DataType>, data: &Data<Self::DataType>,
) -> Result<(), Self::Error>; ) -> Result<(), Self::Error>;
@ -327,8 +324,8 @@ pub trait ApubCollection: Sized {
/// Called when an object is received from HTTP fetch or as part of an activity. This method /// Called when an object is received from HTTP fetch or as part of an activity. This method
/// should also write the received object to database. Note that there is no distinction /// should also write the received object to database. Note that there is no distinction
/// between create and update, so an `upsert` operation should be used. /// between create and update, so an `upsert` operation should be used.
async fn from_apub( async fn from_json(
apub: Self::ApubType, json: Self::Kind,
owner: &Self::Owner, owner: &Self::Owner,
data: &Data<Self::DataType>, data: &Data<Self::DataType>,
) -> Result<Self, Self::Error>; ) -> Result<Self, Self::Error>;
@ -355,7 +352,7 @@ pub mod tests {
pub struct DbConnection; pub struct DbConnection;
impl DbConnection { impl DbConnection {
pub async fn read_post_from_apub_id<T>(&self, _: Url) -> Result<Option<T>, Error> { pub async fn read_post_from_json_id<T>(&self, _: Url) -> Result<Option<T>, Error> {
Ok(None) Ok(None)
} }
pub async fn read_local_user(&self, _: String) -> Result<DbUser, Error> { pub async fn read_local_user(&self, _: String) -> Result<DbUser, Error> {
@ -382,7 +379,7 @@ pub mod tests {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct DbUser { pub struct DbUser {
pub name: String, pub name: String,
pub apub_id: Url, pub federation_id: Url,
pub inbox: Url, pub inbox: Url,
pub public_key: String, pub public_key: String,
#[allow(dead_code)] #[allow(dead_code)]
@ -395,7 +392,7 @@ pub mod tests {
pub static DB_USER: Lazy<DbUser> = Lazy::new(|| DbUser { pub static DB_USER: Lazy<DbUser> = Lazy::new(|| DbUser {
name: String::new(), name: String::new(),
apub_id: "https://localhost/123".parse().unwrap(), federation_id: "https://localhost/123".parse().unwrap(),
inbox: "https://localhost/123/inbox".parse().unwrap(), inbox: "https://localhost/123/inbox".parse().unwrap(),
public_key: DB_USER_KEYPAIR.public_key.clone(), public_key: DB_USER_KEYPAIR.public_key.clone(),
private_key: Some(DB_USER_KEYPAIR.private_key.clone()), private_key: Some(DB_USER_KEYPAIR.private_key.clone()),
@ -404,49 +401,46 @@ pub mod tests {
}); });
#[async_trait] #[async_trait]
impl ApubObject for DbUser { impl Object for DbUser {
type DataType = DbConnection; type DataType = DbConnection;
type ApubType = Person; type Kind = Person;
type Error = Error; type Error = Error;
async fn read_from_apub_id( async fn read_from_id(
_object_id: Url, _object_id: Url,
_data: &Data<Self::DataType>, _data: &Data<Self::DataType>,
) -> Result<Option<Self>, Self::Error> { ) -> Result<Option<Self>, Self::Error> {
Ok(Some(DB_USER.clone())) Ok(Some(DB_USER.clone()))
} }
async fn into_apub( async fn into_json(self, _data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
self,
_data: &Data<Self::DataType>,
) -> Result<Self::ApubType, Self::Error> {
Ok(Person { Ok(Person {
preferred_username: self.name.clone(), preferred_username: self.name.clone(),
kind: Default::default(), kind: Default::default(),
id: self.apub_id.clone().into(), id: self.federation_id.clone().into(),
inbox: self.inbox.clone(), inbox: self.inbox.clone(),
public_key: self.public_key(), public_key: self.public_key(),
}) })
} }
async fn verify( async fn verify(
apub: &Self::ApubType, json: &Self::Kind,
expected_domain: &Url, expected_domain: &Url,
_data: &Data<Self::DataType>, _data: &Data<Self::DataType>,
) -> Result<(), Self::Error> { ) -> Result<(), Self::Error> {
verify_domains_match(apub.id.inner(), expected_domain)?; verify_domains_match(json.id.inner(), expected_domain)?;
Ok(()) Ok(())
} }
async fn from_apub( async fn from_json(
apub: Self::ApubType, json: Self::Kind,
_data: &Data<Self::DataType>, _data: &Data<Self::DataType>,
) -> Result<Self, Self::Error> { ) -> Result<Self, Self::Error> {
Ok(DbUser { Ok(DbUser {
name: apub.preferred_username, name: json.preferred_username,
apub_id: apub.id.into(), federation_id: json.id.into(),
inbox: apub.inbox, inbox: json.inbox,
public_key: apub.public_key.public_key_pem, public_key: json.public_key.public_key_pem,
private_key: None, private_key: None,
followers: vec![], followers: vec![],
local: false, local: false,
@ -456,7 +450,7 @@ pub mod tests {
impl Actor for DbUser { impl Actor for DbUser {
fn id(&self) -> Url { fn id(&self) -> Url {
self.apub_id.clone() self.federation_id.clone()
} }
fn public_key_pem(&self) -> &str { fn public_key_pem(&self) -> &str {
@ -511,34 +505,31 @@ pub mod tests {
pub struct DbPost {} pub struct DbPost {}
#[async_trait] #[async_trait]
impl ApubObject for DbPost { impl Object for DbPost {
type DataType = DbConnection; type DataType = DbConnection;
type ApubType = Note; type Kind = Note;
type Error = Error; type Error = Error;
async fn read_from_apub_id( async fn read_from_id(
_: Url, _: Url,
_: &Data<Self::DataType>, _: &Data<Self::DataType>,
) -> Result<Option<Self>, Self::Error> { ) -> Result<Option<Self>, Self::Error> {
todo!() todo!()
} }
async fn into_apub(self, _: &Data<Self::DataType>) -> Result<Self::ApubType, Self::Error> { async fn into_json(self, _: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
todo!() todo!()
} }
async fn verify( async fn verify(
_: &Self::ApubType, _: &Self::Kind,
_: &Url, _: &Url,
_: &Data<Self::DataType>, _: &Data<Self::DataType>,
) -> Result<(), Self::Error> { ) -> Result<(), Self::Error> {
todo!() todo!()
} }
async fn from_apub( async fn from_json(_: Self::Kind, _: &Data<Self::DataType>) -> Result<Self, Self::Error> {
_: Self::ApubType,
_: &Data<Self::DataType>,
) -> Result<Self, Self::Error> {
todo!() todo!()
} }
} }