From fb2b1bea328d067afbfecc1a73c272e725062f4e Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Tue, 16 Jun 2020 16:03:42 +0200 Subject: [PATCH] Federate stickied posts (ref #647) --- server/src/apub/community.rs | 58 +++++++++++++++---- .../src/apub/extensions/group_extensions.rs | 7 ++- server/src/apub/fetcher.rs | 29 +++++++++- server/src/db/post.rs | 35 +++++++++++ server/src/routes/federation.rs | 4 ++ 5 files changed, 121 insertions(+), 12 deletions(-) diff --git a/server/src/apub/community.rs b/server/src/apub/community.rs index 8c8c3b28..e238a29f 100644 --- a/server/src/apub/community.rs +++ b/server/src/apub/community.rs @@ -17,6 +17,7 @@ use crate::{ activity::insert_activity, community::{Community, CommunityForm}, community_view::{CommunityFollowerView, CommunityModeratorView}, + post::Post, user::User_, }, naive_now, @@ -29,6 +30,7 @@ use activitystreams::{ context, endpoint::EndpointProperties, object::properties::ObjectProperties, + primitives::{XsdAnyUri, XsdAnyUriError, XsdString}, Activity, Base, BaseBox, @@ -40,6 +42,7 @@ use diesel::PgConnection; use failure::{Error, _core::fmt::Debug}; use itertools::Itertools; use serde::{Deserialize, Serialize}; +use std::str::FromStr; #[derive(Deserialize)] pub struct CommunityQuery { @@ -92,7 +95,12 @@ impl ToApub for Community { .set_endpoints(endpoint_props)? .set_followers(self.get_followers_url())?; - let group_extension = GroupExtension::new(conn, self.category_id, self.nsfw)?; + let group_extension = GroupExtension::new( + conn, + self.category_id, + self.nsfw, + self.featured_collection_address()?, + )?; Ok(Ext3::new( group, @@ -124,6 +132,14 @@ impl ActorType for Community { self.private_key.to_owned().unwrap() } + fn send_follow(&self, _follow_actor_id: &str, _conn: &PgConnection) -> Result<(), Error> { + unimplemented!() + } + + fn send_unfollow(&self, _follow_actor_id: &str, _conn: &PgConnection) -> Result<(), Error> { + unimplemented!() + } + /// As a local community, accept the follow request from a remote user. fn send_accept_follow(&self, follow: &Follow, conn: &PgConnection) -> Result<(), Error> { let actor_uri = follow.actor.as_single_xsd_any_uri().unwrap().to_string(); @@ -288,14 +304,6 @@ impl ActorType for Community { .collect(), ) } - - fn send_follow(&self, _follow_actor_id: &str, _conn: &PgConnection) -> Result<(), Error> { - unimplemented!() - } - - fn send_unfollow(&self, _follow_actor_id: &str, _conn: &PgConnection) -> Result<(), Error> { - unimplemented!() - } } impl FromApub for CommunityForm { @@ -371,13 +379,40 @@ pub async fn get_apub_community_followers( let oprops: &mut ObjectProperties = collection.as_mut(); oprops .set_context_xsd_any_uri(context())? - .set_id(community.actor_id)?; + .set_id(community.get_followers_url())?; collection .collection_props .set_total_items(community_followers.len() as u64)?; Ok(create_apub_response(&collection)) } +/// TODO +pub async fn get_featured_posts( + info: Path, + db: DbPoolParam, +) -> Result, Error> { + let community = Community::read_from_name(&&db.get()?, &info.community_name)?; + + let conn = db.get()?; + + let featured_posts = Post::list_stickied_for_community(&conn, community.id)? + .iter() + // TODO: should probably be XsdAnyUri but collection doesnt have a setter for that + .map(|p| XsdString::from_str(&p.ap_id)) + .collect::, std::convert::Infallible>>()?; + + let mut collection = UnorderedCollection::default(); + let oprops: &mut ObjectProperties = collection.as_mut(); + oprops + .set_context_xsd_any_uri(context())? + .set_id(community.featured_collection_address()?)?; + collection + .collection_props + .set_total_items(featured_posts.len() as u64)? + .set_many_items_xsd_strings(featured_posts)?; + Ok(create_apub_response(&collection)) +} + impl Community { pub fn do_announce( activity: A, @@ -411,4 +446,7 @@ impl Community { Ok(HttpResponse::Ok().finish()) } + fn featured_collection_address(&self) -> Result { + XsdAnyUri::from_str(&format!("{}{}", self.actor_id, "/collections/featured")) + } } diff --git a/server/src/apub/extensions/group_extensions.rs b/server/src/apub/extensions/group_extensions.rs index ece97706..f5e6ef11 100644 --- a/server/src/apub/extensions/group_extensions.rs +++ b/server/src/apub/extensions/group_extensions.rs @@ -1,14 +1,17 @@ use crate::db::{category::Category, Crud}; -use activitystreams::{ext::Extension, Actor}; +use activitystreams::{ext::Extension, primitives::XsdAnyUri, Actor}; use diesel::PgConnection; use failure::Error; use serde::{Deserialize, Serialize}; +// TODO: need to specify these fields in the context + #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct GroupExtension { pub category: GroupCategory, pub sensitive: bool, + pub featured: XsdAnyUri, } #[derive(Clone, Debug, Default, Deserialize, Serialize)] @@ -24,6 +27,7 @@ impl GroupExtension { conn: &PgConnection, category_id: i32, sensitive: bool, + featured: XsdAnyUri, ) -> Result { let category = Category::read(conn, category_id)?; let group_category = GroupCategory { @@ -33,6 +37,7 @@ impl GroupExtension { Ok(GroupExtension { category: group_category, sensitive, + featured, }) } } diff --git a/server/src/apub/fetcher.rs b/server/src/apub/fetcher.rs index 7f7a3f97..140ff8ba 100644 --- a/server/src/apub/fetcher.rs +++ b/server/src/apub/fetcher.rs @@ -28,6 +28,7 @@ use crate::{ use crate::{ apub::{ + extensions::group_extensions::GroupExtension, get_apub_protocol_string, is_apub_id_valid, FromApub, @@ -38,6 +39,7 @@ use crate::{ }, db::user_view::UserView, }; +use activitystreams::{collection::UnorderedCollection, primitives::XsdString}; use chrono::NaiveDateTime; static ACTOR_REFETCH_INTERVAL_SECONDS: i64 = 24 * 60 * 60; @@ -215,7 +217,9 @@ pub fn get_or_fetch_and_upsert_remote_community( let group = fetch_remote_object::(&Url::parse(apub_id)?)?; let mut cf = CommunityForm::from_apub(&group, conn)?; cf.last_refreshed_at = Some(naive_now()); - Ok(Community::update(&conn, c.id, &cf)?) + let community = Community::update(&conn, c.id, &cf)?; + fetch_community_stickied_posts(&group.ext_one, community.id, &conn)?; + Ok(community) } else { Ok(c) } @@ -225,6 +229,7 @@ pub fn get_or_fetch_and_upsert_remote_community( let group = fetch_remote_object::(&Url::parse(apub_id)?)?; let cf = CommunityForm::from_apub(&group, conn)?; let community = Community::create(conn, &cf)?; + fetch_community_stickied_posts(&group.ext_one, community.id, &conn)?; // Also add the community moderators too let creator_and_moderator_uris = group @@ -250,6 +255,28 @@ pub fn get_or_fetch_and_upsert_remote_community( } } +fn fetch_community_stickied_posts( + group_ext: &GroupExtension, + community_id: i32, + conn: &PgConnection, +) -> Result<(), Error> { + let featured = + fetch_remote_object::(&Url::parse(group_ext.featured.as_str())?)?; + let featured_items = featured + .collection_props + .get_many_items_xsd_strings() + .map(|o| o.collect::>()) + .unwrap_or_default() + .iter() + .map(|i: &&XsdString| -> Result { + get_or_fetch_and_insert_remote_post(i.as_str(), &conn)?; + Ok(i.as_str().to_string()) + }) + .collect::, Error>>()?; + Post::set_stickied_for_community(&conn, community_id, featured_items)?; + Ok(()) +} + fn upsert_post(post_form: &PostForm, conn: &PgConnection) -> Result { let existing = Post::read_from_apub_id(conn, &post_form.ap_id); match existing { diff --git a/server/src/db/post.rs b/server/src/db/post.rs index d12f98d8..cf2c5d9f 100644 --- a/server/src/db/post.rs +++ b/server/src/db/post.rs @@ -70,6 +70,41 @@ impl Post { .load::(conn) } + pub fn list_stickied_for_community( + conn: &PgConnection, + the_community_id: i32, + ) -> Result, Error> { + use crate::schema::post::dsl::*; + post + .filter(community_id.eq(the_community_id)) + .filter(stickied.eq(true)) + .load::(conn) + } + + pub fn set_stickied_for_community( + conn: &PgConnection, + the_community_id: i32, + stickied_items: Vec, + ) -> Result<(), Error> { + use crate::schema::post::dsl::*; + // TODO: surely there is a better way to do this + diesel::update(post) + .filter(community_id.eq(the_community_id)) + .set(stickied.eq(false)) + .execute(conn)?; + stickied_items + .iter() + .map(|i| { + diesel::update(post) + .filter(community_id.eq(the_community_id)) + .filter(ap_id.eq(i)) + .set(stickied.eq(true)) + .execute(conn) + }) + .collect::, Error>>()?; + Ok(()) + } + pub fn read_from_apub_id(conn: &PgConnection, object_id: &str) -> Result { use crate::schema::post::dsl::*; post.filter(ap_id.eq(object_id)).first::(conn) diff --git a/server/src/routes/federation.rs b/server/src/routes/federation.rs index fe6e3365..eb0d1f74 100644 --- a/server/src/routes/federation.rs +++ b/server/src/routes/federation.rs @@ -28,6 +28,10 @@ pub fn config(cfg: &mut web::ServiceConfig) { "/c/{community_name}/followers", web::get().to(get_apub_community_followers), ) + .route( + "/c/{community_name}/collections/featured", + web::get().to(get_featured_posts), + ) // TODO This is only useful for history which we aren't doing right now // .route( // "/c/{community_name}/outbox", -- 2.40.1