diff --git a/src/lysand/conversion.rs b/src/lysand/conversion.rs index 94d635c..caf48ce 100644 --- a/src/lysand/conversion.rs +++ b/src/lysand/conversion.rs @@ -4,18 +4,19 @@ use anyhow::{anyhow, Ok}; use async_recursion::async_recursion; use chrono::{DateTime, TimeZone, Utc}; use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}; +use time::OffsetDateTime; use url::Url; use crate::{ database::State, entities::{self, post, prelude, user}, objects::post::Mention, - utils::{generate_object_id, generate_user_id}, + utils::{generate_lysand_post_url, generate_object_id, generate_user_id}, API_DOMAIN, DB, FEDERATION_CONFIG, LYSAND_DOMAIN, }; use super::{ - objects::{ContentFormat, Note}, + objects::{CategoryType, ContentEntry, ContentFormat, Note, PublicKey}, superx::request_client, }; @@ -25,6 +26,114 @@ pub async fn fetch_user_from_url(url: Url) -> anyhow::Result().await?) } +pub async fn lysand_post_from_db( + post: entities::post::Model, +) -> anyhow::Result { + let data = FEDERATION_CONFIG.get().unwrap(); + let domain = data.domain(); + let url = generate_lysand_post_url(domain, &post.id)?; + let author = Url::parse(&post.creator.to_string())?; + let visibility = match post.visibility.as_str() { + "public" => super::objects::VisibilityType::Public, + "followers" => super::objects::VisibilityType::Followers, + "direct" => super::objects::VisibilityType::Direct, + "unlisted" => super::objects::VisibilityType::Unlisted, + _ => super::objects::VisibilityType::Public, + }; + //let mut mentions = Vec::new(); + //for obj in post.tag.clone() { + // mentions.push(obj.href.clone()); + //} + let mut content = ContentFormat::default(); + content.x.insert( + "text/html".to_string(), + ContentEntry::from_string(post.content), + ); + let note = super::objects::Note { + rtype: super::objects::LysandType::Note, + id: uuid::Uuid::parse_str(&post.id)?, + author: author.clone(), + uri: url.clone(), + created_at: OffsetDateTime::from_unix_timestamp(post.created_at.timestamp()).unwrap(), + content: Some(content), + mentions: None, + category: Some(CategoryType::Microblog), + device: None, + visibility: Some(visibility), + previews: None, + replies_to: None, + quotes: None, + group: None, + attachments: None, + subject: post.title, + is_sensitive: Some(post.sensitive), + }; + Ok(note) +} + +pub async fn lysand_user_from_db( + user: entities::user::Model, +) -> anyhow::Result { + let url = Url::parse(&user.url)?; + let inbox_url = Url::parse("https://ap.lysand.org/apbridge/lysand/inbox")?; + let outbox_url = Url::parse( + ("https://ap.lysand.org/apbridge/lysand/outbox/".to_string() + &user.id).as_str(), + )?; + let followers_url = Url::parse( + ("https://ap.lysand.org/apbridge/lysand/followers/".to_string() + &user.id).as_str(), + )?; + let following_url = Url::parse( + ("https://ap.lysand.org/apbridge/lysand/following/".to_string() + &user.id).as_str(), + )?; + let featured_url = Url::parse( + ("https://ap.lysand.org/apbridge/lysand/featured/".to_string() + &user.id).as_str(), + )?; + let likes_url = Url::parse( + ("https://ap.lysand.org/apbridge/lysand/likes/".to_string() + &user.id).as_str(), + )?; + let dislikes_url = Url::parse( + ("https://ap.lysand.org/apbridge/lysand/dislikes/".to_string() + &user.id).as_str(), + )?; + let og_displayname_ref = user.name.clone(); + let og_username_ref = user.username.clone(); + let empty = "".to_owned(); + // linter was having a stroke + let display_name = match og_displayname_ref { + og_username_ref => None, + empty => None, + _ => Some(user.name), + }; + let mut bio = ContentFormat::default(); + bio.x.insert( + "text/html".to_string(), + ContentEntry::from_string(user.summary.unwrap_or_default()), + ); + let user = super::objects::User { + rtype: super::objects::LysandType::User, + id: uuid::Uuid::parse_str(&user.id)?, + uri: url.clone(), + username: user.username, + display_name, + inbox: inbox_url, + outbox: outbox_url, + followers: followers_url, + following: following_url, + featured: featured_url, + likes: likes_url, + dislikes: dislikes_url, + bio: Some(bio), + avatar: None, + header: None, + fields: None, + created_at: OffsetDateTime::from_unix_timestamp(user.created_at.timestamp()).unwrap(), + public_key: PublicKey { + actor: url.clone(), + public_key: user.public_key, + }, + }; + Ok(user) +} + pub async fn option_content_format_text(opt: Option) -> Option { if let Some(format) = opt { return Some(format.select_rich_text().await.unwrap()); @@ -47,7 +156,7 @@ pub async fn db_post_from_url(url: Url) -> anyhow::Result Ok(post) } else { let post = fetch_note_from_url(url.clone()).await?; - let res = receive_lysand_note(post, "https://ap.lysand.org/example".to_string()).await?; + let res = receive_lysand_note(post, "https://ap.lysand.org/example".to_string()).await?; // TODO: Replace user id with actual user id Ok(res) } } @@ -118,7 +227,6 @@ pub async fn receive_lysand_note( generate_object_id(data.domain(), ¬e.id.to_string())?.into(); let user_id = generate_user_id(data.domain(), &target.id.to_string())?; let user = fetch_user_from_url(note.author.clone()).await?; - let data = FEDERATION_CONFIG.get().unwrap(); let mut tag: Vec = Vec::new(); for l_tag in note.mentions.clone().unwrap_or_default() { tag.push(Mention { @@ -145,9 +253,7 @@ pub async fn receive_lysand_note( vec.append(&mut mentions.clone()); vec } - super::objects::VisibilityType::Direct => { - mentions.clone() - }, + super::objects::VisibilityType::Direct => mentions.clone(), super::objects::VisibilityType::Unlisted => { let mut vec = vec![Url::parse(&user.followers.to_string().as_str())?]; vec.append(&mut mentions.clone()); diff --git a/src/lysand/http.rs b/src/lysand/http.rs index 4bf0d78..3790edc 100644 --- a/src/lysand/http.rs +++ b/src/lysand/http.rs @@ -1,21 +1,82 @@ use activitypub_federation::{ - protocol::context::WithContext, traits::Object, FEDERATION_CONTENT_TYPE, + fetch::{object_id::ObjectId, webfinger::webfinger_resolve_actor}, + 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}; +use sea_orm::{query, ColumnTrait, EntityTrait, QueryFilter}; +use url::Url; use crate::{ database::State, entities::{ post::{self, Entity}, - prelude, + prelude, user, }, - error, objects, + error, + lysand::conversion::{lysand_post_from_db, lysand_user_from_db}, + objects, utils::{base_url_decode, generate_create_id}, Response, DB, FEDERATION_CONFIG, }; +#[derive(serde::Deserialize)] +struct LysandQuery { + // Post url + url: Option, + // User handle + user: Option, + // User URL + user_url: Option, +} + +#[get("/apbridge/lysand/query")] +async fn query_post( + query: web::Query, + state: web::Data, +) -> actix_web::Result { + if query.url.is_none() && query.user.is_none() && query.user_url.is_none() { + return Ok( + HttpResponse::BadRequest().body("Bad Request. Error code: mrrrmrrrmrrawwawwawwa") + ); + } + + let db = DB.get().unwrap(); + let data = FEDERATION_CONFIG.get().unwrap(); + + if let Some(user) = query.user.clone() { + let target = + webfinger_resolve_actor::(user.as_str(), &data.to_request_data()) + .await?; + let lysand_user = lysand_user_from_db(target).await?; + + return Ok(HttpResponse::Ok() + .content_type("application/json") + .json(lysand_user)); + } + + if let Some(user) = query.user_url.clone() { + let target = ObjectId::::from(user) + .dereference(&data.to_request_data()) + .await?; + let lysand_user = lysand_user_from_db(target).await?; + + return Ok(HttpResponse::Ok() + .content_type("application/json") + .json(lysand_user)); + } + + let target = ObjectId::::from(query.url.clone().unwrap()) + .dereference(&data.to_request_data()) + .await?; + + Ok(HttpResponse::Ok() + .content_type("application/json") + .json(lysand_post_from_db(target).await?)) +} + #[get("/apbridge/object/{post}")] async fn fetch_post( path: web::Path, @@ -38,6 +99,28 @@ async fn fetch_post( .json(crate::objects::post::Note::from_db(&post))) } +#[get("/apbridge/lysand/object/{post}")] +async fn fetch_lysand_post( + path: web::Path, + state: web::Data, +) -> actix_web::Result { + let db = DB.get().unwrap(); + + let post = prelude::Post::find() + .filter(post::Column::Id.eq(path.as_str())) + .one(db) + .await?; + + let post = match post { + Some(post) => post, + None => return Ok(HttpResponse::NotFound().finish()), + }; + + Ok(HttpResponse::Ok() + .content_type("application/json") + .json(lysand_post_from_db(post).await?)) +} + #[get("/apbridge/create/{id}/{base64url}")] async fn create_activity( path: web::Path<(String, String)>, diff --git a/src/lysand/objects.rs b/src/lysand/objects.rs index c50302b..9db91ca 100644 --- a/src/lysand/objects.rs +++ b/src/lysand/objects.rs @@ -96,8 +96,8 @@ pub enum LysandExtensions { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct PublicKey { - public_key: String, - actor: Url, + pub public_key: String, + pub actor: Url, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -108,9 +108,9 @@ pub struct ContentHash { sha512: Option, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct ContentFormat { - x: HashMap, + pub x: HashMap, } impl ContentFormat { @@ -181,7 +181,7 @@ impl<'de> Deserialize<'de> for ContentFormat { } #[derive(Debug, Serialize, Deserialize, Clone)] -struct FieldKV { +pub struct FieldKV { key: ContentFormat, value: ContentFormat, } @@ -198,6 +198,21 @@ pub struct ContentEntry { height: Option, duration: Option, } +impl ContentEntry { + pub fn from_string(string: String) -> ContentEntry { + ContentEntry { + content: string, + description: None, + size: None, + hash: None, + blurhash: None, + fps: None, + width: None, + height: None, + duration: None, + } + } +} #[derive(Debug, Serialize, Deserialize, Clone)] pub struct User { diff --git a/src/main.rs b/src/main.rs index 29c9cac..23ff849 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,7 +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 lysand::http::{create_activity, fetch_lysand_post, fetch_post, query_post}; use objects::person::DbUser; use sea_orm::{ActiveModelTrait, DatabaseConnection, Set}; use serde::{Deserialize, Serialize}; @@ -260,6 +260,8 @@ async fn main() -> actix_web::Result<(), anyhow::Error> { .service(index) .service(fetch_post) .service(create_activity) + .service(query_post) + .service(fetch_lysand_post) }) .bind(SERVER_URL.to_string())? .workers(num_cpus::get()) diff --git a/src/objects/person.rs b/src/objects/person.rs index 0888f3c..c3bbd28 100644 --- a/src/objects/person.rs +++ b/src/objects/person.rs @@ -23,6 +23,7 @@ use serde::{Deserialize, Serialize}; use std::fmt::Debug; use tracing::info; use url::Url; +use uuid::Uuid; #[derive(Debug, Clone)] pub struct DbUser { @@ -135,7 +136,7 @@ impl Object for user::Model { return Ok(user); } let model = user::ActiveModel { - id: Set(json.id.to_string()), + id: Set(Uuid::now_v7().to_string()), username: Set(json.preferred_username), name: Set(json.name), inbox: Set(json.inbox.to_string()), diff --git a/src/objects/post.rs b/src/objects/post.rs index 1f42e23..e115115 100644 --- a/src/objects/post.rs +++ b/src/objects/post.rs @@ -19,6 +19,7 @@ use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}; use serde::{Deserialize, Serialize}; use tracing::info; use url::Url; +use uuid::Uuid; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct DbPost { @@ -119,7 +120,7 @@ impl Object for post::Model { let creator = json.attributed_to.dereference(data).await?; let post: post::ActiveModel = post::ActiveModel { content: Set(json.content.clone()), - id: Set(json.id.to_string()), + id: Set(Uuid::now_v7().to_string()), creator: Set(creator.id.to_string()), created_at: Set(chrono::Utc::now()), //TODO: make this use the real timestamp content_type: Set("text/plain".to_string()), // TODO: make this use the real content type diff --git a/src/utils.rs b/src/utils.rs index 0535352..2603a32 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -18,6 +18,13 @@ pub fn generate_follow_accept_id(domain: &str, db_id: &str) -> Result Result { + Url::parse(&format!( + "https://{}/apbridge/lysand/object/{}", + domain, db_id + )) +} + // TODO for later aprl: needs to be base64url!!! pub fn generate_create_id( domain: &str,