Skip to content

Commit

Permalink
live federation example
Browse files Browse the repository at this point in the history
  • Loading branch information
Nutomic committed Mar 8, 2023
1 parent 3239469 commit d94a2ed
Show file tree
Hide file tree
Showing 27 changed files with 597 additions and 34 deletions.
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
6 changes: 4 additions & 2 deletions docs/03_federating_users.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
}
```

Expand Down
25 changes: 25 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 '*[email protected]*'` (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:[email protected]" | jq` that the server is setup correctly and serving correct responses.
- Login to a Fediverse platform like Mastodon, and search for `@[email protected]`, with the actual domain and username from your `main.rs`. If you send a message, it will automatically send a response.
72 changes: 72 additions & 0 deletions examples/live_federation/activities/create_post.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
}
1 change: 1 addition & 0 deletions examples/live_federation/activities/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod create_post;
26 changes: 26 additions & 0 deletions examples/live_federation/database.rs
Original file line number Diff line number Diff line change
@@ -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())
}
}
}
20 changes: 20 additions & 0 deletions examples/live_federation/error.rs
Original file line number Diff line number Diff line change
@@ -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())
}
}
69 changes: 69 additions & 0 deletions examples/live_federation/http.rs
Original file line number Diff line number Diff line change
@@ -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(),
)))
}
69 changes: 69 additions & 0 deletions examples/live_federation/main.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
2 changes: 2 additions & 0 deletions examples/live_federation/objects/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod person;
pub mod post;
Loading

0 comments on commit d94a2ed

Please sign in to comment.