mirror of
https://github.com/versia-pub/activitypub.git
synced 2026-03-13 02:49:17 +01:00
feat: rename to Versia
This commit is contained in:
parent
29d8fc718a
commit
0b4574b2d1
21 changed files with 148 additions and 148 deletions
585
src/versia/conversion.rs
Normal file
585
src/versia/conversion.rs
Normal file
|
|
@ -0,0 +1,585 @@
|
|||
use activitypub_federation::{fetch::object_id::ObjectId, http_signatures::generate_actor_keypair};
|
||||
use activitystreams_kinds::public;
|
||||
use anyhow::{anyhow, Ok};
|
||||
use async_recursion::async_recursion;
|
||||
use chrono::{DateTime, TimeZone, Utc};
|
||||
use reqwest::header::{self, CONTENT_TYPE};
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::OffsetDateTime;
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
database::State,
|
||||
entities::{self, post, prelude, user},
|
||||
objects::{
|
||||
self,
|
||||
person::{AttachmentType, EndpointType, IconType, Person, TagType},
|
||||
post::Mention,
|
||||
},
|
||||
utils::{generate_versia_post_url, generate_object_id, generate_user_id},
|
||||
API_DOMAIN, DB, FEDERATION_CONFIG, LOCAL_USER_NAME, LYSAND_DOMAIN, USERNAME,
|
||||
};
|
||||
|
||||
use super::{
|
||||
objects::{CategoryType, ContentEntry, ContentFormat, Note, PublicKey},
|
||||
superx::request_client,
|
||||
};
|
||||
|
||||
pub async fn fetch_user_from_url(url: Url) -> anyhow::Result<super::objects::User> {
|
||||
let req_client = request_client();
|
||||
let request = req_client.get(url).send().await?;
|
||||
Ok(request.json::<super::objects::User>().await?)
|
||||
}
|
||||
|
||||
pub async fn versia_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_versia_post_url(domain, &post.id)?;
|
||||
let creator = prelude::User::find()
|
||||
.filter(entities::user::Column::Id.eq(post.creator.clone()))
|
||||
.one(DB.get().unwrap())
|
||||
.await?;
|
||||
let author = Url::parse(&creator.unwrap().url)?;
|
||||
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::VersiaType::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 versia_user_from_db(
|
||||
user: entities::user::Model,
|
||||
) -> anyhow::Result<super::objects::User> {
|
||||
let url = Url::parse(&user.url)?;
|
||||
let ap = user.ap_json.unwrap();
|
||||
let serialized_ap: crate::objects::person::Person = serde_json::from_str(&ap)?;
|
||||
let inbox_url = Url::parse("https://ap.versia.social/apbridge/versia/inbox")?;
|
||||
let outbox_url = Url::parse(
|
||||
("https://ap.versia.social/apbridge/versia/outbox/".to_string() + &user.id).as_str(),
|
||||
)?;
|
||||
let followers_url = Url::parse(
|
||||
("https://ap.versia.social/apbridge/versia/followers/".to_string() + &user.id).as_str(),
|
||||
)?;
|
||||
let following_url = Url::parse(
|
||||
("https://ap.versia.social/apbridge/versia/following/".to_string() + &user.id).as_str(),
|
||||
)?;
|
||||
let featured_url = Url::parse(
|
||||
("https://ap.versia.social/apbridge/versia/featured/".to_string() + &user.id).as_str(),
|
||||
)?;
|
||||
let likes_url = Url::parse(
|
||||
("https://ap.versia.social/apbridge/versia/likes/".to_string() + &user.id).as_str(),
|
||||
)?;
|
||||
let dislikes_url = Url::parse(
|
||||
("https://ap.versia.social/apbridge/versia/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 avatar = match serialized_ap.icon {
|
||||
Some(icon) => {
|
||||
let mut content_format = ContentFormat::default();
|
||||
let content_entry = ContentEntry::from_string(icon.url.to_string());
|
||||
let media_type = icon.media_type.unwrap_or({
|
||||
let req = request_client().get(icon.url.clone()).build()?;
|
||||
let res = request_client().execute(req).await?;
|
||||
let headers = res.headers();
|
||||
let content_type_header = headers.get(CONTENT_TYPE);
|
||||
content_type_header.unwrap().to_str().unwrap().to_string()
|
||||
});
|
||||
content_format.x.insert(media_type, content_entry);
|
||||
Some(content_format)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let header = match serialized_ap.image {
|
||||
Some(image) => {
|
||||
let mut content_format = ContentFormat::default();
|
||||
let content_entry = ContentEntry::from_string(image.url.to_string());
|
||||
let media_type = image.media_type.unwrap_or({
|
||||
let req = request_client().get(image.url.clone()).build()?;
|
||||
let res = request_client().execute(req).await?;
|
||||
let headers = res.headers();
|
||||
let content_type_header = headers.get(CONTENT_TYPE);
|
||||
content_type_header.unwrap().to_str().unwrap().to_string()
|
||||
});
|
||||
content_format.x.insert(media_type, content_entry);
|
||||
Some(content_format)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let mut fields = Vec::new();
|
||||
if let Some(attachments) = serialized_ap.attachment {
|
||||
for attachment in attachments {
|
||||
let mut key = ContentFormat::default();
|
||||
let mut value = ContentFormat::default();
|
||||
key.x.insert(
|
||||
"text/html".to_string(),
|
||||
ContentEntry::from_string(attachment.name),
|
||||
);
|
||||
value.x.insert(
|
||||
"text/html".to_string(),
|
||||
ContentEntry::from_string(attachment.value),
|
||||
);
|
||||
fields.push(super::objects::FieldKV { key, value });
|
||||
}
|
||||
}
|
||||
let emojis = match serialized_ap.tag {
|
||||
Some(tags) => {
|
||||
let mut emojis = Vec::new();
|
||||
for tag in tags {
|
||||
let mut content_format = ContentFormat::default();
|
||||
if tag.icon.is_none() {
|
||||
continue;
|
||||
}
|
||||
let content_entry =
|
||||
ContentEntry::from_string(tag.icon.clone().unwrap().url.to_string());
|
||||
let icon = tag.icon.unwrap();
|
||||
let media_type = icon.media_type.unwrap_or({
|
||||
let req = request_client().get(icon.url.clone()).build()?;
|
||||
let res = request_client().execute(req).await?;
|
||||
let headers = res.headers();
|
||||
let content_type_header = headers.get(CONTENT_TYPE);
|
||||
if content_type_header.is_none() {
|
||||
continue;
|
||||
}
|
||||
content_type_header.unwrap().to_str().unwrap().to_string()
|
||||
});
|
||||
content_format.x.insert(media_type, content_entry);
|
||||
let mut name = tag.name.chars();
|
||||
name.next();
|
||||
name.next_back();
|
||||
emojis.push(super::objects::CustomEmoji {
|
||||
name: name.as_str().to_string(),
|
||||
url: content_format,
|
||||
});
|
||||
}
|
||||
Some(super::objects::CustomEmojis { emojis })
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let extensions = super::objects::ExtensionSpecs {
|
||||
custom_emojis: emojis,
|
||||
};
|
||||
let user = super::objects::User {
|
||||
rtype: super::objects::VersiaType::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,
|
||||
header,
|
||||
fields: Some(fields),
|
||||
indexable: false,
|
||||
created_at: OffsetDateTime::from_unix_timestamp(user.created_at.timestamp()).unwrap(),
|
||||
public_key: PublicKey {
|
||||
actor: url.clone(),
|
||||
public_key: "AAAAC3NzaC1lZDI1NTE5AAAAIMxsX+lEWkHZt9NOvn9yYFP0Z++186LY4b97C4mwj/f2"
|
||||
.to_string(), // dummy key
|
||||
},
|
||||
extensions: Some(extensions),
|
||||
};
|
||||
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());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
#[async_recursion]
|
||||
pub async fn db_post_from_url(url: Url) -> anyhow::Result<entities::post::Model> {
|
||||
if !url.domain().eq(&Some(LYSAND_DOMAIN.as_str())) {
|
||||
return Err(anyhow!("not versias domain"));
|
||||
}
|
||||
let str_url = url.to_string();
|
||||
let post_res: Option<post::Model> = prelude::Post::find()
|
||||
.filter(entities::post::Column::Url.eq(str_url.clone()))
|
||||
.one(DB.get().unwrap())
|
||||
.await?;
|
||||
|
||||
if let Some(post) = post_res {
|
||||
Ok(post)
|
||||
} else {
|
||||
let post = fetch_note_from_url(url.clone()).await?;
|
||||
let res = receive_versia_note(post, "https://ap.versia.social/example".to_string()).await?; // TODO: Replace user id with actual user id
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct ApiUser {
|
||||
uri: Url,
|
||||
}
|
||||
|
||||
pub async fn local_db_user_from_name(name: String) -> anyhow::Result<entities::user::Model> {
|
||||
let user_res: Option<user::Model> = prelude::User::find()
|
||||
.filter(entities::user::Column::Username.eq(name.clone()))
|
||||
.filter(entities::user::Column::Local.eq(true))
|
||||
.one(DB.get().unwrap())
|
||||
.await?;
|
||||
if let Some(user) = user_res {
|
||||
Ok(user)
|
||||
} else {
|
||||
let client = request_client();
|
||||
let api_url = Url::parse(&format!(
|
||||
"https://{}/api/v1/accounts/id?username={}",
|
||||
LYSAND_DOMAIN.to_string(),
|
||||
name
|
||||
))?;
|
||||
let request = client.get(api_url).send().await?;
|
||||
let user_json = request.json::<ApiUser>().await?;
|
||||
Ok(db_user_from_url(user_json.uri).await?)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn db_user_from_url(url: Url) -> anyhow::Result<entities::user::Model> {
|
||||
println!("Fetching user from domain: {}", url.domain().unwrap());
|
||||
if !url.domain().eq(&Some(LYSAND_DOMAIN.as_str()))
|
||||
&& !url.domain().eq(&Some(API_DOMAIN.as_str()))
|
||||
{
|
||||
return Err(anyhow!("not versias domain"));
|
||||
}
|
||||
let user_res: Option<user::Model> = prelude::User::find()
|
||||
.filter(entities::user::Column::Url.eq(url.to_string()))
|
||||
.one(DB.get().unwrap())
|
||||
.await?;
|
||||
|
||||
if let Some(user) = user_res {
|
||||
Ok(user)
|
||||
} else {
|
||||
let ls_user = fetch_user_from_url(url).await?;
|
||||
let keypair = generate_actor_keypair()?;
|
||||
let bridge_user_url = generate_user_id(&API_DOMAIN, &ls_user.id.to_string())?;
|
||||
let inbox = Url::parse(&format!(
|
||||
"https://{}/{}/inbox",
|
||||
API_DOMAIN.to_string(),
|
||||
ls_user.username.clone()
|
||||
))?;
|
||||
let icon = if let Some(avatar) = ls_user.avatar {
|
||||
let avatar_url = avatar.select_rich_img_touple().await?;
|
||||
Some(IconType {
|
||||
type_: "Image".to_string(),
|
||||
media_type: Some(avatar_url.0),
|
||||
url: Url::parse(&avatar_url.1).unwrap(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let image = if let Some(header) = ls_user.header {
|
||||
let header_url = header.select_rich_img_touple().await?;
|
||||
Some(IconType {
|
||||
type_: "Image".to_string(),
|
||||
media_type: Some(header_url.0),
|
||||
url: Url::parse(&header_url.1).unwrap(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut attachments: Vec<AttachmentType> = Vec::new();
|
||||
if let Some(fields) = ls_user.fields {
|
||||
for attachment in fields {
|
||||
attachments.push(AttachmentType {
|
||||
type_: "PropertyValue".to_string(),
|
||||
name: attachment.key.select_rich_text().await?,
|
||||
value: attachment.value.select_rich_text().await?,
|
||||
});
|
||||
}
|
||||
}
|
||||
let mut tags: Vec<TagType> = Vec::new();
|
||||
if let Some(extensions) = ls_user.extensions {
|
||||
if let Some(custom_emojis) = extensions.custom_emojis {
|
||||
for emoji in custom_emojis.emojis {
|
||||
let touple = emoji.url.select_rich_img_touple().await?;
|
||||
tags.push(TagType {
|
||||
id: Some(Url::parse(&touple.1).unwrap()),
|
||||
name: ":".to_string() + &emoji.name + ":",
|
||||
type_: "Emoji".to_string(),
|
||||
updated: Some(Utc::now()),
|
||||
href: None,
|
||||
icon: Some(IconType {
|
||||
type_: "Image".to_string(),
|
||||
media_type: Some(touple.0),
|
||||
url: Url::parse(&touple.1).unwrap(),
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
let ap_json = Person {
|
||||
kind: Default::default(),
|
||||
id: bridge_user_url.clone().into(),
|
||||
preferred_username: ls_user.username.clone(),
|
||||
inbox,
|
||||
public_key: activitypub_federation::protocol::public_key::PublicKey {
|
||||
owner: bridge_user_url.clone(),
|
||||
public_key_pem: keypair.public_key.clone(),
|
||||
id: format!("{}#main-key", bridge_user_url.clone()),
|
||||
},
|
||||
name: ls_user
|
||||
.display_name
|
||||
.clone()
|
||||
.unwrap_or(ls_user.username.clone()),
|
||||
summary: option_content_format_text(ls_user.bio.clone()).await,
|
||||
url: ls_user.uri.clone(),
|
||||
indexable: Some(ls_user.indexable),
|
||||
discoverable: Some(true),
|
||||
manually_approves_followers: Some(false),
|
||||
followers: None,
|
||||
following: None,
|
||||
featured: None,
|
||||
featured_tags: None,
|
||||
also_known_as: None,
|
||||
outbox: None,
|
||||
endpoints: Some(EndpointType {
|
||||
shared_inbox: Url::parse(
|
||||
&format!(
|
||||
"https://{}/{}/inbox",
|
||||
API_DOMAIN.to_string(),
|
||||
&USERNAME.to_string()
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.unwrap(),
|
||||
}),
|
||||
icon,
|
||||
image,
|
||||
attachment: Some(attachments),
|
||||
tag: Some(tags),
|
||||
};
|
||||
let user = entities::user::ActiveModel {
|
||||
id: Set(ls_user.id.to_string()),
|
||||
username: Set(ls_user.username.clone()),
|
||||
name: Set(ls_user.display_name.unwrap_or(ls_user.username)),
|
||||
inbox: Set(ls_user.inbox.to_string()),
|
||||
public_key: Set(keypair.public_key.clone()),
|
||||
private_key: Set(Some(keypair.private_key.clone())),
|
||||
last_refreshed_at: Set(Utc::now()),
|
||||
follower_count: Set(0),
|
||||
following_count: Set(0),
|
||||
url: Set(ls_user.uri.to_string()),
|
||||
local: Set(true),
|
||||
created_at: Set(
|
||||
DateTime::from_timestamp(ls_user.created_at.unix_timestamp(), 0).unwrap(),
|
||||
),
|
||||
summary: Set(option_content_format_text(ls_user.bio).await),
|
||||
updated_at: Set(Some(Utc::now())),
|
||||
followers: Set(Some(ls_user.followers.to_string())),
|
||||
following: Set(Some(ls_user.following.to_string())),
|
||||
ap_json: Set(Some(serde_json::to_string(&ap_json).unwrap())),
|
||||
..Default::default()
|
||||
};
|
||||
let db = DB.get().unwrap();
|
||||
Ok(user.insert(db).await?)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fetch_note_from_url(url: Url) -> anyhow::Result<super::objects::Note> {
|
||||
let req_client = request_client();
|
||||
let request = req_client.get(url).send().await?;
|
||||
Ok(request.json::<super::objects::Note>().await?)
|
||||
}
|
||||
#[async_recursion]
|
||||
pub async fn receive_versia_note(
|
||||
note: Note,
|
||||
db_id: String,
|
||||
) -> anyhow::Result<entities::post::Model> {
|
||||
let versia_author: entities::user::Model = db_user_from_url(note.author.clone()).await?;
|
||||
let user_res = prelude::User::find_by_id(db_id)
|
||||
.one(DB.get().unwrap())
|
||||
.await;
|
||||
if user_res.is_err() {
|
||||
println!("{}", user_res.as_ref().unwrap_err());
|
||||
return Err(user_res.err().unwrap().into());
|
||||
}
|
||||
if let Some(target) = user_res? {
|
||||
let data = FEDERATION_CONFIG.get().unwrap();
|
||||
let id: ObjectId<post::Model> =
|
||||
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 mut tag: Vec<Mention> = Vec::new();
|
||||
for l_tag in note.mentions.clone().unwrap_or_default() {
|
||||
tag.push(Mention {
|
||||
href: l_tag, //TODO convert to ap url
|
||||
kind: Default::default(),
|
||||
})
|
||||
}
|
||||
let mut mentions = Vec::new();
|
||||
for obj in tag.clone() {
|
||||
mentions.push(obj.href.clone());
|
||||
}
|
||||
let to = match note
|
||||
.visibility
|
||||
.clone()
|
||||
.unwrap_or(super::objects::VisibilityType::Public)
|
||||
{
|
||||
super::objects::VisibilityType::Public => {
|
||||
let mut vec = vec![public(), Url::parse(&user.followers.to_string().as_str())?];
|
||||
vec.append(&mut mentions.clone());
|
||||
vec
|
||||
}
|
||||
super::objects::VisibilityType::Followers => {
|
||||
let mut vec = vec![Url::parse(&user.followers.to_string().as_str())?];
|
||||
vec.append(&mut mentions.clone());
|
||||
vec
|
||||
}
|
||||
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());
|
||||
vec
|
||||
}
|
||||
};
|
||||
let cc = match note
|
||||
.visibility
|
||||
.clone()
|
||||
.unwrap_or(super::objects::VisibilityType::Public)
|
||||
{
|
||||
super::objects::VisibilityType::Unlisted => Some(vec![public()]),
|
||||
_ => None,
|
||||
};
|
||||
let reply: Option<ObjectId<entities::post::Model>> =
|
||||
if let Some(rep) = note.replies_to.clone() {
|
||||
let note = fetch_note_from_url(rep).await?;
|
||||
let fake_rep_url = Url::parse(&format!(
|
||||
"https://{}/apbridge/object/{}",
|
||||
API_DOMAIN.to_string(),
|
||||
¬e.id.to_string()
|
||||
))?;
|
||||
Some(fake_rep_url.into())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let quote: Option<ObjectId<entities::post::Model>> = if let Some(rep) = note.quotes.clone()
|
||||
{
|
||||
let note = fetch_note_from_url(rep).await?;
|
||||
let fake_rep_url = Url::parse(&format!(
|
||||
"https://{}/apbridge/object/{}",
|
||||
API_DOMAIN.to_string(),
|
||||
¬e.id.to_string()
|
||||
))?;
|
||||
Some(fake_rep_url.into())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let reply_uuid: Option<String> = if let Some(rep) = note.replies_to.clone() {
|
||||
Some(db_post_from_url(rep).await?.id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let quote_uuid: Option<String> = if let Some(rep) = note.quotes.clone() {
|
||||
Some(db_post_from_url(rep).await?.id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let ap_note = crate::objects::post::Note {
|
||||
kind: Default::default(),
|
||||
id,
|
||||
sensitive: Some(note.is_sensitive.unwrap_or(false)),
|
||||
cc,
|
||||
to,
|
||||
tag,
|
||||
attributed_to: Url::parse(user.uri.clone().as_str()).unwrap().into(),
|
||||
content: option_content_format_text(note.content)
|
||||
.await
|
||||
.unwrap_or_default(),
|
||||
in_reply_to: reply.clone(),
|
||||
};
|
||||
|
||||
let visibility = match note
|
||||
.visibility
|
||||
.clone()
|
||||
.unwrap_or(super::objects::VisibilityType::Public)
|
||||
{
|
||||
super::objects::VisibilityType::Public => "public",
|
||||
super::objects::VisibilityType::Followers => "followers",
|
||||
super::objects::VisibilityType::Direct => "direct",
|
||||
super::objects::VisibilityType::Unlisted => "unlisted",
|
||||
};
|
||||
if let Some(obj) = note.replies_to {
|
||||
println!("Quoting: {}", db_post_from_url(obj).await?.url);
|
||||
}
|
||||
if let Some(obj) = note.quotes {
|
||||
println!("Replying to: {}", db_post_from_url(obj).await?.url);
|
||||
}
|
||||
let post = entities::post::ActiveModel {
|
||||
id: Set(note.id.to_string()),
|
||||
creator: Set(versia_author.id.clone()),
|
||||
content: Set(ap_note.content.clone()),
|
||||
sensitive: Set(ap_note.sensitive.unwrap_or_default()),
|
||||
created_at: Set(Utc
|
||||
.timestamp_micros(note.created_at.unix_timestamp())
|
||||
.unwrap()),
|
||||
local: Set(true),
|
||||
updated_at: Set(Some(Utc::now())),
|
||||
content_type: Set("Note".to_string()),
|
||||
visibility: Set(visibility.to_string()),
|
||||
title: Set(note.subject.clone()),
|
||||
url: Set(note.uri.clone().to_string()),
|
||||
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?;
|
||||
Ok(res)
|
||||
} else {
|
||||
Err(anyhow!("User not found"))
|
||||
}
|
||||
}
|
||||
62
src/versia/funcs.rs
Normal file
62
src/versia/funcs.rs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
||||
use time::OffsetDateTime;
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
entities::{follow_relation, prelude, user},
|
||||
utils::generate_follow_accept_id,
|
||||
API_DOMAIN, DB,
|
||||
};
|
||||
|
||||
use super::{
|
||||
conversion::{fetch_user_from_url, versia_user_from_db},
|
||||
objects::FollowResult,
|
||||
superx::request_client,
|
||||
};
|
||||
|
||||
pub async fn send_follow_accept_to_versia(model: follow_relation::Model) -> anyhow::Result<()> {
|
||||
let request_client = request_client();
|
||||
let db = DB.get().unwrap();
|
||||
|
||||
let id_raw = model.accept_id.unwrap();
|
||||
let id = uuid::Uuid::parse_str(&id_raw)?;
|
||||
let uri = generate_follow_accept_id(API_DOMAIN.as_str(), &id_raw)?;
|
||||
|
||||
let follower_model = prelude::User::find()
|
||||
.filter(user::Column::Id.eq(model.follower_id))
|
||||
.one(db)
|
||||
.await?
|
||||
.unwrap();
|
||||
let versia_follower = fetch_user_from_url(Url::parse(&follower_model.url)?).await?;
|
||||
|
||||
let followee_model = prelude::User::find()
|
||||
.filter(user::Column::Id.eq(model.followee_id))
|
||||
.one(db)
|
||||
.await?
|
||||
.unwrap();
|
||||
let versia_followee = versia_user_from_db(followee_model).await?;
|
||||
|
||||
let entity = FollowResult {
|
||||
rtype: super::objects::VersiaType::FollowAccept,
|
||||
id,
|
||||
uri,
|
||||
created_at: OffsetDateTime::now_utc(),
|
||||
author: versia_followee.uri,
|
||||
follower: versia_follower.uri,
|
||||
};
|
||||
|
||||
let request = request_client
|
||||
.post(versia_follower.inbox.as_str())
|
||||
.header("Content-Type", "application/json; charset=utf-8")
|
||||
.header("Accept", "application/json")
|
||||
.header("Date", entity.created_at.clone().to_string())
|
||||
.json(&entity);
|
||||
|
||||
let response = request.send().await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Failed to send follow accept to Versia"))
|
||||
}
|
||||
}
|
||||
269
src/versia/http.rs
Normal file
269
src/versia/http.rs
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
use activitypub_federation::{
|
||||
fetch::{object_id::ObjectId, webfinger::webfinger_resolve_actor},
|
||||
protocol::{context::WithContext, public_key::PublicKey},
|
||||
traits::Object,
|
||||
FEDERATION_CONTENT_TYPE,
|
||||
};
|
||||
use activitystreams_kinds::{activity::CreateType, object};
|
||||
use actix_web::{get, post, web, HttpResponse};
|
||||
use sea_orm::{query, ColumnTrait, EntityTrait, QueryFilter};
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
database::State,
|
||||
entities::{
|
||||
post::{self, Entity},
|
||||
prelude, user,
|
||||
},
|
||||
error,
|
||||
versia::{
|
||||
conversion::{versia_post_from_db, versia_user_from_db},
|
||||
inbox::inbox_entry,
|
||||
},
|
||||
objects::{self, person::Person},
|
||||
utils::{base_url_decode, generate_create_id, generate_user_id},
|
||||
Response, API_DOMAIN, DB, FEDERATION_CONFIG,
|
||||
};
|
||||
|
||||
use super::conversion::db_user_from_url;
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct VersiaQuery {
|
||||
// Post url
|
||||
url: Option<Url>,
|
||||
// User handle
|
||||
user: Option<String>,
|
||||
// User URL
|
||||
user_url: Option<Url>,
|
||||
}
|
||||
|
||||
#[get("/apbridge/versia/query")]
|
||||
async fn query_post(
|
||||
query: web::Query<VersiaQuery>,
|
||||
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?;
|
||||
println!("!!!!!!! DB USER GOT");
|
||||
let versia_user = versia_user_from_db(target).await?;
|
||||
|
||||
return Ok(HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.json(versia_user));
|
||||
}
|
||||
|
||||
if let Some(user) = query.user_url.clone() {
|
||||
let versia_user = versia_url_to_user(user).await?;
|
||||
|
||||
return Ok(HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.json(versia_user));
|
||||
}
|
||||
|
||||
let opt_model = prelude::Post::find()
|
||||
.filter(post::Column::Url.eq(query.url.clone().unwrap().as_str()))
|
||||
.one(db)
|
||||
.await?;
|
||||
let target;
|
||||
if let Some(model) = opt_model {
|
||||
target = model;
|
||||
} else {
|
||||
target = ObjectId::<post::Model>::from(Url::parse(query.url.clone().unwrap().as_str())?)
|
||||
.dereference(&data.to_request_data())
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.json(versia_post_from_db(target).await?))
|
||||
}
|
||||
|
||||
#[post("/apbridge/versia/inbox")]
|
||||
async fn versia_inbox(
|
||||
body: web::Bytes,
|
||||
state: web::Data<State>,
|
||||
) -> actix_web::Result<HttpResponse, error::Error> {
|
||||
let string = String::from_utf8(body.to_vec())?;
|
||||
inbox_entry(&string).await?;
|
||||
Ok(HttpResponse::Created().finish())
|
||||
}
|
||||
|
||||
#[get("/apbridge/object/{post}")]
|
||||
async fn fetch_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(FEDERATION_CONTENT_TYPE)
|
||||
.json(crate::objects::post::Note::from_db(&post)))
|
||||
}
|
||||
|
||||
#[get("/apbridge/user/{user}")]
|
||||
async fn fetch_user(
|
||||
path: web::Path<String>,
|
||||
state: web::Data<State>,
|
||||
) -> actix_web::Result<HttpResponse, error::Error> {
|
||||
let db = DB.get().unwrap();
|
||||
|
||||
let user = prelude::User::find()
|
||||
.filter(user::Column::Id.eq(path.as_str()))
|
||||
.one(db)
|
||||
.await?;
|
||||
|
||||
let user = match user {
|
||||
Some(user) => user,
|
||||
None => return Ok(HttpResponse::NotFound().finish()),
|
||||
};
|
||||
|
||||
let deserialized_user: Person = serde_json::from_str(user.ap_json.as_ref().unwrap().as_str())?;
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type(FEDERATION_CONTENT_TYPE)
|
||||
.json(WithContext::new_default(deserialized_user)))
|
||||
}
|
||||
|
||||
#[get("/apbridge/versia/object/{post}")]
|
||||
async fn fetch_versia_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(versia_post_from_db(post).await?))
|
||||
}
|
||||
|
||||
#[get("/apbridge/create/{id}/{base64url}")]
|
||||
async fn create_activity(
|
||||
path: web::Path<(String, String)>,
|
||||
state: web::Data<State>,
|
||||
) -> actix_web::Result<HttpResponse, error::Error> {
|
||||
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))
|
||||
}
|
||||
|
||||
pub async fn versia_url_to_user(url: Url) -> anyhow::Result<super::objects::User> {
|
||||
let db = DB.get().unwrap();
|
||||
let data = FEDERATION_CONFIG.get().unwrap();
|
||||
|
||||
let opt_model = prelude::User::find()
|
||||
.filter(user::Column::Url.eq(url.as_str()))
|
||||
.one(db)
|
||||
.await?;
|
||||
let target;
|
||||
if let Some(model) = opt_model {
|
||||
target = model;
|
||||
} else {
|
||||
target = ObjectId::<user::Model>::from(url)
|
||||
.dereference(&data.to_request_data())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
Ok(versia_user_from_db(target).await?)
|
||||
}
|
||||
|
||||
pub async fn versia_url_to_user_and_model(
|
||||
url: Url,
|
||||
) -> anyhow::Result<(super::objects::User, user::Model)> {
|
||||
let db = DB.get().unwrap();
|
||||
let data = FEDERATION_CONFIG.get().unwrap();
|
||||
|
||||
let opt_model = prelude::User::find()
|
||||
.filter(user::Column::Url.eq(url.to_string()))
|
||||
.one(db)
|
||||
.await?;
|
||||
let target;
|
||||
if let Some(model) = opt_model {
|
||||
target = model;
|
||||
} else {
|
||||
target = ObjectId::<user::Model>::from(url)
|
||||
.dereference(&data.to_request_data())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
Ok((versia_user_from_db(target.clone()).await?, target))
|
||||
}
|
||||
|
||||
pub async fn main_versia_url_to_user_and_model(
|
||||
url: Url,
|
||||
) -> anyhow::Result<(super::objects::User, user::Model)> {
|
||||
let db = DB.get().unwrap();
|
||||
let data = FEDERATION_CONFIG.get().unwrap();
|
||||
|
||||
let opt_model = prelude::User::find()
|
||||
.filter(user::Column::Url.eq(url.as_str()))
|
||||
.one(db)
|
||||
.await?;
|
||||
let target;
|
||||
if let Some(model) = opt_model {
|
||||
target = model;
|
||||
} else {
|
||||
target = db_user_from_url(url.clone()).await?;
|
||||
}
|
||||
|
||||
Ok((versia_user_from_db(target.clone()).await?, target))
|
||||
}
|
||||
133
src/versia/inbox.rs
Normal file
133
src/versia/inbox.rs
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
use crate::{
|
||||
activities::follow::Follow,
|
||||
entities::{
|
||||
self, follow_relation,
|
||||
prelude::{self, FollowRelation},
|
||||
user,
|
||||
},
|
||||
versia::http::main_versia_url_to_user_and_model,
|
||||
utils::generate_follow_req_id,
|
||||
API_DOMAIN, DB, FEDERATION_CONFIG,
|
||||
};
|
||||
use activitypub_federation::{
|
||||
activity_sending::SendActivityTask, fetch::object_id::ObjectId, protocol::context::WithContext,
|
||||
};
|
||||
use activitystreams_kinds::activity::FollowType;
|
||||
use anyhow::Result;
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityOrSelect, EntityTrait, QueryFilter, Set};
|
||||
use serde::Deserialize;
|
||||
use url::Url;
|
||||
|
||||
use super::{
|
||||
conversion::versia_user_from_db,
|
||||
http::{versia_url_to_user, versia_url_to_user_and_model},
|
||||
objects::VersiaType,
|
||||
};
|
||||
|
||||
pub async fn inbox_entry(json: &str) -> Result<()> {
|
||||
// Deserialize the JSON string into a dynamic value
|
||||
let value: serde_json::Value = serde_json::from_str(json).unwrap();
|
||||
|
||||
// Extract the "type" field from the JSON
|
||||
if let Some(json_type) = value.get("type") {
|
||||
// Match the "type" field with the corresponding VersiaType
|
||||
match json_type.as_str() {
|
||||
Some("Note") => {
|
||||
let note: super::objects::Note = serde_json::from_str(json)?;
|
||||
}
|
||||
Some("Patch") => {
|
||||
let patch: super::objects::Patch = serde_json::from_str(json)?;
|
||||
}
|
||||
Some("Follow") => {
|
||||
let follow_req: super::objects::Follow = serde_json::from_str(json)?;
|
||||
follow_request(follow_req).await?;
|
||||
}
|
||||
Some("FollowAccept") => {
|
||||
let follow_accept: super::objects::FollowResult = serde_json::from_str(json)?;
|
||||
}
|
||||
Some("FollowReject") => {
|
||||
let follow_rej: super::objects::FollowResult = serde_json::from_str(json)?;
|
||||
}
|
||||
// Add more cases for other types as needed
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Unknown 'type' field in JSON, it is {}",
|
||||
json_type
|
||||
));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("Missing 'type' field in JSON"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn follow_request(follow: super::objects::Follow) -> Result<()> {
|
||||
// Check if the user is already following the requester
|
||||
let db = DB.get().unwrap();
|
||||
let query = FollowRelation::find()
|
||||
.filter(follow_relation::Column::FollowerId.eq(follow.author.to_string().as_str()))
|
||||
.filter(follow_relation::Column::FolloweeId.eq(follow.followee.to_string().as_str()))
|
||||
.one(db)
|
||||
.await?;
|
||||
if query.is_some() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"User is already follow requesting / following the followee"
|
||||
));
|
||||
}
|
||||
let data = FEDERATION_CONFIG.get().unwrap();
|
||||
let author = main_versia_url_to_user_and_model(follow.author.into()).await?;
|
||||
println!("Followee URL: {}", &follow.followee.to_string());
|
||||
let followee = versia_url_to_user_and_model(follow.followee.into()).await?;
|
||||
let serial_ap_author = serde_json::from_str::<crate::objects::person::Person>(
|
||||
&(author.1.ap_json.clone()).unwrap(),
|
||||
)?;
|
||||
let serial_ap_followee = serde_json::from_str::<crate::objects::person::Person>(
|
||||
&(followee.1.ap_json.clone()).unwrap(),
|
||||
)?;
|
||||
|
||||
let id = uuid::Uuid::now_v7().to_string();
|
||||
|
||||
let followee_object: ObjectId<user::Model> = serial_ap_followee.id;
|
||||
let localuser_object: ObjectId<user::Model> = serial_ap_author.id;
|
||||
|
||||
println!(
|
||||
"Sending follow request to {}",
|
||||
&followee.0.display_name.unwrap_or(followee.0.username)
|
||||
);
|
||||
let create = Follow {
|
||||
actor: localuser_object.clone(),
|
||||
object: followee_object.clone(),
|
||||
kind: FollowType::Follow,
|
||||
id: generate_follow_req_id(&API_DOMAIN.to_string(), id.clone().as_str())?,
|
||||
};
|
||||
|
||||
let ap_json = serde_json::to_string(&create)?;
|
||||
|
||||
let create_with_context = WithContext::new_default(create);
|
||||
|
||||
let follow_db_entry = follow_relation::ActiveModel {
|
||||
id: Set(id.clone()),
|
||||
followee_id: Set(followee.0.id.to_string()),
|
||||
follower_id: Set(author.0.id.to_string()),
|
||||
ap_id: Set(Some(id.clone())),
|
||||
ap_json: Set(ap_json),
|
||||
remote: Set(false),
|
||||
..Default::default()
|
||||
};
|
||||
follow_db_entry.insert(db).await?;
|
||||
|
||||
let sends = SendActivityTask::prepare(
|
||||
&create_with_context,
|
||||
&author.1,
|
||||
vec![serial_ap_followee.inbox],
|
||||
&data.to_request_data(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
for send in sends {
|
||||
send.sign_and_send(&data.to_request_data()).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
7
src/versia/mod.rs
Normal file
7
src/versia/mod.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
pub mod conversion;
|
||||
pub mod funcs;
|
||||
pub mod http;
|
||||
pub mod inbox;
|
||||
pub mod objects;
|
||||
pub mod superx;
|
||||
pub mod test;
|
||||
428
src/versia/objects.rs
Normal file
428
src/versia/objects.rs
Normal file
|
|
@ -0,0 +1,428 @@
|
|||
extern crate serde; // 1.0.68
|
||||
extern crate serde_derive; // 1.0.68
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::{Display, Formatter},
|
||||
};
|
||||
|
||||
use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
use time::{
|
||||
format_description::well_known::{iso8601, Iso8601},
|
||||
OffsetDateTime,
|
||||
};
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
const FORMAT: Iso8601<6651332276412969266533270467398074368> = Iso8601::<
|
||||
{
|
||||
iso8601::Config::DEFAULT
|
||||
.set_year_is_six_digits(false)
|
||||
.encode()
|
||||
},
|
||||
>;
|
||||
time::serde::format_description!(iso_versia, OffsetDateTime, FORMAT);
|
||||
|
||||
fn sort_alphabetically<T: Serialize, S: serde::Serializer>(
|
||||
value: &T,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
let value = serde_json::to_value(value).map_err(serde::ser::Error::custom)?;
|
||||
value.serialize(serializer)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct SortAlphabetically<T: Serialize>(#[serde(serialize_with = "sort_alphabetically")] pub T);
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub enum VersiaType {
|
||||
User,
|
||||
Note,
|
||||
Patch,
|
||||
Like,
|
||||
Dislike,
|
||||
Follow,
|
||||
FollowAccept,
|
||||
FollowReject,
|
||||
Undo,
|
||||
Extension,
|
||||
ServerMetadata,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub enum CategoryType {
|
||||
Microblog,
|
||||
Forum,
|
||||
Blog,
|
||||
Image,
|
||||
Video,
|
||||
Audio,
|
||||
Messaging,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum VisibilityType {
|
||||
Public,
|
||||
Unlisted,
|
||||
Followers,
|
||||
Direct,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub enum VersiaExtensions {
|
||||
#[serde(rename = "org.versia:microblogging/Announce")]
|
||||
Announce,
|
||||
#[serde(rename = "org.versia:custom_emojis")]
|
||||
CustomEmojis,
|
||||
#[serde(rename = "org.versia:reactions/Reaction")]
|
||||
Reaction,
|
||||
#[serde(rename = "org.versia:reactions")]
|
||||
Reactions,
|
||||
#[serde(rename = "org.versia:polls")]
|
||||
Polls,
|
||||
#[serde(rename = "org.versia:is_cat")]
|
||||
IsCat,
|
||||
#[serde(rename = "org.versia:server_endorsement/Endorsement")]
|
||||
Endorsement,
|
||||
#[serde(rename = "org.versia:server_endorsement")]
|
||||
EndorsementCollection,
|
||||
#[serde(rename = "org.versia:reports/Report")]
|
||||
Report,
|
||||
#[serde(rename = "org.versia:vanity")]
|
||||
Vanity,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct PublicKey {
|
||||
pub public_key: String,
|
||||
pub actor: Url,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ContentHash {
|
||||
md5: Option<String>,
|
||||
sha1: Option<String>,
|
||||
sha256: Option<String>,
|
||||
sha512: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ContentFormat {
|
||||
pub x: HashMap<String, ContentEntry>,
|
||||
}
|
||||
|
||||
impl ContentFormat {
|
||||
pub async fn select_rich_text(&self) -> anyhow::Result<String> {
|
||||
if let Some(entry) = self.x.get("text/x.misskeymarkdown") {
|
||||
return Ok(entry.content.clone());
|
||||
}
|
||||
if let Some(entry) = self.x.get("text/html") {
|
||||
return Ok(entry.content.clone());
|
||||
}
|
||||
if let Some(entry) = self.x.get("text/markdown") {
|
||||
return Ok(entry.content.clone());
|
||||
}
|
||||
if let Some(entry) = self.x.get("text/plain") {
|
||||
return Ok(entry.content.clone());
|
||||
}
|
||||
|
||||
Ok(self.x.clone().values().next().unwrap().content.clone())
|
||||
}
|
||||
|
||||
pub async fn select_rich_img(&self) -> anyhow::Result<String> {
|
||||
if let Some(entry) = self.x.get("image/webp") {
|
||||
return Ok(entry.content.clone());
|
||||
}
|
||||
if let Some(entry) = self.x.get("image/png") {
|
||||
return Ok(entry.content.clone());
|
||||
}
|
||||
if let Some(entry) = self.x.get("image/avif") {
|
||||
return Ok(entry.content.clone());
|
||||
}
|
||||
if let Some(entry) = self.x.get("image/jxl") {
|
||||
return Ok(entry.content.clone());
|
||||
}
|
||||
if let Some(entry) = self.x.get("image/jpeg") {
|
||||
return Ok(entry.content.clone());
|
||||
}
|
||||
if let Some(entry) = self.x.get("image/gif") {
|
||||
return Ok(entry.content.clone());
|
||||
}
|
||||
if let Some(entry) = self.x.get("image/bmp") {
|
||||
return Ok(entry.content.clone());
|
||||
}
|
||||
|
||||
Ok(self.x.clone().values().next().unwrap().content.clone())
|
||||
}
|
||||
|
||||
pub async fn select_rich_img_touple(&self) -> anyhow::Result<(String, String)> {
|
||||
if let Some(entry) = self.x.get("image/webp") {
|
||||
return Ok(("image/webp".to_string(), entry.content.clone()));
|
||||
}
|
||||
if let Some(entry) = self.x.get("image/png") {
|
||||
return Ok(("image/png".to_string(), entry.content.clone()));
|
||||
}
|
||||
if let Some(entry) = self.x.get("image/avif") {
|
||||
return Ok(("image/avif".to_string(), entry.content.clone()));
|
||||
}
|
||||
if let Some(entry) = self.x.get("image/jxl") {
|
||||
return Ok(("image/jxl".to_string(), entry.content.clone()));
|
||||
}
|
||||
if let Some(entry) = self.x.get("image/jpeg") {
|
||||
return Ok(("image/jpeg".to_string(), entry.content.clone()));
|
||||
}
|
||||
if let Some(entry) = self.x.get("image/gif") {
|
||||
return Ok(("image/gif".to_string(), entry.content.clone()));
|
||||
}
|
||||
if let Some(entry) = self.x.get("image/bmp") {
|
||||
return Ok(("image/bmp".to_string(), entry.content.clone()));
|
||||
}
|
||||
|
||||
let touple = self.x.iter().next().unwrap();
|
||||
|
||||
Ok((touple.0.clone(), touple.1.content.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for ContentFormat {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut seq = serializer.serialize_map(Some(self.x.len()))?;
|
||||
for (k, v) in &self.x {
|
||||
seq.serialize_entry(&k.to_string(), &v)?;
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
}
|
||||
impl<'de> Deserialize<'de> for ContentFormat {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let map = HashMap::deserialize(deserializer)?;
|
||||
Ok(ContentFormat { x: map })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct FieldKV {
|
||||
pub key: ContentFormat,
|
||||
pub value: ContentFormat,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ContentEntry {
|
||||
content: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
description: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
size: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
hash: Option<ContentHash>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
blurhash: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
fps: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
width: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
height: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
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 {
|
||||
pub public_key: PublicKey,
|
||||
#[serde(rename = "type")]
|
||||
pub rtype: VersiaType,
|
||||
pub id: Uuid,
|
||||
pub uri: Url,
|
||||
#[serde(with = "iso_versia")]
|
||||
pub created_at: OffsetDateTime,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub display_name: Option<String>,
|
||||
pub inbox: Url,
|
||||
pub outbox: Url,
|
||||
pub featured: Url,
|
||||
pub followers: Url,
|
||||
pub following: Url,
|
||||
pub likes: Url,
|
||||
pub dislikes: Url,
|
||||
pub username: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bio: Option<ContentFormat>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub avatar: Option<ContentFormat>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub header: Option<ContentFormat>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub fields: Option<Vec<FieldKV>>,
|
||||
pub indexable: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extensions: Option<ExtensionSpecs>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ExtensionSpecs {
|
||||
#[serde(rename = "org.versia:custom_emojis")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub custom_emojis: Option<CustomEmojis>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct CustomEmojis {
|
||||
pub emojis: Vec<CustomEmoji>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct CustomEmoji {
|
||||
pub name: String,
|
||||
pub url: ContentFormat,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct DeviceInfo {
|
||||
name: String,
|
||||
version: String,
|
||||
url: Url,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct LinkPreview {
|
||||
description: String,
|
||||
title: String,
|
||||
link: Url,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
image: Option<Url>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
icon: Option<Url>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Note {
|
||||
#[serde(rename = "type")]
|
||||
pub rtype: VersiaType,
|
||||
pub id: Uuid,
|
||||
pub uri: Url,
|
||||
pub author: Url,
|
||||
#[serde(with = "iso_versia")]
|
||||
pub created_at: OffsetDateTime,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub category: Option<CategoryType>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub content: Option<ContentFormat>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub device: Option<DeviceInfo>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub previews: Option<Vec<LinkPreview>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub group: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub attachments: Option<Vec<ContentFormat>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub replies_to: Option<Url>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub quotes: Option<Url>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mentions: Option<Vec<Url>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub subject: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub is_sensitive: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub visibility: Option<VisibilityType>,
|
||||
//TODO extensions
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Patch {
|
||||
#[serde(rename = "type")]
|
||||
pub rtype: VersiaType,
|
||||
pub id: Uuid,
|
||||
pub uri: Url,
|
||||
pub author: Url,
|
||||
#[serde(with = "iso_versia")]
|
||||
pub created_at: OffsetDateTime,
|
||||
#[serde(with = "iso_versia")]
|
||||
pub patched_at: OffsetDateTime,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub category: Option<CategoryType>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub content: Option<ContentFormat>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub device: Option<DeviceInfo>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub previews: Option<Vec<LinkPreview>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub group: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub attachments: Option<Vec<ContentFormat>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub replies_to: Option<Url>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub quotes: Option<Url>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mentions: Option<Vec<Url>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub subject: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub is_sensitive: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub visibility: Option<VisibilityType>,
|
||||
//TODO extensions
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Outbox {
|
||||
pub first: Url,
|
||||
pub last: Url,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub next: Option<Url>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub prev: Option<Url>,
|
||||
pub items: Vec<Note>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Follow {
|
||||
#[serde(rename = "type")]
|
||||
pub rtype: VersiaType,
|
||||
pub id: Uuid,
|
||||
pub uri: Url,
|
||||
pub author: Url,
|
||||
#[serde(with = "iso_versia")]
|
||||
pub created_at: OffsetDateTime,
|
||||
pub followee: Url,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct FollowResult {
|
||||
#[serde(rename = "type")]
|
||||
pub rtype: VersiaType,
|
||||
pub id: Uuid,
|
||||
pub uri: Url,
|
||||
pub author: Url,
|
||||
#[serde(with = "iso_versia")]
|
||||
pub created_at: OffsetDateTime,
|
||||
pub follower: Url,
|
||||
}
|
||||
55
src/versia/superx.rs
Normal file
55
src/versia/superx.rs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
use super::objects::SortAlphabetically;
|
||||
|
||||
pub async fn deserialize_user(data: String) -> anyhow::Result<super::objects::User> {
|
||||
let user: super::objects::User = serde_json::from_str(&data)?;
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
pub async fn serialize_user(user: super::objects::User) -> anyhow::Result<String> {
|
||||
let data = serde_json::to_string(&SortAlphabetically(&user))?;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
pub async fn deserialize_versia_type(data: String) -> anyhow::Result<super::objects::VersiaType> {
|
||||
let versia_type: super::objects::VersiaType = serde_json::from_str(&data)?;
|
||||
Ok(versia_type)
|
||||
}
|
||||
|
||||
pub async fn serialize_versia_type(
|
||||
versia_type: super::objects::VersiaType,
|
||||
) -> anyhow::Result<String> {
|
||||
let data = serde_json::to_string(&versia_type)?;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
pub async fn deserialize_note(data: String) -> anyhow::Result<super::objects::Note> {
|
||||
let post: super::objects::Note = serde_json::from_str(&data)?;
|
||||
Ok(post)
|
||||
}
|
||||
|
||||
pub async fn serialize_note(post: super::objects::Note) -> anyhow::Result<String> {
|
||||
let data = serde_json::to_string(&SortAlphabetically(&post))?;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
pub async fn deserialize_outbox(data: String) -> anyhow::Result<super::objects::Outbox> {
|
||||
let outbox: super::objects::Outbox = serde_json::from_str(&data)?;
|
||||
Ok(outbox)
|
||||
}
|
||||
|
||||
pub async fn serialize_outbox(outbox: super::objects::Outbox) -> anyhow::Result<String> {
|
||||
let data = serde_json::to_string(&SortAlphabetically(&outbox))?;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn request_client() -> reqwest::Client {
|
||||
reqwest::Client::builder()
|
||||
.user_agent(concat!(
|
||||
env!("CARGO_PKG_NAME"),
|
||||
"/",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
))
|
||||
.build()
|
||||
.unwrap()
|
||||
}
|
||||
62
src/versia/test.rs
Normal file
62
src/versia/test.rs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
use crate::versia::objects::SortAlphabetically;
|
||||
|
||||
use super::superx::request_client;
|
||||
|
||||
#[actix_web::test]
|
||||
async fn test_user_serial() {
|
||||
let client = request_client();
|
||||
let response = client
|
||||
.get("https://versia.social/users/018ec082-0ae1-761c-b2c5-22275a611771")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
let user = super::superx::deserialize_user(response.text().await.unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
let response_outbox = client.get(user.outbox.as_str()).send().await.unwrap();
|
||||
let outbox = super::superx::deserialize_outbox(response_outbox.text().await.unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(outbox.items.len() > 0);
|
||||
}
|
||||
|
||||
pub async fn main() -> anyhow::Result<()> {
|
||||
let client = request_client();
|
||||
|
||||
println!("Requesting user");
|
||||
let response = client
|
||||
.get("https://versia.social/users/018ec082-0ae1-761c-b2c5-22275a611771")
|
||||
.send()
|
||||
.await?;
|
||||
println!("Response: {:?}", response);
|
||||
let user_json = response.text().await?;
|
||||
println!("User JSON: {:?}", user_json);
|
||||
let user = super::superx::deserialize_user(user_json).await?;
|
||||
|
||||
println!("\n\n\nUser: ");
|
||||
print!("{:#?}", user);
|
||||
|
||||
println!("\n\n\nas JSON:");
|
||||
let user_json = serde_json::to_string_pretty(&SortAlphabetically(&user))?;
|
||||
println!("{}", user_json);
|
||||
|
||||
let response_outbox = client.get(user.outbox.as_str()).send().await?;
|
||||
|
||||
let outbox_json = response_outbox.text().await?;
|
||||
let outbox = super::superx::deserialize_outbox(outbox_json).await?;
|
||||
|
||||
println!("\n\n\nOutbox: ");
|
||||
print!("{:#?}", outbox);
|
||||
|
||||
println!("\n\n\nas AP:");
|
||||
for item in outbox.items {
|
||||
let ap_item = super::conversion::receive_versia_note(
|
||||
item,
|
||||
"https://ap.versia.social/example".to_string(),
|
||||
)
|
||||
.await?;
|
||||
println!("{:#?}", ap_item);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue