You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
lemmy/crates/apub_receive/src/inbox/community_inbox.rs

344 lines
9.5 KiB
Rust

use crate::{
activities::receive::verify_activity_domains_valid,
inbox::{
assert_activity_not_local,
get_activity_id,
inbox_verify_http_signature,
is_activity_already_known,
receive_for_community::{
receive_add_for_community,
receive_block_user_for_community,
receive_create_for_community,
receive_delete_for_community,
receive_dislike_for_community,
receive_like_for_community,
receive_remove_for_community,
receive_undo_for_community,
receive_update_for_community,
},
verify_is_addressed_to_public,
},
};
use activitystreams::{
activity::{kind::FollowType, ActorAndObject, Follow, Undo},
base::AnyBase,
prelude::*,
};
use actix_web::{web, HttpRequest, HttpResponse};
use anyhow::{anyhow, Context};
use lemmy_api_common::blocking;
use lemmy_apub::{
check_community_or_site_ban,
get_activity_to_and_cc,
insert_activity,
ActorType,
CommunityType,
};
use lemmy_db_queries::{source::community::Community_, ApubObject, Followable};
use lemmy_db_schema::source::{
community::{Community, CommunityFollower, CommunityFollowerForm},
person::Person,
};
use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::LemmyContext;
use log::info;
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use url::Url;
/// Allowed activities for community inbox.
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
#[serde(rename_all = "PascalCase")]
pub enum CommunityValidTypes {
Follow, // follow request from a person
Undo, // unfollow from a person
Create, // create post or comment
Update, // update post or comment
Like, // upvote post or comment
Dislike, // downvote post or comment
Delete, // post or comment deleted by creator
Remove, // post or comment removed by mod or admin, or mod removed from community
Add, // mod added to community
Block, // user blocked by community
}
pub type CommunityAcceptedActivities = ActorAndObject<CommunityValidTypes>;
/// Handler for all incoming receive to community inboxes.
pub async fn community_inbox(
request: HttpRequest,
input: web::Json<CommunityAcceptedActivities>,
path: web::Path<String>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse, LemmyError> {
let activity = input.into_inner();
// First of all check the http signature
let request_counter = &mut 0;
let actor = inbox_verify_http_signature(&activity, &context, request, request_counter).await?;
// Do nothing if we received the same activity before
let activity_id = get_activity_id(&activity, &actor.actor_id())?;
if is_activity_already_known(context.pool(), &activity_id).await? {
return Ok(HttpResponse::Ok().finish());
}
// Check if the activity is actually meant for us
let path = path.into_inner();
let community = blocking(&context.pool(), move |conn| {
Community::read_from_name(&conn, &path)
})
.await??;
let to_and_cc = get_activity_to_and_cc(&activity);
if !to_and_cc.contains(&&community.actor_id()) {
return Err(anyhow!("Activity delivered to wrong community").into());
}
assert_activity_not_local(&activity)?;
insert_activity(&activity_id, activity.clone(), false, true, context.pool()).await?;
info!(
"Community {} received activity {:?} from {}",
community.name,
&activity.id_unchecked(),
&actor.actor_id()
);
community_receive_message(
activity.clone(),
community.clone(),
actor.as_ref(),
&context,
request_counter,
)
.await
}
/// Receives Follow, Undo/Follow, post actions, comment actions (including votes)
pub(crate) async fn community_receive_message(
activity: CommunityAcceptedActivities,
to_community: Community,
actor: &dyn ActorType,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<HttpResponse, LemmyError> {
// Only persons can send activities to the community, so we can get the actor as person
// unconditionally.
let actor_id = actor.actor_id();
let person = blocking(&context.pool(), move |conn| {
Person::read_from_apub_id(&conn, &actor_id.into())
})
.await??;
check_community_or_site_ban(&person, to_community.id, context.pool()).await?;
let any_base = activity.clone().into_any_base()?;
let actor_url = actor.actor_id();
let activity_kind = activity.kind().context(location_info!())?;
let do_announce = match activity_kind {
CommunityValidTypes::Follow => {
Box::pin(handle_follow(
any_base.clone(),
person,
&to_community,
&context,
))
.await?;
false
}
CommunityValidTypes::Undo => {
Box::pin(handle_undo(
context,
activity.clone(),
actor_url,
&to_community,
request_counter,
))
.await?
}
CommunityValidTypes::Create => {
Box::pin(receive_create_for_community(
context,
any_base.clone(),
&actor_url,
request_counter,
))
.await?;
true
}
CommunityValidTypes::Update => {
Box::pin(receive_update_for_community(
context,
any_base.clone(),
None,
&actor_url,
request_counter,
))
.await?;
true
}
CommunityValidTypes::Like => {
Box::pin(receive_like_for_community(
context,
any_base.clone(),
&actor_url,
request_counter,
))
.await?;
true
}
CommunityValidTypes::Dislike => {
Box::pin(receive_dislike_for_community(
context,
any_base.clone(),
&actor_url,
request_counter,
))
.await?;
true
}
CommunityValidTypes::Delete => {
Box::pin(receive_delete_for_community(
context,
any_base.clone(),
None,
&actor_url,
request_counter,
))
.await?;
true
}
CommunityValidTypes::Add => {
Box::pin(receive_add_for_community(
context,
any_base.clone(),
None,
request_counter,
))
.await?;
true
}
CommunityValidTypes::Remove => {
Box::pin(receive_remove_for_community(
context,
any_base.clone(),
None,
request_counter,
))
.await?;
true
}
CommunityValidTypes::Block => {
Box::pin(receive_block_user_for_community(
context,
any_base.clone(),
None,
request_counter,
))
.await?;
true
}
};
if do_announce {
// Check again that the activity is public, just to be sure
verify_is_addressed_to_public(&activity)?;
let mut object_actor = activity.object().clone().single_xsd_any_uri();
// If activity is something like Undo/Block, we need to access activity.object.object
if object_actor.is_none() {
object_actor = activity
.object()
.as_one()
.map(|a| ActorAndObject::from_any_base(a.to_owned()).ok())
.flatten()
.flatten()
.map(|a: ActorAndObject<CommunityValidTypes>| a.object().to_owned().single_xsd_any_uri())
.flatten();
}
to_community
.send_announce(activity.into_any_base()?, object_actor, context)
.await?;
}
Ok(HttpResponse::Ok().finish())
}
/// Handle a follow request from a remote person, adding the person as follower and returning an
/// Accept activity.
async fn handle_follow(
activity: AnyBase,
person: Person,
community: &Community,
context: &LemmyContext,
) -> Result<HttpResponse, LemmyError> {
let follow = Follow::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&follow, &person.actor_id(), false)?;
let community_follower_form = CommunityFollowerForm {
community_id: community.id,
person_id: person.id,
pending: false,
};
// This will fail if they're already a follower, but ignore the error.
blocking(&context.pool(), move |conn| {
CommunityFollower::follow(&conn, &community_follower_form).ok()
})
.await?;
community.send_accept_follow(follow, context).await?;
Ok(HttpResponse::Ok().finish())
}
async fn handle_undo(
context: &LemmyContext,
activity: CommunityAcceptedActivities,
actor_url: Url,
to_community: &Community,
request_counter: &mut i32,
) -> Result<bool, LemmyError> {
let inner_kind = activity
.object()
.is_single_kind(&FollowType::Follow.to_string());
let any_base = activity.into_any_base()?;
if inner_kind {
handle_undo_follow(any_base, actor_url, to_community, &context).await?;
Ok(false)
} else {
receive_undo_for_community(context, any_base, None, &actor_url, request_counter).await?;
Ok(true)
}
}
/// Handle `Undo/Follow` from a person, removing the person from followers list.
async fn handle_undo_follow(
activity: AnyBase,
person_url: Url,
community: &Community,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let undo = Undo::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&undo, &person_url, true)?;
let object = undo.object().to_owned().one().context(location_info!())?;
let follow = Follow::from_any_base(object)?.context(location_info!())?;
verify_activity_domains_valid(&follow, &person_url, false)?;
let person = blocking(&context.pool(), move |conn| {
Person::read_from_apub_id(&conn, &person_url.into())
})
.await??;
let community_follower_form = CommunityFollowerForm {
community_id: community.id,
person_id: person.id,
pending: false,
};
// This will fail if they aren't a follower, but ignore the error.
blocking(&context.pool(), move |conn| {
CommunityFollower::unfollow(&conn, &community_follower_form).ok()
})
.await?;
Ok(())
}