diff --git a/upub/core/src/selector.rs b/upub/core/src/selector.rs deleted file mode 100644 index dd46c66..0000000 --- a/upub/core/src/selector.rs +++ /dev/null @@ -1,402 +0,0 @@ -use std::collections::{hash_map::Entry, HashMap}; - -use apb::{ActivityMut, LinkMut, ObjectMut}; -use sea_orm::{sea_query::{IntoColumnRef, IntoCondition}, ColumnTrait, Condition, ConnectionTrait, DbErr, EntityName, EntityTrait, FromQueryResult, Iden, Iterable, ModelTrait, Order, QueryFilter, QueryOrder, QueryResult, QuerySelect, RelationTrait, Select, SelectColumns}; - -use crate::model; - -pub struct Query; - -impl Query { - pub fn activities(my_id: Option) -> Select { - let mut select = model::addressing::Entity::find() - .distinct_on([ - (model::addressing::Entity, model::addressing::Column::Published).into_column_ref(), - (model::activity::Entity, model::activity::Column::Internal).into_column_ref(), - ]) - .join(sea_orm::JoinType::InnerJoin, model::addressing::Relation::Activities.def()) - .join(sea_orm::JoinType::LeftJoin, model::addressing::Relation::Objects.def()) - .filter( - // TODO ghetto double inner join because i want to filter out tombstones - Condition::any() - .add(model::activity::Column::Id.is_not_null()) - .add(model::object::Column::Id.is_not_null()) - ) - .order_by(model::addressing::Column::Published, Order::Desc) - .order_by(model::activity::Column::Internal, Order::Desc) - .select_only(); - - for col in model::activity::Column::iter() { - select = select.select_column_as(col, format!("{}{}", model::activity::Entity.table_name(), col.to_string())); - } - - for col in model::object::Column::iter() { - select = select.select_column_as(col, format!("{}{}", model::object::Entity.table_name(), col.to_string())); - } - - if let Some(uid) = my_id { - select = select - .join( - sea_orm::JoinType::LeftJoin, - model::object::Relation::Likes.def() - .on_condition(move |_l, _r| model::like::Column::Actor.eq(uid).into_condition()), - ) - .select_column_as(model::like::Column::Actor, format!("{}{}", model::like::Entity.table_name(), model::like::Column::Actor.to_string())); - } - - select - } - - pub fn objects(my_id: Option) -> Select { - let mut select = model::addressing::Entity::find() - .distinct_on([ - (model::addressing::Entity, model::addressing::Column::Published).into_column_ref(), - (model::object::Entity, model::object::Column::Internal).into_column_ref(), - ]) - .join(sea_orm::JoinType::InnerJoin, model::addressing::Relation::Objects.def()) - .order_by(model::addressing::Column::Published, Order::Desc) - .order_by(model::object::Column::Internal, Order::Desc) - .select_only(); - - for col in model::object::Column::iter() { - select = select.select_column_as(col, format!("{}{}", model::object::Entity.table_name(), col.to_string())); - } - - if let Some(uid) = my_id { - select = select - .join( - sea_orm::JoinType::LeftJoin, - model::object::Relation::Likes.def() - .on_condition(move |_l, _r| model::like::Column::Actor.eq(uid).into_condition()), - ) - .select_column_as(model::like::Column::Actor, format!("{}{}", model::like::Entity.table_name(), model::like::Column::Actor.to_string())); - } - - select - } -} - - - -pub struct RichActivity { - pub activity: model::activity::Model, - pub object: Option, - pub liked: Option, - pub attachments: Option>, - pub hashtags: Option>, - pub mentions: Option>, -} - -impl FromQueryResult for RichActivity { - fn from_query_result(res: &QueryResult, _pre: &str) -> Result { - Ok(RichActivity { - activity: model::activity::Model::from_query_result(res, model::activity::Entity.table_name())?, - object: model::object::Model::from_query_result(res, model::object::Entity.table_name()).ok(), - liked: res.try_get(model::like::Entity.table_name(), &model::like::Column::Actor.to_string()).ok(), - attachments: None, hashtags: None, mentions: None, - }) - } -} - -impl RichActivity { - pub fn ap(self) -> serde_json::Value { - let object = match self.object { - None => apb::Node::maybe_link(self.activity.object.clone()), - Some(o) => { - // TODO can we avoid repeating this tags code? - let mut tags = Vec::new(); - if let Some(mentions) = self.mentions { - for mention in mentions { - tags.push( - apb::new() - .set_link_type(Some(apb::LinkType::Mention)) - .set_href(&mention.actor) - // TODO do i need to set name? i could join while batch loading or put the @name in - // each mention object... - ); - } - } - if let Some(hashtags) = self.hashtags { - for hash in hashtags { - tags.push( - // TODO ewwww set_name clash and cant use builder, wtf is this - LinkMut::set_name(apb::new(), Some(&format!("#{}", hash.name))) - .set_link_type(Some(apb::LinkType::Hashtag)) - // TODO do we need to set href too? we can't access context here, quite an issue! - ); - } - } - apb::Node::object( - o.ap() - .set_liked_by_me(if self.liked.is_some() { Some(true) } else { None }) - .set_tag(apb::Node::array(tags)) - .set_attachment(match self.attachments { - None => apb::Node::Empty, - Some(vec) => apb::Node::array( - vec.into_iter().map(|x| x.ap()).collect() - ), - }) - ) - }, - }; - self.activity.ap().set_object(object) - } -} - -pub struct RichObject { - pub object: model::object::Model, - pub liked: Option, - pub attachments: Option>, - pub hashtags: Option>, - pub mentions: Option>, -} - -impl FromQueryResult for RichObject { - fn from_query_result(res: &QueryResult, _pre: &str) -> Result { - Ok(RichObject { - object: model::object::Model::from_query_result(res, model::object::Entity.table_name())?, - liked: res.try_get(model::like::Entity.table_name(), &model::like::Column::Actor.to_string()).ok(), - attachments: None, hashtags: None, mentions: None, - }) - } -} - -impl RichObject { - pub fn ap(self) -> serde_json::Value { - // TODO can we avoid repeating this tags code? - let mut tags = Vec::new(); - if let Some(mentions) = self.mentions { - for mention in mentions { - tags.push( - apb::new() - .set_link_type(Some(apb::LinkType::Mention)) - .set_href(&mention.actor) - // TODO do i need to set name? i could join while batch loading or put the @name in - // each mention object... - ); - } - } - if let Some(hashtags) = self.hashtags { - for hash in hashtags { - tags.push( - // TODO ewwww set_name clash and cant use builder, wtf is this - LinkMut::set_name(apb::new(), Some(&format!("#{}", hash.name))) - .set_link_type(Some(apb::LinkType::Hashtag)) - // TODO do we need to set href too? we can't access context here, quite an issue! - ); - } - } - self.object.ap() - .set_liked_by_me(if self.liked.is_some() { Some(true) } else { None }) - .set_tag(apb::Node::array(tags)) - .set_attachment(match self.attachments { - None => apb::Node::Empty, - Some(vec) => apb::Node::array( - vec.into_iter().map(|x| x.ap()).collect() - ) - }) - } -} - -#[async_trait::async_trait] -pub trait BatchFillable: Sized { - async fn with_batched(self, tx: &impl ConnectionTrait) -> Result - where - E: BatchFillableComparison + EntityTrait, - E::Model: BatchFillableKey + Send + FromQueryResult + ModelTrait, - RichActivity: BatchFillableAcceptor>, - RichObject: BatchFillableAcceptor>; -} - -#[async_trait::async_trait] -impl BatchFillable for Vec { - // TODO 3 iterations... can we make it in less passes? - async fn with_batched(mut self, tx: &impl ConnectionTrait) -> Result - where - E: BatchFillableComparison + EntityTrait, - E::Model: BatchFillableKey + Send + FromQueryResult + ModelTrait, - RichActivity: BatchFillableAcceptor>, - RichObject: BatchFillableAcceptor> - { - let ids : Vec = self.iter().filter_map(|x| Some(x.object.as_ref()?.internal)).collect(); - let batch = E::find() - .filter(E::comparison(ids)) - .all(tx) - .await?; - let mut map : HashMap> = HashMap::new(); - for element in batch { - match map.entry(element.key()) { - Entry::Occupied(mut x) => { x.get_mut().push(element); }, - Entry::Vacant(x) => { x.insert(vec![element]); }, - } - } - for element in self.iter_mut() { - if let Some(ref object) = element.object { - if let Some(v) = map.remove(&object.internal) { - element.accept(v); - } - } - } - Ok(self) - } -} - -#[async_trait::async_trait] -impl BatchFillable for Vec { - // TODO 3 iterations... can we make it in less passes? - async fn with_batched(mut self, tx: &impl ConnectionTrait) -> Result - where - E: BatchFillableComparison + EntityTrait, - E::Model: BatchFillableKey + Send + FromQueryResult + ModelTrait, - RichActivity: BatchFillableAcceptor>, - RichObject: BatchFillableAcceptor> - { - let ids : Vec = self.iter().map(|x| x.object.internal).collect(); - let batch = E::find() - .filter(E::comparison(ids)) - .all(tx) - .await?; - let mut map : HashMap> = HashMap::new(); - for element in batch { - match map.entry(element.key()) { - Entry::Occupied(mut x) => { x.get_mut().push(element); }, - Entry::Vacant(x) => { x.insert(vec![element]); }, - } - } - for element in self.iter_mut() { - if let Some(v) = map.remove(&element.object.internal) { - element.accept(v); - } - } - Ok(self) - } -} - -#[async_trait::async_trait] -impl BatchFillable for RichActivity { - async fn with_batched(mut self, tx: &impl ConnectionTrait) -> Result - where - E: BatchFillableComparison + EntityTrait, - E::Model: BatchFillableKey + Send + FromQueryResult + ModelTrait, - RichActivity: BatchFillableAcceptor>, - RichObject: BatchFillableAcceptor> - { - if let Some(ref obj) = self.object { - let batch =E::find() - .filter(E::comparison(vec![obj.internal])) - .all(tx) - .await?; - self.accept(batch); - } - Ok(self) - } -} - -#[async_trait::async_trait] -impl BatchFillable for RichObject { - async fn with_batched(mut self, tx: &impl ConnectionTrait) -> Result - where - E: BatchFillableComparison + EntityTrait, - E::Model: BatchFillableKey + Send + FromQueryResult + ModelTrait, - RichActivity: BatchFillableAcceptor>, - RichObject: BatchFillableAcceptor> - { - let batch = E::find() - .filter(E::comparison(vec![self.object.internal])) - .all(tx) - .await?; - self.accept(batch); - Ok(self) - } -} - - -// welcome to interlocking trait hell, enjoy your stay -mod hell { - use sea_orm::{sea_query::IntoCondition, ColumnTrait}; - - pub trait BatchFillableComparison { - fn comparison(ids: Vec) -> sea_orm::Condition; - } - - impl BatchFillableComparison for crate::model::attachment::Entity { - fn comparison(ids: Vec) -> sea_orm::Condition { - crate::model::attachment::Column::Object.is_in(ids).into_condition() - } - } - - impl BatchFillableComparison for crate::model::mention::Entity { - fn comparison(ids: Vec) -> sea_orm::Condition { - crate::model::mention::Column::Object.is_in(ids).into_condition() - } - } - - impl BatchFillableComparison for crate::model::hashtag::Entity { - fn comparison(ids: Vec) -> sea_orm::Condition { - crate::model::hashtag::Column::Object.is_in(ids).into_condition() - } - } - - pub trait BatchFillableKey { - fn key(&self) -> i64; - } - - impl BatchFillableKey for crate::model::attachment::Model { - fn key(&self) -> i64 { - self.object - } - } - - impl BatchFillableKey for crate::model::mention::Model { - fn key(&self) -> i64 { - self.object - } - } - - impl BatchFillableKey for crate::model::hashtag::Model { - fn key(&self) -> i64 { - self.object - } - } - - pub trait BatchFillableAcceptor { - fn accept(&mut self, batch: B); - } - - impl BatchFillableAcceptor> for super::RichActivity { - fn accept(&mut self, batch: Vec) { - self.attachments = Some(batch); - } - } - - impl BatchFillableAcceptor> for super::RichActivity { - fn accept(&mut self, batch: Vec) { - self.hashtags = Some(batch); - } - } - - impl BatchFillableAcceptor> for super::RichActivity { - fn accept(&mut self, batch: Vec) { - self.mentions = Some(batch); - } - } - - impl BatchFillableAcceptor> for super::RichObject { - fn accept(&mut self, batch: Vec) { - self.attachments = Some(batch); - } - } - - impl BatchFillableAcceptor> for super::RichObject { - fn accept(&mut self, batch: Vec) { - self.hashtags = Some(batch); - } - } - - impl BatchFillableAcceptor> for super::RichObject { - fn accept(&mut self, batch: Vec) { - self.mentions = Some(batch); - } - } -} -use hell::*; diff --git a/upub/core/src/selector/batch.rs b/upub/core/src/selector/batch.rs new file mode 100644 index 0000000..2c52270 --- /dev/null +++ b/upub/core/src/selector/batch.rs @@ -0,0 +1,209 @@ +use std::collections::{hash_map::Entry, HashMap}; + +use sea_orm::{ConnectionTrait, DbErr, EntityTrait, FromQueryResult, ModelTrait, QueryFilter}; +use super::{RichActivity, RichObject}; + +#[async_trait::async_trait] +pub trait BatchFillable: Sized { + async fn with_batched(self, tx: &impl ConnectionTrait) -> Result + where + E: BatchFillableComparison + EntityTrait, + E::Model: BatchFillableKey + Send + FromQueryResult + ModelTrait, + RichActivity: BatchFillableAcceptor>, + RichObject: BatchFillableAcceptor>; +} + + +#[async_trait::async_trait] +impl BatchFillable for Vec { + // TODO 3 iterations... can we make it in less passes? + async fn with_batched(mut self, tx: &impl ConnectionTrait) -> Result + where + E: BatchFillableComparison + EntityTrait, + E::Model: BatchFillableKey + Send + FromQueryResult + ModelTrait, + RichActivity: BatchFillableAcceptor>, + RichObject: BatchFillableAcceptor> + { + let ids : Vec = self.iter().filter_map(|x| Some(x.object.as_ref()?.internal)).collect(); + let batch = E::find() + .filter(E::comparison(ids)) + .all(tx) + .await?; + let mut map : HashMap> = HashMap::new(); + for element in batch { + match map.entry(element.key()) { + Entry::Occupied(mut x) => { x.get_mut().push(element); }, + Entry::Vacant(x) => { x.insert(vec![element]); }, + } + } + for element in self.iter_mut() { + if let Some(ref object) = element.object { + if let Some(v) = map.remove(&object.internal) { + element.accept(v); + } + } + } + Ok(self) + } +} + +#[async_trait::async_trait] +impl BatchFillable for Vec { + // TODO 3 iterations... can we make it in less passes? + async fn with_batched(mut self, tx: &impl ConnectionTrait) -> Result + where + E: BatchFillableComparison + EntityTrait, + E::Model: BatchFillableKey + Send + FromQueryResult + ModelTrait, + RichActivity: BatchFillableAcceptor>, + RichObject: BatchFillableAcceptor> + { + let ids : Vec = self.iter().map(|x| x.object.internal).collect(); + let batch = E::find() + .filter(E::comparison(ids)) + .all(tx) + .await?; + let mut map : HashMap> = HashMap::new(); + for element in batch { + match map.entry(element.key()) { + Entry::Occupied(mut x) => { x.get_mut().push(element); }, + Entry::Vacant(x) => { x.insert(vec![element]); }, + } + } + for element in self.iter_mut() { + if let Some(v) = map.remove(&element.object.internal) { + element.accept(v); + } + } + Ok(self) + } +} + +#[async_trait::async_trait] +impl BatchFillable for RichActivity { + async fn with_batched(mut self, tx: &impl ConnectionTrait) -> Result + where + E: BatchFillableComparison + EntityTrait, + E::Model: BatchFillableKey + Send + FromQueryResult + ModelTrait, + RichActivity: BatchFillableAcceptor>, + RichObject: BatchFillableAcceptor> + { + if let Some(ref obj) = self.object { + let batch =E::find() + .filter(E::comparison(vec![obj.internal])) + .all(tx) + .await?; + self.accept(batch); + } + Ok(self) + } +} + +#[async_trait::async_trait] +impl BatchFillable for RichObject { + async fn with_batched(mut self, tx: &impl ConnectionTrait) -> Result + where + E: BatchFillableComparison + EntityTrait, + E::Model: BatchFillableKey + Send + FromQueryResult + ModelTrait, + RichActivity: BatchFillableAcceptor>, + RichObject: BatchFillableAcceptor> + { + let batch = E::find() + .filter(E::comparison(vec![self.object.internal])) + .all(tx) + .await?; + self.accept(batch); + Ok(self) + } +} + + +// welcome to interlocking trait hell, enjoy your stay +mod hell { + use sea_orm::{sea_query::IntoCondition, ColumnTrait}; + + pub trait BatchFillableComparison { + fn comparison(ids: Vec) -> sea_orm::Condition; + } + + impl BatchFillableComparison for crate::model::attachment::Entity { + fn comparison(ids: Vec) -> sea_orm::Condition { + crate::model::attachment::Column::Object.is_in(ids).into_condition() + } + } + + impl BatchFillableComparison for crate::model::mention::Entity { + fn comparison(ids: Vec) -> sea_orm::Condition { + crate::model::mention::Column::Object.is_in(ids).into_condition() + } + } + + impl BatchFillableComparison for crate::model::hashtag::Entity { + fn comparison(ids: Vec) -> sea_orm::Condition { + crate::model::hashtag::Column::Object.is_in(ids).into_condition() + } + } + + pub trait BatchFillableKey { + fn key(&self) -> i64; + } + + impl BatchFillableKey for crate::model::attachment::Model { + fn key(&self) -> i64 { + self.object + } + } + + impl BatchFillableKey for crate::model::mention::Model { + fn key(&self) -> i64 { + self.object + } + } + + impl BatchFillableKey for crate::model::hashtag::Model { + fn key(&self) -> i64 { + self.object + } + } + + pub trait BatchFillableAcceptor { + fn accept(&mut self, batch: B); + } + + impl BatchFillableAcceptor> for super::RichActivity { + fn accept(&mut self, batch: Vec) { + self.attachments = Some(batch); + } + } + + impl BatchFillableAcceptor> for super::RichActivity { + fn accept(&mut self, batch: Vec) { + self.hashtags = Some(batch); + } + } + + impl BatchFillableAcceptor> for super::RichActivity { + fn accept(&mut self, batch: Vec) { + self.mentions = Some(batch); + } + } + + impl BatchFillableAcceptor> for super::RichObject { + fn accept(&mut self, batch: Vec) { + self.attachments = Some(batch); + } + } + + impl BatchFillableAcceptor> for super::RichObject { + fn accept(&mut self, batch: Vec) { + self.hashtags = Some(batch); + } + } + + impl BatchFillableAcceptor> for super::RichObject { + fn accept(&mut self, batch: Vec) { + self.mentions = Some(batch); + } + } +} +use hell::*; + diff --git a/upub/core/src/selector/mod.rs b/upub/core/src/selector/mod.rs new file mode 100644 index 0000000..6652c97 --- /dev/null +++ b/upub/core/src/selector/mod.rs @@ -0,0 +1,12 @@ +mod batch; +pub use batch::BatchFillable; + +mod query; +pub use query::Query; + +mod rich; +pub use rich::{RichObject, RichActivity}; + + + + diff --git a/upub/core/src/selector/query.rs b/upub/core/src/selector/query.rs new file mode 100644 index 0000000..8d6bb04 --- /dev/null +++ b/upub/core/src/selector/query.rs @@ -0,0 +1,73 @@ +use sea_orm::{sea_query::{IntoColumnRef, IntoCondition}, ColumnTrait, Condition, EntityName, EntityTrait, Iden, Iterable, Order, QueryFilter, QueryOrder, QuerySelect, RelationTrait, Select, SelectColumns}; +use crate::model; + +pub struct Query; + +impl Query { + pub fn activities(my_id: Option) -> Select { + let mut select = model::addressing::Entity::find() + .distinct_on([ + (model::addressing::Entity, model::addressing::Column::Published).into_column_ref(), + (model::activity::Entity, model::activity::Column::Internal).into_column_ref(), + ]) + .join(sea_orm::JoinType::InnerJoin, model::addressing::Relation::Activities.def()) + .join(sea_orm::JoinType::LeftJoin, model::addressing::Relation::Objects.def()) + .filter( + // TODO ghetto double inner join because i want to filter out tombstones + Condition::any() + .add(model::activity::Column::Id.is_not_null()) + .add(model::object::Column::Id.is_not_null()) + ) + .order_by(model::addressing::Column::Published, Order::Desc) + .order_by(model::activity::Column::Internal, Order::Desc) + .select_only(); + + for col in model::activity::Column::iter() { + select = select.select_column_as(col, format!("{}{}", model::activity::Entity.table_name(), col.to_string())); + } + + for col in model::object::Column::iter() { + select = select.select_column_as(col, format!("{}{}", model::object::Entity.table_name(), col.to_string())); + } + + if let Some(uid) = my_id { + select = select + .join( + sea_orm::JoinType::LeftJoin, + model::object::Relation::Likes.def() + .on_condition(move |_l, _r| model::like::Column::Actor.eq(uid).into_condition()), + ) + .select_column_as(model::like::Column::Actor, format!("{}{}", model::like::Entity.table_name(), model::like::Column::Actor.to_string())); + } + + select + } + + pub fn objects(my_id: Option) -> Select { + let mut select = model::addressing::Entity::find() + .distinct_on([ + (model::addressing::Entity, model::addressing::Column::Published).into_column_ref(), + (model::object::Entity, model::object::Column::Internal).into_column_ref(), + ]) + .join(sea_orm::JoinType::InnerJoin, model::addressing::Relation::Objects.def()) + .order_by(model::addressing::Column::Published, Order::Desc) + .order_by(model::object::Column::Internal, Order::Desc) + .select_only(); + + for col in model::object::Column::iter() { + select = select.select_column_as(col, format!("{}{}", model::object::Entity.table_name(), col.to_string())); + } + + if let Some(uid) = my_id { + select = select + .join( + sea_orm::JoinType::LeftJoin, + model::object::Relation::Likes.def() + .on_condition(move |_l, _r| model::like::Column::Actor.eq(uid).into_condition()), + ) + .select_column_as(model::like::Column::Actor, format!("{}{}", model::like::Entity.table_name(), model::like::Column::Actor.to_string())); + } + + select + } +} diff --git a/upub/core/src/selector/rich.rs b/upub/core/src/selector/rich.rs new file mode 100644 index 0000000..77f7161 --- /dev/null +++ b/upub/core/src/selector/rich.rs @@ -0,0 +1,123 @@ +use apb::{ActivityMut, LinkMut, ObjectMut}; +use sea_orm::{DbErr, EntityName, FromQueryResult, Iden, QueryResult}; + + +pub struct RichActivity { + pub activity: crate::model::activity::Model, + pub object: Option, + pub liked: Option, + pub attachments: Option>, + pub hashtags: Option>, + pub mentions: Option>, +} + +impl FromQueryResult for RichActivity { + fn from_query_result(res: &QueryResult, _pre: &str) -> Result { + Ok(RichActivity { + activity: crate::model::activity::Model::from_query_result(res, crate::model::activity::Entity.table_name())?, + object: crate::model::object::Model::from_query_result(res, crate::model::object::Entity.table_name()).ok(), + liked: res.try_get(crate::model::like::Entity.table_name(), &crate::model::like::Column::Actor.to_string()).ok(), + attachments: None, hashtags: None, mentions: None, + }) + } +} + +impl RichActivity { + pub fn ap(self) -> serde_json::Value { + let object = match self.object { + None => apb::Node::maybe_link(self.activity.object.clone()), + Some(o) => { + // TODO can we avoid repeating this tags code? + let mut tags = Vec::new(); + if let Some(mentions) = self.mentions { + for mention in mentions { + tags.push( + apb::new() + .set_link_type(Some(apb::LinkType::Mention)) + .set_href(&mention.actor) + // TODO do i need to set name? i could join while batch loading or put the @name in + // each mention object... + ); + } + } + if let Some(hashtags) = self.hashtags { + for hash in hashtags { + tags.push( + // TODO ewwww set_name clash and cant use builder, wtf is this + LinkMut::set_name(apb::new(), Some(&format!("#{}", hash.name))) + .set_link_type(Some(apb::LinkType::Hashtag)) + // TODO do we need to set href too? we can't access context here, quite an issue! + ); + } + } + apb::Node::object( + o.ap() + .set_liked_by_me(if self.liked.is_some() { Some(true) } else { None }) + .set_tag(apb::Node::array(tags)) + .set_attachment(match self.attachments { + None => apb::Node::Empty, + Some(vec) => apb::Node::array( + vec.into_iter().map(|x| x.ap()).collect() + ), + }) + ) + }, + }; + self.activity.ap().set_object(object) + } +} + +pub struct RichObject { + pub object: crate::model::object::Model, + pub liked: Option, + pub attachments: Option>, + pub hashtags: Option>, + pub mentions: Option>, +} + +impl FromQueryResult for RichObject { + fn from_query_result(res: &QueryResult, _pre: &str) -> Result { + Ok(RichObject { + object: crate::model::object::Model::from_query_result(res, crate::model::object::Entity.table_name())?, + liked: res.try_get(crate::model::like::Entity.table_name(), &crate::model::like::Column::Actor.to_string()).ok(), + attachments: None, hashtags: None, mentions: None, + }) + } +} + +impl RichObject { + pub fn ap(self) -> serde_json::Value { + // TODO can we avoid repeating this tags code? + let mut tags = Vec::new(); + if let Some(mentions) = self.mentions { + for mention in mentions { + tags.push( + apb::new() + .set_link_type(Some(apb::LinkType::Mention)) + .set_href(&mention.actor) + // TODO do i need to set name? i could join while batch loading or put the @name in + // each mention object... + ); + } + } + if let Some(hashtags) = self.hashtags { + for hash in hashtags { + tags.push( + // TODO ewwww set_name clash and cant use builder, wtf is this + LinkMut::set_name(apb::new(), Some(&format!("#{}", hash.name))) + .set_link_type(Some(apb::LinkType::Hashtag)) + // TODO do we need to set href too? we can't access context here, quite an issue! + ); + } + } + self.object.ap() + .set_liked_by_me(if self.liked.is_some() { Some(true) } else { None }) + .set_tag(apb::Node::array(tags)) + .set_attachment(match self.attachments { + None => apb::Node::Empty, + Some(vec) => apb::Node::array( + vec.into_iter().map(|x| x.ap()).collect() + ) + }) + } +}