Federate stickied posts (ref #647)

This commit is contained in:
Felix Ableitner 2020-06-16 16:03:42 +02:00
parent 68bcc26ff6
commit fb2b1bea32
5 changed files with 121 additions and 12 deletions

View file

@ -17,6 +17,7 @@ use crate::{
activity::insert_activity, activity::insert_activity,
community::{Community, CommunityForm}, community::{Community, CommunityForm},
community_view::{CommunityFollowerView, CommunityModeratorView}, community_view::{CommunityFollowerView, CommunityModeratorView},
post::Post,
user::User_, user::User_,
}, },
naive_now, naive_now,
@ -29,6 +30,7 @@ use activitystreams::{
context, context,
endpoint::EndpointProperties, endpoint::EndpointProperties,
object::properties::ObjectProperties, object::properties::ObjectProperties,
primitives::{XsdAnyUri, XsdAnyUriError, XsdString},
Activity, Activity,
Base, Base,
BaseBox, BaseBox,
@ -40,6 +42,7 @@ use diesel::PgConnection;
use failure::{Error, _core::fmt::Debug}; use failure::{Error, _core::fmt::Debug};
use itertools::Itertools; use itertools::Itertools;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct CommunityQuery { pub struct CommunityQuery {
@ -92,7 +95,12 @@ impl ToApub for Community {
.set_endpoints(endpoint_props)? .set_endpoints(endpoint_props)?
.set_followers(self.get_followers_url())?; .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( Ok(Ext3::new(
group, group,
@ -124,6 +132,14 @@ impl ActorType for Community {
self.private_key.to_owned().unwrap() 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. /// As a local community, accept the follow request from a remote user.
fn send_accept_follow(&self, follow: &Follow, conn: &PgConnection) -> Result<(), Error> { fn send_accept_follow(&self, follow: &Follow, conn: &PgConnection) -> Result<(), Error> {
let actor_uri = follow.actor.as_single_xsd_any_uri().unwrap().to_string(); let actor_uri = follow.actor.as_single_xsd_any_uri().unwrap().to_string();
@ -288,14 +304,6 @@ impl ActorType for Community {
.collect(), .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 { impl FromApub for CommunityForm {
@ -371,13 +379,40 @@ pub async fn get_apub_community_followers(
let oprops: &mut ObjectProperties = collection.as_mut(); let oprops: &mut ObjectProperties = collection.as_mut();
oprops oprops
.set_context_xsd_any_uri(context())? .set_context_xsd_any_uri(context())?
.set_id(community.actor_id)?; .set_id(community.get_followers_url())?;
collection collection
.collection_props .collection_props
.set_total_items(community_followers.len() as u64)?; .set_total_items(community_followers.len() as u64)?;
Ok(create_apub_response(&collection)) Ok(create_apub_response(&collection))
} }
/// TODO
pub async fn get_featured_posts(
info: Path<CommunityQuery>,
db: DbPoolParam,
) -> Result<HttpResponse<Body>, 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::<Result<Vec<_>, 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 { impl Community {
pub fn do_announce<A>( pub fn do_announce<A>(
activity: A, activity: A,
@ -411,4 +446,7 @@ impl Community {
Ok(HttpResponse::Ok().finish()) Ok(HttpResponse::Ok().finish())
} }
fn featured_collection_address(&self) -> Result<XsdAnyUri, XsdAnyUriError> {
XsdAnyUri::from_str(&format!("{}{}", self.actor_id, "/collections/featured"))
}
} }

View file

@ -1,14 +1,17 @@
use crate::db::{category::Category, Crud}; use crate::db::{category::Category, Crud};
use activitystreams::{ext::Extension, Actor}; use activitystreams::{ext::Extension, primitives::XsdAnyUri, Actor};
use diesel::PgConnection; use diesel::PgConnection;
use failure::Error; use failure::Error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
// TODO: need to specify these fields in the context
#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct GroupExtension { pub struct GroupExtension {
pub category: GroupCategory, pub category: GroupCategory,
pub sensitive: bool, pub sensitive: bool,
pub featured: XsdAnyUri,
} }
#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Deserialize, Serialize)]
@ -24,6 +27,7 @@ impl GroupExtension {
conn: &PgConnection, conn: &PgConnection,
category_id: i32, category_id: i32,
sensitive: bool, sensitive: bool,
featured: XsdAnyUri,
) -> Result<GroupExtension, Error> { ) -> Result<GroupExtension, Error> {
let category = Category::read(conn, category_id)?; let category = Category::read(conn, category_id)?;
let group_category = GroupCategory { let group_category = GroupCategory {
@ -33,6 +37,7 @@ impl GroupExtension {
Ok(GroupExtension { Ok(GroupExtension {
category: group_category, category: group_category,
sensitive, sensitive,
featured,
}) })
} }
} }

View file

@ -28,6 +28,7 @@ use crate::{
use crate::{ use crate::{
apub::{ apub::{
extensions::group_extensions::GroupExtension,
get_apub_protocol_string, get_apub_protocol_string,
is_apub_id_valid, is_apub_id_valid,
FromApub, FromApub,
@ -38,6 +39,7 @@ use crate::{
}, },
db::user_view::UserView, db::user_view::UserView,
}; };
use activitystreams::{collection::UnorderedCollection, primitives::XsdString};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
static ACTOR_REFETCH_INTERVAL_SECONDS: i64 = 24 * 60 * 60; 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::<GroupExt>(&Url::parse(apub_id)?)?; let group = fetch_remote_object::<GroupExt>(&Url::parse(apub_id)?)?;
let mut cf = CommunityForm::from_apub(&group, conn)?; let mut cf = CommunityForm::from_apub(&group, conn)?;
cf.last_refreshed_at = Some(naive_now()); 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 { } else {
Ok(c) Ok(c)
} }
@ -225,6 +229,7 @@ pub fn get_or_fetch_and_upsert_remote_community(
let group = fetch_remote_object::<GroupExt>(&Url::parse(apub_id)?)?; let group = fetch_remote_object::<GroupExt>(&Url::parse(apub_id)?)?;
let cf = CommunityForm::from_apub(&group, conn)?; let cf = CommunityForm::from_apub(&group, conn)?;
let community = Community::create(conn, &cf)?; let community = Community::create(conn, &cf)?;
fetch_community_stickied_posts(&group.ext_one, community.id, &conn)?;
// Also add the community moderators too // Also add the community moderators too
let creator_and_moderator_uris = group 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::<UnorderedCollection>(&Url::parse(group_ext.featured.as_str())?)?;
let featured_items = featured
.collection_props
.get_many_items_xsd_strings()
.map(|o| o.collect::<Vec<_>>())
.unwrap_or_default()
.iter()
.map(|i: &&XsdString| -> Result<String, Error> {
get_or_fetch_and_insert_remote_post(i.as_str(), &conn)?;
Ok(i.as_str().to_string())
})
.collect::<Result<Vec<_>, Error>>()?;
Post::set_stickied_for_community(&conn, community_id, featured_items)?;
Ok(())
}
fn upsert_post(post_form: &PostForm, conn: &PgConnection) -> Result<Post, Error> { fn upsert_post(post_form: &PostForm, conn: &PgConnection) -> Result<Post, Error> {
let existing = Post::read_from_apub_id(conn, &post_form.ap_id); let existing = Post::read_from_apub_id(conn, &post_form.ap_id);
match existing { match existing {

View file

@ -70,6 +70,41 @@ impl Post {
.load::<Self>(conn) .load::<Self>(conn)
} }
pub fn list_stickied_for_community(
conn: &PgConnection,
the_community_id: i32,
) -> Result<Vec<Self>, Error> {
use crate::schema::post::dsl::*;
post
.filter(community_id.eq(the_community_id))
.filter(stickied.eq(true))
.load::<Self>(conn)
}
pub fn set_stickied_for_community(
conn: &PgConnection,
the_community_id: i32,
stickied_items: Vec<String>,
) -> 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::<Result<Vec<_>, Error>>()?;
Ok(())
}
pub fn read_from_apub_id(conn: &PgConnection, object_id: &str) -> Result<Self, Error> { pub fn read_from_apub_id(conn: &PgConnection, object_id: &str) -> Result<Self, Error> {
use crate::schema::post::dsl::*; use crate::schema::post::dsl::*;
post.filter(ap_id.eq(object_id)).first::<Self>(conn) post.filter(ap_id.eq(object_id)).first::<Self>(conn)

View file

@ -28,6 +28,10 @@ pub fn config(cfg: &mut web::ServiceConfig) {
"/c/{community_name}/followers", "/c/{community_name}/followers",
web::get().to(get_apub_community_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 // TODO This is only useful for history which we aren't doing right now
// .route( // .route(
// "/c/{community_name}/outbox", // "/c/{community_name}/outbox",