From 86e84d88aa89f12d42e994e5f2b2d6853042c8a7 Mon Sep 17 00:00:00 2001 From: alemi Date: Thu, 11 Apr 2024 00:29:32 +0200 Subject: [PATCH] chore: updated apb things, restructured a bit --- src/main.rs | 9 +- src/model/faker.rs | 15 +- src/model/user.rs | 13 +- src/routes/activitypub/application.rs | 25 ++++ src/routes/activitypub/auth.rs | 48 +++++++ src/routes/activitypub/inbox.rs | 4 +- src/routes/activitypub/mod.rs | 196 +++++--------------------- src/routes/activitypub/router.rs | 46 ------ src/routes/activitypub/user/inbox.rs | 4 +- src/routes/activitypub/user/outbox.rs | 4 +- src/routes/mastodon/mod.rs | 129 ++++++++--------- src/routes/mod.rs | 9 ++ src/server/context.rs | 4 +- src/server/inbox.rs | 9 +- src/server/outbox.rs | 10 +- 15 files changed, 237 insertions(+), 288 deletions(-) create mode 100644 src/routes/activitypub/application.rs create mode 100644 src/routes/activitypub/auth.rs delete mode 100644 src/routes/activitypub/router.rs diff --git a/src/main.rs b/src/main.rs index f66a06b..3c12cff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -99,7 +99,13 @@ async fn main() { let ctx = server::Context::new(db, args.domain) .await.expect("failed creating server context"); - let router = routes::activitypub::router().with_state(ctx); + use routes::activitypub::ActivityPubRouter; + use routes::mastodon::MastodonRouter; + + let router = axum::Router::new() + .ap_routes() + .mastodon_routes() // no-op if mastodon feature is disabled + .with_state(ctx); // run our app with hyper, listening locally on port 3000 let listener = tokio::net::TcpListener::bind("127.0.0.1:3000") @@ -113,7 +119,6 @@ async fn main() { } - async fn fetch(db: &sea_orm::DatabaseConnection, uri: &str, save: bool) -> reqwest::Result<()> { use apb::{Base, Object}; diff --git a/src/model/faker.rs b/src/model/faker.rs index 68db65d..fbf6f3a 100644 --- a/src/model/faker.rs +++ b/src/model/faker.rs @@ -1,4 +1,4 @@ -use crate::{routes::activitypub::PUBLIC_TARGET, model::{config, credential}}; +use crate::model::{config, credential}; use super::{activity, object, user, Audience}; use openssl::rsa::Rsa; use sea_orm::IntoActiveModel; @@ -10,7 +10,7 @@ pub async fn faker(db: &sea_orm::DatabaseConnection, domain: String, count: u64) let test_user = super::user::Model { id: format!("{domain}/users/test"), name: Some("μpub".into()), - domain: crate::routes::activitypub::domain(&domain), + domain: clean_domain(&domain), preferred_username: "test".to_string(), summary: Some("hello world! i'm manually generated but served dynamically from db! check progress at https://git.alemi.dev/upub.git".to_string()), following: None, @@ -64,7 +64,7 @@ pub async fn faker(db: &sea_orm::DatabaseConnection, domain: String, count: u64) comments: Set(0), likes: Set(0), shares: Set(0), - to: Set(Audience(vec![PUBLIC_TARGET.to_string()])), + to: Set(Audience(vec![apb::target::PUBLIC.to_string()])), bto: Set(Audience::default()), cc: Set(Audience(vec![])), bcc: Set(Audience::default()), @@ -77,7 +77,7 @@ pub async fn faker(db: &sea_orm::DatabaseConnection, domain: String, count: u64) object: Set(Some(format!("{domain}/objects/{oid}"))), target: Set(None), published: Set(chrono::Utc::now() - std::time::Duration::from_secs(60*i)), - to: Set(Audience(vec![PUBLIC_TARGET.to_string()])), + to: Set(Audience(vec![apb::target::PUBLIC.to_string()])), bto: Set(Audience::default()), cc: Set(Audience(vec![])), bcc: Set(Audience::default()), @@ -86,3 +86,10 @@ pub async fn faker(db: &sea_orm::DatabaseConnection, domain: String, count: u64) Ok(()) } + +fn clean_domain(domain: &str) -> String { + domain + .replace("http://", "") + .replace("https://", "") + .replace('/', "") +} diff --git a/src/model/user.rs b/src/model/user.rs index c090444..233481d 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -1,7 +1,6 @@ use sea_orm::entity::prelude::*; use apb::{Collection, Actor, PublicKey, ActorType}; -use crate::routes::activitypub; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "users")] @@ -40,7 +39,7 @@ pub struct Model { impl Model { pub fn new(object: &impl Actor) -> Result { let ap_id = object.id().ok_or(super::FieldError("id"))?.to_string(); - let (domain, preferred_username) = activitypub::split_id(&ap_id); + let (domain, preferred_username) = split_user_id(&ap_id); Ok(Model { id: ap_id, preferred_username, domain, actor_type: object.actor_type().ok_or(super::FieldError("type"))?, @@ -121,3 +120,13 @@ impl Related for Entity { } impl ActiveModelBehavior for ActiveModel {} + +fn split_user_id(id: &str) -> (String, String) { + let clean = id + .replace("http://", "") + .replace("https://", ""); + let mut splits = clean.split('/'); + let first = splits.next().unwrap_or(""); + let last = splits.last().unwrap_or(first); + (first.to_string(), last.to_string()) +} diff --git a/src/routes/activitypub/application.rs b/src/routes/activitypub/application.rs new file mode 100644 index 0000000..a828f04 --- /dev/null +++ b/src/routes/activitypub/application.rs @@ -0,0 +1,25 @@ +use apb::{ActorMut, BaseMut, ObjectMut, PublicKeyMut}; +use axum::{extract::State, http::StatusCode, Json}; + +use crate::{server::Context, url}; + +use super::jsonld::LD; + + +pub async fn view(State(ctx): State) -> Result, StatusCode> { + Ok(Json( + serde_json::Value::new_object() + .set_id(Some(&url!(ctx, ""))) + .set_actor_type(Some(apb::ActorType::Application)) + .set_name(Some("μpub")) + .set_summary(Some("micro social network, federated")) + .set_published(Some(ctx.app().created)) + .set_public_key(apb::Node::object( + serde_json::Value::new_object() + .set_id(Some(&url!(ctx, "#main-key"))) + .set_owner(Some(&url!(ctx, ""))) + .set_public_key_pem(&ctx.app().public_key) + )) + .ld_context() + )) +} diff --git a/src/routes/activitypub/auth.rs b/src/routes/activitypub/auth.rs new file mode 100644 index 0000000..c513068 --- /dev/null +++ b/src/routes/activitypub/auth.rs @@ -0,0 +1,48 @@ +use axum::{http::StatusCode, extract::State, Json}; +use rand::Rng; +use sea_orm::{ColumnTrait, Condition, EntityTrait, QueryFilter}; + +use crate::{model, server::Context}; + + +#[derive(Debug, Clone, serde::Deserialize)] +pub struct LoginForm { + email: String, + password: String, +} + +pub async fn login(State(ctx): State, Json(login): Json) -> Result, StatusCode> { + // TODO salt the pwd + match model::credential::Entity::find() + .filter(Condition::all() + .add(model::credential::Column::Email.eq(login.email)) + .add(model::credential::Column::Password.eq(sha256::digest(login.password))) + ) + .one(ctx.db()) + .await + { + Ok(Some(x)) => { + // TODO should probably use crypto-safe rng + let token : String = rand::thread_rng() + .sample_iter(&rand::distributions::Alphanumeric) + .take(128) + .map(char::from) + .collect(); + model::session::Entity::insert( + model::session::ActiveModel { + id: sea_orm::ActiveValue::Set(token.clone()), + actor: sea_orm::ActiveValue::Set(x.id), + expires: sea_orm::ActiveValue::Set(chrono::Utc::now() + std::time::Duration::from_secs(3600 * 6)), + } + ) + .exec(ctx.db()) + .await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + Ok(Json(serde_json::Value::String(token))) + }, + Ok(None) => Err(StatusCode::UNAUTHORIZED), + Err(e) => { + tracing::error!("error querying db for user credentials: {e}"); + Err(StatusCode::INTERNAL_SERVER_ERROR) + } + } +} diff --git a/src/routes/activitypub/inbox.rs b/src/routes/activitypub/inbox.rs index 409031c..6b53c75 100644 --- a/src/routes/activitypub/inbox.rs +++ b/src/routes/activitypub/inbox.rs @@ -3,7 +3,7 @@ use sea_orm::{ColumnTrait, Condition, EntityTrait, Order, QueryFilter, QueryOrde use crate::{server::auth::{AuthIdentity, Identity}, errors::UpubError, model, server::Context, url}; -use super::{activity::ap_activity, jsonld::LD, JsonLD, Pagination, PUBLIC_TARGET}; +use super::{activity::ap_activity, jsonld::LD, JsonLD, Pagination}; pub async fn get( @@ -20,7 +20,7 @@ pub async fn page( let limit = page.batch.unwrap_or(20).min(50); let offset = page.offset.unwrap_or(0); let mut condition = Condition::any() - .add(model::addressing::Column::Actor.eq(PUBLIC_TARGET)); + .add(model::addressing::Column::Actor.eq(apb::target::PUBLIC)); if let Identity::Local(user) = auth { condition = condition .add(model::addressing::Column::Actor.eq(user)); diff --git a/src/routes/activitypub/mod.rs b/src/routes/activitypub/mod.rs index e56e26a..30565f3 100644 --- a/src/routes/activitypub/mod.rs +++ b/src/routes/activitypub/mod.rs @@ -3,90 +3,56 @@ pub mod inbox; pub mod outbox; pub mod object; pub mod activity; +pub mod application; +pub mod auth; pub mod well_known; -pub mod router; pub mod jsonld; pub use jsonld::JsonLD; -use axum::{extract::State, http::StatusCode, response::IntoResponse, routing::{get, post}, Json, Router}; -use rand::Rng; -use sea_orm::{ColumnTrait, Condition, EntityTrait, QueryFilter}; +use axum::{http::StatusCode, response::IntoResponse, routing::{get, post}, Router}; -use apb::{PublicKeyMut, ActorMut, ActorType, Link, Object, ObjectMut, BaseMut, Node}; -use crate::{model, server::Context, url}; - -use self::jsonld::LD; - -pub fn router() -> Router { - use crate::routes::activitypub as ap; // TODO use self ? - - Router::new() - // core server inbox/outbox, maybe for feeds? TODO do we need these? - .route("/", get(ap::view)) - // TODO shared inboxes and instance stream will come later, just use users *boxes for now - .route("/inbox", get(ap::inbox::get)) - // .route("/inbox", post(ap::inbox::post)) - // .route("/outbox", get(ap::outbox::get)) - // .route("/outbox", get(ap::outbox::post)) - // AUTH routes - .route("/auth", post(ap::auth)) - // .well-known and discovery - .route("/.well-known/webfinger", get(ap::well_known::webfinger)) - .route("/.well-known/host-meta", get(ap::well_known::host_meta)) - .route("/.well-known/nodeinfo", get(ap::well_known::nodeinfo_discovery)) - .route("/nodeinfo/:version", get(ap::well_known::nodeinfo)) - // actor routes - .route("/users/:id", get(ap::user::view)) - .route("/users/:id/inbox", post(ap::user::inbox::post)) - .route("/users/:id/inbox", get(ap::user::inbox::get)) - .route("/users/:id/inbox/page", get(ap::user::inbox::page)) - .route("/users/:id/outbox", post(ap::user::outbox::post)) - .route("/users/:id/outbox", get(ap::user::outbox::get)) - .route("/users/:id/outbox/page", get(ap::user::outbox::page)) - .route("/users/:id/followers", get(ap::user::following::get::)) - .route("/users/:id/followers/page", get(ap::user::following::page::)) - .route("/users/:id/following", get(ap::user::following::get::)) - .route("/users/:id/following/page", get(ap::user::following::page::)) - // specific object routes - .route("/activities/:id", get(ap::activity::view)) - .route("/objects/:id", get(ap::object::view)) +pub trait ActivityPubRouter { + fn ap_routes(self) -> Self; } -pub trait Addressed : Object { - fn addressed(&self) -> Vec; -} - -impl Addressed for serde_json::Value { - fn addressed(&self) -> Vec { - let mut to : Vec = self.to().map(|x| x.href().to_string()).collect(); - to.append(&mut self.bto().map(|x| x.href().to_string()).collect()); - to.append(&mut self.cc().map(|x| x.href().to_string()).collect()); - to.append(&mut self.bcc().map(|x| x.href().to_string()).collect()); - to +impl ActivityPubRouter for Router { + fn ap_routes(self) -> Self { + use crate::routes::activitypub as ap; // TODO use self ? + + self + // core server inbox/outbox, maybe for feeds? TODO do we need these? + .route("/", get(ap::application::view)) + // TODO shared inboxes and instance stream will come later, just use users *boxes for now + .route("/inbox", get(ap::inbox::get)) + // .route("/inbox", post(ap::inbox::post)) + // .route("/outbox", get(ap::outbox::get)) + // .route("/outbox", get(ap::outbox::post)) + // AUTH routes + .route("/auth", post(ap::auth::login)) + // .well-known and discovery + .route("/.well-known/webfinger", get(ap::well_known::webfinger)) + .route("/.well-known/host-meta", get(ap::well_known::host_meta)) + .route("/.well-known/nodeinfo", get(ap::well_known::nodeinfo_discovery)) + .route("/nodeinfo/:version", get(ap::well_known::nodeinfo)) + // actor routes + .route("/users/:id", get(ap::user::view)) + .route("/users/:id/inbox", post(ap::user::inbox::post)) + .route("/users/:id/inbox", get(ap::user::inbox::get)) + .route("/users/:id/inbox/page", get(ap::user::inbox::page)) + .route("/users/:id/outbox", post(ap::user::outbox::post)) + .route("/users/:id/outbox", get(ap::user::outbox::get)) + .route("/users/:id/outbox/page", get(ap::user::outbox::page)) + .route("/users/:id/followers", get(ap::user::following::get::)) + .route("/users/:id/followers/page", get(ap::user::following::page::)) + .route("/users/:id/following", get(ap::user::following::get::)) + .route("/users/:id/following/page", get(ap::user::following::page::)) + // specific object routes + .route("/activities/:id", get(ap::activity::view)) + .route("/objects/:id", get(ap::object::view)) } } -pub const PUBLIC_TARGET : &str = "https://www.w3.org/ns/activitystreams#Public"; - -pub fn split_id(id: &str) -> (String, String) { - let clean = id - .replace("http://", "") - .replace("https://", ""); - let mut splits = clean.split('/'); - let first = splits.next().unwrap_or(""); - let last = splits.last().unwrap_or(first); - (first.to_string(), last.to_string()) -} - -pub fn domain(domain: &str) -> String { - domain - .replace("http://", "") - .replace("https://", "") - .replace('/', "") -} - - #[derive(Debug, serde::Deserialize)] // TODO i don't really like how pleroma/mastodon do it actually, maybe change this? pub struct Pagination { @@ -104,87 +70,3 @@ impl IntoResponse for CreationResult { .into_response() } } - -pub async fn view(State(ctx): State) -> Result, StatusCode> { - Ok(Json( - serde_json::Value::new_object() - .set_id(Some(&url!(ctx, ""))) - .set_actor_type(Some(ActorType::Application)) - .set_name(Some("μpub")) - .set_summary(Some("micro social network, federated")) - .set_published(Some(ctx.app().created)) - .set_public_key(Node::object( - serde_json::Value::new_object() - .set_id(Some(&url!(ctx, "#main-key"))) - .set_owner(Some(&url!(ctx, ""))) - .set_public_key_pem(&ctx.app().public_key) - )) - .ld_context() - )) -} - - -#[derive(Debug, Clone, serde::Deserialize)] -pub struct LoginForm { - email: String, - password: String, -} - -pub async fn auth(State(ctx): State, Json(login): Json) -> Result, StatusCode> { - // TODO salt the pwd - match model::credential::Entity::find() - .filter(Condition::all() - .add(model::credential::Column::Email.eq(login.email)) - .add(model::credential::Column::Password.eq(sha256::digest(login.password))) - ) - .one(ctx.db()) - .await - { - Ok(Some(x)) => { - // TODO should probably use crypto-safe rng - let token : String = rand::thread_rng() - .sample_iter(&rand::distributions::Alphanumeric) - .take(128) - .map(char::from) - .collect(); - model::session::Entity::insert( - model::session::ActiveModel { - id: sea_orm::ActiveValue::Set(token.clone()), - actor: sea_orm::ActiveValue::Set(x.id), - expires: sea_orm::ActiveValue::Set(chrono::Utc::now() + std::time::Duration::from_secs(3600 * 6)), - } - ) - .exec(ctx.db()) - .await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - Ok(Json(serde_json::Value::String(token))) - }, - Ok(None) => Err(StatusCode::UNAUTHORIZED), - Err(e) => { - tracing::error!("error querying db for user credentials: {e}"); - Err(StatusCode::INTERNAL_SERVER_ERROR) - } - } -} - -#[axum::async_trait] -pub trait APOutbox { - async fn create_note(&self, uid: String, object: serde_json::Value) -> crate::Result; - async fn create(&self, uid: String, activity: serde_json::Value) -> crate::Result; - async fn like(&self, uid: String, activity: serde_json::Value) -> crate::Result; - async fn follow(&self, uid: String, activity: serde_json::Value) -> crate::Result; - async fn accept(&self, uid: String, activity: serde_json::Value) -> crate::Result; - async fn reject(&self, _uid: String, _activity: serde_json::Value) -> crate::Result; - async fn undo(&self, uid: String, activity: serde_json::Value) -> crate::Result; -} - -#[axum::async_trait] -pub trait APInbox { - async fn create(&self, activity: serde_json::Value) -> crate::Result<()>; - async fn like(&self, activity: serde_json::Value) -> crate::Result<()>; - async fn follow(&self, activity: serde_json::Value) -> crate::Result<()>; - async fn accept(&self, activity: serde_json::Value) -> crate::Result<()>; - async fn reject(&self, activity: serde_json::Value) -> crate::Result<()>; - async fn undo(&self, activity: serde_json::Value) -> crate::Result<()>; - async fn delete(&self, activity: serde_json::Value) -> crate::Result<()>; - async fn update(&self, activity: serde_json::Value) -> crate::Result<()>; -} diff --git a/src/routes/activitypub/router.rs b/src/routes/activitypub/router.rs deleted file mode 100644 index 46164c0..0000000 --- a/src/routes/activitypub/router.rs +++ /dev/null @@ -1,46 +0,0 @@ -use axum::{routing::{get, post}, Router}; -use sea_orm::DatabaseConnection; -use crate::routes::activitypub as ap; - -pub async fn serve(db: DatabaseConnection, domain: String) -> std::io::Result<()> { - // build our application with a single route - let app = Router::new() - // core server inbox/outbox, maybe for feeds? TODO do we need these? - .route("/", get(ap::view)) - // TODO shared inboxes and instance stream will come later, just use users *boxes for now - .route("/inbox", get(ap::inbox::get)) - // .route("/inbox", post(ap::inbox::post)) - // .route("/outbox", get(ap::outbox::get)) - // .route("/outbox", get(ap::outbox::post)) - // AUTH routes - .route("/auth", post(ap::auth)) - // .well-known and discovery - .route("/.well-known/webfinger", get(ap::well_known::webfinger)) - .route("/.well-known/host-meta", get(ap::well_known::host_meta)) - .route("/.well-known/nodeinfo", get(ap::well_known::nodeinfo_discovery)) - .route("/nodeinfo/:version", get(ap::well_known::nodeinfo)) - // actor routes - .route("/users/:id", get(ap::user::view)) - .route("/users/:id/inbox", post(ap::user::inbox::post)) - .route("/users/:id/inbox", get(ap::user::inbox::get)) - .route("/users/:id/inbox/page", get(ap::user::inbox::page)) - .route("/users/:id/outbox", post(ap::user::outbox::post)) - .route("/users/:id/outbox", get(ap::user::outbox::get)) - .route("/users/:id/outbox/page", get(ap::user::outbox::page)) - .route("/users/:id/followers", get(ap::user::following::get::)) - .route("/users/:id/followers/page", get(ap::user::following::page::)) - .route("/users/:id/following", get(ap::user::following::get::)) - .route("/users/:id/following/page", get(ap::user::following::page::)) - // specific object routes - .route("/activities/:id", get(ap::activity::view)) - .route("/objects/:id", get(ap::object::view)) - .with_state( - crate::server::Context::new(db, domain).await.expect("could not create server state") - ); - - // run our app with hyper, listening locally on port 3000 - let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await?; - - axum::serve(listener, app) - .await -} diff --git a/src/routes/activitypub/user/inbox.rs b/src/routes/activitypub/user/inbox.rs index a53c93c0..d59a7cc 100644 --- a/src/routes/activitypub/user/inbox.rs +++ b/src/routes/activitypub/user/inbox.rs @@ -1,8 +1,8 @@ use axum::{extract::{Path, Query, State}, http::StatusCode, Json}; use sea_orm::{ColumnTrait, Condition, EntityTrait, Order, QueryFilter, QueryOrder, QuerySelect}; -use apb::{ActivityType, ObjectType, Base, BaseType}; -use crate::{routes::activitypub::{activity::ap_activity, jsonld::LD, APInbox, JsonLD, Pagination}, server::{Context, auth::{AuthIdentity, Identity}}, errors::UpubError, model, url}; +use apb::{server::Inbox, ActivityType, Base, BaseType, ObjectType}; +use crate::{routes::activitypub::{activity::ap_activity, jsonld::LD, JsonLD, Pagination}, server::{Context, auth::{AuthIdentity, Identity}}, errors::UpubError, model, url}; pub async fn get( State(ctx): State, diff --git a/src/routes/activitypub/user/outbox.rs b/src/routes/activitypub/user/outbox.rs index 30f1279..1c97acd 100644 --- a/src/routes/activitypub/user/outbox.rs +++ b/src/routes/activitypub/user/outbox.rs @@ -1,8 +1,8 @@ use axum::{extract::{Path, Query, State}, http::StatusCode, Json}; use sea_orm::{EntityTrait, Order, QueryOrder, QuerySelect}; -use apb::{AcceptType, ActivityMut, ActivityType, Base, BaseType, Node, ObjectType, RejectType}; -use crate::{routes::activitypub::{jsonld::LD, APOutbox, CreationResult, JsonLD, Pagination}, server::auth::{AuthIdentity, Identity}, errors::UpubError, model, server::Context, url}; +use apb::{server::Outbox, AcceptType, ActivityMut, ActivityType, Base, BaseType, Node, ObjectType, RejectType}; +use crate::{routes::activitypub::{jsonld::LD, CreationResult, JsonLD, Pagination}, server::auth::{AuthIdentity, Identity}, errors::UpubError, model, server::Context, url}; pub async fn get( State(ctx): State, diff --git a/src/routes/mastodon/mod.rs b/src/routes/mastodon/mod.rs index b1dd944..1965191 100644 --- a/src/routes/mastodon/mod.rs +++ b/src/routes/mastodon/mod.rs @@ -1,69 +1,72 @@ use axum::{http::StatusCode, routing::{delete, get, patch, post}, Router}; use crate::server::Context; -#[allow(unused)] async fn todo() -> StatusCode { StatusCode::NOT_IMPLEMENTED } -#[allow(unused)] -pub async fn mastodon_api_routes(router: Router) -> Router { - router.nest( - // TODO Oauth is just under /oauth - "/api/v1", Router::new() - .route("/apps", post(todo)) // create an application - .route("/apps/verify_credentials", post(todo)) // confirm that the app's oauth2 credentials work - .route("/emails/confirmations", post(todo)) - .route("/accounts", post(todo)) - .route("/accounts/verify_credentials", get(todo)) - .route("/accounts/update_credentials", patch(todo)) - .route("/accounts/:id", get(todo)) - .route("/accounts/:id/statuses", get(todo)) - .route("/accounts/:id/followers", get(todo)) - .route("/accounts/:id/following", get(todo)) - .route("/accounts/:id/featured_tags", get(todo)) - .route("/accounts/:id/lists", get(todo)) - .route("/accounts/:id/follow", post(todo)) - .route("/accounts/:id/unfollow", post(todo)) - .route("/accounts/:id/remove_from_followers", post(todo)) - .route("/accounts/:id/block", post(todo)) - .route("/accounts/:id/unblock", post(todo)) - .route("/accounts/:id/mute", post(todo)) - .route("/accounts/:id/unmute", post(todo)) - .route("/accounts/:id/pin", post(todo)) - .route("/accounts/:id/unpin", post(todo)) - .route("/accounts/:id/note", post(todo)) - .route("/accounts/relationships", get(todo)) - .route("/accounts/familiar_followers", get(todo)) - .route("/accounts/search", get(todo)) - .route("/accounts/lookup", get(todo)) - .route("/accounts/:id/identity_proofs", get(todo)) - .route("/bookmarks", get(todo)) - .route("/favourites", get(todo)) - .route("/mutes", get(todo)) - .route("/blocks", get(todo)) - .route("/domain_blocks", get(todo)) - .route("/domain_blocks", post(todo)) - .route("/domain_blocks", delete(todo)) - // TODO filters! api v2 - .route("/reports", post(todo)) - .route("/follow_requests", get(todo)) - .route("/follow_requests/:account_id/authorize", get(todo)) - .route("/follow_requests/:account_id/reject", get(todo)) - .route("/endorsements", get(todo)) - .route("/featured_tags", get(todo)) - .route("/featured_tags", post(todo)) - .route("/featured_tags/:id", delete(todo)) - .route("/featured_tags/suggestions", get(todo)) - .route("/preferences", get(todo)) - .route("/followed_tags", get(todo)) - // TODO suggestions! api v2 - .route("/suggestions", get(todo)) - .route("/suggestions/:account_id", delete(todo)) - .route("/tags/:id", get(todo)) - .route("/tags/:id/follow", post(todo)) - .route("/tags/:id/unfollow", post(todo)) - .route("/profile/avatar", delete(todo)) - .route("/profile/header", delete(todo)) - .route("/statuses", post(todo)) - - ) +pub trait MastodonRouter { + fn mastodon_routes(self) -> Self; +} + +impl MastodonRouter for Router { + fn mastodon_routes(self) -> Self { + self.nest( + // TODO Oauth is just under /oauth + "/api/v1", Router::new() + .route("/apps", post(todo)) // create an application + .route("/apps/verify_credentials", post(todo)) // confirm that the app's oauth2 credentials work + .route("/emails/confirmations", post(todo)) + .route("/accounts", post(todo)) + .route("/accounts/verify_credentials", get(todo)) + .route("/accounts/update_credentials", patch(todo)) + .route("/accounts/:id", get(todo)) + .route("/accounts/:id/statuses", get(todo)) + .route("/accounts/:id/followers", get(todo)) + .route("/accounts/:id/following", get(todo)) + .route("/accounts/:id/featured_tags", get(todo)) + .route("/accounts/:id/lists", get(todo)) + .route("/accounts/:id/follow", post(todo)) + .route("/accounts/:id/unfollow", post(todo)) + .route("/accounts/:id/remove_from_followers", post(todo)) + .route("/accounts/:id/block", post(todo)) + .route("/accounts/:id/unblock", post(todo)) + .route("/accounts/:id/mute", post(todo)) + .route("/accounts/:id/unmute", post(todo)) + .route("/accounts/:id/pin", post(todo)) + .route("/accounts/:id/unpin", post(todo)) + .route("/accounts/:id/note", post(todo)) + .route("/accounts/relationships", get(todo)) + .route("/accounts/familiar_followers", get(todo)) + .route("/accounts/search", get(todo)) + .route("/accounts/lookup", get(todo)) + .route("/accounts/:id/identity_proofs", get(todo)) + .route("/bookmarks", get(todo)) + .route("/favourites", get(todo)) + .route("/mutes", get(todo)) + .route("/blocks", get(todo)) + .route("/domain_blocks", get(todo)) + .route("/domain_blocks", post(todo)) + .route("/domain_blocks", delete(todo)) + // TODO filters! api v2 + .route("/reports", post(todo)) + .route("/follow_requests", get(todo)) + .route("/follow_requests/:account_id/authorize", get(todo)) + .route("/follow_requests/:account_id/reject", get(todo)) + .route("/endorsements", get(todo)) + .route("/featured_tags", get(todo)) + .route("/featured_tags", post(todo)) + .route("/featured_tags/:id", delete(todo)) + .route("/featured_tags/suggestions", get(todo)) + .route("/preferences", get(todo)) + .route("/followed_tags", get(todo)) + // TODO suggestions! api v2 + .route("/suggestions", get(todo)) + .route("/suggestions/:account_id", delete(todo)) + .route("/tags/:id", get(todo)) + .route("/tags/:id/follow", post(todo)) + .route("/tags/:id/unfollow", post(todo)) + .route("/profile/avatar", delete(todo)) + .route("/profile/header", delete(todo)) + .route("/statuses", post(todo)) + ) + } } diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 4095bfb..2803347 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -5,3 +5,12 @@ pub mod web; #[cfg(feature = "mastodon")] pub mod mastodon; + +#[cfg(not(feature = "mastodon"))] +pub mod mastodon { + pub trait MastodonRouter { + fn mastodon_routes(self) -> Self { self } + } + + impl MastodonRouter for axum::Router {} +} diff --git a/src/server/context.rs b/src/server/context.rs index b1af279..dfe1f46 100644 --- a/src/server/context.rs +++ b/src/server/context.rs @@ -4,7 +4,7 @@ use apb::{BaseMut, CollectionMut, CollectionPageMut}; use openssl::rsa::Rsa; use sea_orm::{ColumnTrait, Condition, DatabaseConnection, EntityTrait, QueryFilter, QuerySelect, SelectColumns, Set}; -use crate::{model, routes::activitypub::{jsonld::LD, PUBLIC_TARGET}}; +use crate::{model, routes::activitypub::jsonld::LD}; use super::{dispatcher::Dispatcher, fetcher::Fetcher}; @@ -176,7 +176,7 @@ impl Context { .iter() .filter(|to| !to.is_empty()) .filter(|to| Context::server(to) != self.base()) - .filter(|to| to != &PUBLIC_TARGET) + .filter(|to| to != &apb::target::PUBLIC) .map(|to| model::delivery::ActiveModel { id: sea_orm::ActiveValue::NotSet, actor: Set(from.to_string()), diff --git a/src/server/inbox.rs b/src/server/inbox.rs index 3268101..f1cf665 100644 --- a/src/server/inbox.rs +++ b/src/server/inbox.rs @@ -1,13 +1,16 @@ -use apb::{Activity, Base, Object}; +use apb::{target::Addressed, Activity, Base, Object}; use sea_orm::{sea_query::Expr, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, Set}; -use crate::{errors::{LoggableError, UpubError}, model, routes::activitypub::{APInbox, Addressed}}; +use crate::{errors::{LoggableError, UpubError}, model}; use super::Context; #[axum::async_trait] -impl APInbox for Context { +impl apb::server::Inbox for Context { + type Error = UpubError; + type Activity = serde_json::Value; + async fn create(&self, activity: serde_json::Value) -> crate::Result<()> { let activity_model = model::activity::Model::new(&activity)?; let activity_targets = activity.addressed(); diff --git a/src/server/outbox.rs b/src/server/outbox.rs index bbfe7fd..a91510e 100644 --- a/src/server/outbox.rs +++ b/src/server/outbox.rs @@ -1,13 +1,17 @@ -use apb::{Activity, ActivityMut, BaseMut, Node, ObjectMut}; +use apb::{target::Addressed, Activity, ActivityMut, BaseMut, Node, ObjectMut}; use sea_orm::{EntityTrait, IntoActiveModel, Set}; -use crate::{errors::UpubError, model, routes::activitypub::{APOutbox, Addressed}}; +use crate::{errors::UpubError, model}; use super::Context; #[axum::async_trait] -impl APOutbox for Context { +impl apb::server::Outbox for Context { + type Error = UpubError; + type Object = serde_json::Value; + type Activity = serde_json::Value; + async fn create_note(&self, uid: String, object: serde_json::Value) -> crate::Result { let oid = self.oid(uuid::Uuid::new_v4().to_string()); let aid = self.aid(uuid::Uuid::new_v4().to_string());