diff --git a/Cargo.lock b/Cargo.lock index da118f7..92138c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -571,6 +571,15 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64-url" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2b6c78c06f7288d5e3c3d683bde35a79531127c83b087e5d0d77c974b4b28" +dependencies = [ + "base64 0.22.1", +] + [[package]] name = "base64ct" version = "1.6.0" @@ -2061,6 +2070,7 @@ dependencies = [ "async-recursion", "async-trait", "async_once", + "base64-url", "chrono", "clap", "dotenv", diff --git a/Cargo.toml b/Cargo.toml index 8566309..74d7f0f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ time = { version = "0.3.36", features = ["serde"] } serde_derive = "1.0.201" dotenv = "0.15.0" async-recursion = "1.1.1" +base64-url = "3.0.0" [dependencies.sea-orm] version = "0.12.0" diff --git a/migration/src/lib.rs b/migration/src/lib.rs index 7e4572a..08b1e65 100644 --- a/migration/src/lib.rs +++ b/migration/src/lib.rs @@ -4,6 +4,7 @@ mod m20220101_000001_post_table; mod m20240417_230111_user_table; mod m20240417_233430_post_user_keys; mod m20240505_002524_user_follow_relation; +mod m20240626_030922_store_ap_json_in_posts; pub struct Migrator; @@ -15,6 +16,7 @@ impl MigratorTrait for Migrator { Box::new(m20240417_230111_user_table::Migration), Box::new(m20240417_233430_post_user_keys::Migration), Box::new(m20240505_002524_user_follow_relation::Migration), + Box::new(m20240626_030922_store_ap_json_in_posts::Migration), ] } } diff --git a/migration/src/m20240626_030922_store_ap_json_in_posts.rs b/migration/src/m20240626_030922_store_ap_json_in_posts.rs new file mode 100644 index 0000000..fd3aad7 --- /dev/null +++ b/migration/src/m20240626_030922_store_ap_json_in_posts.rs @@ -0,0 +1,35 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(Post::Table) + .add_column_if_not_exists(ColumnDef::new(Post::ApJson).string()) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(Post::Table) + .drop_column(Post::ApJson) + .to_owned(), + ) + .await + } +} + +#[derive(DeriveIden)] +pub enum Post { + Table, + ApJson, +} diff --git a/src/activities/create_post.rs b/src/activities/create_post.rs index 771bb46..cf8e825 100644 --- a/src/activities/create_post.rs +++ b/src/activities/create_post.rs @@ -6,7 +6,7 @@ use crate::{ person::DbUser, post::{DbPost, Note}, }, - utils::generate_random_object_id, + utils::{base_url_encode, generate_create_id, generate_random_object_id}, }; use activitypub_federation::{ activity_sending::SendActivityTask, @@ -32,14 +32,20 @@ pub struct CreatePost { } impl CreatePost { - pub async fn send(note: Note, inbox: Url, data: &Data) -> Result<(), Error> { + pub async fn send( + note: Note, + db_entry: post::Model, + inbox: Url, + data: &Data, + ) -> Result<(), Error> { print!("Sending reply to {}", ¬e.attributed_to); + let encoded_url = base_url_encode(¬e.id.clone().into()); let create = CreatePost { actor: note.attributed_to.clone(), to: note.to.clone(), object: note, kind: CreateType::Create, - id: generate_random_object_id(data.domain())?, + id: generate_create_id(data.domain(), &db_entry.id, &encoded_url)?, }; let create_with_context = WithContext::new_default(create); let sends = SendActivityTask::prepare( diff --git a/src/activities/follow.rs b/src/activities/follow.rs index b9986de..fe175e0 100644 --- a/src/activities/follow.rs +++ b/src/activities/follow.rs @@ -77,7 +77,7 @@ impl Accept { actor: follow_req.object.clone(), object: follow_req, kind: AcceptType::Accept, - id: generate_follow_accept_id(data.domain(), follow_relation.id)?, + id: generate_follow_accept_id(data.domain(), follow_relation.id.to_string().as_str())?, }; let create_with_context = WithContext::new_default(create); let sends = SendActivityTask::prepare( diff --git a/src/entities/post.rs b/src/entities/post.rs index 0062f05..92aa9db 100644 --- a/src/entities/post.rs +++ b/src/entities/post.rs @@ -24,6 +24,7 @@ pub struct Model { pub spoiler_text: Option, pub creator: String, pub url: String, + pub ap_json: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/src/lysand/conversion.rs b/src/lysand/conversion.rs index 0c940ee..42f7cbe 100644 --- a/src/lysand/conversion.rs +++ b/src/lysand/conversion.rs @@ -229,6 +229,7 @@ pub async fn receive_lysand_note( reply_id: Set(reply_uuid), quoting_id: Set(quote_uuid), spoiler_text: Set(note.subject), + ap_json: Set(Some(serde_json::to_string(&ap_note).unwrap())), ..Default::default() }; let res = post.insert(DB.get().unwrap()).await?; diff --git a/src/lysand/http.rs b/src/lysand/http.rs index d1e594d..6bc9446 100644 --- a/src/lysand/http.rs +++ b/src/lysand/http.rs @@ -1,4 +1,7 @@ -use activitypub_federation::{traits::Object, FEDERATION_CONTENT_TYPE}; +use activitypub_federation::{ + protocol::context::WithContext, traits::Object, FEDERATION_CONTENT_TYPE, +}; +use activitystreams_kinds::{activity::CreateType, object}; use actix_web::{get, web, HttpResponse}; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; @@ -8,7 +11,9 @@ use crate::{ post::{self, Entity}, prelude, }, - error, Response, DB, FEDERATION_CONFIG, + error, objects, + utils::{base_url_decode, generate_create_id}, + Response, DB, FEDERATION_CONFIG, }; #[get("/apbridge/object/{post}")] @@ -35,3 +40,40 @@ async fn fetch_post( .await?, )) } + +#[get("/apbridge/create/{id}/{base64url}")] +async fn create_activity( + path: web::Path<(String, String)>, + state: web::Data, +) -> actix_web::Result { + let db = DB.get().unwrap(); + + let url = base_url_decode(path.1.as_str()); + + let post = prelude::Post::find() + .filter(post::Column::Id.eq(path.0.as_str())) + .one(db) + .await?; + + let post = match post { + Some(post) => post, + None => return Ok(HttpResponse::NotFound().finish()), + }; + + let ap_post = crate::objects::post::Note::from_db(&post); + + let data = FEDERATION_CONFIG.get().unwrap(); + + let create = crate::activities::create_post::CreatePost { + actor: ap_post.attributed_to.clone(), + to: ap_post.to.clone(), + object: ap_post, + kind: CreateType::Create, + id: generate_create_id(&data.to_request_data().domain(), &path.0, &path.1)?, + }; + let create_with_context = WithContext::new_default(create); + + Ok(HttpResponse::Ok() + .content_type(FEDERATION_CONTENT_TYPE) + .json(create_with_context)) +} diff --git a/src/main.rs b/src/main.rs index 5e1036f..badfc29 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,7 @@ use clap::Parser; use database::Database; use entities::post; use http::{http_get_user, http_post_user_inbox, webfinger}; +use lysand::http::{create_activity, fetch_post}; use objects::person::DbUser; use sea_orm::{ActiveModelTrait, DatabaseConnection, Set}; use serde::{Deserialize, Serialize}; @@ -89,7 +90,7 @@ async fn post_manually( let id: ObjectId = generate_random_object_id(data.domain())?.into(); let note = Note { kind: Default::default(), - id, + id: id.clone(), sensitive: false, attributed_to: Url::parse(&local_user.id).unwrap().into(), to: vec![public()], @@ -99,8 +100,26 @@ async fn post_manually( cc: vec![].into(), }; + let post = entities::post::ActiveModel { + id: Set(uuid::Uuid::now_v7().to_string()), + creator: Set(local_user.id.clone()), + content: Set(note.content.clone()), + sensitive: Set(false), + created_at: Set(Utc::now()), + local: Set(true), + updated_at: Set(Some(Utc::now())), + content_type: Set("Note".to_string()), + visibility: Set("public".to_string()), + url: Set(id.to_string()), + ap_json: Set(Some(serde_json::to_string(¬e).unwrap())), + ..Default::default() + }; + + let post = post.insert(DB.get().unwrap()).await?; + CreatePost::send( note, + post, target.shared_inbox_or_inbox(), &data.to_request_data(), ) @@ -238,6 +257,8 @@ async fn main() -> actix_web::Result<(), anyhow::Error> { .route("/{user}/inbox", web::post().to(http_post_user_inbox)) .route("/.well-known/webfinger", web::get().to(webfinger)) .service(index) + .service(fetch_post) + .service(create_activity) }) .bind(SERVER_URL.to_string())? .workers(num_cpus::get()) diff --git a/src/objects/person.rs b/src/objects/person.rs index 304c32f..0888f3c 100644 --- a/src/objects/person.rs +++ b/src/objects/person.rs @@ -176,4 +176,9 @@ impl Actor for user::Model { fn inbox(&self) -> Url { Url::parse(&self.inbox).unwrap() } + + //TODO: Differenciate shared inbox + fn shared_inbox(&self) -> Option { + None + } } diff --git a/src/objects/post.rs b/src/objects/post.rs index e67403c..1f42e23 100644 --- a/src/objects/post.rs +++ b/src/objects/post.rs @@ -44,6 +44,12 @@ pub struct Note { pub(crate) cc: Option>, } +impl Note { + pub fn from_db(post: &post::Model) -> Self { + serde_json::from_str(&post.ap_json.as_ref().unwrap()).unwrap() + } +} + #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Mention { pub href: Url, diff --git a/src/utils.rs b/src/utils.rs index daa0274..d901109 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -12,13 +12,35 @@ pub fn generate_user_id(domain: &str, uuid: &str) -> Result { pub fn generate_random_object_id(domain: &str) -> Result { let id: String = uuid::Uuid::new_v4().to_string(); - Url::parse(&format!("https://{}/apbridge/object/{}", domain, id)) + generate_object_id(domain, &id) } /// Generate a follow accept id -pub fn generate_follow_accept_id(domain: &str, db_id: i32) -> Result { +pub fn generate_follow_accept_id(domain: &str, db_id: &str) -> Result { + Url::parse(&format!("https://{}/apbridge/follow/{}", domain, db_id)) +} + +// TODO for later aprl: needs to be base64url!!! +pub fn generate_create_id( + domain: &str, + create_db_id: &str, + basesixfour_url: &str, +) -> Result { Url::parse(&format!( - "https://{}/apbridge/activity/follow/{}", - domain, db_id + "https://{}/apbridge/create/{}/{}", + domain, create_db_id, basesixfour_url )) } + +pub fn generate_random_create_id(domain: &str, basesixfour_url: &str) -> Result { + let id: String = uuid::Uuid::new_v4().to_string(); + generate_create_id(domain, &id, basesixfour_url) +} + +pub fn base_url_encode(url: &Url) -> String { + base64_url::encode(&url.to_string()) +} + +pub fn base_url_decode(encoded: &str) -> String { + String::from_utf8(base64_url::decode(encoded).unwrap()).unwrap() +}