-
Notifications
You must be signed in to change notification settings - Fork 46
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
27 changed files
with
597 additions
and
34 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {}", ¬e.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(()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
pub mod create_post; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(), | ||
))) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
pub mod person; | ||
pub mod post; |
Oops, something went wrong.