No default feature, rename to actix-web, merge examples
This commit is contained in:
parent
83ad4bfdc1
commit
5a5c015bfc
33 changed files with 276 additions and 842 deletions
|
@ -48,11 +48,11 @@ steps:
|
||||||
CARGO_HOME: .cargo
|
CARGO_HOME: .cargo
|
||||||
RUST_BACKTRACE: 1
|
RUST_BACKTRACE: 1
|
||||||
commands:
|
commands:
|
||||||
- cargo run --example simple_federation_actix
|
- cargo run --example simple_federation --features actix-web
|
||||||
- name: cargo run axum
|
- name: cargo run axum
|
||||||
image: rust:1.65-bullseye
|
image: rust:1.65-bullseye
|
||||||
environment:
|
environment:
|
||||||
CARGO_HOME: .cargo
|
CARGO_HOME: .cargo
|
||||||
RUST_BACKTRACE: 1
|
RUST_BACKTRACE: 1
|
||||||
commands:
|
commands:
|
||||||
- cargo run --example simple_federation_axum --features axum
|
- cargo run --example simple_federation --features axum
|
||||||
|
|
28
Cargo.toml
28
Cargo.toml
|
@ -37,24 +37,21 @@ bytes = "1.3.0"
|
||||||
futures-core = { version = "0.3.25", default-features = false }
|
futures-core = { version = "0.3.25", default-features = false }
|
||||||
pin-project-lite = "0.2.9"
|
pin-project-lite = "0.2.9"
|
||||||
|
|
||||||
|
# Actix-web
|
||||||
actix-web = { version = "4.2.1", default-features = false, optional = true }
|
actix-web = { version = "4.2.1", default-features = false, optional = true }
|
||||||
axum = { version = "0.6.0", features = ["json", "headers", "macros", "original-uri"], optional = true }
|
|
||||||
|
|
||||||
# Axum
|
# Axum
|
||||||
tower-http = { version = "0.3", features = ["map-request-body", "util", "trace"], optional = true }
|
axum = { version = "0.6.0", features = ["json", "headers", "macros", "original-uri"], optional = true }
|
||||||
tower = { version = "0.4.13", optional = true }
|
tower = { version = "0.4.13", optional = true }
|
||||||
hyper = { version = "0.14", optional = true }
|
hyper = { version = "0.14", optional = true }
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"], optional = true }
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["actix"]
|
default = []
|
||||||
actix = ["dep:actix-web"]
|
actix-web = ["dep:actix-web"]
|
||||||
axum = [
|
axum = [
|
||||||
"dep:axum",
|
"dep:axum",
|
||||||
"dep:tower-http",
|
|
||||||
"dep:tower",
|
"dep:tower",
|
||||||
"dep:hyper",
|
"dep:hyper",
|
||||||
"dep:tracing-subscriber",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
@ -63,14 +60,13 @@ rand = "0.8.5"
|
||||||
actix-rt = "2.7.0"
|
actix-rt = "2.7.0"
|
||||||
tokio = { version = "1.21.2", features = ["full"] }
|
tokio = { version = "1.21.2", features = ["full"] }
|
||||||
env_logger = { version = "0.9.3", default-features = false }
|
env_logger = { version = "0.9.3", default-features = false }
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
tower-http = { version = "0.3", features = ["map-request-body", "util", "trace"] }
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
strip = "symbols"
|
||||||
|
debug = 0
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "simple_federation_actix"
|
name = "simple_federation"
|
||||||
path = "examples/federation-actix/main.rs"
|
path = "examples/simple_federation/main.rs"
|
||||||
required-features = ["actix"]
|
|
||||||
|
|
||||||
[[example]]
|
|
||||||
name = "simple_federation_axum"
|
|
||||||
path = "examples/federation-axum/main.rs"
|
|
||||||
required-features = ["axum"]
|
|
||||||
|
|
|
@ -1,123 +0,0 @@
|
||||||
use crate::{
|
|
||||||
error::Error,
|
|
||||||
generate_object_id,
|
|
||||||
objects::{
|
|
||||||
note::MyPost,
|
|
||||||
person::{MyUser, PersonAcceptedActivities},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use activitypub_federation::{
|
|
||||||
core::{
|
|
||||||
actix::inbox::receive_activity,
|
|
||||||
object_id::ObjectId,
|
|
||||||
signatures::generate_actor_keypair,
|
|
||||||
},
|
|
||||||
deser::context::WithContext,
|
|
||||||
request_data::{ApubContext, ApubMiddleware, RequestData},
|
|
||||||
traits::ApubObject,
|
|
||||||
FederationSettings,
|
|
||||||
InstanceConfig,
|
|
||||||
UrlVerifier,
|
|
||||||
APUB_JSON_CONTENT_TYPE,
|
|
||||||
};
|
|
||||||
use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer};
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use reqwest::Client;
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
use tokio::task;
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
pub type DatabaseHandle = Arc<Database>;
|
|
||||||
|
|
||||||
/// Our "database" which contains all known posts users (local and federated)
|
|
||||||
pub struct Database {
|
|
||||||
pub users: Mutex<Vec<MyUser>>,
|
|
||||||
pub posts: Mutex<Vec<MyPost>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Use this to store your federation blocklist, or a database connection needed to retrieve it.
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct MyUrlVerifier();
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl UrlVerifier for MyUrlVerifier {
|
|
||||||
async fn verify(&self, url: &Url) -> Result<(), &'static str> {
|
|
||||||
if url.domain() == Some("malicious.com") {
|
|
||||||
Err("malicious domain")
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Database {
|
|
||||||
pub fn new(hostname: String) -> Result<ApubContext<DatabaseHandle>, Error> {
|
|
||||||
let settings = FederationSettings::builder()
|
|
||||||
.debug(true)
|
|
||||||
.url_verifier(Box::new(MyUrlVerifier()))
|
|
||||||
.build()?;
|
|
||||||
let local_instance =
|
|
||||||
InstanceConfig::new(hostname.clone(), Client::default().into(), settings);
|
|
||||||
let local_user = MyUser::new(generate_object_id(&hostname)?, generate_actor_keypair()?);
|
|
||||||
let instance = Arc::new(Database {
|
|
||||||
users: Mutex::new(vec![local_user]),
|
|
||||||
posts: Mutex::new(vec![]),
|
|
||||||
});
|
|
||||||
Ok(ApubContext::new(instance, local_instance))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn local_user(&self) -> MyUser {
|
|
||||||
self.users.lock().unwrap().first().cloned().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn listen(data: &ApubContext<DatabaseHandle>) -> Result<(), Error> {
|
|
||||||
let hostname = data.local_instance().hostname();
|
|
||||||
let data = data.clone();
|
|
||||||
let server = HttpServer::new(move || {
|
|
||||||
App::new()
|
|
||||||
.wrap(ApubMiddleware::new(data.clone()))
|
|
||||||
.route("/objects/{user_name}", web::get().to(http_get_user))
|
|
||||||
.service(
|
|
||||||
web::scope("")
|
|
||||||
// Just a single, global inbox for simplicity
|
|
||||||
.route("/inbox", web::post().to(http_post_user_inbox)),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.bind(hostname)?
|
|
||||||
.run();
|
|
||||||
task::spawn(server);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handles requests to fetch user json over HTTP
|
|
||||||
async fn http_get_user(
|
|
||||||
request: HttpRequest,
|
|
||||||
data: RequestData<DatabaseHandle>,
|
|
||||||
) -> Result<HttpResponse, Error> {
|
|
||||||
let hostname: String = data.local_instance().hostname().to_string();
|
|
||||||
let request_url = format!("http://{}{}", hostname, &request.uri().to_string());
|
|
||||||
let url = Url::parse(&request_url)?;
|
|
||||||
let user = ObjectId::<MyUser>::new(url)
|
|
||||||
.dereference_local(&data)
|
|
||||||
.await?
|
|
||||||
.into_apub(&data)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(HttpResponse::Ok()
|
|
||||||
.content_type(APUB_JSON_CONTENT_TYPE)
|
|
||||||
.json(WithContext::new_default(user)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handles messages received in user inbox
|
|
||||||
async fn http_post_user_inbox(
|
|
||||||
request: HttpRequest,
|
|
||||||
payload: String,
|
|
||||||
data: RequestData<DatabaseHandle>,
|
|
||||||
) -> Result<HttpResponse, Error> {
|
|
||||||
let activity = serde_json::from_str(&payload)?;
|
|
||||||
receive_activity::<WithContext<PersonAcceptedActivities>, MyUser, DatabaseHandle>(
|
|
||||||
request, activity, &data,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
|
@ -1,86 +0,0 @@
|
||||||
use crate::{generate_object_id, instance::DatabaseHandle, objects::person::MyUser};
|
|
||||||
use activitypub_federation::{
|
|
||||||
core::object_id::ObjectId,
|
|
||||||
deser::helpers::deserialize_one_or_many,
|
|
||||||
request_data::RequestData,
|
|
||||||
traits::ApubObject,
|
|
||||||
};
|
|
||||||
use activitystreams_kinds::{object::NoteType, public};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct MyPost {
|
|
||||||
pub text: String,
|
|
||||||
pub ap_id: ObjectId<MyPost>,
|
|
||||||
pub creator: ObjectId<MyUser>,
|
|
||||||
pub local: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MyPost {
|
|
||||||
pub fn new(text: String, creator: ObjectId<MyUser>) -> MyPost {
|
|
||||||
MyPost {
|
|
||||||
text,
|
|
||||||
ap_id: ObjectId::new(generate_object_id(creator.inner().domain().unwrap()).unwrap()),
|
|
||||||
creator,
|
|
||||||
local: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct Note {
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
kind: NoteType,
|
|
||||||
id: ObjectId<MyPost>,
|
|
||||||
pub(crate) attributed_to: ObjectId<MyUser>,
|
|
||||||
#[serde(deserialize_with = "deserialize_one_or_many")]
|
|
||||||
pub(crate) to: Vec<Url>,
|
|
||||||
content: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl ApubObject for MyPost {
|
|
||||||
type DataType = DatabaseHandle;
|
|
||||||
type ApubType = Note;
|
|
||||||
type DbType = ();
|
|
||||||
type Error = crate::error::Error;
|
|
||||||
|
|
||||||
async fn read_from_apub_id(
|
|
||||||
_object_id: Url,
|
|
||||||
_data: &RequestData<Self::DataType>,
|
|
||||||
) -> Result<Option<Self>, Self::Error> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn into_apub(
|
|
||||||
self,
|
|
||||||
data: &RequestData<Self::DataType>,
|
|
||||||
) -> Result<Self::ApubType, Self::Error> {
|
|
||||||
let creator = self.creator.dereference_local(data).await?;
|
|
||||||
Ok(Note {
|
|
||||||
kind: Default::default(),
|
|
||||||
id: self.ap_id,
|
|
||||||
attributed_to: self.creator,
|
|
||||||
to: vec![public(), creator.followers_url()?],
|
|
||||||
content: self.text,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn from_apub(
|
|
||||||
apub: Self::ApubType,
|
|
||||||
data: &RequestData<Self::DataType>,
|
|
||||||
) -> Result<Self, Self::Error> {
|
|
||||||
let post = MyPost {
|
|
||||||
text: apub.content,
|
|
||||||
ap_id: apub.id,
|
|
||||||
creator: apub.attributed_to,
|
|
||||||
local: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut lock = data.posts.lock().unwrap();
|
|
||||||
lock.push(post.clone());
|
|
||||||
Ok(post)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,48 +0,0 @@
|
||||||
use crate::{activities::follow::Follow, instance::DatabaseHandle, objects::person::MyUser};
|
|
||||||
use activitypub_federation::{
|
|
||||||
core::object_id::ObjectId,
|
|
||||||
request_data::RequestData,
|
|
||||||
traits::ActivityHandler,
|
|
||||||
};
|
|
||||||
use activitystreams_kinds::activity::AcceptType;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct Accept {
|
|
||||||
actor: ObjectId<MyUser>,
|
|
||||||
object: Follow,
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
kind: AcceptType,
|
|
||||||
id: Url,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Accept {
|
|
||||||
pub fn new(actor: ObjectId<MyUser>, object: Follow, id: Url) -> Accept {
|
|
||||||
Accept {
|
|
||||||
actor,
|
|
||||||
object,
|
|
||||||
kind: Default::default(),
|
|
||||||
id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl ActivityHandler for Accept {
|
|
||||||
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> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
use crate::{
|
|
||||||
instance::DatabaseHandle,
|
|
||||||
objects::{note::Note, person::MyUser},
|
|
||||||
MyPost,
|
|
||||||
};
|
|
||||||
use activitypub_federation::{
|
|
||||||
core::object_id::ObjectId,
|
|
||||||
deser::helpers::deserialize_one_or_many,
|
|
||||||
request_data::RequestData,
|
|
||||||
traits::{ActivityHandler, ApubObject},
|
|
||||||
};
|
|
||||||
use activitystreams_kinds::activity::CreateType;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct CreateNote {
|
|
||||||
pub(crate) actor: ObjectId<MyUser>,
|
|
||||||
#[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 CreateNote {
|
|
||||||
pub fn new(note: Note, id: Url) -> CreateNote {
|
|
||||||
CreateNote {
|
|
||||||
actor: note.attributed_to.clone(),
|
|
||||||
to: note.to.clone(),
|
|
||||||
object: note,
|
|
||||||
kind: CreateType::Create,
|
|
||||||
id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl ActivityHandler for CreateNote {
|
|
||||||
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> {
|
|
||||||
MyPost::from_apub(self.object, data).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,70 +0,0 @@
|
||||||
use crate::{
|
|
||||||
activities::accept::Accept,
|
|
||||||
generate_object_id,
|
|
||||||
instance::DatabaseHandle,
|
|
||||||
objects::person::MyUser,
|
|
||||||
};
|
|
||||||
use activitypub_federation::{
|
|
||||||
core::object_id::ObjectId,
|
|
||||||
request_data::RequestData,
|
|
||||||
traits::{ActivityHandler, Actor},
|
|
||||||
};
|
|
||||||
use activitystreams_kinds::activity::FollowType;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct Follow {
|
|
||||||
pub(crate) actor: ObjectId<MyUser>,
|
|
||||||
pub(crate) object: ObjectId<MyUser>,
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
kind: FollowType,
|
|
||||||
id: Url,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Follow {
|
|
||||||
pub fn new(actor: ObjectId<MyUser>, object: ObjectId<MyUser>, id: Url) -> Follow {
|
|
||||||
Follow {
|
|
||||||
actor,
|
|
||||||
object,
|
|
||||||
kind: Default::default(),
|
|
||||||
id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl ActivityHandler for Follow {
|
|
||||||
type DataType = DatabaseHandle;
|
|
||||||
type Error = crate::error::Error;
|
|
||||||
|
|
||||||
fn id(&self) -> &Url {
|
|
||||||
&self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
fn actor(&self) -> &Url {
|
|
||||||
self.actor.inner()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore clippy false positive: https://github.com/rust-lang/rust-clippy/issues/6446
|
|
||||||
#[allow(clippy::await_holding_lock)]
|
|
||||||
async fn receive(self, data: &RequestData<Self::DataType>) -> Result<(), Self::Error> {
|
|
||||||
// add to followers
|
|
||||||
let local_user = {
|
|
||||||
let mut users = data.users.lock().unwrap();
|
|
||||||
let local_user = users.first_mut().unwrap();
|
|
||||||
local_user.followers.push(self.actor.inner().clone());
|
|
||||||
local_user.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
// send back an accept
|
|
||||||
let follower = self.actor.dereference(data).await?;
|
|
||||||
let id = generate_object_id(data.local_instance().hostname())?;
|
|
||||||
let accept = Accept::new(local_user.ap_id.clone(), self, id.clone());
|
|
||||||
local_user
|
|
||||||
.send(accept, vec![follower.shared_inbox_or_inbox()], data)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
pub mod accept;
|
|
||||||
pub mod create_note;
|
|
||||||
pub mod follow;
|
|
|
@ -1,24 +0,0 @@
|
||||||
/// Necessary because of this issue: https://github.com/actix/actix-web/issues/1711
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Error(anyhow::Error);
|
|
||||||
|
|
||||||
impl<T> From<T> for Error
|
|
||||||
where
|
|
||||||
T: Into<anyhow::Error>,
|
|
||||||
{
|
|
||||||
fn from(t: T) -> Self {
|
|
||||||
Error(t.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod axum {
|
|
||||||
use super::Error;
|
|
||||||
use axum::response::{IntoResponse, Response};
|
|
||||||
use http::StatusCode;
|
|
||||||
|
|
||||||
impl IntoResponse for Error {
|
|
||||||
fn into_response(self) -> Response {
|
|
||||||
(StatusCode::INTERNAL_SERVER_ERROR, format!("{}", self.0)).into_response()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,148 +0,0 @@
|
||||||
use crate::{
|
|
||||||
error::Error,
|
|
||||||
generate_object_id,
|
|
||||||
objects::{
|
|
||||||
note::MyPost,
|
|
||||||
person::{MyUser, Person, PersonAcceptedActivities},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use activitypub_federation::{
|
|
||||||
core::{
|
|
||||||
axum::{inbox::receive_activity, json::ApubJson, verify_request_payload, DigestVerified},
|
|
||||||
object_id::ObjectId,
|
|
||||||
signatures::generate_actor_keypair,
|
|
||||||
},
|
|
||||||
deser::context::WithContext,
|
|
||||||
request_data::{ApubContext, ApubMiddleware, RequestData},
|
|
||||||
traits::ApubObject,
|
|
||||||
FederationSettings,
|
|
||||||
InstanceConfig,
|
|
||||||
UrlVerifier,
|
|
||||||
};
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use axum::{
|
|
||||||
body,
|
|
||||||
body::Body,
|
|
||||||
extract::{Json, OriginalUri},
|
|
||||||
middleware,
|
|
||||||
response::IntoResponse,
|
|
||||||
routing::{get, post},
|
|
||||||
Extension,
|
|
||||||
Router,
|
|
||||||
};
|
|
||||||
use http::{HeaderMap, Method, Request};
|
|
||||||
use reqwest::Client;
|
|
||||||
use std::{
|
|
||||||
net::ToSocketAddrs,
|
|
||||||
sync::{Arc, Mutex},
|
|
||||||
};
|
|
||||||
use tokio::task;
|
|
||||||
use tower::ServiceBuilder;
|
|
||||||
use tower_http::{trace::TraceLayer, ServiceBuilderExt};
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
pub type DatabaseHandle = Arc<Database>;
|
|
||||||
|
|
||||||
/// Our "database" which contains all known posts and users (local and federated)
|
|
||||||
pub struct Database {
|
|
||||||
pub users: Mutex<Vec<MyUser>>,
|
|
||||||
pub posts: Mutex<Vec<MyPost>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Use this to store your federation blocklist, or a database connection needed to retrieve it.
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct MyUrlVerifier();
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl UrlVerifier for MyUrlVerifier {
|
|
||||||
async fn verify(&self, url: &Url) -> Result<(), &'static str> {
|
|
||||||
if url.domain() == Some("malicious.com") {
|
|
||||||
Err("malicious domain")
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Database {
|
|
||||||
pub fn new(hostname: String) -> Result<ApubContext<DatabaseHandle>, Error> {
|
|
||||||
let settings = FederationSettings::builder()
|
|
||||||
.debug(true)
|
|
||||||
.url_verifier(Box::new(MyUrlVerifier()))
|
|
||||||
.build()?;
|
|
||||||
let local_instance =
|
|
||||||
InstanceConfig::new(hostname.clone(), Client::default().into(), settings);
|
|
||||||
let local_user = MyUser::new(generate_object_id(&hostname)?, generate_actor_keypair()?);
|
|
||||||
let instance = Arc::new(Database {
|
|
||||||
users: Mutex::new(vec![local_user]),
|
|
||||||
posts: Mutex::new(vec![]),
|
|
||||||
});
|
|
||||||
Ok(ApubContext::new(instance, local_instance))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn local_user(&self) -> MyUser {
|
|
||||||
self.users.lock().unwrap().first().cloned().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn listen(data: &ApubContext<DatabaseHandle>) -> Result<(), Error> {
|
|
||||||
let hostname = data.local_instance().hostname();
|
|
||||||
let data = data.clone();
|
|
||||||
let app = Router::new()
|
|
||||||
.route("/inbox", post(http_post_user_inbox))
|
|
||||||
.layer(
|
|
||||||
ServiceBuilder::new()
|
|
||||||
.map_request_body(body::boxed)
|
|
||||||
.layer(middleware::from_fn(verify_request_payload)),
|
|
||||||
)
|
|
||||||
.route("/objects/:user_name", get(http_get_user))
|
|
||||||
.layer(ApubMiddleware::new(data))
|
|
||||||
.layer(TraceLayer::new_for_http());
|
|
||||||
|
|
||||||
// run it
|
|
||||||
let addr = hostname
|
|
||||||
.to_socket_addrs()?
|
|
||||||
.next()
|
|
||||||
.expect("Failed to lookup domain name");
|
|
||||||
let server = axum::Server::bind(&addr).serve(app.into_make_service());
|
|
||||||
|
|
||||||
task::spawn(server);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn http_get_user(
|
|
||||||
data: RequestData<DatabaseHandle>,
|
|
||||||
request: Request<Body>,
|
|
||||||
) -> Result<ApubJson<WithContext<Person>>, Error> {
|
|
||||||
let hostname: String = data.local_instance().hostname().to_string();
|
|
||||||
let request_url = format!("http://{}{}", hostname, &request.uri());
|
|
||||||
|
|
||||||
let url = Url::parse(&request_url).expect("Failed to parse url");
|
|
||||||
|
|
||||||
let user = ObjectId::<MyUser>::new(url)
|
|
||||||
.dereference_local(&data)
|
|
||||||
.await?
|
|
||||||
.into_apub(&data)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(ApubJson(WithContext::new_default(user)))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn http_post_user_inbox(
|
|
||||||
headers: HeaderMap,
|
|
||||||
method: Method,
|
|
||||||
OriginalUri(uri): OriginalUri,
|
|
||||||
data: RequestData<DatabaseHandle>,
|
|
||||||
Extension(digest_verified): Extension<DigestVerified>,
|
|
||||||
Json(activity): Json<WithContext<PersonAcceptedActivities>>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
receive_activity::<WithContext<PersonAcceptedActivities>, MyUser, DatabaseHandle>(
|
|
||||||
digest_verified,
|
|
||||||
activity,
|
|
||||||
&data,
|
|
||||||
headers,
|
|
||||||
method,
|
|
||||||
uri,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
use crate::{error::Error, instance::Database, objects::note::MyPost, utils::generate_object_id};
|
|
||||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
|
||||||
|
|
||||||
mod activities;
|
|
||||||
mod error;
|
|
||||||
mod instance;
|
|
||||||
mod objects;
|
|
||||||
mod utils;
|
|
||||||
|
|
||||||
#[actix_rt::main]
|
|
||||||
async fn main() -> Result<(), Error> {
|
|
||||||
tracing_subscriber::registry()
|
|
||||||
.with(tracing_subscriber::EnvFilter::new(
|
|
||||||
std::env::var("RUST_LOG").unwrap_or_else(|_| {
|
|
||||||
"activitypub_federation=debug,federation-axum=debug,tower_http=debug".into()
|
|
||||||
}),
|
|
||||||
))
|
|
||||||
.with(tracing_subscriber::fmt::layer())
|
|
||||||
.init();
|
|
||||||
|
|
||||||
let alpha = Database::new("localhost:8001".to_string())?;
|
|
||||||
let beta = Database::new("localhost:8002".to_string())?;
|
|
||||||
Database::listen(&alpha)?;
|
|
||||||
Database::listen(&beta)?;
|
|
||||||
|
|
||||||
// alpha user follows beta user
|
|
||||||
alpha
|
|
||||||
.local_user()
|
|
||||||
.follow(&beta.local_user(), &alpha.to_request_data())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// assert that follow worked correctly
|
|
||||||
assert_eq!(
|
|
||||||
beta.local_user().followers(),
|
|
||||||
&vec![alpha.local_user().ap_id.inner().clone()]
|
|
||||||
);
|
|
||||||
|
|
||||||
// beta sends a post to its followers
|
|
||||||
let sent_post = MyPost::new("hello world!".to_string(), beta.local_user().ap_id);
|
|
||||||
beta.local_user()
|
|
||||||
.post(sent_post.clone(), &beta.to_request_data())
|
|
||||||
.await?;
|
|
||||||
let received_post = alpha.posts.lock().unwrap().first().cloned().unwrap();
|
|
||||||
|
|
||||||
// assert that alpha received the post
|
|
||||||
assert_eq!(received_post.text, sent_post.text);
|
|
||||||
assert_eq!(received_post.ap_id.inner(), sent_post.ap_id.inner());
|
|
||||||
assert_eq!(received_post.creator.inner(), sent_post.creator.inner());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
pub mod note;
|
|
||||||
pub mod person;
|
|
|
@ -1,188 +0,0 @@
|
||||||
use crate::{
|
|
||||||
activities::{accept::Accept, create_note::CreateNote, follow::Follow},
|
|
||||||
error::Error,
|
|
||||||
instance::DatabaseHandle,
|
|
||||||
objects::note::MyPost,
|
|
||||||
utils::generate_object_id,
|
|
||||||
};
|
|
||||||
use activitypub_federation::{
|
|
||||||
core::{
|
|
||||||
activity_queue::send_activity,
|
|
||||||
object_id::ObjectId,
|
|
||||||
signatures::{Keypair, PublicKey},
|
|
||||||
},
|
|
||||||
deser::context::WithContext,
|
|
||||||
request_data::RequestData,
|
|
||||||
traits::{ActivityHandler, Actor, ApubObject},
|
|
||||||
};
|
|
||||||
use activitystreams_kinds::actor::PersonType;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct MyUser {
|
|
||||||
pub ap_id: ObjectId<MyUser>,
|
|
||||||
pub inbox: Url,
|
|
||||||
// exists for all users (necessary to verify http signatures)
|
|
||||||
public_key: String,
|
|
||||||
// exists only for local users
|
|
||||||
private_key: Option<String>,
|
|
||||||
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 {
|
|
||||||
Follow(Follow),
|
|
||||||
Accept(Accept),
|
|
||||||
CreateNote(CreateNote),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MyUser {
|
|
||||||
pub fn new(ap_id: Url, keypair: Keypair) -> MyUser {
|
|
||||||
let mut inbox = ap_id.clone();
|
|
||||||
inbox.set_path("/inbox");
|
|
||||||
let ap_id = ObjectId::new(ap_id);
|
|
||||||
MyUser {
|
|
||||||
ap_id,
|
|
||||||
inbox,
|
|
||||||
public_key: keypair.public_key,
|
|
||||||
private_key: Some(keypair.private_key),
|
|
||||||
followers: vec![],
|
|
||||||
local: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct Person {
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
kind: PersonType,
|
|
||||||
id: ObjectId<MyUser>,
|
|
||||||
inbox: Url,
|
|
||||||
public_key: PublicKey,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MyUser {
|
|
||||||
pub fn followers(&self) -> &Vec<Url> {
|
|
||||||
&self.followers
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn followers_url(&self) -> Result<Url, Error> {
|
|
||||||
Ok(Url::parse(&format!("{}/followers", self.ap_id.inner()))?)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn public_key(&self) -> PublicKey {
|
|
||||||
PublicKey::new_main_key(self.ap_id.clone().into_inner(), self.public_key.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn follow(
|
|
||||||
&self,
|
|
||||||
other: &MyUser,
|
|
||||||
data: &RequestData<DatabaseHandle>,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let id = generate_object_id(data.local_instance().hostname())?;
|
|
||||||
let follow = Follow::new(self.ap_id.clone(), other.ap_id.clone(), id.clone());
|
|
||||||
self.send(follow, vec![other.shared_inbox_or_inbox()], data)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn post(
|
|
||||||
&self,
|
|
||||||
post: MyPost,
|
|
||||||
data: &RequestData<DatabaseHandle>,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let id = generate_object_id(data.local_instance().hostname())?;
|
|
||||||
let create = CreateNote::new(post.into_apub(data).await?, id.clone());
|
|
||||||
let mut inboxes = vec![];
|
|
||||||
for f in self.followers.clone() {
|
|
||||||
let user: MyUser = ObjectId::new(f).dereference(data).await?;
|
|
||||||
inboxes.push(user.shared_inbox_or_inbox());
|
|
||||||
}
|
|
||||||
self.send(create, inboxes, data).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn send<Activity>(
|
|
||||||
&self,
|
|
||||||
activity: Activity,
|
|
||||||
recipients: Vec<Url>,
|
|
||||||
data: &RequestData<DatabaseHandle>,
|
|
||||||
) -> Result<(), <Activity as ActivityHandler>::Error>
|
|
||||||
where
|
|
||||||
Activity: ActivityHandler + Serialize + Send + Sync,
|
|
||||||
<Activity as ActivityHandler>::Error: From<anyhow::Error> + From<serde_json::Error>,
|
|
||||||
{
|
|
||||||
let activity = WithContext::new_default(activity);
|
|
||||||
send_activity(
|
|
||||||
activity,
|
|
||||||
self.public_key(),
|
|
||||||
self.private_key.clone().expect("has private key"),
|
|
||||||
recipients,
|
|
||||||
data.local_instance(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl ApubObject for MyUser {
|
|
||||||
type DataType = DatabaseHandle;
|
|
||||||
type ApubType = Person;
|
|
||||||
type DbType = MyUser;
|
|
||||||
type Error = crate::error::Error;
|
|
||||||
|
|
||||||
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> {
|
|
||||||
Ok(Person {
|
|
||||||
kind: Default::default(),
|
|
||||||
id: self.ap_id.clone(),
|
|
||||||
inbox: self.inbox.clone(),
|
|
||||||
public_key: self.public_key(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn from_apub(
|
|
||||||
apub: Self::ApubType,
|
|
||||||
_data: &RequestData<Self::DataType>,
|
|
||||||
) -> Result<Self, Self::Error> {
|
|
||||||
Ok(MyUser {
|
|
||||||
ap_id: apub.id,
|
|
||||||
inbox: apub.inbox,
|
|
||||||
public_key: apub.public_key.public_key_pem,
|
|
||||||
private_key: None,
|
|
||||||
followers: vec![],
|
|
||||||
local: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Actor for MyUser {
|
|
||||||
fn public_key(&self) -> &str {
|
|
||||||
&self.public_key
|
|
||||||
}
|
|
||||||
|
|
||||||
fn inbox(&self) -> Url {
|
|
||||||
self.inbox.clone()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
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(hostname: &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))
|
|
||||||
}
|
|
66
examples/simple_federation/actix_web/http.rs
Normal file
66
examples/simple_federation/actix_web/http.rs
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
use crate::{
|
||||||
|
error::Error,
|
||||||
|
instance::DatabaseHandle,
|
||||||
|
objects::person::{MyUser, PersonAcceptedActivities},
|
||||||
|
};
|
||||||
|
use activitypub_federation::{
|
||||||
|
core::{actix_web::inbox::receive_activity, object_id::ObjectId},
|
||||||
|
deser::context::WithContext,
|
||||||
|
request_data::{ApubContext, ApubMiddleware, RequestData},
|
||||||
|
traits::ApubObject,
|
||||||
|
APUB_JSON_CONTENT_TYPE,
|
||||||
|
};
|
||||||
|
use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer};
|
||||||
|
use tokio::task;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
pub fn listen(data: &ApubContext<DatabaseHandle>) -> Result<(), Error> {
|
||||||
|
let hostname = data.local_instance().hostname();
|
||||||
|
let data = data.clone();
|
||||||
|
let server = HttpServer::new(move || {
|
||||||
|
App::new()
|
||||||
|
.wrap(ApubMiddleware::new(data.clone()))
|
||||||
|
.route("/objects/{user_name}", web::get().to(http_get_user))
|
||||||
|
.service(
|
||||||
|
web::scope("")
|
||||||
|
// Just a single, global inbox for simplicity
|
||||||
|
.route("/inbox", web::post().to(http_post_user_inbox)),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.bind(hostname)?
|
||||||
|
.run();
|
||||||
|
task::spawn(server);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles requests to fetch user json over HTTP
|
||||||
|
pub async fn http_get_user(
|
||||||
|
request: HttpRequest,
|
||||||
|
data: RequestData<DatabaseHandle>,
|
||||||
|
) -> Result<HttpResponse, Error> {
|
||||||
|
let hostname: String = data.local_instance().hostname().to_string();
|
||||||
|
let request_url = format!("http://{}{}", hostname, &request.uri().to_string());
|
||||||
|
let url = Url::parse(&request_url)?;
|
||||||
|
let user = ObjectId::<MyUser>::new(url)
|
||||||
|
.dereference_local(&data)
|
||||||
|
.await?
|
||||||
|
.into_apub(&data)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok()
|
||||||
|
.content_type(APUB_JSON_CONTENT_TYPE)
|
||||||
|
.json(WithContext::new_default(user)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles messages received in user inbox
|
||||||
|
pub async fn http_post_user_inbox(
|
||||||
|
request: HttpRequest,
|
||||||
|
payload: String,
|
||||||
|
data: RequestData<DatabaseHandle>,
|
||||||
|
) -> Result<HttpResponse, Error> {
|
||||||
|
let activity = serde_json::from_str(&payload)?;
|
||||||
|
receive_activity::<WithContext<PersonAcceptedActivities>, MyUser, DatabaseHandle>(
|
||||||
|
request, activity, &data,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
6
examples/simple_federation/actix_web/mod.rs
Normal file
6
examples/simple_federation/actix_web/mod.rs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
use crate::error::Error;
|
||||||
|
use actix_web::ResponseError;
|
||||||
|
|
||||||
|
pub(crate) mod http;
|
||||||
|
|
||||||
|
impl ResponseError for Error {}
|
93
examples/simple_federation/axum/http.rs
Normal file
93
examples/simple_federation/axum/http.rs
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
use crate::{
|
||||||
|
error::Error,
|
||||||
|
instance::DatabaseHandle,
|
||||||
|
objects::person::{MyUser, Person, PersonAcceptedActivities},
|
||||||
|
};
|
||||||
|
use activitypub_federation::{
|
||||||
|
core::{
|
||||||
|
axum::{inbox::receive_activity, json::ApubJson, verify_request_payload, DigestVerified},
|
||||||
|
object_id::ObjectId,
|
||||||
|
},
|
||||||
|
deser::context::WithContext,
|
||||||
|
request_data::{ApubContext, ApubMiddleware, RequestData},
|
||||||
|
traits::ApubObject,
|
||||||
|
};
|
||||||
|
use axum::{
|
||||||
|
body,
|
||||||
|
extract::OriginalUri,
|
||||||
|
middleware,
|
||||||
|
response::IntoResponse,
|
||||||
|
routing::{get, post},
|
||||||
|
Extension,
|
||||||
|
Json,
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
use http::{HeaderMap, Method, Request};
|
||||||
|
use hyper::Body;
|
||||||
|
use std::net::ToSocketAddrs;
|
||||||
|
use tokio::task;
|
||||||
|
use tower::ServiceBuilder;
|
||||||
|
use tower_http::{trace::TraceLayer, ServiceBuilderExt};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
pub fn listen(data: &ApubContext<DatabaseHandle>) -> Result<(), Error> {
|
||||||
|
let hostname = data.local_instance().hostname();
|
||||||
|
let data = data.clone();
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/inbox", post(http_post_user_inbox))
|
||||||
|
.layer(
|
||||||
|
ServiceBuilder::new()
|
||||||
|
.map_request_body(body::boxed)
|
||||||
|
.layer(middleware::from_fn(verify_request_payload)),
|
||||||
|
)
|
||||||
|
.route("/objects/:user_name", get(http_get_user))
|
||||||
|
.layer(ApubMiddleware::new(data))
|
||||||
|
.layer(TraceLayer::new_for_http());
|
||||||
|
|
||||||
|
// run it
|
||||||
|
let addr = hostname
|
||||||
|
.to_socket_addrs()?
|
||||||
|
.next()
|
||||||
|
.expect("Failed to lookup domain name");
|
||||||
|
let server = axum::Server::bind(&addr).serve(app.into_make_service());
|
||||||
|
|
||||||
|
task::spawn(server);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn http_get_user(
|
||||||
|
data: RequestData<DatabaseHandle>,
|
||||||
|
request: Request<Body>,
|
||||||
|
) -> Result<ApubJson<WithContext<Person>>, Error> {
|
||||||
|
let hostname: String = data.local_instance().hostname().to_string();
|
||||||
|
let request_url = format!("http://{}{}", hostname, &request.uri());
|
||||||
|
|
||||||
|
let url = Url::parse(&request_url).expect("Failed to parse url");
|
||||||
|
|
||||||
|
let user = ObjectId::<MyUser>::new(url)
|
||||||
|
.dereference_local(&data)
|
||||||
|
.await?
|
||||||
|
.into_apub(&data)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(ApubJson(WithContext::new_default(user)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn http_post_user_inbox(
|
||||||
|
headers: HeaderMap,
|
||||||
|
method: Method,
|
||||||
|
OriginalUri(uri): OriginalUri,
|
||||||
|
data: RequestData<DatabaseHandle>,
|
||||||
|
Extension(digest_verified): Extension<DigestVerified>,
|
||||||
|
Json(activity): Json<WithContext<PersonAcceptedActivities>>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
receive_activity::<WithContext<PersonAcceptedActivities>, MyUser, DatabaseHandle>(
|
||||||
|
digest_verified,
|
||||||
|
activity,
|
||||||
|
&data,
|
||||||
|
headers,
|
||||||
|
method,
|
||||||
|
uri,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
11
examples/simple_federation/axum/mod.rs
Normal file
11
examples/simple_federation/axum/mod.rs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
use crate::error::Error;
|
||||||
|
use ::http::StatusCode;
|
||||||
|
use axum::response::{IntoResponse, Response};
|
||||||
|
|
||||||
|
pub mod http;
|
||||||
|
|
||||||
|
impl IntoResponse for Error {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, format!("{}", self.0)).into_response()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,8 @@
|
||||||
use actix_web::ResponseError;
|
|
||||||
use std::fmt::{Display, Formatter};
|
use std::fmt::{Display, Formatter};
|
||||||
|
|
||||||
/// Necessary because of this issue: https://github.com/actix/actix-web/issues/1711
|
/// Necessary because of this issue: https://github.com/actix/actix-web/issues/1711
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Error(anyhow::Error);
|
pub struct Error(pub(crate) anyhow::Error);
|
||||||
|
|
||||||
impl Display for Error {
|
impl Display for Error {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
@ -19,5 +18,3 @@ where
|
||||||
Error(t.into())
|
Error(t.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResponseError for Error {}
|
|
68
examples/simple_federation/instance.rs
Normal file
68
examples/simple_federation/instance.rs
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
use crate::{
|
||||||
|
generate_object_id,
|
||||||
|
objects::{note::MyPost, person::MyUser},
|
||||||
|
Error,
|
||||||
|
};
|
||||||
|
use activitypub_federation::{
|
||||||
|
core::signatures::generate_actor_keypair,
|
||||||
|
request_data::ApubContext,
|
||||||
|
FederationSettings,
|
||||||
|
InstanceConfig,
|
||||||
|
UrlVerifier,
|
||||||
|
};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use reqwest::Client;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
pub type DatabaseHandle = Arc<Database>;
|
||||||
|
|
||||||
|
/// Our "database" which contains all known posts users (local and federated)
|
||||||
|
pub struct Database {
|
||||||
|
pub users: Mutex<Vec<MyUser>>,
|
||||||
|
pub posts: Mutex<Vec<MyPost>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Use this to store your federation blocklist, or a database connection needed to retrieve it.
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct MyUrlVerifier();
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl UrlVerifier for MyUrlVerifier {
|
||||||
|
async fn verify(&self, url: &Url) -> Result<(), &'static str> {
|
||||||
|
if url.domain() == Some("malicious.com") {
|
||||||
|
Err("malicious domain")
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn listen(data: &ApubContext<DatabaseHandle>) -> Result<(), Error> {
|
||||||
|
#[cfg(feature = "actix-web")]
|
||||||
|
crate::actix_web::http::listen(data)?;
|
||||||
|
#[cfg(feature = "axum")]
|
||||||
|
crate::axum::http::listen(data)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Database {
|
||||||
|
pub fn new(hostname: String) -> Result<ApubContext<DatabaseHandle>, Error> {
|
||||||
|
let settings = FederationSettings::builder()
|
||||||
|
.debug(true)
|
||||||
|
.url_verifier(Box::new(MyUrlVerifier()))
|
||||||
|
.build()?;
|
||||||
|
let local_instance =
|
||||||
|
InstanceConfig::new(hostname.clone(), Client::default().into(), settings);
|
||||||
|
let local_user = MyUser::new(generate_object_id(&hostname)?, generate_actor_keypair()?);
|
||||||
|
let instance = Arc::new(Database {
|
||||||
|
users: Mutex::new(vec![local_user]),
|
||||||
|
posts: Mutex::new(vec![]),
|
||||||
|
});
|
||||||
|
Ok(ApubContext::new(instance, local_instance))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn local_user(&self) -> MyUser {
|
||||||
|
self.users.lock().unwrap().first().cloned().unwrap()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,16 @@
|
||||||
use crate::{error::Error, instance::Database, objects::note::MyPost, utils::generate_object_id};
|
use crate::{
|
||||||
|
instance::{listen, Database},
|
||||||
|
objects::note::MyPost,
|
||||||
|
utils::generate_object_id,
|
||||||
|
};
|
||||||
|
use error::Error;
|
||||||
use tracing::log::LevelFilter;
|
use tracing::log::LevelFilter;
|
||||||
|
|
||||||
mod activities;
|
mod activities;
|
||||||
|
#[cfg(feature = "actix-web")]
|
||||||
|
mod actix_web;
|
||||||
|
#[cfg(feature = "axum")]
|
||||||
|
mod axum;
|
||||||
mod error;
|
mod error;
|
||||||
mod instance;
|
mod instance;
|
||||||
mod objects;
|
mod objects;
|
||||||
|
@ -15,8 +24,8 @@ async fn main() -> Result<(), Error> {
|
||||||
|
|
||||||
let alpha = Database::new("localhost:8001".to_string())?;
|
let alpha = Database::new("localhost:8001".to_string())?;
|
||||||
let beta = Database::new("localhost:8002".to_string())?;
|
let beta = Database::new("localhost:8002".to_string())?;
|
||||||
Database::listen(&alpha)?;
|
listen(&alpha)?;
|
||||||
Database::listen(&beta)?;
|
listen(&beta)?;
|
||||||
|
|
||||||
// alpha user follows beta user
|
// alpha user follows beta user
|
||||||
alpha
|
alpha
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::{generate_object_id, instance::DatabaseHandle, objects::person::MyUser};
|
use crate::{error::Error, generate_object_id, instance::DatabaseHandle, objects::person::MyUser};
|
||||||
use activitypub_federation::{
|
use activitypub_federation::{
|
||||||
core::object_id::ObjectId,
|
core::object_id::ObjectId,
|
||||||
deser::helpers::deserialize_one_or_many,
|
deser::helpers::deserialize_one_or_many,
|
||||||
|
@ -45,7 +45,7 @@ impl ApubObject for MyPost {
|
||||||
type DataType = DatabaseHandle;
|
type DataType = DatabaseHandle;
|
||||||
type ApubType = Note;
|
type ApubType = Note;
|
||||||
type DbType = ();
|
type DbType = ();
|
||||||
type Error = crate::error::Error;
|
type Error = Error;
|
||||||
|
|
||||||
async fn read_from_apub_id(
|
async fn read_from_apub_id(
|
||||||
_object_id: Url,
|
_object_id: Url,
|
|
@ -136,7 +136,7 @@ impl ApubObject for MyUser {
|
||||||
type DataType = DatabaseHandle;
|
type DataType = DatabaseHandle;
|
||||||
type ApubType = Person;
|
type ApubType = Person;
|
||||||
type DbType = MyUser;
|
type DbType = MyUser;
|
||||||
type Error = crate::error::Error;
|
type Error = Error;
|
||||||
|
|
||||||
async fn read_from_apub_id(
|
async fn read_from_apub_id(
|
||||||
object_id: Url,
|
object_id: Url,
|
|
@ -5,5 +5,5 @@ pub mod signatures;
|
||||||
#[cfg(feature = "axum")]
|
#[cfg(feature = "axum")]
|
||||||
pub mod axum;
|
pub mod axum;
|
||||||
|
|
||||||
#[cfg(feature = "actix")]
|
#[cfg(feature = "actix-web")]
|
||||||
pub mod actix;
|
pub mod actix_web;
|
||||||
|
|
Loading…
Reference in a new issue