use actix_web::{ get, post, web, HttpRequest, HttpResponse, Scope, }; use serde::Deserialize; use uuid::Uuid; use crate::config::Config; use crate::database::{Pool, get_database_client}; use crate::errors::HttpError; use crate::frontend::{get_post_page_url, get_profile_page_url}; use crate::http_signatures::verify::verify_http_signature; use crate::models::posts::queries::get_thread; use crate::models::users::queries::get_user_by_name; use super::activity::{create_note, OrderedCollection}; use super::actor::get_local_actor; use super::constants::ACTIVITY_CONTENT_TYPE; use super::receiver::receive_activity; pub fn get_actor_url(instance_url: &str, username: &str) -> String { format!("{}/users/{}", instance_url, username) } pub fn get_inbox_url(instance_url: &str, username: &str) -> String { format!("{}/users/{}/inbox", instance_url, username) } pub fn get_outbox_url(instance_url: &str, username: &str) -> String { format!("{}/users/{}/outbox", instance_url, username) } pub fn get_followers_url(instance_url: &str, username: &str) -> String { format!("{}/users/{}/followers", instance_url, username) } pub fn get_following_url(instance_url: &str, username: &str) -> String { format!("{}/users/{}/following", instance_url, username) } pub fn get_object_url(instance_url: &str, object_uuid: &Uuid) -> String { format!("{}/objects/{}", instance_url, object_uuid) } fn is_activitypub_request(request: &HttpRequest) -> bool { const CONTENT_TYPES: [&str; 3] = [ ACTIVITY_CONTENT_TYPE, "application/ld+json", "application/json", ]; if let Some(content_type) = request.headers().get("Accept") { let content_type_str = content_type.to_str().unwrap_or(""); return CONTENT_TYPES.contains(&content_type_str); }; false } #[get("")] async fn get_actor( config: web::Data, db_pool: web::Data, request: HttpRequest, web::Path(username): web::Path, ) -> Result { let db_client = &**get_database_client(&db_pool).await?; let user = get_user_by_name(db_client, &username).await?; if !is_activitypub_request(&request) { let page_url = get_profile_page_url(&user.id, &config.instance_url()); let response = HttpResponse::Found() .header("Location", page_url) .finish(); return Ok(response); }; let actor = get_local_actor(&user, &config.instance_url()) .map_err(|_| HttpError::InternalError)?; let response = HttpResponse::Ok() .content_type(ACTIVITY_CONTENT_TYPE) .json(actor); Ok(response) } #[post("/inbox")] async fn inbox( config: web::Data, db_pool: web::Data, request: HttpRequest, web::Path(username): web::Path, activity: web::Json, ) -> Result { log::info!("received to '{}' inbox: {}", username, activity); if let Err(err) = verify_http_signature(&config, &db_pool, &request).await { log::warn!("invalid signature: {}", err); } receive_activity(&config, &db_pool, username, activity.into_inner()).await?; Ok(HttpResponse::Ok().body("success")) } #[derive(Deserialize)] struct CollectionQueryParams { page: Option, } #[get("/followers")] async fn followers_collection( config: web::Data, web::Path(username): web::Path, query_params: web::Query, ) -> Result { if query_params.page.is_some() { // Social graph is not available return Err(HttpError::PermissionError); } let collection_url = get_followers_url(&config.instance_url(), &username); let collection = OrderedCollection::new(collection_url); let response = HttpResponse::Ok() .content_type(ACTIVITY_CONTENT_TYPE) .json(collection); Ok(response) } #[get("/following")] async fn following_collection( config: web::Data, web::Path(username): web::Path, query_params: web::Query, ) -> Result { if query_params.page.is_some() { // Social graph is not available return Err(HttpError::PermissionError); } let collection_url = get_following_url(&config.instance_url(), &username); let collection = OrderedCollection::new(collection_url); let response = HttpResponse::Ok() .content_type(ACTIVITY_CONTENT_TYPE) .json(collection); Ok(response) } pub fn activitypub_scope() -> Scope { web::scope("/users/{username}") .service(get_actor) .service(inbox) .service(followers_collection) .service(following_collection) } #[get("/objects/{object_id}")] pub async fn get_object( config: web::Data, db_pool: web::Data, request: HttpRequest, web::Path(object_id): web::Path, ) -> Result { let db_client = &**get_database_client(&db_pool).await?; // Try to find local post by ID, return 404 if not found let thread = get_thread(db_client, &object_id).await?; let post = thread.iter() .find(|post| post.id == object_id && post.author.is_local()) .ok_or(HttpError::NotFoundError("post"))?; if !is_activitypub_request(&request) { let page_url = get_post_page_url(&post.id, &config.instance_url()); let response = HttpResponse::Found() .header("Location", page_url) .finish(); return Ok(response); }; let in_reply_to = match post.in_reply_to_id { Some(in_reply_to_id) => { thread.iter().find(|post| post.id == in_reply_to_id) }, None => None, }; let object = create_note( &config.instance().host(), &config.instance().url(), post, in_reply_to, ); let response = HttpResponse::Ok() .content_type(ACTIVITY_CONTENT_TYPE) .json(object); Ok(response) }