Skip to content

Commit

Permalink
feat: basic lysand -> ap fetching api
Browse files Browse the repository at this point in the history
  • Loading branch information
CutestNekoAqua committed Jul 16, 2024
1 parent b1af17c commit a840fee
Show file tree
Hide file tree
Showing 7 changed files with 234 additions and 19 deletions.
120 changes: 113 additions & 7 deletions src/lysand/conversion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand All @@ -25,6 +26,114 @@ pub async fn fetch_user_from_url(url: Url) -> anyhow::Result<super::objects::Use
Ok(request.json::<super::objects::User>().await?)
}

pub async fn lysand_post_from_db(
post: entities::post::Model,
) -> anyhow::Result<super::objects::Note> {
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<super::objects::User> {
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<ContentFormat>) -> Option<String> {
if let Some(format) = opt {
return Some(format.select_rich_text().await.unwrap());
Expand All @@ -47,7 +156,7 @@ pub async fn db_post_from_url(url: Url) -> anyhow::Result<entities::post::Model>
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)
}
}
Expand Down Expand Up @@ -118,7 +227,6 @@ pub async fn receive_lysand_note(
generate_object_id(data.domain(), &note.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<Mention> = Vec::new();
for l_tag in note.mentions.clone().unwrap_or_default() {
tag.push(Mention {
Expand All @@ -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());
Expand Down
91 changes: 87 additions & 4 deletions src/lysand/http.rs
Original file line number Diff line number Diff line change
@@ -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<Url>,
// User handle
user: Option<String>,
// User URL
user_url: Option<Url>,
}

#[get("/apbridge/lysand/query")]
async fn query_post(
query: web::Query<LysandQuery>,
state: web::Data<State>,
) -> actix_web::Result<HttpResponse, error::Error> {
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::<State, user::Model>(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::<user::Model>::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::<post::Model>::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<String>,
Expand All @@ -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<String>,
state: web::Data<State>,
) -> actix_web::Result<HttpResponse, error::Error> {
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)>,
Expand Down
25 changes: 20 additions & 5 deletions src/lysand/objects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -108,9 +108,9 @@ pub struct ContentHash {
sha512: Option<String>,
}

#[derive(Debug, Clone)]
#[derive(Debug, Clone, Default)]
pub struct ContentFormat {
x: HashMap<String, ContentEntry>,
pub x: HashMap<String, ContentEntry>,
}

impl ContentFormat {
Expand Down Expand Up @@ -181,7 +181,7 @@ impl<'de> Deserialize<'de> for ContentFormat {
}

#[derive(Debug, Serialize, Deserialize, Clone)]
struct FieldKV {
pub struct FieldKV {
key: ContentFormat,
value: ContentFormat,
}
Expand All @@ -198,6 +198,21 @@ pub struct ContentEntry {
height: Option<u64>,
duration: Option<u64>,
}
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 {
Expand Down
4 changes: 3 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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())
Expand Down
3 changes: 2 additions & 1 deletion src/objects/person.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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()),
Expand Down
3 changes: 2 additions & 1 deletion src/objects/post.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit a840fee

Please sign in to comment.