diff --git a/plume-models/src/posts.rs b/plume-models/src/posts.rs index 65c98735..bbf3d865 100644 --- a/plume-models/src/posts.rs +++ b/plume-models/src/posts.rs @@ -3,20 +3,29 @@ use crate::{ post_authors::*, safe_string::SafeString, schema::posts, tags::*, timeline::*, users::User, Connection, Error, PostEvent::*, Result, CONFIG, POST_CHAN, }; -use activitypub::{ +use activitystreams::{ activity::{Create, Delete, Update}, + base::{AnyBase, AsBase, Base}, + iri, // CustomObject, link, - object::{Article, Image, Tombstone}, - CustomObject, + object::{kind::ImageType, ApObject, Article, Image, Object, Tombstone}, + prelude::*, + primitives::OneOrMany, + time::OffsetDateTime, }; -use chrono::{NaiveDateTime, TimeZone, Utc}; +use chrono::{NaiveDateTime, Utc}; use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl}; use once_cell::sync::Lazy; use plume_common::{ activity_pub::{ inbox::{AsActor, AsObject, FromId}, sign::Signer, - Hashtag, Id, IntoId, Licensed, Source, PUBLIC_VISIBILITY, + // Hashtag, Id, IntoId, /Licensed, Source, PUBLIC_VISIBILITY, + Hashtag, + Id, + IntoId, + // Source, + PUBLIC_VISIBILITY, }, utils::{iri_percent_encode_seg, md_to_html}, }; @@ -24,7 +33,43 @@ use riker::actors::{Publish, Tell}; use std::collections::{HashMap, HashSet}; use std::sync::{Arc, Mutex}; -pub type LicensedArticle = CustomObject; +// pub type LicensedArticle = CustomObject; + +#[derive( + Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, serde::Deserialize, serde::Serialize, +)] +enum LinkType { + Link, +} + +impl std::fmt::Display for LinkType { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, stringify!(Link)) + } +} +impl Default for LinkType { + fn default() -> Self { + LinkType::Link + } +} + +#[derive( + Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, serde::Deserialize, serde::Serialize, +)] +enum SourceType { + Source, +} + +impl std::fmt::Display for SourceType { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, stringify!(Source)) + } +} +impl Default for SourceType { + fn default() -> Self { + SourceType::Source + } +} static BLOG_FQN_CACHE: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new())); @@ -339,7 +384,8 @@ impl Post { })) } - pub fn to_activity(&self, conn: &Connection) -> Result { + pub fn to_activity(&self, conn: &Connection) -> Result> { + // pub fn to_activity(&self, conn: &Connection) -> Result { let cc = self.get_receivers_urls(conn)?; let to = vec![PUBLIC_VISIBILITY.to_string()]; @@ -353,9 +399,9 @@ impl Post { .collect::>(); mentions_json.append(&mut tags_json); - let mut article = Article::default(); - article.object_props.set_name_string(self.title.clone())?; - article.object_props.set_id_string(self.ap_url.clone())?; + let mut article = ApObject::new(Article::new()); + article.set_name(self.title.clone()); + article.set_id(iri!(self.ap_url)); let mut authors = self .get_authors(conn)? @@ -363,82 +409,110 @@ impl Post { .map(|x| Id::new(x.ap_url)) .collect::>(); authors.push(self.get_blog(conn)?.into_id()); // add the blog URL here too - article - .object_props - .set_attributed_to_link_vec::(authors)?; - article - .object_props - .set_content_string(self.content.get().clone())?; - article.ap_object_props.set_source_object(Source { - content: self.source.clone(), - media_type: String::from("text/markdown"), - })?; - article - .object_props - .set_published_utctime(Utc.from_utc_datetime(&self.creation_date))?; - article - .object_props - .set_summary_string(self.subtitle.clone())?; - article.object_props.tag = Some(json!(mentions_json)); + // article.set_attributed_to(authors.into()); + article.set_content(self.content.get().clone()); + article.set_source( + // FIXME + // article.ap_object_props.set_source_object(Source { + // content: self.source.clone(), + // media_type: String::from("text/markdown"), + // })?; + AnyBase::from_base( + *ApObject::new(Object::new()) + .set_content(self.source.clone()) + .set_media_type("text/markdown".parse().expect("Unreachable")) + .base_ref(), + ), + ); + article.set_published( + OffsetDateTime::from_unix_timestamp_nanos(self.creation_date.timestamp_nanos().into()) + .expect("Unreachable"), + ); + article.set_summary(self.subtitle.clone()); + article.set_tag(AnyBase::from_arbitrary_json(json!(mentions_json))?); if let Some(media_id) = self.cover_id { let media = Media::get(conn, media_id)?; - let mut cover = Image::default(); - cover.object_props.set_url_string(media.url()?)?; + let mut cover = Image::new(); + cover.set_url(media.url()?); if media.sensitive { - cover - .object_props - .set_summary_string(media.content_warning.unwrap_or_default())?; + cover.set_summary(media.content_warning.unwrap_or_default()); } - cover.object_props.set_content_string(media.alt_text)?; - cover - .object_props - .set_attributed_to_link_vec(vec![User::get(conn, media.owner_id)?.into_id()])?; - article.object_props.set_icon_object(cover)?; + cover.set_content(media.alt_text); + cover.set_attributed_to(iri!(User::get(conn, media.owner_id)?.into_id())); + let base = Base::retract(cover)?.into_generic()?; + article.set_icon(AnyBase::from_base(base)); } - article.object_props.set_url_string(self.ap_url.clone())?; - article - .object_props - .set_to_link_vec::(to.into_iter().map(Id::new).collect())?; - article - .object_props - .set_cc_link_vec::(cc.into_iter().map(Id::new).collect())?; - let mut license = Licensed::default(); - license.set_license_string(self.license.clone())?; - Ok(LicensedArticle::new(article, license)) + article.set_url(self.ap_url.clone()); + let tos = Vec::with_capacity(to.len()); + for addr in to.into_iter() { + tos.push(iri!(addr)); + } + article.set_many_tos(tos); + let ccs = Vec::with_capacity(cc.len()); + for addr in cc.into_iter() { + ccs.push(iri!(addr)); + } + article.set_many_ccs(ccs); + // let mut license = Licensed::default(); + // license.set_license_string(self.license.clone())?; + // Ok(LicensedArticle::new(article, license)) + Ok(article) } pub fn create_activity(&self, conn: &Connection) -> Result { let article = self.to_activity(conn)?; - let mut act = Create::default(); - act.object_props - .set_id_string(format!("{}/activity", self.ap_url))?; - act.object_props - .set_to_link_vec::(article.object.object_props.to_link_vec()?)?; - act.object_props - .set_cc_link_vec::(article.object.object_props.cc_link_vec()?)?; - act.create_props - .set_actor_link(Id::new(self.get_authors(conn)?[0].clone().ap_url))?; - act.create_props.set_object_object(article)?; + let base = Base::retract(article)?.into_generic()?; + let mut act = Create::new::<_, OneOrMany>( + iri!(Id::new(self.get_authors(conn)?[0].clone().ap_url)), + base.into(), + ); + let id_string = format!("{}/activity", self.ap_url); + act.set_id(iri!(id_string)); + act.set_many_tos( + article + .to() + .expect("exists") + .into_iter() + .map(|addr| AnyBase::from_xsd_any_uri(*addr.id().expect("exists"))), + ); + act.set_many_ccs( + article + .cc() + .expect("exists") + .into_iter() + .map(|addr| AnyBase::from_xsd_any_uri(*addr.id().expect("exists"))), + ); Ok(act) } pub fn update_activity(&self, conn: &Connection) -> Result { let article = self.to_activity(conn)?; - let mut act = Update::default(); - act.object_props.set_id_string(format!( + let base = Base::retract(article)?.into_generic()?; + let mut act = Update::new::<_, OneOrMany>( + iri!(Id::new(self.get_authors(conn)?[0].clone().ap_url)), + base.into(), + ); + act.set_id(iri!(format!( "{}/update-{}", self.ap_url, Utc::now().timestamp() - ))?; - act.object_props - .set_to_link_vec::(article.object.object_props.to_link_vec()?)?; - act.object_props - .set_cc_link_vec::(article.object.object_props.cc_link_vec()?)?; - act.update_props - .set_actor_link(Id::new(self.get_authors(conn)?[0].clone().ap_url))?; - act.update_props.set_object_object(article)?; + ))); + act.set_many_tos( + article + .to() + .expect("exists") + .into_iter() + .map(|addr| AnyBase::from_xsd_any_uri(*addr.id().expect("exists"))), + ); + act.set_many_ccs( + article + .cc() + .expect("exists") + .into_iter() + .map(|addr| AnyBase::from_xsd_any_uri(*addr.id().expect("exists"))), + ); Ok(act) } @@ -447,10 +521,8 @@ impl Post { .into_iter() .map(|m| { ( - m.link_props - .href_string() - .ok() - .and_then(|ap_url| User::find_by_ap_url(conn, &ap_url).ok()) + m.href() + .and_then(|ap_url| User::find_by_ap_url(conn, ap_url.as_str()).ok()) .map(|u| u.id), m, ) @@ -566,18 +638,17 @@ impl Post { } pub fn build_delete(&self, conn: &Connection) -> Result { - let mut act = Delete::default(); - act.delete_props - .set_actor_link(self.get_authors(conn)?[0].clone().into_id())?; + let mut tombstone = Tombstone::new(); + let ap_url = iri!(self.ap_url); + tombstone.set_id(ap_url); + let base = Base::retract(tombstone)?.into_generic()?; + let mut act = Delete::new::<_, OneOrMany>( + iri!(self.get_authors(conn)?[0].clone().into_id()), + base.into(), + ); - let mut tombstone = Tombstone::default(); - tombstone.object_props.set_id_string(self.ap_url.clone())?; - act.delete_props.set_object_object(tombstone)?; - - act.object_props - .set_id_string(format!("{}#delete", self.ap_url))?; - act.object_props - .set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY)])?; + act.set_id(format!("{}#delete", self.ap_url).parse()?); + act.set_many_tos(vec![iri!(Id::new(PUBLIC_VISIBILITY))]); Ok(act) } @@ -614,54 +685,96 @@ impl Post { impl FromId for Post { type Error = Error; - type Object = LicensedArticle; + // type Object = LicensedArticle; + type Object = Article; fn from_db(conn: &DbConn, id: &str) -> Result { Self::find_by_ap_url(conn, id) } - fn from_activity(conn: &DbConn, article: LicensedArticle) -> Result { + // fn from_activity(conn: &DbConn, article: LicensedArticle) -> Result { + fn from_activity(conn: &DbConn, article: Article) -> Result { let conn = conn; - let license = article.custom_props.license_string().unwrap_or_default(); - let article = article.object; + // let license = article.license().unwrap_or_default(); - let (blog, authors) = article - .object_props - .attributed_to_link_vec::()? - .into_iter() - .fold((None, vec![]), |(blog, mut authors), link| { - let url = link; - match User::from_id(conn, &url, None, CONFIG.proxy()) { + let (blog, authors) = article.attributed_to().into_iter().fold( + (None, vec![]), + |(blog, mut authors), link| { + let href = link + .as_one() + .expect("exists and only") + .extend::, LinkType>() + .expect("exists") + .expect("object") + .href() + .expect("exists") + .as_str(); + match User::from_id(conn, href, None, CONFIG.proxy()) { Ok(u) => { authors.push(u); (blog, authors) } Err(_) => ( - blog.or_else(|| Blog::from_id(conn, &url, None, CONFIG.proxy()).ok()), + blog.or_else(|| Blog::from_id(conn, href, None, CONFIG.proxy()).ok()), authors, ), } - }); + }, + ); - let cover = article - .object_props - .icon_object::() + let cover = article.icon().and_then(|img| { + Media::from_activity( + conn, + &img.as_one() + .expect("possible") + .extend::() + .expect("possilbe") + .expect("exists"), + ) .ok() - .and_then(|img| Media::from_activity(conn, &img).ok().map(|m| m.id)); + .map(|m| m.id) + }); - let title = article.object_props.name_string()?; + let title = article + .name() + .expect("exists") + .as_single_xsd_string() + .expect("exists and only") + .to_string(); let ap_url = article - .object_props - .url_string() - .or_else(|_| article.object_props.id_string())?; - let post = Post::from_db(conn, &ap_url) + .url() + .map(|url| url.as_single_id().expect("exists")) + .or_else(|| article.id()) + .expect("exists") + .to_string(); + let post = Post::from_db(conn, ap_url.as_str()) .and_then(|mut post| { let mut updated = false; let slug = Self::slug(&title); - let content = SafeString::new(&article.object_props.content_string()?); - let subtitle = article.object_props.summary_string()?; - let source = article.ap_object_props.source_object::()?.content; + let content = SafeString::new( + &article + .content() + .expect("exists and only") + .as_one() + .expect("only") + .as_xsd_string() + .expect("possible"), + ); + let subtitle = article + .summary() + .expect("exists and only") + .as_one() + .expect("possible") + .as_xsd_string() + .expect("possible") + .to_string(); + let source = ApObject::new(article) + .source() + .ok_or(Error::MissingApProperty)? + .as_xsd_string() + .expect("possible") + .to_string(); if post.slug != slug { post.slug = slug.to_string(); updated = true; @@ -674,10 +787,10 @@ impl FromId for Post { post.content = content; updated = true; } - if post.license != license { - post.license = license.clone(); - updated = true; - } + // if post.license != license { + // post.license = license.clone(); + // updated = true; + // } if post.subtitle != subtitle { post.subtitle = subtitle; updated = true; @@ -698,20 +811,50 @@ impl FromId for Post { Ok(post) }) .or_else(|_| { + let published = article.published().ok_or(Error::MissingApProperty)?; + let published_secs = published.unix_timestamp(); + let published_nanos = + published.unix_timestamp_nanos() - (published_secs * 1000 * 1000) as i128; + use std::convert::TryInto; + let creation_date = NaiveDateTime::from_timestamp( + published_secs, + published_nanos.try_into().expect("Unreachable"), + ); Post::insert( conn, NewPost { blog_id: blog.ok_or(Error::NotFound)?.id, slug: Self::slug(&title).to_string(), title, - content: SafeString::new(&article.object_props.content_string()?), + content: SafeString::new( + &article + .content() + .expect("exists and only") + .as_one() + .expect("only") + .as_xsd_string() + .expect("possible"), + ), published: true, - license, + // license, + license: "".into(), // FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields ap_url, - creation_date: Some(article.object_props.published_utctime()?.naive_utc()), - subtitle: article.object_props.summary_string()?, - source: article.ap_object_props.source_object::()?.content, + creation_date: Some(creation_date), + subtitle: article + .summary() + .expect("exists and only") + .as_one() + .expect("possible") + .as_xsd_string() + .expect("possible") + .to_string(), + source: ApObject::new(article) + .source() + .ok_or(Error::MissingApProperty)? + .as_xsd_string() + .expect("possible") + .to_string(), cover_id: cover, }, ) @@ -735,24 +878,27 @@ impl FromId for Post { .2 .into_iter() .collect::>(); - if let Some(serde_json::Value::Array(tags)) = article.object_props.tag { - for tag in tags { - serde_json::from_value::(tag.clone()) - .map(|m| Mention::from_activity(conn, &m, post.id, true, true)) - .ok(); + if let Some(tags) = article.tag() { + for tag in tags.iter() { + Mention::from_activity( + conn, + &tag.extend::() + .expect("possible") + .expect("exists"), + post.id, + true, + true, + ) + .ok(); - serde_json::from_value::(tag.clone()) - .map_err(Error::from) - .and_then(|t| { - let tag_name = t.name_string()?; - Ok(Tag::from_activity( - conn, - &t, - post.id, - hashtags.remove(&tag_name), - )) - }) - .ok(); + // TODO + // let tag_name = tag.as_xsd_string().ok_or(Error::MissingApProperty)?; + // Ok(Tag::from_activity( + // conn, + // &tag, + // post.id, + // hashtags.remove(&tag_name), + // )); } } @@ -806,33 +952,54 @@ pub struct PostUpdate { impl FromId for PostUpdate { type Error = Error; - type Object = LicensedArticle; + // type Object = LicensedArticle; + type Object = Article; fn from_db(_: &DbConn, _: &str) -> Result { // Always fail because we always want to deserialize the AP object Err(Error::NotFound) } - fn from_activity(conn: &DbConn, updated: LicensedArticle) -> Result { + // fn from_activity(conn: &DbConn, updated: LicensedArticle) -> Result { + fn from_activity(conn: &DbConn, updated: Article) -> Result { Ok(PostUpdate { - ap_url: updated.object.object_props.id_string()?, - title: updated.object.object_props.name_string().ok(), - subtitle: updated.object.object_props.summary_string().ok(), - content: updated.object.object_props.content_string().ok(), - cover: updated - .object - .object_props - .icon_object::() + ap_url: updated.id().ok_or(Error::MissingApProperty)?.to_string(), + title: updated + .name() + .map(|name| name.as_single_xsd_string().expect("exists").into()), + subtitle: updated + .summary() + .map(|summary| summary.as_single_xsd_string().expect("exists").into()), + content: updated + .content() + .map(|content| content.as_single_xsd_string().expect("exists").into()), + cover: updated.icon().map_or(None, |img| { + Media::from_activity( + conn, + &img.as_one() + .expect("possible") + .extend::() + .expect("possible") + .expect("exists"), + ) + .map(|m| m.id) .ok() - .and_then(|img| Media::from_activity(conn, &img).ok().map(|m| m.id)), - source: updated - .object - .ap_object_props - .source_object::() - .ok() - .map(|x| x.content), - license: updated.custom_props.license_string().ok(), - tags: updated.object.object_props.tag, + }), + source: ApObject::new(updated).source().map(|x| { + x.extend::, SourceType>() + .expect("possible") + .expect("exists") + .content() + .expect("exists and only") + .as_one() + .expect("only") + .xsd_string() + .expect("possible") + }), + license: None, // updated.custom_props.license_string().ok(), // FIXME + tags: updated.tag().and_then(|tags| { + serde_json::to_value(tags.as_single_base().expect("possible")).ok() + }), }) } @@ -1021,10 +1188,12 @@ mod tests { article.object_props.set_id_string("Yo".into()).unwrap(); let mut license = Licensed::default(); license.set_license_string("WTFPL".into()).unwrap(); - let full_article = LicensedArticle::new(article, license); + // let full_article = LicensedArticle::new(article, license); + let full_article = Article::new(article, license); let json = serde_json::to_value(full_article).unwrap(); - let article_from_json: LicensedArticle = serde_json::from_value(json).unwrap(); + // let article_from_json: LicensedArticle = serde_json::from_value(json).unwrap(); + let article_from_json: Article = serde_json::from_value(json).unwrap(); assert_eq!( "Yo", &article_from_json.object.object_props.id_string().unwrap() @@ -1051,7 +1220,8 @@ mod tests { "published": "2014-12-12T12:12:12Z", "to": [plume_common::activity_pub::PUBLIC_VISIBILITY] }); - let article: LicensedArticle = serde_json::from_value(json).unwrap(); + // let article: LicensedArticle = serde_json::from_value(json).unwrap(); + let article: Article = serde_json::from_value(json).unwrap(); assert_eq!( "https://plu.me/~/Blog/my-article", &article.object.object_props.id_string().unwrap()