mirror of
https://github.com/versia-pub/activitypub.git
synced 2025-12-06 06:38:20 +01:00
feat: add missing fields on AP users
This commit is contained in:
parent
692e4bff22
commit
1a741c6420
|
|
@ -5,6 +5,7 @@ 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;
|
||||
mod m20240719_235452_user_ap_column;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
|
|
@ -17,6 +18,7 @@ impl MigratorTrait for Migrator {
|
|||
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),
|
||||
Box::new(m20240719_235452_user_ap_column::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
35
migration/src/m20240719_235452_user_ap_column.rs
Normal file
35
migration/src/m20240719_235452_user_ap_column.rs
Normal file
|
|
@ -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(User::Table)
|
||||
.add_column_if_not_exists(ColumnDef::new(User::ApJson).string())
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(User::Table)
|
||||
.drop_column(User::ApJson)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
pub enum User {
|
||||
Table,
|
||||
ApJson,
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ pub struct Model {
|
|||
pub following: Option<String>,
|
||||
pub followers: Option<String>,
|
||||
pub inbox: String,
|
||||
pub ap_json: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ use activitystreams_kinds::public;
|
|||
use anyhow::{anyhow, Ok};
|
||||
use async_recursion::async_recursion;
|
||||
use chrono::{DateTime, TimeZone, Utc};
|
||||
use reqwest::header;
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::OffsetDateTime;
|
||||
|
|
@ -11,9 +12,13 @@ use url::Url;
|
|||
use crate::{
|
||||
database::State,
|
||||
entities::{self, post, prelude, user},
|
||||
objects::post::Mention,
|
||||
objects::{
|
||||
self,
|
||||
person::{AttachmentType, EndpointType, IconType, Person, TagType},
|
||||
post::Mention,
|
||||
},
|
||||
utils::{generate_lysand_post_url, generate_object_id, generate_user_id},
|
||||
API_DOMAIN, DB, FEDERATION_CONFIG, LYSAND_DOMAIN,
|
||||
API_DOMAIN, DB, FEDERATION_CONFIG, LOCAL_USER_NAME, LYSAND_DOMAIN, USERNAME,
|
||||
};
|
||||
|
||||
use super::{
|
||||
|
|
@ -80,6 +85,8 @@ pub async fn lysand_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.lysand.org/apbridge/lysand/inbox")?;
|
||||
let outbox_url = Url::parse(
|
||||
("https://ap.lysand.org/apbridge/lysand/outbox/".to_string() + &user.id).as_str(),
|
||||
|
|
@ -113,6 +120,59 @@ pub async fn lysand_user_from_db(
|
|||
"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());
|
||||
content_format.x.insert(icon.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());
|
||||
content_format.x.insert(image.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();
|
||||
let content_entry = ContentEntry::from_string(tag.id.to_string());
|
||||
content_format.x.insert(tag.type_, content_entry);
|
||||
emojis.push(super::objects::CustomEmoji {
|
||||
name: tag.name,
|
||||
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::LysandType::User,
|
||||
id: uuid::Uuid::try_parse(&user.id)?,
|
||||
|
|
@ -127,9 +187,9 @@ pub async fn lysand_user_from_db(
|
|||
likes: likes_url,
|
||||
dislikes: dislikes_url,
|
||||
bio: Some(bio),
|
||||
avatar: None,
|
||||
header: None,
|
||||
fields: None,
|
||||
avatar,
|
||||
header,
|
||||
fields: Some(fields),
|
||||
indexable: false,
|
||||
created_at: OffsetDateTime::from_unix_timestamp(user.created_at.timestamp()).unwrap(),
|
||||
public_key: PublicKey {
|
||||
|
|
@ -137,6 +197,7 @@ pub async fn lysand_user_from_db(
|
|||
public_key: "AAAAC3NzaC1lZDI1NTE5AAAAIMxsX+lEWkHZt9NOvn9yYFP0Z++186LY4b97C4mwj/f2"
|
||||
.to_string(), // dummy key
|
||||
},
|
||||
extensions: Some(extensions),
|
||||
};
|
||||
Ok(user)
|
||||
}
|
||||
|
|
@ -211,6 +272,101 @@ pub async fn db_user_from_url(url: Url) -> anyhow::Result<entities::user::Model>
|
|||
} 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: 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: 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: Url::parse(&touple.1).unwrap(),
|
||||
name: emoji.name,
|
||||
type_: "Emoji".to_string(),
|
||||
updated: Utc::now(),
|
||||
icon: IconType {
|
||||
type_: "Image".to_string(),
|
||||
media_type: 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,
|
||||
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()),
|
||||
|
|
@ -230,6 +386,7 @@ pub async fn db_user_from_url(url: Url) -> anyhow::Result<entities::user::Model>
|
|||
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();
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ use crate::{
|
|||
},
|
||||
error,
|
||||
lysand::conversion::{lysand_post_from_db, lysand_user_from_db},
|
||||
objects,
|
||||
objects::{self, person::Person},
|
||||
utils::{base_url_decode, generate_create_id, generate_user_id},
|
||||
Response, API_DOMAIN, DB, FEDERATION_CONFIG,
|
||||
};
|
||||
|
|
@ -134,29 +134,11 @@ async fn fetch_user(
|
|||
None => return Ok(HttpResponse::NotFound().finish()),
|
||||
};
|
||||
|
||||
let bridge_user_url = generate_user_id(&API_DOMAIN, &user.id)?;
|
||||
let inbox = Url::parse(&format!(
|
||||
"https://{}/{}/inbox",
|
||||
API_DOMAIN.to_string(),
|
||||
&user.username.clone()
|
||||
))?;
|
||||
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(crate::objects::person::Person {
|
||||
kind: Default::default(),
|
||||
id: bridge_user_url.clone().into(),
|
||||
preferred_username: user.username.clone(),
|
||||
name: user.name.clone(),
|
||||
summary: user.summary.clone(),
|
||||
url: Url::parse(user.url.as_str()).unwrap(),
|
||||
inbox,
|
||||
public_key: PublicKey {
|
||||
owner: bridge_user_url.clone(),
|
||||
public_key_pem: user.public_key,
|
||||
id: format!("{}#main-key", bridge_user_url.clone()),
|
||||
},
|
||||
})))
|
||||
.json(WithContext::new_default(deserialized_user)))
|
||||
}
|
||||
|
||||
#[get("/apbridge/lysand/object/{post}")]
|
||||
|
|
|
|||
|
|
@ -156,6 +156,34 @@ impl ContentFormat {
|
|||
|
||||
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 {
|
||||
|
|
@ -182,8 +210,8 @@ impl<'de> Deserialize<'de> for ContentFormat {
|
|||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct FieldKV {
|
||||
key: ContentFormat,
|
||||
value: ContentFormat,
|
||||
pub key: ContentFormat,
|
||||
pub value: ContentFormat,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
|
|
@ -224,7 +252,6 @@ pub struct User {
|
|||
#[serde(with = "iso_lysand")]
|
||||
pub created_at: OffsetDateTime,
|
||||
pub display_name: Option<String>,
|
||||
// TODO bio: Option<String>,
|
||||
pub inbox: Url,
|
||||
pub outbox: Url,
|
||||
pub featured: Url,
|
||||
|
|
@ -238,6 +265,24 @@ pub struct User {
|
|||
pub header: Option<ContentFormat>,
|
||||
pub fields: Option<Vec<FieldKV>>,
|
||||
pub indexable: bool,
|
||||
pub extensions: Option<ExtensionSpecs>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ExtensionSpecs {
|
||||
#[serde(rename = "org.lysand:custom_emojis")]
|
||||
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)]
|
||||
|
|
|
|||
|
|
@ -79,6 +79,50 @@ pub struct Person {
|
|||
pub id: ObjectId<user::Model>,
|
||||
pub inbox: Url,
|
||||
pub public_key: PublicKey,
|
||||
pub indexable: Option<bool>,
|
||||
pub discoverable: Option<bool>,
|
||||
pub manually_approves_followers: Option<bool>,
|
||||
pub followers: Option<Url>,
|
||||
pub following: Option<Url>,
|
||||
pub featured: Option<Url>,
|
||||
pub endpoints: Option<EndpointType>,
|
||||
pub outbox: Option<Url>,
|
||||
pub featured_tags: Option<Url>,
|
||||
pub tag: Option<Vec<TagType>>,
|
||||
pub icon: Option<IconType>,
|
||||
pub image: Option<IconType>,
|
||||
pub attachment: Option<Vec<AttachmentType>>,
|
||||
}
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct TagType {
|
||||
pub id: Url,
|
||||
pub name: String,
|
||||
#[serde(rename = "type")]
|
||||
pub type_: String,
|
||||
pub updated: DateTime<Utc>,
|
||||
pub icon: IconType,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EndpointType {
|
||||
pub shared_inbox: Url,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct IconType {
|
||||
#[serde(rename = "type")]
|
||||
pub type_: String, //Always "Image"
|
||||
pub media_type: String,
|
||||
pub url: Url,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct AttachmentType {
|
||||
#[serde(rename = "type")]
|
||||
pub type_: String, //Always "PropertyValue"
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
|
|
@ -103,16 +147,8 @@ impl Object for user::Model {
|
|||
}
|
||||
|
||||
async fn into_json(self, _data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
|
||||
Ok(Person {
|
||||
preferred_username: self.username.clone(),
|
||||
kind: Default::default(),
|
||||
id: Url::parse(&self.id).unwrap().into(),
|
||||
inbox: Url::parse(&self.inbox).unwrap(),
|
||||
public_key: self.public_key(),
|
||||
name: self.name.clone(),
|
||||
summary: self.summary.clone(),
|
||||
url: Url::parse(&self.url).unwrap(),
|
||||
})
|
||||
let serialized = serde_json::from_str(self.ap_json.as_ref().unwrap().as_str())?;
|
||||
Ok(serialized)
|
||||
}
|
||||
|
||||
async fn verify(
|
||||
|
|
@ -135,6 +171,7 @@ impl Object for user::Model {
|
|||
if let Some(user) = query {
|
||||
return Ok(user);
|
||||
}
|
||||
let copied_json = json.clone();
|
||||
let model = user::ActiveModel {
|
||||
id: Set(Uuid::now_v7().to_string()),
|
||||
username: Set(json.preferred_username),
|
||||
|
|
@ -148,6 +185,7 @@ impl Object for user::Model {
|
|||
following_count: Set(0),
|
||||
created_at: Set(Utc::now()),
|
||||
last_refreshed_at: Set(Utc::now()),
|
||||
ap_json: Set(Some(serde_json::to_string(&copied_json).unwrap())),
|
||||
..Default::default()
|
||||
};
|
||||
let model = model.insert(_data.database_connection.as_ref()).await;
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ impl Object for post::Model {
|
|||
visibility: Set("public".to_string()), // TODO: make this use the real visibility
|
||||
sensitive: Set(json.sensitive.clone().unwrap_or_default()),
|
||||
url: Set(json.id.clone().to_string()),
|
||||
ap_json: Set(Some(serde_json::to_string(&json).unwrap())),
|
||||
..Default::default()
|
||||
};
|
||||
let post = post
|
||||
|
|
|
|||
Loading…
Reference in a new issue