Federated Moderation #185

Manually merged
dessalines merged 20 commits from federated-moderation into main 2021-03-24 15:49:38 +00:00
10 changed files with 97 additions and 64 deletions
Showing only changes of commit 0c484e8c76 - Show all commits

View File

@ -1,7 +1,4 @@
use activitystreams::{
collection::{CollectionExt, OrderedCollection},
unparsed::UnparsedMutExt,
};
use activitystreams::unparsed::UnparsedMutExt;
use activitystreams_ext::UnparsedExtension;
use lemmy_utils::LemmyError;
use serde::{Deserialize, Serialize};
@ -13,17 +10,14 @@ use url::Url;
#[serde(rename_all = "camelCase")]
pub struct GroupExtension {
pub sensitive: Option<bool>,
pub moderators: Option<OrderedCollection>,
pub moderators: Option<Url>,
}
impl GroupExtension {
pub fn new(sensitive: bool, moderators: Vec<Url>) -> Result<GroupExtension, LemmyError> {
let mut mods = OrderedCollection::new();
mods.set_total_items(moderators.len() as u64);
mods.set_many_items(moderators);
pub fn new(sensitive: bool, moderators_url: Url) -> Result<GroupExtension, LemmyError> {
Ok(GroupExtension {
sensitive: Some(sensitive),
moderators: Some(mods),
moderators: Some(moderators_url),
})
}
}

View File

@ -103,3 +103,27 @@ async fn fetch_community_outbox(
Ok(())
}
pub(crate) async fn fetch_community_mods(
context: &LemmyContext,
group: &GroupExt,
recursion_counter: &mut i32,
) -> Result<Vec<Url>, LemmyError> {
if let Some(mods_url) = &group.ext_one.moderators {
let mods =
dessalines marked this conversation as resolved Outdated

I see this is fetched every time from_apub is run on community. This doesn't seem right, it should be a smart upsert like the others.

I see this is fetched every time `from_apub` is run on community. This doesn't seem right, it should be a smart upsert like the others.

Any idea how? I dont see how an upsert works here, because we cant simply overwrite a single database row, but need to insert or delete rows.

Any idea how? I dont see how an upsert works here, because we cant simply overwrite a single database row, but need to insert or delete rows.

Check out get_or_fetch_upsert_community, it looks like its already doing the initial moderators fetching.

It should be keyed just like the should_refetch_actor(c.last_refreshed_at), after the initial community fetch. In fact I don't even know that that's necessary, since the only reason for fetching is to:

  1. Get initial data
  2. When we don't receive atomic updates (the last_refresh_at is only necessary bc community and user edits aren't pushed). New community moderators are pushed, so I don't know that this is necessary.
Check out `get_or_fetch_upsert_community`, it looks like its already doing the initial moderators fetching. It should be keyed just like the `should_refetch_actor(c.last_refreshed_at)`, after the initial community fetch. In fact I don't even know that that's necessary, since the only reason for fetching is to: 1. Get initial data 2. When we don't receive atomic updates (the last_refresh_at is only necessary bc community and user edits aren't pushed). New community moderators are pushed, so I don't know that this is necessary.

Ah that makes sense, I'm moving the update logic from objects/community.rs to fetcher/community.rs. But we still need to run this every time, because its possible that our instance doesnt follow the community, and doesnt receive any activities (we could possibly add a check for this later).

And it is still necessary to fetch the community creator in FromApubToForm, until that field is removed.

Ah that makes sense, I'm moving the update logic from objects/community.rs to fetcher/community.rs. But we still need to run this every time, because its possible that our instance doesnt follow the community, and doesnt receive any activities (we could possibly add a check for this later). And it is still necessary to fetch the community creator in FromApubToForm, until that field is removed.
fetch_remote_object::<OrderedCollection>(context.client(), mods_url, recursion_counter)
.await?;
let mods = mods
.items()
.map(|i| i.as_many())
.flatten()
.context(location_info!())?
.iter()
.filter_map(|i| i.as_xsd_any_uri())
.map(|u| u.to_owned())
.collect();
Ok(mods)
} else {
Ok(vec![])
}
}

View File

@ -12,12 +12,12 @@ use lemmy_websocket::LemmyContext;
use serde::Deserialize;
#[derive(Deserialize)]
pub struct CommentQuery {
pub(crate) struct CommentQuery {
comment_id: String,
}
/// Return the ActivityPub json representation of a local comment over HTTP.
pub async fn get_apub_comment(
pub(crate) async fn get_apub_comment(
info: Path<CommentQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {

View File

@ -1,5 +1,6 @@
use crate::{
extensions::context::lemmy_context,
generate_moderators_url,
http::{create_apub_response, create_apub_tombstone_response},
objects::ToApub,
ActorType,
@ -7,23 +8,27 @@ use crate::{
use activitystreams::{
base::{AnyBase, BaseExt},
collection::{CollectionExt, OrderedCollection, UnorderedCollection},
url::Url,
};
use actix_web::{body::Body, web, HttpResponse};
use lemmy_api_structs::blocking;
use lemmy_db_queries::source::{activity::Activity_, community::Community_};
use lemmy_db_schema::source::{activity::Activity, community::Community};
use lemmy_db_views_actor::community_follower_view::CommunityFollowerView;
use lemmy_db_views_actor::{
community_follower_view::CommunityFollowerView,
community_moderator_view::CommunityModeratorView,
};
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use serde::Deserialize;
#[derive(Deserialize)]
pub struct CommunityQuery {
pub(crate) struct CommunityQuery {
community_name: String,
}
/// Return the ActivityPub json representation of a local community over HTTP.
pub async fn get_apub_community_http(
pub(crate) async fn get_apub_community_http(
info: web::Path<CommunityQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {
@ -42,7 +47,7 @@ pub async fn get_apub_community_http(
}
/// Returns an empty followers collection, only populating the size (for privacy).
pub async fn get_apub_community_followers(
pub(crate) async fn get_apub_community_followers(
info: web::Path<CommunityQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {
@ -67,7 +72,7 @@ pub async fn get_apub_community_followers(
/// Returns the community outbox, which is populated by a maximum of 20 posts (but no other
/// activites like votes or comments).
pub async fn get_apub_community_outbox(
pub(crate) async fn get_apub_community_outbox(
info: web::Path<CommunityQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {
@ -96,7 +101,7 @@ pub async fn get_apub_community_outbox(
Ok(create_apub_response(&collection))
}
pub async fn get_apub_community_inbox(
pub(crate) async fn get_apub_community_inbox(
info: web::Path<CommunityQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {
@ -107,7 +112,39 @@ pub async fn get_apub_community_inbox(
let mut collection = OrderedCollection::new();
collection
.set_id(format!("{}/inbox", community.actor_id).parse()?)
.set_id(community.inbox_url.into())
.set_many_contexts(lemmy_context()?);
Ok(create_apub_response(&collection))
}
pub(crate) async fn get_apub_community_moderators(
info: web::Path<CommunityQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {
let community = blocking(context.pool(), move |conn| {
Community::read_from_name(&conn, &info.community_name)
})
.await??;
// The attributed to, is an ordered vector with the creator actor_ids first,
// then the rest of the moderators
// TODO Technically the instance admins can mod the community, but lets
// ignore that for now
let cid = community.id;
let moderators = blocking(context.pool(), move |conn| {
CommunityModeratorView::for_community(&conn, cid)
})
.await??;
let moderators: Vec<Url> = moderators
.into_iter()
.map(|m| m.moderator.actor_id.into_inner())
.collect();
let mut collection = OrderedCollection::new();
collection
.set_id(generate_moderators_url(&community.actor_id)?.into())
.set_total_items(moderators.len() as u64)
.set_many_items(moderators)
.set_many_contexts(lemmy_context()?);
Ok(create_apub_response(&collection))
}

View File

@ -42,7 +42,7 @@ pub struct CommunityQuery {
}
/// Return the ActivityPub json representation of a local community over HTTP.
pub async fn get_activity(
pub(crate) async fn get_activity(
info: web::Path<CommunityQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {

View File

@ -12,12 +12,12 @@ use lemmy_websocket::LemmyContext;
use serde::Deserialize;
#[derive(Deserialize)]
pub struct PostQuery {
pub(crate) struct PostQuery {
post_id: String,
}
/// Return the ActivityPub json representation of a local post over HTTP.
pub async fn get_apub_post(
pub(crate) async fn get_apub_post(
info: web::Path<PostQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {

View File

@ -18,12 +18,12 @@ use serde::Deserialize;
use url::Url;
#[derive(Deserialize)]
pub struct UserQuery {
pub(crate) struct UserQuery {
user_name: String,
}
/// Return the ActivityPub json representation of a local user over HTTP.
pub async fn get_apub_user_http(
pub(crate) async fn get_apub_user_http(
info: web::Path<UserQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {
@ -43,7 +43,7 @@ pub async fn get_apub_user_http(
}
}
pub async fn get_apub_user_outbox(
pub(crate) async fn get_apub_user_outbox(
info: web::Path<UserQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {
@ -61,7 +61,7 @@ pub async fn get_apub_user_outbox(
Ok(create_apub_response(&collection))
}
pub async fn get_apub_user_inbox(
pub(crate) async fn get_apub_user_inbox(
info: web::Path<UserQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {
@ -72,7 +72,7 @@ pub async fn get_apub_user_inbox(
let mut collection = OrderedCollection::new();
collection
.set_id(format!("{}/inbox", user.actor_id.into_inner()).parse()?)
.set_id(user.inbox_url.into())
.set_many_contexts(lemmy_context()?);
Ok(create_apub_response(&collection))
}

View File

@ -262,6 +262,10 @@ pub fn generate_shared_inbox_url(actor_id: &DbUrl) -> Result<DbUrl, LemmyError>
Ok(Url::parse(&url)?.into())
}
pub(crate) fn generate_moderators_url(community_id: &DbUrl) -> Result<DbUrl, LemmyError> {
Ok(Url::parse(&format!("{}/moderators", community_id))?.into())
}
/// Store a sent or received activity in the database, for logging purposes. These records are not
/// persistent.
pub(crate) async fn insert_activity<T>(

View File

@ -1,6 +1,7 @@
use crate::{
extensions::{context::lemmy_context, group_extensions::GroupExtension},
fetcher::user::get_or_fetch_and_upsert_user,
fetcher::{community::fetch_community_mods, user::get_or_fetch_and_upsert_user},
generate_moderators_url,
objects::{
check_object_domain,
create_tombstone,
@ -42,17 +43,7 @@ use url::Url;
impl ToApub for Community {
type ApubType = GroupExt;
async fn to_apub(&self, pool: &DbPool) -> Result<GroupExt, LemmyError> {
// The attributed to, is an ordered vector with the creator actor_ids first,
// then the rest of the moderators
// TODO Technically the instance admins can mod the community, but lets
// ignore that for now
let id = self.id;
let moderators = blocking(pool, move |conn| {
CommunityModeratorView::for_community(&conn, id)
})
.await??;
async fn to_apub(&self, _pool: &DbPool) -> Result<GroupExt, LemmyError> {
let mut group = ApObject::new(Group::new());
group
.set_many_contexts(lemmy_context()?)
@ -89,14 +80,9 @@ impl ToApub for Community {
..Default::default()
});
let moderators: Vec<Url> = moderators
.into_iter()
.map(|m| m.moderator.actor_id.into_inner())
.collect();
Ok(Ext2::new(
ap_actor,
GroupExtension::new(self.nsfw, moderators)?,
GroupExtension::new(self.nsfw, generate_moderators_url(&self.actor_id)?.into())?,
self.get_public_key_ext()?,
))
}
@ -125,7 +111,7 @@ impl FromApub for Community {
let community: Community =
get_object_from_apub(group, context, expected_domain, request_counter).await?;
let new_moderators = get_community_moderators(group)?;
let new_moderators = fetch_community_mods(context, group, request_counter).await?;
let community_id = community.id;
let current_moderators = blocking(context.pool(), move |conn| {
CommunityModeratorView::for_community(&conn, community_id)
@ -177,7 +163,7 @@ impl FromApubToForm<GroupExt> for CommunityForm {
expected_domain: Url,
request_counter: &mut i32,
) -> Result<Self, LemmyError> {
let moderator_uris = get_community_moderators(group)?;
let moderator_uris = fetch_community_mods(context, group, request_counter).await?;
let creator_uri = moderator_uris.first().context(location_info!())?;
let creator = get_or_fetch_and_upsert_user(creator_uri, context, request_counter).await?;
@ -263,20 +249,3 @@ impl FromApubToForm<GroupExt> for CommunityForm {
})
}
}
fn get_community_moderators(group: &GroupExt) -> Result<Vec<&Url>, LemmyError> {
if let Some(moderators) = &group.ext_one.moderators {
Ok(
moderators
.items()
.map(|i| i.as_many())
.flatten()
.context(location_info!())?
.iter()
.filter_map(|i| i.as_xsd_any_uri())
.collect(),
)
} else {
Ok(vec![])
}
}

View File

@ -5,6 +5,7 @@ use crate::{
get_apub_community_followers,
get_apub_community_http,
get_apub_community_inbox,
get_apub_community_moderators,
get_apub_community_outbox,
},
get_activity,
@ -53,6 +54,10 @@ pub fn config(cfg: &mut web::ServiceConfig) {
"/c/{community_name}/inbox",
web::get().to(get_apub_community_inbox),
)
.route(
"/c/{community_name}/moderators",
web::get().to(get_apub_community_moderators),
)
.route("/u/{user_name}", web::get().to(get_apub_user_http))
.route("/u/{user_name}/outbox", web::get().to(get_apub_user_outbox))
.route("/u/{user_name}/inbox", web::get().to(get_apub_user_inbox))