live federation example

This commit is contained in:
Felix Ableitner 2023-03-07 23:01:36 +01:00
parent 32394696a5
commit d94a2ed0fc
27 changed files with 597 additions and 34 deletions

View file

@ -67,3 +67,7 @@ debug = 0
[[example]]
name = "local_federation"
path = "examples/local_federation/main.rs"
[[example]]
name = "live_federation"
path = "examples/live_federation/main.rs"

View file

@ -65,6 +65,7 @@ Besides we also need a second struct to represent the data which gets stored in
```rust
# use url::Url;
# use chrono::NaiveDateTime;
pub struct DbUser {
pub id: i32,
@ -76,8 +77,9 @@ pub struct DbUser {
pub inbox: Url,
pub outbox: Url,
pub local: bool,
public_key: String,
private_key: Option<String>,
pub public_key: String,
pub private_key: Option<String>,
pub last_refreshed_at: NaiveDateTime,
}
```

View file

@ -11,3 +11,28 @@ Use one of the following commands to run the example with the specified web fram
`cargo run --example local_federation axum`
`cargo run --example local_federation actix-web`
## Live Federation
A minimal application which can be deployed on a server and federate with other platforms such as Mastodon. For this it needs run at the root of a (sub)domain which is available over HTTPS. Edit `main.rs` to configure the server domain and your Fediverse handle.
Setup instructions:
- Deploy the project to a server. For this you can clone the git repository on the server and execute `cargo run --example live_federation`. Alternatively run `cargo build --example live_federation` and copy the binary at `target/debug/examples/live_federation` to the server.
- Create a TLS certificate. With Let's Encrypt certbot you can use a command like `certbot certonly --nginx -d 'example.com' -m '*your-email@domain.com*'` (replace with your actual domain and email).
- Setup a reverse proxy which handles TLS and passes requests to the example project. With nginx you can use the following basic config, again using your actual domain:
```
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
location / {
proxy_pass "http://localhost:8003";
proxy_set_header Host $host;
}
}
```
- Test with `curl -H 'Accept: application/activity+json' https://example.com/alison | jq` and `curl -H 'Accept: application/activity+json' "https://example.com/.well-known/webfinger?resource=acct:alison@example.com" | jq` that the server is setup correctly and serving correct responses.
- Login to a Fediverse platform like Mastodon, and search for `@alison@example.com`, with the actual domain and username from your `main.rs`. If you send a message, it will automatically send a response.

View file

@ -0,0 +1,72 @@
use crate::{
database::DatabaseHandle,
error::Error,
objects::{person::DbUser, post::Note},
utils::generate_object_id,
DbPost,
};
use activitypub_federation::{
activity_queue::send_activity,
config::RequestData,
fetch::object_id::ObjectId,
kinds::activity::CreateType,
protocol::{context::WithContext, helpers::deserialize_one_or_many},
traits::{ActivityHandler, ApubObject},
};
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct CreatePost {
pub(crate) actor: ObjectId<DbUser>,
#[serde(deserialize_with = "deserialize_one_or_many")]
pub(crate) to: Vec<Url>,
pub(crate) object: Note,
#[serde(rename = "type")]
pub(crate) kind: CreateType,
pub(crate) id: Url,
}
impl CreatePost {
pub async fn send(
note: Note,
inbox: Url,
data: &RequestData<DatabaseHandle>,
) -> Result<(), Error> {
print!("Sending reply to {}", &note.attributed_to);
let create = CreatePost {
actor: note.attributed_to.clone(),
to: note.to.clone(),
object: note,
kind: CreateType::Create,
id: generate_object_id(data.domain())?,
};
let create_with_context = WithContext::new_default(create);
let private_key = data
.local_user()
.private_key
.expect("local user always has private key");
send_activity(create_with_context, private_key, vec![inbox], data).await?;
Ok(())
}
}
#[async_trait::async_trait]
impl ActivityHandler for CreatePost {
type DataType = DatabaseHandle;
type Error = crate::error::Error;
fn id(&self) -> &Url {
&self.id
}
fn actor(&self) -> &Url {
self.actor.inner()
}
async fn receive(self, data: &RequestData<Self::DataType>) -> Result<(), Self::Error> {
DbPost::from_apub(self.object, data).await?;
Ok(())
}
}

View file

@ -0,0 +1 @@
pub mod create_post;

View file

@ -0,0 +1,26 @@
use crate::{objects::person::DbUser, Error};
use anyhow::anyhow;
use std::sync::{Arc, Mutex};
pub type DatabaseHandle = Arc<Database>;
/// Our "database" which contains all known users (local and federated)
pub struct Database {
pub users: Mutex<Vec<DbUser>>,
}
impl Database {
pub fn local_user(&self) -> DbUser {
let lock = self.users.lock().unwrap();
lock.first().unwrap().clone()
}
pub fn read_user(&self, name: &str) -> Result<DbUser, Error> {
let db_user = self.local_user();
if name == db_user.name {
Ok(db_user)
} else {
Err(anyhow!("Invalid user {name}").into())
}
}
}

View file

@ -0,0 +1,20 @@
use std::fmt::{Display, Formatter};
/// Necessary because of this issue: https://github.com/actix/actix-web/issues/1711
#[derive(Debug)]
pub struct Error(pub(crate) anyhow::Error);
impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, f)
}
}
impl<T> From<T> for Error
where
T: Into<anyhow::Error>,
{
fn from(t: T) -> Self {
Error(t.into())
}
}

View file

@ -0,0 +1,69 @@
use crate::{
database::DatabaseHandle,
error::Error,
objects::person::{DbUser, Person, PersonAcceptedActivities},
};
use activitypub_federation::{
axum::{
inbox::{receive_activity, ActivityData},
json::ApubJson,
},
config::RequestData,
fetch::webfinger::{build_webfinger_response, extract_webfinger_name, Webfinger},
protocol::context::WithContext,
traits::ApubObject,
};
use axum::{
extract::{Path, Query},
response::{IntoResponse, Response},
Json,
};
use axum_macros::debug_handler;
use http::StatusCode;
use serde::Deserialize;
impl IntoResponse for Error {
fn into_response(self) -> Response {
(StatusCode::INTERNAL_SERVER_ERROR, format!("{}", self.0)).into_response()
}
}
#[debug_handler]
pub async fn http_get_user(
Path(name): Path<String>,
data: RequestData<DatabaseHandle>,
) -> Result<ApubJson<WithContext<Person>>, Error> {
let db_user = data.read_user(&name)?;
let apub_user = db_user.into_apub(&data).await?;
Ok(ApubJson(WithContext::new_default(apub_user)))
}
#[debug_handler]
pub async fn http_post_user_inbox(
data: RequestData<DatabaseHandle>,
activity_data: ActivityData,
) -> impl IntoResponse {
receive_activity::<WithContext<PersonAcceptedActivities>, DbUser, DatabaseHandle>(
activity_data,
&data,
)
.await
}
#[derive(Deserialize)]
pub struct WebfingerQuery {
resource: String,
}
#[debug_handler]
pub async fn webfinger(
Query(query): Query<WebfingerQuery>,
data: RequestData<DatabaseHandle>,
) -> Result<Json<Webfinger>, Error> {
let name = extract_webfinger_name(&query.resource, &data)?;
let db_user = data.read_user(&name)?;
Ok(Json(build_webfinger_response(
query.resource,
db_user.ap_id.into_inner(),
)))
}

View file

@ -0,0 +1,69 @@
use crate::{
database::Database,
http::{http_get_user, http_post_user_inbox, webfinger},
objects::{person::DbUser, post::DbPost},
utils::generate_object_id,
};
use activitypub_federation::config::{ApubMiddleware, FederationConfig};
use axum::{
routing::{get, post},
Router,
};
use error::Error;
use std::{
net::ToSocketAddrs,
sync::{Arc, Mutex},
};
use tracing::log::{info, LevelFilter};
mod activities;
mod database;
mod error;
#[allow(clippy::diverging_sub_expression, clippy::items_after_statements)]
mod http;
mod objects;
mod utils;
const DOMAIN: &str = "example.com";
const LOCAL_USER_NAME: &str = "alison";
const BIND_ADDRESS: &str = "localhost:8003";
#[actix_rt::main]
async fn main() -> Result<(), Error> {
env_logger::builder()
.filter_level(LevelFilter::Warn)
.filter_module("activitypub_federation", LevelFilter::Info)
.filter_module("live_federation", LevelFilter::Info)
.format_timestamp(None)
.init();
info!("Setup local user and database");
let local_user = DbUser::new(DOMAIN, LOCAL_USER_NAME)?;
let database = Arc::new(Database {
users: Mutex::new(vec![local_user]),
});
info!("Setup configuration");
let config = FederationConfig::builder()
.domain(DOMAIN)
.app_data(database)
.build()?;
info!("Listen with HTTP server on {BIND_ADDRESS}");
let config = config.clone();
let app = Router::new()
.route("/:user", get(http_get_user))
.route("/:user/inbox", post(http_post_user_inbox))
.route("/.well-known/webfinger", get(webfinger))
.layer(ApubMiddleware::new(config));
let addr = BIND_ADDRESS
.to_socket_addrs()?
.next()
.expect("Failed to lookup domain name");
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await?;
Ok(())
}

View file

@ -0,0 +1,2 @@
pub mod person;
pub mod post;

View file

@ -0,0 +1,127 @@
use crate::{activities::create_post::CreatePost, database::DatabaseHandle, error::Error};
use activitypub_federation::{
config::RequestData,
fetch::object_id::ObjectId,
http_signatures::generate_actor_keypair,
kinds::actor::PersonType,
protocol::public_key::PublicKey,
traits::{ActivityHandler, Actor, ApubObject},
};
use chrono::{Local, NaiveDateTime};
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use url::Url;
#[derive(Debug, Clone)]
pub struct DbUser {
pub name: String,
pub ap_id: ObjectId<DbUser>,
pub inbox: Url,
// exists for all users (necessary to verify http signatures)
pub public_key: String,
// exists only for local users
pub private_key: Option<String>,
last_refreshed_at: NaiveDateTime,
pub followers: Vec<Url>,
pub local: bool,
}
/// List of all activities which this actor can receive.
#[derive(Deserialize, Serialize, Debug)]
#[serde(untagged)]
#[enum_delegate::implement(ActivityHandler)]
pub enum PersonAcceptedActivities {
CreateNote(CreatePost),
}
impl DbUser {
pub fn new(hostname: &str, name: &str) -> Result<DbUser, Error> {
let ap_id = Url::parse(&format!("https://{}/{}", hostname, &name))?.into();
let inbox = Url::parse(&format!("https://{}/{}/inbox", hostname, &name))?;
let keypair = generate_actor_keypair()?;
Ok(DbUser {
name: name.to_string(),
ap_id,
inbox,
public_key: keypair.public_key,
private_key: Some(keypair.private_key),
last_refreshed_at: Local::now().naive_local(),
followers: vec![],
local: true,
})
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Person {
#[serde(rename = "type")]
kind: PersonType,
preferred_username: String,
id: ObjectId<DbUser>,
inbox: Url,
public_key: PublicKey,
}
#[async_trait::async_trait]
impl ApubObject for DbUser {
type DataType = DatabaseHandle;
type ApubType = Person;
type Error = Error;
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
Some(self.last_refreshed_at)
}
async fn read_from_apub_id(
object_id: Url,
data: &RequestData<Self::DataType>,
) -> Result<Option<Self>, Self::Error> {
let users = data.users.lock().unwrap();
let res = users
.clone()
.into_iter()
.find(|u| u.ap_id.inner() == &object_id);
Ok(res)
}
async fn into_apub(
self,
_data: &RequestData<Self::DataType>,
) -> Result<Self::ApubType, Self::Error> {
let public_key = PublicKey::new(self.ap_id.clone().into_inner(), self.public_key.clone());
Ok(Person {
preferred_username: self.name.clone(),
kind: Default::default(),
id: self.ap_id.clone(),
inbox: self.inbox,
public_key,
})
}
async fn from_apub(
apub: Self::ApubType,
_data: &RequestData<Self::DataType>,
) -> Result<Self, Self::Error> {
Ok(DbUser {
name: apub.preferred_username,
ap_id: apub.id,
inbox: apub.inbox,
public_key: apub.public_key.public_key_pem,
private_key: None,
last_refreshed_at: Local::now().naive_local(),
followers: vec![],
local: false,
})
}
}
impl Actor for DbUser {
fn public_key(&self) -> &str {
&self.public_key
}
fn inbox(&self) -> Url {
self.inbox.clone()
}
}

View file

@ -0,0 +1,101 @@
use crate::{
activities::create_post::CreatePost,
database::DatabaseHandle,
error::Error,
generate_object_id,
objects::person::DbUser,
};
use activitypub_federation::{
config::RequestData,
fetch::object_id::ObjectId,
kinds::{object::NoteType, public},
protocol::helpers::deserialize_one_or_many,
traits::{Actor, ApubObject},
};
use activitystreams_kinds::link::MentionType;
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Clone, Debug)]
pub struct DbPost {
pub text: String,
pub ap_id: ObjectId<DbPost>,
pub creator: ObjectId<DbUser>,
pub local: bool,
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Note {
#[serde(rename = "type")]
kind: NoteType,
id: ObjectId<DbPost>,
pub(crate) attributed_to: ObjectId<DbUser>,
#[serde(deserialize_with = "deserialize_one_or_many")]
pub(crate) to: Vec<Url>,
content: String,
in_reply_to: Option<ObjectId<DbPost>>,
tag: Vec<Mention>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Mention {
pub href: Url,
#[serde(rename = "type")]
pub kind: MentionType,
}
#[async_trait::async_trait]
impl ApubObject for DbPost {
type DataType = DatabaseHandle;
type ApubType = Note;
type Error = Error;
async fn read_from_apub_id(
_object_id: Url,
_data: &RequestData<Self::DataType>,
) -> Result<Option<Self>, Self::Error> {
Ok(None)
}
async fn into_apub(
self,
_data: &RequestData<Self::DataType>,
) -> Result<Self::ApubType, Self::Error> {
unimplemented!()
}
async fn from_apub(
apub: Self::ApubType,
data: &RequestData<Self::DataType>,
) -> Result<Self, Self::Error> {
println!(
"Received post with content {} and id {}",
&apub.content, &apub.id
);
let creator = apub.attributed_to.dereference(data).await?;
let post = DbPost {
text: apub.content,
ap_id: apub.id.clone(),
creator: apub.attributed_to.clone(),
local: false,
};
let mention = Mention {
href: creator.ap_id.clone().into_inner(),
kind: Default::default(),
};
let note = Note {
kind: Default::default(),
id: generate_object_id(data.domain())?.into(),
attributed_to: data.local_user().ap_id,
to: vec![public()],
content: format!("Hello {}", creator.name),
in_reply_to: Some(apub.id.clone()),
tag: vec![mention],
};
CreatePost::send(note, creator.shared_inbox_or_inbox(), data).await?;
Ok(post)
}
}

View file

@ -0,0 +1,13 @@
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use url::{ParseError, Url};
/// Just generate random url as object id. In a real project, you probably want to use
/// an url which contains the database id for easy retrieval (or store the random id in db).
pub fn generate_object_id(domain: &str) -> Result<Url, ParseError> {
let id: String = thread_rng()
.sample_iter(&Alphanumeric)
.take(7)
.map(char::from)
.collect();
Url::parse(&format!("https://{}/objects/{}", domain, id))
}

View file

@ -15,7 +15,7 @@ use url::Url;
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct CreateNote {
pub struct CreatePost {
pub(crate) actor: ObjectId<DbUser>,
#[serde(deserialize_with = "deserialize_one_or_many")]
pub(crate) to: Vec<Url>,
@ -25,9 +25,9 @@ pub struct CreateNote {
pub(crate) id: Url,
}
impl CreateNote {
pub fn new(note: Note, id: Url) -> CreateNote {
CreateNote {
impl CreatePost {
pub fn new(note: Note, id: Url) -> CreatePost {
CreatePost {
actor: note.attributed_to.clone(),
to: note.to.clone(),
object: note,
@ -38,7 +38,7 @@ impl CreateNote {
}
#[async_trait::async_trait]
impl ActivityHandler for CreateNote {
impl ActivityHandler for CreatePost {
type DataType = DatabaseHandle;
type Error = crate::error::Error;

View file

@ -17,7 +17,7 @@ use serde::Deserialize;
use tracing::info;
pub fn listen(config: &FederationConfig<DatabaseHandle>) -> Result<(), Error> {
let hostname = config.hostname();
let hostname = config.domain();
info!("Listening with actix-web on {hostname}");
let config = config.clone();
let server = HttpServer::new(move || {

View file

@ -26,7 +26,7 @@ use std::net::ToSocketAddrs;
use tracing::info;
pub fn listen(config: &FederationConfig<DatabaseHandle>) -> Result<(), Error> {
let hostname = config.hostname();
let hostname = config.domain();
info!("Listening with axum on {hostname}");
let config = config.clone();
let app = Router::new()

View file

@ -30,7 +30,7 @@ pub fn new_instance(
pub type DatabaseHandle = Arc<Database>;
/// Our "database" which contains all known posts users (local and federated)
/// Our "database" which contains all known posts and users (local and federated)
pub struct Database {
pub users: Mutex<Vec<DbUser>>,
pub posts: Mutex<Vec<DbPost>>,

View file

@ -21,6 +21,7 @@ mod utils;
async fn main() -> Result<(), Error> {
env_logger::builder()
.filter_level(LevelFilter::Warn)
.filter_module("activitypub_federation", LevelFilter::Info)
.filter_module("local_federation", LevelFilter::Info)
.format_timestamp(None)
.init();

View file

@ -1,5 +1,5 @@
use crate::{
activities::{accept::Accept, create_post::CreateNote, follow::Follow},
activities::{accept::Accept, create_post::CreatePost, follow::Follow},
error::Error,
instance::DatabaseHandle,
objects::post::DbPost,
@ -14,6 +14,7 @@ use activitypub_federation::{
protocol::{context::WithContext, public_key::PublicKey},
traits::{ActivityHandler, Actor, ApubObject},
};
use chrono::{Local, NaiveDateTime};
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use url::Url;
@ -27,6 +28,7 @@ pub struct DbUser {
public_key: String,
// exists only for local users
private_key: Option<String>,
last_refreshed_at: NaiveDateTime,
pub followers: Vec<Url>,
pub local: bool,
}
@ -38,7 +40,7 @@ pub struct DbUser {
pub enum PersonAcceptedActivities {
Follow(Follow),
Accept(Accept),
CreateNote(CreateNote),
CreateNote(CreatePost),
}
impl DbUser {
@ -52,6 +54,7 @@ impl DbUser {
inbox,
public_key: keypair.public_key,
private_key: Some(keypair.private_key),
last_refreshed_at: Local::now().naive_local(),
followers: vec![],
local: true,
})
@ -101,7 +104,7 @@ impl DbUser {
data: &RequestData<DatabaseHandle>,
) -> Result<(), Error> {
let id = generate_object_id(data.domain())?;
let create = CreateNote::new(post.into_apub(data).await?, id.clone());
let create = CreatePost::new(post.into_apub(data).await?, id.clone());
let mut inboxes = vec![];
for f in self.followers.clone() {
let user: DbUser = ObjectId::from(f).dereference(data).await?;
@ -139,6 +142,10 @@ impl ApubObject for DbUser {
type ApubType = Person;
type Error = Error;
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
Some(self.last_refreshed_at)
}
async fn read_from_apub_id(
object_id: Url,
data: &RequestData<Self::DataType>,
@ -166,17 +173,21 @@ impl ApubObject for DbUser {
async fn from_apub(
apub: Self::ApubType,
_data: &RequestData<Self::DataType>,
data: &RequestData<Self::DataType>,
) -> Result<Self, Self::Error> {
Ok(DbUser {
let user = DbUser {
name: apub.preferred_username,
ap_id: apub.id,
inbox: apub.inbox,
public_key: apub.public_key.public_key_pem,
private_key: None,
last_refreshed_at: Local::now().naive_local(),
followers: vec![],
local: false,
})
};
let mut mutex = data.users.lock().unwrap();
mutex.push(user.clone());
Ok(user)
}
}

View file

@ -48,10 +48,15 @@ impl ApubObject for DbPost {
type Error = Error;
async fn read_from_apub_id(
_object_id: Url,
_data: &RequestData<Self::DataType>,
object_id: Url,
data: &RequestData<Self::DataType>,
) -> Result<Option<Self>, Self::Error> {
todo!()
let posts = data.posts.lock().unwrap();
let res = posts
.clone()
.into_iter()
.find(|u| u.ap_id.inner() == &object_id);
Ok(res)
}
async fn into_apub(

View file

@ -3,11 +3,11 @@ use url::{ParseError, Url};
/// Just generate random url as object id. In a real project, you probably want to use
/// an url which contains the database id for easy retrieval (or store the random id in db).
pub fn generate_object_id(hostname: &str) -> Result<Url, ParseError> {
pub fn generate_object_id(domain: &str) -> Result<Url, ParseError> {
let id: String = thread_rng()
.sample_iter(&Alphanumeric)
.take(7)
.map(char::from)
.collect();
Url::parse(&format!("http://{}/objects/{}", hostname, id))
Url::parse(&format!("http://{}/objects/{}", domain, id))
}

View file

@ -162,9 +162,10 @@ async fn do_send(
Ok(())
}
Ok(o) if o.status().is_client_error() => {
let text = o.text_limited().await.map_err(Error::other)?;
info!(
"Target server {} rejected {}, aborting",
task.inbox, task.activity_id,
"Activity {} was rejected by {}, aborting: {}",
task.activity_id, task.inbox, text,
);
Ok(())
}

View file

@ -44,6 +44,7 @@ where
.dereference(data)
.await?;
// TODO: why do errors here not get returned over http?
verify_signature(
&activity_data.headers,
&activity_data.method,

View file

@ -160,8 +160,8 @@ impl<T: Clone> FederationConfig<T> {
domain == self.domain
}
/// Returns the local hostname
pub fn hostname(&self) -> &str {
/// Returns the local domain
pub fn domain(&self) -> &str {
&self.domain
}
}

View file

@ -125,10 +125,10 @@ pub struct Webfinger {
/// Links where further data about `subject` can be retrieved
pub links: Vec<WebfingerLink>,
/// Other Urls which identify the same actor as the `subject`
#[serde(default)]
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub aliases: Vec<Url>,
/// Additional data about the subject
#[serde(default)]
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub properties: HashMap<Url, String>,
}
@ -143,7 +143,7 @@ pub struct WebfingerLink {
/// Url pointing to the target resource
pub href: Option<Url>,
/// Additional data about the link
#[serde(default)]
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub properties: HashMap<Url, String>,
}

View file

@ -15,7 +15,7 @@
//! };
//! let note_with_context = WithContext::new_default(note);
//! let serialized = serde_json::to_string(&note_with_context)?;
//! assert_eq!(serialized, r#"{"@context":[["https://www.w3.org/ns/activitystreams"]],"content":"Hello world"}"#);
//! assert_eq!(serialized, r#"{"@context":["https://www.w3.org/ns/activitystreams"],"content":"Hello world"}"#);
//! Ok::<(), serde_json::error::Error>(())
//! ```
@ -26,11 +26,10 @@ use crate::{
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::str::FromStr;
use url::Url;
/// Default context used in Activitypub
const DEFAULT_CONTEXT: &str = "[\"https://www.w3.org/ns/activitystreams\"]";
const DEFAULT_CONTEXT: &str = "https://www.w3.org/ns/activitystreams";
/// Wrapper for federated structs which handles `@context` field.
#[derive(Serialize, Deserialize, Debug)]
@ -45,7 +44,7 @@ pub struct WithContext<T> {
impl<T> WithContext<T> {
/// Create a new wrapper with the default Activitypub context.
pub fn new_default(inner: T) -> WithContext<T> {
let context = vec![Value::from_str(DEFAULT_CONTEXT).expect("valid context")];
let context = vec![Value::String(DEFAULT_CONTEXT.to_string())];
WithContext::new(inner, context)
}

View file

@ -9,6 +9,7 @@ use url::Url;
/// Helper for converting between database structs and federated protocol structs.
///
/// ```
/// # use chrono::{Local, NaiveDateTime};
/// # use url::Url;
/// # use activitypub_federation::protocol::public_key::PublicKey;
/// # use activitypub_federation::config::RequestData;
@ -19,7 +20,9 @@ use url::Url;
/// # pub ap_id: Url,
/// # pub inbox: Url,
/// # pub public_key: String,
/// # pub private_key: Option<String>,
/// # pub local: bool,
/// # pub last_refreshed_at: NaiveDateTime,
/// # }
///
/// #[async_trait::async_trait]
@ -28,6 +31,10 @@ use url::Url;
/// type ApubType = Person;
/// type Error = anyhow::Error;
///
/// fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
/// Some(self.last_refreshed_at)
/// }
///
/// async fn read_from_apub_id(object_id: Url, data: &RequestData<Self::DataType>) -> Result<Option<Self>, Self::Error> {
/// // Attempt to read object from local database. Return Ok(None) if not found.
/// let user: Option<DbUser> = data.read_user_from_apub_id(object_id).await?;
@ -55,7 +62,9 @@ use url::Url;
/// ap_id: apub.id.into_inner(),
/// inbox: apub.inbox,
/// public_key: apub.public_key.public_key_pem,
/// private_key: None,
/// local: false,
/// last_refreshed_at: Local::now().naive_local(),
/// };
///
/// // Make sure not to overwrite any local object
@ -80,8 +89,13 @@ pub trait ApubObject: Sized {
/// Returns the last time this object was updated.
///
/// Used to avoid refetching an object over HTTP every time it is dereferenced. Only called
/// for remote objects.
/// If this returns `Some` and the value is too long ago, the object is refetched from the
/// original instance. This should always be implemented for actors, because there is no active
/// update mechanism prescribed. It is possible to send `Update/Person` activities for profile
/// changes, but not all implementations do this, so `last_refreshed_at` is still necessary.
///
/// The object is refetched if `last_refreshed_at` value is more than 24 hours ago. In debug
/// mode this is reduced to 20 seconds.
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
None
}