--- /dev/null
+pub mod receive;
+pub mod send;
-use crate::inbox::{
- activities::{
+use crate::{
+ activities::receive::{
create::receive_create,
delete::receive_delete,
dislike::receive_dislike,
like::receive_like,
+ receive_unhandled_activity,
remove::receive_remove,
undo::receive_undo,
update::receive_update,
},
- shared_inbox::{get_community_id_from_activity, receive_unhandled_activity},
+ inbox::shared_inbox::get_community_id_from_activity,
};
use activitystreams::{
activity::*,
use crate::{
- inbox::shared_inbox::{
- announce_if_community_is_local,
- get_user_from_activity,
- receive_unhandled_activity,
- },
+ activities::receive::{announce_if_community_is_local, receive_unhandled_activity},
+ inbox::shared_inbox::get_user_from_activity,
ActorType,
FromApub,
PageExt,
use crate::{
+ activities::receive::{announce_if_community_is_local, receive_unhandled_activity},
fetcher::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post},
- inbox::shared_inbox::{
- announce_if_community_is_local,
- get_user_from_activity,
- receive_unhandled_activity,
- },
+ inbox::shared_inbox::get_user_from_activity,
ActorType,
FromApub,
GroupExt,
use crate::{
+ activities::receive::{announce_if_community_is_local, receive_unhandled_activity},
fetcher::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post},
- inbox::shared_inbox::{
- announce_if_community_is_local,
- get_user_from_activity,
- receive_unhandled_activity,
- },
+ inbox::shared_inbox::get_user_from_activity,
FromApub,
PageExt,
};
use crate::{
+ activities::receive::{announce_if_community_is_local, receive_unhandled_activity},
fetcher::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post},
- inbox::shared_inbox::{
- announce_if_community_is_local,
- get_user_from_activity,
- receive_unhandled_activity,
- },
+ inbox::shared_inbox::get_user_from_activity,
FromApub,
PageExt,
};
--- /dev/null
+use crate::{fetcher::get_or_fetch_and_upsert_community, ActorType};
+use activitystreams::{
+ base::{Extends, ExtendsExt},
+ object::{AsObject, ObjectExt},
+};
+use actix_web::HttpResponse;
+use anyhow::Context;
+use lemmy_db::user::User_;
+use lemmy_utils::{location_info, LemmyError};
+use lemmy_websocket::LemmyContext;
+use log::debug;
+use serde::Serialize;
+use std::fmt::Debug;
+use url::Url;
+
+pub mod announce;
+pub mod create;
+pub mod delete;
+pub mod dislike;
+pub mod like;
+pub mod remove;
+pub mod undo;
+mod undo_comment;
+mod undo_post;
+pub mod update;
+
+fn receive_unhandled_activity<A>(activity: A) -> Result<HttpResponse, LemmyError>
+where
+ A: Debug,
+{
+ debug!("received unhandled activity type: {:?}", activity);
+ Ok(HttpResponse::NotImplemented().finish())
+}
+
+async fn announce_if_community_is_local<T, Kind>(
+ activity: T,
+ user: &User_,
+ context: &LemmyContext,
+) -> Result<(), LemmyError>
+where
+ T: AsObject<Kind>,
+ T: Extends<Kind>,
+ Kind: Serialize,
+ <T as Extends<Kind>>::Error: From<serde_json::Error> + Send + Sync + 'static,
+{
+ let cc = activity.cc().context(location_info!())?;
+ let cc = cc.as_many().context(location_info!())?;
+ let community_followers_uri = cc
+ .first()
+ .context(location_info!())?
+ .as_xsd_any_uri()
+ .context(location_info!())?;
+ // TODO: this is hacky but seems to be the only way to get the community ID
+ let community_uri = community_followers_uri
+ .to_string()
+ .replace("/followers", "");
+ let community = get_or_fetch_and_upsert_community(&Url::parse(&community_uri)?, context).await?;
+
+ if community.local {
+ community
+ .send_announce(activity.into_any_base()?, &user, context)
+ .await?;
+ }
+ Ok(())
+}
use crate::{
+ activities::receive::{announce_if_community_is_local, receive_unhandled_activity},
fetcher::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post},
- inbox::shared_inbox::{
- announce_if_community_is_local,
- get_community_id_from_activity,
- get_user_from_activity,
- receive_unhandled_activity,
- },
+ inbox::shared_inbox::{get_community_id_from_activity, get_user_from_activity},
ActorType,
FromApub,
GroupExt,
let actor = get_user_from_activity(&remove, context).await?;
let community = get_community_id_from_activity(&remove)?;
if actor.actor_id()?.domain() != community.domain() {
- return Err(anyhow!("Remove activities are only allowed on local objects").into());
+ return Err(anyhow!("Remove receive are only allowed on local objects").into());
}
match remove.object().as_single_kind_str() {
--- /dev/null
+use crate::{
+ activities::receive::{
+ announce_if_community_is_local,
+ receive_unhandled_activity,
+ undo_comment::*,
+ undo_post::*,
+ },
+ inbox::shared_inbox::get_user_from_activity,
+ ActorType,
+ FromApub,
+ GroupExt,
+};
+use activitystreams::{
+ activity::*,
+ base::{AnyBase, AsBase},
+ prelude::*,
+};
+use actix_web::HttpResponse;
+use anyhow::{anyhow, Context};
+use lemmy_db::{
+ community::{Community, CommunityForm},
+ community_view::CommunityView,
+ naive_now,
+ Crud,
+};
+use lemmy_structs::{blocking, community::CommunityResponse};
+use lemmy_utils::{location_info, LemmyError};
+use lemmy_websocket::{messages::SendCommunityRoomMessage, LemmyContext, UserOperation};
+
+pub async fn receive_undo(
+ activity: AnyBase,
+ context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+ let undo = Undo::from_any_base(activity)?.context(location_info!())?;
+ match undo.object().as_single_kind_str() {
+ Some("Delete") => receive_undo_delete(undo, context).await,
+ Some("Remove") => receive_undo_remove(undo, context).await,
+ Some("Like") => receive_undo_like(undo, context).await,
+ Some("Dislike") => receive_undo_dislike(undo, context).await,
+ _ => receive_unhandled_activity(undo),
+ }
+}
+
+fn check_is_undo_valid<T, A>(outer_activity: &Undo, inner_activity: &T) -> Result<(), LemmyError>
+where
+ T: AsBase<A> + ActorAndObjectRef,
+{
+ let outer_actor = outer_activity.actor()?;
+ let outer_actor_uri = outer_actor
+ .as_single_xsd_any_uri()
+ .context(location_info!())?;
+
+ let inner_actor = inner_activity.actor()?;
+ let inner_actor_uri = inner_actor
+ .as_single_xsd_any_uri()
+ .context(location_info!())?;
+
+ if outer_actor_uri.domain() != inner_actor_uri.domain() {
+ Err(anyhow!("Cant undo receive from a different instance").into())
+ } else {
+ Ok(())
+ }
+}
+
+async fn receive_undo_delete(
+ undo: Undo,
+ context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+ let delete = Delete::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
+ .context(location_info!())?;
+ check_is_undo_valid(&undo, &delete)?;
+ let type_ = delete
+ .object()
+ .as_single_kind_str()
+ .context(location_info!())?;
+ match type_ {
+ "Note" => receive_undo_delete_comment(undo, &delete, context).await,
+ "Page" => receive_undo_delete_post(undo, &delete, context).await,
+ "Group" => receive_undo_delete_community(undo, &delete, context).await,
+ d => Err(anyhow!("Undo Delete type {} not supported", d).into()),
+ }
+}
+
+async fn receive_undo_remove(
+ undo: Undo,
+ context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+ let remove = Remove::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
+ .context(location_info!())?;
+ check_is_undo_valid(&undo, &remove)?;
+
+ let type_ = remove
+ .object()
+ .as_single_kind_str()
+ .context(location_info!())?;
+ match type_ {
+ "Note" => receive_undo_remove_comment(undo, &remove, context).await,
+ "Page" => receive_undo_remove_post(undo, &remove, context).await,
+ "Group" => receive_undo_remove_community(undo, &remove, context).await,
+ d => Err(anyhow!("Undo Delete type {} not supported", d).into()),
+ }
+}
+
+async fn receive_undo_like(undo: Undo, context: &LemmyContext) -> Result<HttpResponse, LemmyError> {
+ let like = Like::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
+ .context(location_info!())?;
+ check_is_undo_valid(&undo, &like)?;
+
+ let type_ = like
+ .object()
+ .as_single_kind_str()
+ .context(location_info!())?;
+ match type_ {
+ "Note" => receive_undo_like_comment(undo, &like, context).await,
+ "Page" => receive_undo_like_post(undo, &like, context).await,
+ d => Err(anyhow!("Undo Delete type {} not supported", d).into()),
+ }
+}
+
+async fn receive_undo_dislike(
+ undo: Undo,
+ context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+ let dislike = Dislike::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
+ .context(location_info!())?;
+ check_is_undo_valid(&undo, &dislike)?;
+
+ let type_ = dislike
+ .object()
+ .as_single_kind_str()
+ .context(location_info!())?;
+ match type_ {
+ "Note" => receive_undo_dislike_comment(undo, &dislike, context).await,
+ "Page" => receive_undo_dislike_post(undo, &dislike, context).await,
+ d => Err(anyhow!("Undo Delete type {} not supported", d).into()),
+ }
+}
+
+async fn receive_undo_delete_community(
+ undo: Undo,
+ delete: &Delete,
+ context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+ let user = get_user_from_activity(delete, context).await?;
+ let group = GroupExt::from_any_base(delete.object().to_owned().one().context(location_info!())?)?
+ .context(location_info!())?;
+
+ let community_actor_id = CommunityForm::from_apub(&group, context, Some(user.actor_id()?))
+ .await?
+ .actor_id
+ .context(location_info!())?;
+
+ let community = blocking(context.pool(), move |conn| {
+ Community::read_from_actor_id(conn, &community_actor_id)
+ })
+ .await??;
+
+ let community_form = CommunityForm {
+ name: community.name.to_owned(),
+ title: community.title.to_owned(),
+ description: community.description.to_owned(),
+ category_id: community.category_id, // Note: need to keep this due to foreign key constraint
+ creator_id: community.creator_id, // Note: need to keep this due to foreign key constraint
+ removed: None,
+ published: None,
+ updated: Some(naive_now()),
+ deleted: Some(false),
+ nsfw: community.nsfw,
+ actor_id: Some(community.actor_id),
+ local: community.local,
+ private_key: community.private_key,
+ public_key: community.public_key,
+ last_refreshed_at: None,
+ icon: Some(community.icon.to_owned()),
+ banner: Some(community.banner.to_owned()),
+ };
+
+ let community_id = community.id;
+ blocking(context.pool(), move |conn| {
+ Community::update(conn, community_id, &community_form)
+ })
+ .await??;
+
+ let community_id = community.id;
+ let res = CommunityResponse {
+ community: blocking(context.pool(), move |conn| {
+ CommunityView::read(conn, community_id, None)
+ })
+ .await??,
+ };
+
+ let community_id = res.community.id;
+
+ context.chat_server().do_send(SendCommunityRoomMessage {
+ op: UserOperation::EditCommunity,
+ response: res,
+ community_id,
+ websocket_id: None,
+ });
+
+ announce_if_community_is_local(undo, &user, context).await?;
+ Ok(HttpResponse::Ok().finish())
+}
+
+async fn receive_undo_remove_community(
+ undo: Undo,
+ remove: &Remove,
+ context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+ let mod_ = get_user_from_activity(remove, context).await?;
+ let group = GroupExt::from_any_base(remove.object().to_owned().one().context(location_info!())?)?
+ .context(location_info!())?;
+
+ let community_actor_id = CommunityForm::from_apub(&group, context, Some(mod_.actor_id()?))
+ .await?
+ .actor_id
+ .context(location_info!())?;
+
+ let community = blocking(context.pool(), move |conn| {
+ Community::read_from_actor_id(conn, &community_actor_id)
+ })
+ .await??;
+
+ let community_form = CommunityForm {
+ name: community.name.to_owned(),
+ title: community.title.to_owned(),
+ description: community.description.to_owned(),
+ category_id: community.category_id, // Note: need to keep this due to foreign key constraint
+ creator_id: community.creator_id, // Note: need to keep this due to foreign key constraint
+ removed: Some(false),
+ published: None,
+ updated: Some(naive_now()),
+ deleted: None,
+ nsfw: community.nsfw,
+ actor_id: Some(community.actor_id),
+ local: community.local,
+ private_key: community.private_key,
+ public_key: community.public_key,
+ last_refreshed_at: None,
+ icon: Some(community.icon.to_owned()),
+ banner: Some(community.banner.to_owned()),
+ };
+
+ let community_id = community.id;
+ blocking(context.pool(), move |conn| {
+ Community::update(conn, community_id, &community_form)
+ })
+ .await??;
+
+ let community_id = community.id;
+ let res = CommunityResponse {
+ community: blocking(context.pool(), move |conn| {
+ CommunityView::read(conn, community_id, None)
+ })
+ .await??,
+ };
+
+ let community_id = res.community.id;
+
+ context.chat_server().do_send(SendCommunityRoomMessage {
+ op: UserOperation::EditCommunity,
+ response: res,
+ community_id,
+ websocket_id: None,
+ });
+
+ announce_if_community_is_local(undo, &mod_, context).await?;
+ Ok(HttpResponse::Ok().finish())
+}
--- /dev/null
+use crate::{
+ activities::receive::announce_if_community_is_local,
+ fetcher::get_or_fetch_and_insert_comment,
+ inbox::shared_inbox::get_user_from_activity,
+ ActorType,
+ FromApub,
+};
+use activitystreams::{activity::*, object::Note, prelude::*};
+use actix_web::HttpResponse;
+use anyhow::Context;
+use lemmy_db::{
+ comment::{Comment, CommentForm, CommentLike},
+ comment_view::CommentView,
+ naive_now,
+ Crud,
+ Likeable,
+};
+use lemmy_structs::{blocking, comment::CommentResponse};
+use lemmy_utils::{location_info, LemmyError};
+use lemmy_websocket::{messages::SendComment, LemmyContext, UserOperation};
+
+pub(crate) async fn receive_undo_like_comment(
+ undo: Undo,
+ like: &Like,
+ context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+ let user = get_user_from_activity(like, context).await?;
+ let note = Note::from_any_base(like.object().to_owned().one().context(location_info!())?)?
+ .context(location_info!())?;
+
+ let comment = CommentForm::from_apub(¬e, context, None).await?;
+
+ let comment_id = get_or_fetch_and_insert_comment(&comment.get_ap_id()?, context)
+ .await?
+ .id;
+
+ let user_id = user.id;
+ blocking(context.pool(), move |conn| {
+ CommentLike::remove(conn, user_id, comment_id)
+ })
+ .await??;
+
+ // Refetch the view
+ let comment_view = blocking(context.pool(), move |conn| {
+ CommentView::read(conn, comment_id, None)
+ })
+ .await??;
+
+ // TODO get those recipient actor ids from somewhere
+ let recipient_ids = vec![];
+ let res = CommentResponse {
+ comment: comment_view,
+ recipient_ids,
+ form_id: None,
+ };
+
+ context.chat_server().do_send(SendComment {
+ op: UserOperation::CreateCommentLike,
+ comment: res,
+ websocket_id: None,
+ });
+
+ announce_if_community_is_local(undo, &user, context).await?;
+ Ok(HttpResponse::Ok().finish())
+}
+
+pub(crate) async fn receive_undo_dislike_comment(
+ undo: Undo,
+ dislike: &Dislike,
+ context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+ let user = get_user_from_activity(dislike, context).await?;
+ let note = Note::from_any_base(
+ dislike
+ .object()
+ .to_owned()
+ .one()
+ .context(location_info!())?,
+ )?
+ .context(location_info!())?;
+
+ let comment = CommentForm::from_apub(¬e, context, None).await?;
+
+ let comment_id = get_or_fetch_and_insert_comment(&comment.get_ap_id()?, context)
+ .await?
+ .id;
+
+ let user_id = user.id;
+ blocking(context.pool(), move |conn| {
+ CommentLike::remove(conn, user_id, comment_id)
+ })
+ .await??;
+
+ // Refetch the view
+ let comment_view = blocking(context.pool(), move |conn| {
+ CommentView::read(conn, comment_id, None)
+ })
+ .await??;
+
+ // TODO get those recipient actor ids from somewhere
+ let recipient_ids = vec![];
+ let res = CommentResponse {
+ comment: comment_view,
+ recipient_ids,
+ form_id: None,
+ };
+
+ context.chat_server().do_send(SendComment {
+ op: UserOperation::CreateCommentLike,
+ comment: res,
+ websocket_id: None,
+ });
+
+ announce_if_community_is_local(undo, &user, context).await?;
+ Ok(HttpResponse::Ok().finish())
+}
+
+pub(crate) async fn receive_undo_delete_comment(
+ undo: Undo,
+ delete: &Delete,
+ context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+ let user = get_user_from_activity(delete, context).await?;
+ let note = Note::from_any_base(delete.object().to_owned().one().context(location_info!())?)?
+ .context(location_info!())?;
+
+ let comment_ap_id = CommentForm::from_apub(¬e, context, Some(user.actor_id()?))
+ .await?
+ .get_ap_id()?;
+
+ let comment = get_or_fetch_and_insert_comment(&comment_ap_id, context).await?;
+
+ let comment_form = CommentForm {
+ content: comment.content.to_owned(),
+ parent_id: comment.parent_id,
+ post_id: comment.post_id,
+ creator_id: comment.creator_id,
+ removed: None,
+ deleted: Some(false),
+ read: None,
+ published: None,
+ updated: Some(naive_now()),
+ ap_id: Some(comment.ap_id),
+ local: comment.local,
+ };
+ let comment_id = comment.id;
+ blocking(context.pool(), move |conn| {
+ Comment::update(conn, comment_id, &comment_form)
+ })
+ .await??;
+
+ // Refetch the view
+ let comment_id = comment.id;
+ let comment_view = blocking(context.pool(), move |conn| {
+ CommentView::read(conn, comment_id, None)
+ })
+ .await??;
+
+ // TODO get those recipient actor ids from somewhere
+ let recipient_ids = vec![];
+ let res = CommentResponse {
+ comment: comment_view,
+ recipient_ids,
+ form_id: None,
+ };
+
+ context.chat_server().do_send(SendComment {
+ op: UserOperation::EditComment,
+ comment: res,
+ websocket_id: None,
+ });
+
+ announce_if_community_is_local(undo, &user, context).await?;
+ Ok(HttpResponse::Ok().finish())
+}
+
+pub(crate) async fn receive_undo_remove_comment(
+ undo: Undo,
+ remove: &Remove,
+ context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+ let mod_ = get_user_from_activity(remove, context).await?;
+ let note = Note::from_any_base(remove.object().to_owned().one().context(location_info!())?)?
+ .context(location_info!())?;
+
+ let comment_ap_id = CommentForm::from_apub(¬e, context, None)
+ .await?
+ .get_ap_id()?;
+
+ let comment = get_or_fetch_and_insert_comment(&comment_ap_id, context).await?;
+
+ let comment_form = CommentForm {
+ content: comment.content.to_owned(),
+ parent_id: comment.parent_id,
+ post_id: comment.post_id,
+ creator_id: comment.creator_id,
+ removed: Some(false),
+ deleted: None,
+ read: None,
+ published: None,
+ updated: Some(naive_now()),
+ ap_id: Some(comment.ap_id),
+ local: comment.local,
+ };
+ let comment_id = comment.id;
+ blocking(context.pool(), move |conn| {
+ Comment::update(conn, comment_id, &comment_form)
+ })
+ .await??;
+
+ // Refetch the view
+ let comment_id = comment.id;
+ let comment_view = blocking(context.pool(), move |conn| {
+ CommentView::read(conn, comment_id, None)
+ })
+ .await??;
+
+ // TODO get those recipient actor ids from somewhere
+ let recipient_ids = vec![];
+ let res = CommentResponse {
+ comment: comment_view,
+ recipient_ids,
+ form_id: None,
+ };
+
+ context.chat_server().do_send(SendComment {
+ op: UserOperation::EditComment,
+ comment: res,
+ websocket_id: None,
+ });
+
+ announce_if_community_is_local(undo, &mod_, context).await?;
+ Ok(HttpResponse::Ok().finish())
+}
--- /dev/null
+use crate::{
+ activities::receive::announce_if_community_is_local,
+ fetcher::get_or_fetch_and_insert_post,
+ inbox::shared_inbox::get_user_from_activity,
+ ActorType,
+ FromApub,
+ PageExt,
+};
+use activitystreams::{activity::*, prelude::*};
+use actix_web::HttpResponse;
+use anyhow::Context;
+use lemmy_db::{
+ naive_now,
+ post::{Post, PostForm, PostLike},
+ post_view::PostView,
+ Crud,
+ Likeable,
+};
+use lemmy_structs::{blocking, post::PostResponse};
+use lemmy_utils::{location_info, LemmyError};
+use lemmy_websocket::{messages::SendPost, LemmyContext, UserOperation};
+
+pub(crate) async fn receive_undo_like_post(
+ undo: Undo,
+ like: &Like,
+ context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+ let user = get_user_from_activity(like, context).await?;
+ let page = PageExt::from_any_base(like.object().to_owned().one().context(location_info!())?)?
+ .context(location_info!())?;
+
+ let post = PostForm::from_apub(&page, context, None).await?;
+
+ let post_id = get_or_fetch_and_insert_post(&post.get_ap_id()?, context)
+ .await?
+ .id;
+
+ let user_id = user.id;
+ blocking(context.pool(), move |conn| {
+ PostLike::remove(conn, user_id, post_id)
+ })
+ .await??;
+
+ // Refetch the view
+ let post_view = blocking(context.pool(), move |conn| {
+ PostView::read(conn, post_id, None)
+ })
+ .await??;
+
+ let res = PostResponse { post: post_view };
+
+ context.chat_server().do_send(SendPost {
+ op: UserOperation::CreatePostLike,
+ post: res,
+ websocket_id: None,
+ });
+
+ announce_if_community_is_local(undo, &user, context).await?;
+ Ok(HttpResponse::Ok().finish())
+}
+
+pub(crate) async fn receive_undo_dislike_post(
+ undo: Undo,
+ dislike: &Dislike,
+ context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+ let user = get_user_from_activity(dislike, context).await?;
+ let page = PageExt::from_any_base(
+ dislike
+ .object()
+ .to_owned()
+ .one()
+ .context(location_info!())?,
+ )?
+ .context(location_info!())?;
+
+ let post = PostForm::from_apub(&page, context, None).await?;
+
+ let post_id = get_or_fetch_and_insert_post(&post.get_ap_id()?, context)
+ .await?
+ .id;
+
+ let user_id = user.id;
+ blocking(context.pool(), move |conn| {
+ PostLike::remove(conn, user_id, post_id)
+ })
+ .await??;
+
+ // Refetch the view
+ let post_view = blocking(context.pool(), move |conn| {
+ PostView::read(conn, post_id, None)
+ })
+ .await??;
+
+ let res = PostResponse { post: post_view };
+
+ context.chat_server().do_send(SendPost {
+ op: UserOperation::CreatePostLike,
+ post: res,
+ websocket_id: None,
+ });
+
+ announce_if_community_is_local(undo, &user, context).await?;
+ Ok(HttpResponse::Ok().finish())
+}
+
+pub(crate) async fn receive_undo_delete_post(
+ undo: Undo,
+ delete: &Delete,
+ context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+ let user = get_user_from_activity(delete, context).await?;
+ let page = PageExt::from_any_base(delete.object().to_owned().one().context(location_info!())?)?
+ .context(location_info!())?;
+
+ let post_ap_id = PostForm::from_apub(&page, context, Some(user.actor_id()?))
+ .await?
+ .get_ap_id()?;
+
+ let post = get_or_fetch_and_insert_post(&post_ap_id, context).await?;
+
+ let post_form = PostForm {
+ name: post.name.to_owned(),
+ url: post.url.to_owned(),
+ body: post.body.to_owned(),
+ creator_id: post.creator_id.to_owned(),
+ community_id: post.community_id,
+ removed: None,
+ deleted: Some(false),
+ nsfw: post.nsfw,
+ locked: None,
+ stickied: None,
+ updated: Some(naive_now()),
+ embed_title: post.embed_title,
+ embed_description: post.embed_description,
+ embed_html: post.embed_html,
+ thumbnail_url: post.thumbnail_url,
+ ap_id: Some(post.ap_id),
+ local: post.local,
+ published: None,
+ };
+ let post_id = post.id;
+ blocking(context.pool(), move |conn| {
+ Post::update(conn, post_id, &post_form)
+ })
+ .await??;
+
+ // Refetch the view
+ let post_id = post.id;
+ let post_view = blocking(context.pool(), move |conn| {
+ PostView::read(conn, post_id, None)
+ })
+ .await??;
+
+ let res = PostResponse { post: post_view };
+
+ context.chat_server().do_send(SendPost {
+ op: UserOperation::EditPost,
+ post: res,
+ websocket_id: None,
+ });
+
+ announce_if_community_is_local(undo, &user, context).await?;
+ Ok(HttpResponse::Ok().finish())
+}
+
+pub(crate) async fn receive_undo_remove_post(
+ undo: Undo,
+ remove: &Remove,
+ context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+ let mod_ = get_user_from_activity(remove, context).await?;
+ let page = PageExt::from_any_base(remove.object().to_owned().one().context(location_info!())?)?
+ .context(location_info!())?;
+
+ let post_ap_id = PostForm::from_apub(&page, context, None)
+ .await?
+ .get_ap_id()?;
+
+ let post = get_or_fetch_and_insert_post(&post_ap_id, context).await?;
+
+ let post_form = PostForm {
+ name: post.name.to_owned(),
+ url: post.url.to_owned(),
+ body: post.body.to_owned(),
+ creator_id: post.creator_id.to_owned(),
+ community_id: post.community_id,
+ removed: Some(false),
+ deleted: None,
+ nsfw: post.nsfw,
+ locked: None,
+ stickied: None,
+ updated: Some(naive_now()),
+ embed_title: post.embed_title,
+ embed_description: post.embed_description,
+ embed_html: post.embed_html,
+ thumbnail_url: post.thumbnail_url,
+ ap_id: Some(post.ap_id),
+ local: post.local,
+ published: None,
+ };
+ let post_id = post.id;
+ blocking(context.pool(), move |conn| {
+ Post::update(conn, post_id, &post_form)
+ })
+ .await??;
+
+ // Refetch the view
+ let post_id = post.id;
+ let post_view = blocking(context.pool(), move |conn| {
+ PostView::read(conn, post_id, None)
+ })
+ .await??;
+
+ let res = PostResponse { post: post_view };
+
+ context.chat_server().do_send(SendPost {
+ op: UserOperation::EditPost,
+ post: res,
+ websocket_id: None,
+ });
+
+ announce_if_community_is_local(undo, &mod_, context).await?;
+ Ok(HttpResponse::Ok().finish())
+}
use crate::{
+ activities::receive::{announce_if_community_is_local, receive_unhandled_activity},
fetcher::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post},
- inbox::shared_inbox::{
- announce_if_community_is_local,
- get_user_from_activity,
- receive_unhandled_activity,
- },
+ inbox::shared_inbox::get_user_from_activity,
ActorType,
FromApub,
PageExt,
use crate::{
+ activities::send::generate_activity_id,
activity_queue::{send_comment_mentions, send_to_community},
- check_actor_domain,
- create_apub_response,
- create_apub_tombstone_response,
- create_tombstone,
- fetch_webfinger_url,
- fetcher::{
- get_or_fetch_and_insert_comment,
- get_or_fetch_and_insert_post,
- get_or_fetch_and_upsert_user,
- },
- generate_activity_id,
+ fetcher::get_or_fetch_and_upsert_user,
ActorType,
ApubLikeableType,
ApubObjectType,
- FromApub,
ToApub,
};
use activitystreams::{
},
base::AnyBase,
link::Mention,
- object::{kind::NoteType, Note, Tombstone},
prelude::*,
public,
};
-use actix_web::{body::Body, web, web::Path, HttpResponse};
-use anyhow::Context;
-use diesel::result::Error::NotFound;
+use anyhow::anyhow;
use itertools::Itertools;
-use lemmy_db::{
- comment::{Comment, CommentForm},
- community::Community,
- post::Post,
- user::User_,
- Crud,
- DbPool,
-};
-use lemmy_structs::blocking;
+use lemmy_db::{comment::Comment, community::Community, post::Post, user::User_, Crud};
+use lemmy_structs::{blocking, WebFingerResponse};
use lemmy_utils::{
- location_info,
- utils::{convert_datetime, remove_slurs, scrape_text_for_mentions, MentionData},
+ request::{retry, RecvError},
+ settings::Settings,
+ utils::{scrape_text_for_mentions, MentionData},
LemmyError,
};
use lemmy_websocket::LemmyContext;
use log::debug;
-use serde::Deserialize;
+use reqwest::Client;
use serde_json::Error;
use url::Url;
-#[derive(Deserialize)]
-pub struct CommentQuery {
- comment_id: String,
-}
-
-/// Return the post json over HTTP.
-pub async fn get_apub_comment(
- info: Path<CommentQuery>,
- context: web::Data<LemmyContext>,
-) -> Result<HttpResponse<Body>, LemmyError> {
- let id = info.comment_id.parse::<i32>()?;
- let comment = blocking(context.pool(), move |conn| Comment::read(conn, id)).await??;
- if !comment.local {
- return Err(NotFound.into());
- }
-
- if !comment.deleted {
- Ok(create_apub_response(
- &comment.to_apub(context.pool()).await?,
- ))
- } else {
- Ok(create_apub_tombstone_response(&comment.to_tombstone()?))
- }
-}
-
-#[async_trait::async_trait(?Send)]
-impl ToApub for Comment {
- type Response = Note;
-
- async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> {
- let mut comment = Note::new();
-
- let creator_id = self.creator_id;
- let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??;
-
- let post_id = self.post_id;
- let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
-
- let community_id = post.community_id;
- let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
-
- // Add a vector containing some important info to the "in_reply_to" field
- // [post_ap_id, Option(parent_comment_ap_id)]
- let mut in_reply_to_vec = vec![post.ap_id];
-
- if let Some(parent_id) = self.parent_id {
- let parent_comment = blocking(pool, move |conn| Comment::read(conn, parent_id)).await??;
-
- in_reply_to_vec.push(parent_comment.ap_id);
- }
-
- comment
- // Not needed when the Post is embedded in a collection (like for community outbox)
- .set_context(activitystreams::context())
- .set_id(Url::parse(&self.ap_id)?)
- .set_published(convert_datetime(self.published))
- .set_to(community.actor_id)
- .set_many_in_reply_tos(in_reply_to_vec)
- .set_content(self.content.to_owned())
- .set_attributed_to(creator.actor_id);
-
- if let Some(u) = self.updated {
- comment.set_updated(convert_datetime(u));
- }
-
- Ok(comment)
- }
-
- fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
- create_tombstone(self.deleted, &self.ap_id, self.updated, NoteType::Note)
- }
-}
-
-#[async_trait::async_trait(?Send)]
-impl FromApub for CommentForm {
- type ApubType = Note;
-
- /// Parse an ActivityPub note received from another instance into a Lemmy comment
- async fn from_apub(
- note: &Note,
- context: &LemmyContext,
- expected_domain: Option<Url>,
- ) -> Result<CommentForm, LemmyError> {
- let creator_actor_id = ¬e
- .attributed_to()
- .context(location_info!())?
- .as_single_xsd_any_uri()
- .context(location_info!())?;
-
- let creator = get_or_fetch_and_upsert_user(creator_actor_id, context).await?;
-
- let mut in_reply_tos = note
- .in_reply_to()
- .as_ref()
- .context(location_info!())?
- .as_many()
- .context(location_info!())?
- .iter()
- .map(|i| i.as_xsd_any_uri().context(""));
- let post_ap_id = in_reply_tos.next().context(location_info!())??;
-
- // This post, or the parent comment might not yet exist on this server yet, fetch them.
- let post = get_or_fetch_and_insert_post(&post_ap_id, context).await?;
-
- // The 2nd item, if it exists, is the parent comment apub_id
- // For deeply nested comments, FromApub automatically gets called recursively
- let parent_id: Option<i32> = match in_reply_tos.next() {
- Some(parent_comment_uri) => {
- let parent_comment_ap_id = &parent_comment_uri?;
- let parent_comment =
- get_or_fetch_and_insert_comment(&parent_comment_ap_id, context).await?;
-
- Some(parent_comment.id)
- }
- None => None,
- };
- let content = note
- .content()
- .context(location_info!())?
- .as_single_xsd_string()
- .context(location_info!())?
- .to_string();
- let content_slurs_removed = remove_slurs(&content);
-
- Ok(CommentForm {
- creator_id: creator.id,
- post_id: post.id,
- parent_id,
- content: content_slurs_removed,
- removed: None,
- read: None,
- published: note.published().map(|u| u.to_owned().naive_local()),
- updated: note.updated().map(|u| u.to_owned().naive_local()),
- deleted: None,
- ap_id: Some(check_actor_domain(note, expected_domain)?),
- local: false,
- })
- }
-}
-
#[async_trait::async_trait(?Send)]
impl ApubObjectType for Comment {
/// Send out information about a newly created comment, to the followers of the community.
tags,
})
}
+
+async fn fetch_webfinger_url(mention: &MentionData, client: &Client) -> Result<Url, LemmyError> {
+ let fetch_url = format!(
+ "{}://{}/.well-known/webfinger?resource=acct:{}@{}",
+ Settings::get().get_protocol_string(),
+ mention.domain,
+ mention.name,
+ mention.domain
+ );
+ debug!("Fetching webfinger url: {}", &fetch_url);
+
+ let response = retry(|| client.get(&fetch_url).send()).await?;
+
+ let res: WebFingerResponse = response
+ .json()
+ .await
+ .map_err(|e| RecvError(e.to_string()))?;
+
+ let link = res
+ .links
+ .iter()
+ .find(|l| l.type_.eq(&Some("application/activity+json".to_string())))
+ .ok_or_else(|| anyhow!("No application/activity+json link found."))?;
+ link
+ .href
+ .to_owned()
+ .map(|u| Url::parse(&u))
+ .transpose()?
+ .ok_or_else(|| anyhow!("No href found.").into())
+}
--- /dev/null
+use crate::{
+ activities::send::generate_activity_id,
+ activity_queue::{send_activity_single_dest, send_to_community_followers},
+ check_is_apub_id_valid,
+ fetcher::get_or_fetch_and_upsert_actor,
+ ActorType,
+ ToApub,
+};
+use activitystreams::{
+ activity::{
+ kind::{AcceptType, AnnounceType, DeleteType, LikeType, RemoveType, UndoType},
+ Accept,
+ ActorAndObjectRefExt,
+ Announce,
+ Delete,
+ Follow,
+ Remove,
+ Undo,
+ },
+ base::{AnyBase, BaseExt, ExtendsExt},
+ object::ObjectExt,
+ public,
+};
+use anyhow::Context;
+use itertools::Itertools;
+use lemmy_db::{community::Community, community_view::CommunityFollowerView, user::User_, DbPool};
+use lemmy_structs::blocking;
+use lemmy_utils::{location_info, settings::Settings, LemmyError};
+use lemmy_websocket::LemmyContext;
+use url::Url;
+
+#[async_trait::async_trait(?Send)]
+impl ActorType for Community {
+ fn actor_id_str(&self) -> String {
+ self.actor_id.to_owned()
+ }
+
+ fn public_key(&self) -> Option<String> {
+ self.public_key.to_owned()
+ }
+ fn private_key(&self) -> Option<String> {
+ self.private_key.to_owned()
+ }
+
+ fn user_id(&self) -> i32 {
+ self.creator_id
+ }
+
+ async fn send_follow(
+ &self,
+ _follow_actor_id: &Url,
+ _context: &LemmyContext,
+ ) -> Result<(), LemmyError> {
+ unimplemented!()
+ }
+
+ async fn send_unfollow(
+ &self,
+ _follow_actor_id: &Url,
+ _context: &LemmyContext,
+ ) -> Result<(), LemmyError> {
+ unimplemented!()
+ }
+
+ /// As a local community, accept the follow request from a remote user.
+ async fn send_accept_follow(
+ &self,
+ follow: Follow,
+ context: &LemmyContext,
+ ) -> Result<(), LemmyError> {
+ let actor_uri = follow
+ .actor()?
+ .as_single_xsd_any_uri()
+ .context(location_info!())?;
+ let actor = get_or_fetch_and_upsert_actor(actor_uri, context).await?;
+
+ let mut accept = Accept::new(self.actor_id.to_owned(), follow.into_any_base()?);
+ let to = actor.get_inbox_url()?;
+ accept
+ .set_context(activitystreams::context())
+ .set_id(generate_activity_id(AcceptType::Accept)?)
+ .set_to(to.clone());
+
+ send_activity_single_dest(accept, self, to, context).await?;
+ Ok(())
+ }
+
+ async fn send_delete(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
+ let group = self.to_apub(context.pool()).await?;
+
+ let mut delete = Delete::new(creator.actor_id.to_owned(), group.into_any_base()?);
+ delete
+ .set_context(activitystreams::context())
+ .set_id(generate_activity_id(DeleteType::Delete)?)
+ .set_to(public())
+ .set_many_ccs(vec![self.get_followers_url()?]);
+
+ send_to_community_followers(delete, self, context, None).await?;
+ Ok(())
+ }
+
+ async fn send_undo_delete(
+ &self,
+ creator: &User_,
+ context: &LemmyContext,
+ ) -> Result<(), LemmyError> {
+ let group = self.to_apub(context.pool()).await?;
+
+ let mut delete = Delete::new(creator.actor_id.to_owned(), group.into_any_base()?);
+ delete
+ .set_context(activitystreams::context())
+ .set_id(generate_activity_id(DeleteType::Delete)?)
+ .set_to(public())
+ .set_many_ccs(vec![self.get_followers_url()?]);
+
+ let mut undo = Undo::new(creator.actor_id.to_owned(), delete.into_any_base()?);
+ undo
+ .set_context(activitystreams::context())
+ .set_id(generate_activity_id(UndoType::Undo)?)
+ .set_to(public())
+ .set_many_ccs(vec![self.get_followers_url()?]);
+
+ send_to_community_followers(undo, self, context, None).await?;
+ Ok(())
+ }
+
+ async fn send_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
+ let group = self.to_apub(context.pool()).await?;
+
+ let mut remove = Remove::new(mod_.actor_id.to_owned(), group.into_any_base()?);
+ remove
+ .set_context(activitystreams::context())
+ .set_id(generate_activity_id(RemoveType::Remove)?)
+ .set_to(public())
+ .set_many_ccs(vec![self.get_followers_url()?]);
+
+ send_to_community_followers(remove, self, context, None).await?;
+ Ok(())
+ }
+
+ async fn send_undo_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
+ let group = self.to_apub(context.pool()).await?;
+
+ let mut remove = Remove::new(mod_.actor_id.to_owned(), group.into_any_base()?);
+ remove
+ .set_context(activitystreams::context())
+ .set_id(generate_activity_id(RemoveType::Remove)?)
+ .set_to(public())
+ .set_many_ccs(vec![self.get_followers_url()?]);
+
+ // Undo that fake activity
+ let mut undo = Undo::new(mod_.actor_id.to_owned(), remove.into_any_base()?);
+ undo
+ .set_context(activitystreams::context())
+ .set_id(generate_activity_id(LikeType::Like)?)
+ .set_to(public())
+ .set_many_ccs(vec![self.get_followers_url()?]);
+
+ send_to_community_followers(undo, self, context, None).await?;
+ Ok(())
+ }
+
+ async fn send_announce(
+ &self,
+ activity: AnyBase,
+ sender: &User_,
+ context: &LemmyContext,
+ ) -> Result<(), LemmyError> {
+ let mut announce = Announce::new(self.actor_id.to_owned(), activity);
+ announce
+ .set_context(activitystreams::context())
+ .set_id(generate_activity_id(AnnounceType::Announce)?)
+ .set_to(public())
+ .set_many_ccs(vec![self.get_followers_url()?]);
+
+ send_to_community_followers(
+ announce,
+ self,
+ context,
+ Some(sender.get_shared_inbox_url()?),
+ )
+ .await?;
+
+ Ok(())
+ }
+
+ /// For a given community, returns the inboxes of all followers.
+ ///
+ /// TODO: this function is very badly implemented, we should just store shared_inbox_url in
+ /// CommunityFollowerView
+ async fn get_follower_inboxes(&self, pool: &DbPool) -> Result<Vec<Url>, LemmyError> {
+ let id = self.id;
+
+ let inboxes = blocking(pool, move |conn| {
+ CommunityFollowerView::for_community(conn, id)
+ })
+ .await??;
+ let inboxes = inboxes
+ .into_iter()
+ .filter(|i| !i.user_local)
+ .map(|u| -> Result<Url, LemmyError> {
+ let url = Url::parse(&u.user_actor_id)?;
+ let domain = url.domain().context(location_info!())?;
+ let port = if let Some(port) = url.port() {
+ format!(":{}", port)
+ } else {
+ "".to_string()
+ };
+ Ok(Url::parse(&format!(
+ "{}://{}{}/inbox",
+ Settings::get().get_protocol_string(),
+ domain,
+ port,
+ ))?)
+ })
+ .filter_map(Result::ok)
+ // Don't send to blocked instances
+ .filter(|inbox| check_is_apub_id_valid(inbox).is_ok())
+ .unique()
+ .collect();
+
+ Ok(inboxes)
+ }
+}
--- /dev/null
+use lemmy_utils::settings::Settings;
+use url::{ParseError, Url};
+use uuid::Uuid;
+
+pub mod comment;
+pub mod community;
+pub mod post;
+pub mod private_message;
+pub mod user;
+
+fn generate_activity_id<T>(kind: T) -> Result<Url, ParseError>
+where
+ T: ToString,
+{
+ let id = format!(
+ "{}/receive/{}/{}",
+ Settings::get().get_protocol_and_hostname(),
+ kind.to_string().to_lowercase(),
+ Uuid::new_v4()
+ );
+ Url::parse(&id)
+}
use crate::{
+ activities::send::generate_activity_id,
activity_queue::send_to_community,
- check_actor_domain,
- create_apub_response,
- create_apub_tombstone_response,
- create_tombstone,
- extensions::page_extension::PageExtension,
- fetcher::{get_or_fetch_and_upsert_community, get_or_fetch_and_upsert_user},
- generate_activity_id,
ActorType,
ApubLikeableType,
ApubObjectType,
- FromApub,
- PageExt,
ToApub,
};
use activitystreams::{
Undo,
Update,
},
- object::{kind::PageType, Image, Page, Tombstone},
prelude::*,
public,
};
-use activitystreams_ext::Ext1;
-use actix_web::{body::Body, web, HttpResponse};
-use anyhow::Context;
-use diesel::result::Error::NotFound;
-use lemmy_db::{
- community::Community,
- post::{Post, PostForm},
- user::User_,
- Crud,
- DbPool,
-};
+use lemmy_db::{community::Community, post::Post, user::User_, Crud};
use lemmy_structs::blocking;
-use lemmy_utils::{
- location_info,
- request::fetch_iframely_and_pictrs_data,
- utils::{check_slurs, convert_datetime, remove_slurs},
- LemmyError,
-};
+use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
-use serde::Deserialize;
-use url::Url;
-
-#[derive(Deserialize)]
-pub struct PostQuery {
- post_id: String,
-}
-
-/// Return the post json over HTTP.
-pub async fn get_apub_post(
- info: web::Path<PostQuery>,
- context: web::Data<LemmyContext>,
-) -> Result<HttpResponse<Body>, LemmyError> {
- let id = info.post_id.parse::<i32>()?;
- let post = blocking(context.pool(), move |conn| Post::read(conn, id)).await??;
- if !post.local {
- return Err(NotFound.into());
- }
-
- if !post.deleted {
- Ok(create_apub_response(&post.to_apub(context.pool()).await?))
- } else {
- Ok(create_apub_tombstone_response(&post.to_tombstone()?))
- }
-}
-
-#[async_trait::async_trait(?Send)]
-impl ToApub for Post {
- type Response = PageExt;
-
- // Turn a Lemmy post into an ActivityPub page that can be sent out over the network.
- async fn to_apub(&self, pool: &DbPool) -> Result<PageExt, LemmyError> {
- let mut page = Page::new();
-
- let creator_id = self.creator_id;
- let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??;
-
- let community_id = self.community_id;
- let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
-
- page
- // Not needed when the Post is embedded in a collection (like for community outbox)
- // TODO: need to set proper context defining sensitive/commentsEnabled fields
- // https://git.asonix.dog/Aardwolf/activitystreams/issues/5
- .set_context(activitystreams::context())
- .set_id(self.ap_id.parse::<Url>()?)
- // Use summary field to be consistent with mastodon content warning.
- // https://mastodon.xyz/@Louisa/103987265222901387.json
- .set_summary(self.name.to_owned())
- .set_published(convert_datetime(self.published))
- .set_to(community.actor_id)
- .set_attributed_to(creator.actor_id);
-
- if let Some(body) = &self.body {
- page.set_content(body.to_owned());
- }
-
- // TODO: hacky code because we get self.url == Some("")
- // https://github.com/LemmyNet/lemmy/issues/602
- let url = self.url.as_ref().filter(|u| !u.is_empty());
- if let Some(u) = url {
- page.set_url(u.to_owned());
- }
-
- if let Some(thumbnail_url) = &self.thumbnail_url {
- let mut image = Image::new();
- image.set_url(thumbnail_url.to_string());
- page.set_image(image.into_any_base()?);
- }
-
- if let Some(u) = self.updated {
- page.set_updated(convert_datetime(u));
- }
-
- let ext = PageExtension {
- comments_enabled: !self.locked,
- sensitive: self.nsfw,
- stickied: self.stickied,
- };
- Ok(Ext1::new(page, ext))
- }
-
- fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
- create_tombstone(self.deleted, &self.ap_id, self.updated, PageType::Page)
- }
-}
-
-#[async_trait::async_trait(?Send)]
-impl FromApub for PostForm {
- type ApubType = PageExt;
-
- /// Parse an ActivityPub page received from another instance into a Lemmy post.
- async fn from_apub(
- page: &PageExt,
- context: &LemmyContext,
- expected_domain: Option<Url>,
- ) -> Result<PostForm, LemmyError> {
- let ext = &page.ext_one;
- let creator_actor_id = page
- .inner
- .attributed_to()
- .as_ref()
- .context(location_info!())?
- .as_single_xsd_any_uri()
- .context(location_info!())?;
-
- let creator = get_or_fetch_and_upsert_user(creator_actor_id, context).await?;
-
- let community_actor_id = page
- .inner
- .to()
- .as_ref()
- .context(location_info!())?
- .as_single_xsd_any_uri()
- .context(location_info!())?;
-
- let community = get_or_fetch_and_upsert_community(community_actor_id, context).await?;
-
- let thumbnail_url = match &page.inner.image() {
- Some(any_image) => Image::from_any_base(
- any_image
- .to_owned()
- .as_one()
- .context(location_info!())?
- .to_owned(),
- )?
- .context(location_info!())?
- .url()
- .context(location_info!())?
- .as_single_xsd_any_uri()
- .map(|u| u.to_string()),
- None => None,
- };
- let url = page
- .inner
- .url()
- .map(|u| u.as_single_xsd_any_uri())
- .flatten()
- .map(|s| s.to_string());
-
- let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
- if let Some(url) = &url {
- fetch_iframely_and_pictrs_data(context.client(), Some(url.to_owned())).await
- } else {
- (None, None, None, thumbnail_url)
- };
-
- let name = page
- .inner
- .summary()
- .as_ref()
- .context(location_info!())?
- .as_single_xsd_string()
- .context(location_info!())?
- .to_string();
- let body = page
- .inner
- .content()
- .as_ref()
- .map(|c| c.as_single_xsd_string())
- .flatten()
- .map(|s| s.to_string());
- check_slurs(&name)?;
- let body_slurs_removed = body.map(|b| remove_slurs(&b));
- Ok(PostForm {
- name,
- url,
- body: body_slurs_removed,
- creator_id: creator.id,
- community_id: community.id,
- removed: None,
- locked: Some(!ext.comments_enabled),
- published: page
- .inner
- .published()
- .as_ref()
- .map(|u| u.to_owned().naive_local()),
- updated: page
- .inner
- .updated()
- .as_ref()
- .map(|u| u.to_owned().naive_local()),
- deleted: None,
- nsfw: ext.sensitive,
- stickied: Some(ext.stickied),
- embed_title: iframely_title,
- embed_description: iframely_description,
- embed_html: iframely_html,
- thumbnail_url: pictrs_thumbnail,
- ap_id: Some(check_actor_domain(page, expected_domain)?),
- local: false,
- })
- }
-}
#[async_trait::async_trait(?Send)]
impl ApubObjectType for Post {
use crate::{
+ activities::send::generate_activity_id,
activity_queue::send_activity_single_dest,
- check_actor_domain,
- check_is_apub_id_valid,
- create_tombstone,
- fetcher::get_or_fetch_and_upsert_user,
- generate_activity_id,
ActorType,
ApubObjectType,
- FromApub,
ToApub,
};
use activitystreams::{
Undo,
Update,
},
- object::{kind::NoteType, Note, Tombstone},
prelude::*,
};
-use anyhow::Context;
-use lemmy_db::{
- private_message::{PrivateMessage, PrivateMessageForm},
- user::User_,
- Crud,
- DbPool,
-};
+use lemmy_db::{private_message::PrivateMessage, user::User_, Crud};
use lemmy_structs::blocking;
-use lemmy_utils::{location_info, utils::convert_datetime, LemmyError};
+use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
-use url::Url;
-
-#[async_trait::async_trait(?Send)]
-impl ToApub for PrivateMessage {
- type Response = Note;
-
- async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> {
- let mut private_message = Note::new();
-
- let creator_id = self.creator_id;
- let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??;
-
- let recipient_id = self.recipient_id;
- let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??;
-
- private_message
- .set_context(activitystreams::context())
- .set_id(Url::parse(&self.ap_id.to_owned())?)
- .set_published(convert_datetime(self.published))
- .set_content(self.content.to_owned())
- .set_to(recipient.actor_id)
- .set_attributed_to(creator.actor_id);
-
- if let Some(u) = self.updated {
- private_message.set_updated(convert_datetime(u));
- }
-
- Ok(private_message)
- }
-
- fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
- create_tombstone(self.deleted, &self.ap_id, self.updated, NoteType::Note)
- }
-}
-
-#[async_trait::async_trait(?Send)]
-impl FromApub for PrivateMessageForm {
- type ApubType = Note;
-
- /// Parse an ActivityPub note received from another instance into a Lemmy Private message
- async fn from_apub(
- note: &Note,
- context: &LemmyContext,
- expected_domain: Option<Url>,
- ) -> Result<PrivateMessageForm, LemmyError> {
- let creator_actor_id = note
- .attributed_to()
- .context(location_info!())?
- .clone()
- .single_xsd_any_uri()
- .context(location_info!())?;
-
- let creator = get_or_fetch_and_upsert_user(&creator_actor_id, context).await?;
- let recipient_actor_id = note
- .to()
- .context(location_info!())?
- .clone()
- .single_xsd_any_uri()
- .context(location_info!())?;
- let recipient = get_or_fetch_and_upsert_user(&recipient_actor_id, context).await?;
- let ap_id = note.id_unchecked().context(location_info!())?.to_string();
- check_is_apub_id_valid(&Url::parse(&ap_id)?)?;
-
- Ok(PrivateMessageForm {
- creator_id: creator.id,
- recipient_id: recipient.id,
- content: note
- .content()
- .context(location_info!())?
- .as_single_xsd_string()
- .context(location_info!())?
- .to_string(),
- published: note.published().map(|u| u.to_owned().naive_local()),
- updated: note.updated().map(|u| u.to_owned().naive_local()),
- deleted: None,
- read: None,
- ap_id: Some(check_actor_domain(note, expected_domain)?),
- local: false,
- })
- }
-}
#[async_trait::async_trait(?Send)]
impl ApubObjectType for PrivateMessage {
--- /dev/null
+use crate::{
+ activities::send::generate_activity_id,
+ activity_queue::send_activity_single_dest,
+ fetcher::get_or_fetch_and_upsert_actor,
+ ActorType,
+};
+use activitystreams::{
+ activity::{
+ kind::{FollowType, UndoType},
+ Follow,
+ Undo,
+ },
+ base::{AnyBase, BaseExt, ExtendsExt},
+};
+use lemmy_db::{user::User_, DbPool};
+use lemmy_utils::LemmyError;
+use lemmy_websocket::LemmyContext;
+use url::Url;
+
+#[async_trait::async_trait(?Send)]
+impl ActorType for User_ {
+ fn actor_id_str(&self) -> String {
+ self.actor_id.to_owned()
+ }
+
+ fn public_key(&self) -> Option<String> {
+ self.public_key.to_owned()
+ }
+
+ fn private_key(&self) -> Option<String> {
+ self.private_key.to_owned()
+ }
+
+ fn user_id(&self) -> i32 {
+ self.id
+ }
+
+ /// As a given local user, send out a follow request to a remote community.
+ async fn send_follow(
+ &self,
+ follow_actor_id: &Url,
+ context: &LemmyContext,
+ ) -> Result<(), LemmyError> {
+ let mut follow = Follow::new(self.actor_id.to_owned(), follow_actor_id.as_str());
+ follow
+ .set_context(activitystreams::context())
+ .set_id(generate_activity_id(FollowType::Follow)?);
+ let follow_actor = get_or_fetch_and_upsert_actor(follow_actor_id, context).await?;
+ let to = follow_actor.get_inbox_url()?;
+
+ send_activity_single_dest(follow, self, to, context).await?;
+ Ok(())
+ }
+
+ async fn send_unfollow(
+ &self,
+ follow_actor_id: &Url,
+ context: &LemmyContext,
+ ) -> Result<(), LemmyError> {
+ let mut follow = Follow::new(self.actor_id.to_owned(), follow_actor_id.as_str());
+ follow
+ .set_context(activitystreams::context())
+ .set_id(generate_activity_id(FollowType::Follow)?);
+ let follow_actor = get_or_fetch_and_upsert_actor(follow_actor_id, context).await?;
+
+ let to = follow_actor.get_inbox_url()?;
+
+ // Undo that fake activity
+ let mut undo = Undo::new(Url::parse(&self.actor_id)?, follow.into_any_base()?);
+ undo
+ .set_context(activitystreams::context())
+ .set_id(generate_activity_id(UndoType::Undo)?);
+
+ send_activity_single_dest(undo, self, to, context).await?;
+ Ok(())
+ }
+
+ async fn send_accept_follow(
+ &self,
+ _follow: Follow,
+ _context: &LemmyContext,
+ ) -> Result<(), LemmyError> {
+ unimplemented!()
+ }
+
+ async fn send_delete(&self, _creator: &User_, _context: &LemmyContext) -> Result<(), LemmyError> {
+ unimplemented!()
+ }
+
+ async fn send_undo_delete(
+ &self,
+ _creator: &User_,
+ _context: &LemmyContext,
+ ) -> Result<(), LemmyError> {
+ unimplemented!()
+ }
+
+ async fn send_remove(&self, _creator: &User_, _context: &LemmyContext) -> Result<(), LemmyError> {
+ unimplemented!()
+ }
+
+ async fn send_undo_remove(
+ &self,
+ _creator: &User_,
+ _context: &LemmyContext,
+ ) -> Result<(), LemmyError> {
+ unimplemented!()
+ }
+
+ async fn send_announce(
+ &self,
+ _activity: AnyBase,
+ _sender: &User_,
+ _context: &LemmyContext,
+ ) -> Result<(), LemmyError> {
+ unimplemented!()
+ }
+
+ async fn get_follower_inboxes(&self, _pool: &DbPool) -> Result<Vec<Url>, LemmyError> {
+ unimplemented!()
+ }
+}
use crate::{
check_is_apub_id_valid,
- community::do_announce,
extensions::signatures::sign_and_send,
insert_activity,
ActorType,
{
// if this is a local community, we need to do an announce from the community instead
if community.local {
- do_announce(activity.into_any_base()?, &community, creator, context).await?;
+ community
+ .send_announce(activity.into_any_base()?, creator, context)
+ .await?;
} else {
let inbox = community.get_shared_inbox_url()?;
check_is_apub_id_valid(&inbox)?;
+++ /dev/null
-use crate::{
- activity_queue::{send_activity_single_dest, send_to_community_followers},
- check_actor_domain,
- check_is_apub_id_valid,
- create_apub_response,
- create_apub_tombstone_response,
- create_tombstone,
- extensions::group_extensions::GroupExtension,
- fetcher::{get_or_fetch_and_upsert_actor, get_or_fetch_and_upsert_user},
- generate_activity_id,
- ActorType,
- FromApub,
- GroupExt,
- ToApub,
-};
-use activitystreams::{
- activity::{
- kind::{AcceptType, AnnounceType, DeleteType, LikeType, RemoveType, UndoType},
- Accept,
- Announce,
- Delete,
- Follow,
- Remove,
- Undo,
- },
- actor::{kind::GroupType, ApActor, Endpoints, Group},
- base::{AnyBase, BaseExt},
- collection::{OrderedCollection, UnorderedCollection},
- object::{Image, Tombstone},
- prelude::*,
- public,
-};
-use activitystreams_ext::Ext2;
-use actix_web::{body::Body, web, HttpResponse};
-use anyhow::Context;
-use itertools::Itertools;
-use lemmy_db::{
- community::{Community, CommunityForm},
- community_view::{CommunityFollowerView, CommunityModeratorView},
- naive_now,
- post::Post,
- user::User_,
- DbPool,
-};
-use lemmy_structs::blocking;
-use lemmy_utils::{
- location_info,
- settings::Settings,
- utils::{check_slurs, check_slurs_opt, convert_datetime},
- LemmyError,
-};
-use lemmy_websocket::LemmyContext;
-use serde::Deserialize;
-use url::Url;
-
-#[derive(Deserialize)]
-pub struct CommunityQuery {
- community_name: String,
-}
-
-#[async_trait::async_trait(?Send)]
-impl ToApub for Community {
- type Response = GroupExt;
-
- // Turn a Lemmy Community into an ActivityPub group that can be sent out over the network.
- 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??;
- let moderators: Vec<String> = moderators.into_iter().map(|m| m.user_actor_id).collect();
-
- let mut group = Group::new();
- group
- .set_context(activitystreams::context())
- .set_id(Url::parse(&self.actor_id)?)
- .set_name(self.name.to_owned())
- .set_published(convert_datetime(self.published))
- .set_many_attributed_tos(moderators);
-
- if let Some(u) = self.updated.to_owned() {
- group.set_updated(convert_datetime(u));
- }
- if let Some(d) = self.description.to_owned() {
- // TODO: this should be html, also add source field with raw markdown
- // -> same for post.content and others
- group.set_content(d);
- }
-
- if let Some(icon) = &self.icon {
- let mut image = Image::new();
- image.set_url(icon.to_owned());
- group.set_icon(image.into_any_base()?);
- }
-
- if let Some(banner_url) = &self.banner {
- let mut image = Image::new();
- image.set_url(banner_url.to_owned());
- group.set_image(image.into_any_base()?);
- }
-
- let mut ap_actor = ApActor::new(self.get_inbox_url()?, group);
- ap_actor
- .set_preferred_username(self.title.to_owned())
- .set_outbox(self.get_outbox_url()?)
- .set_followers(self.get_followers_url()?)
- .set_endpoints(Endpoints {
- shared_inbox: Some(self.get_shared_inbox_url()?),
- ..Default::default()
- });
-
- let nsfw = self.nsfw;
- let category_id = self.category_id;
- let group_extension = blocking(pool, move |conn| {
- GroupExtension::new(conn, category_id, nsfw)
- })
- .await??;
-
- Ok(Ext2::new(
- ap_actor,
- group_extension,
- self.get_public_key_ext()?,
- ))
- }
-
- fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
- create_tombstone(self.deleted, &self.actor_id, self.updated, GroupType::Group)
- }
-}
-
-#[async_trait::async_trait(?Send)]
-impl ActorType for Community {
- fn actor_id_str(&self) -> String {
- self.actor_id.to_owned()
- }
-
- fn public_key(&self) -> Option<String> {
- self.public_key.to_owned()
- }
- fn private_key(&self) -> Option<String> {
- self.private_key.to_owned()
- }
-
- /// As a local community, accept the follow request from a remote user.
- async fn send_accept_follow(
- &self,
- follow: Follow,
- context: &LemmyContext,
- ) -> Result<(), LemmyError> {
- let actor_uri = follow
- .actor()?
- .as_single_xsd_any_uri()
- .context(location_info!())?;
- let actor = get_or_fetch_and_upsert_actor(actor_uri, context).await?;
-
- let mut accept = Accept::new(self.actor_id.to_owned(), follow.into_any_base()?);
- let to = actor.get_inbox_url()?;
- accept
- .set_context(activitystreams::context())
- .set_id(generate_activity_id(AcceptType::Accept)?)
- .set_to(to.clone());
-
- send_activity_single_dest(accept, self, to, context).await?;
- Ok(())
- }
-
- async fn send_delete(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
- let group = self.to_apub(context.pool()).await?;
-
- let mut delete = Delete::new(creator.actor_id.to_owned(), group.into_any_base()?);
- delete
- .set_context(activitystreams::context())
- .set_id(generate_activity_id(DeleteType::Delete)?)
- .set_to(public())
- .set_many_ccs(vec![self.get_followers_url()?]);
-
- send_to_community_followers(delete, self, context, None).await?;
- Ok(())
- }
-
- async fn send_undo_delete(
- &self,
- creator: &User_,
- context: &LemmyContext,
- ) -> Result<(), LemmyError> {
- let group = self.to_apub(context.pool()).await?;
-
- let mut delete = Delete::new(creator.actor_id.to_owned(), group.into_any_base()?);
- delete
- .set_context(activitystreams::context())
- .set_id(generate_activity_id(DeleteType::Delete)?)
- .set_to(public())
- .set_many_ccs(vec![self.get_followers_url()?]);
-
- let mut undo = Undo::new(creator.actor_id.to_owned(), delete.into_any_base()?);
- undo
- .set_context(activitystreams::context())
- .set_id(generate_activity_id(UndoType::Undo)?)
- .set_to(public())
- .set_many_ccs(vec![self.get_followers_url()?]);
-
- send_to_community_followers(undo, self, context, None).await?;
- Ok(())
- }
-
- async fn send_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
- let group = self.to_apub(context.pool()).await?;
-
- let mut remove = Remove::new(mod_.actor_id.to_owned(), group.into_any_base()?);
- remove
- .set_context(activitystreams::context())
- .set_id(generate_activity_id(RemoveType::Remove)?)
- .set_to(public())
- .set_many_ccs(vec![self.get_followers_url()?]);
-
- send_to_community_followers(remove, self, context, None).await?;
- Ok(())
- }
-
- async fn send_undo_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
- let group = self.to_apub(context.pool()).await?;
-
- let mut remove = Remove::new(mod_.actor_id.to_owned(), group.into_any_base()?);
- remove
- .set_context(activitystreams::context())
- .set_id(generate_activity_id(RemoveType::Remove)?)
- .set_to(public())
- .set_many_ccs(vec![self.get_followers_url()?]);
-
- // Undo that fake activity
- let mut undo = Undo::new(mod_.actor_id.to_owned(), remove.into_any_base()?);
- undo
- .set_context(activitystreams::context())
- .set_id(generate_activity_id(LikeType::Like)?)
- .set_to(public())
- .set_many_ccs(vec![self.get_followers_url()?]);
-
- send_to_community_followers(undo, self, context, None).await?;
- Ok(())
- }
-
- /// For a given community, returns the inboxes of all followers.
- ///
- /// TODO: this function is very badly implemented, we should just store shared_inbox_url in
- /// CommunityFollowerView
- async fn get_follower_inboxes(&self, pool: &DbPool) -> Result<Vec<Url>, LemmyError> {
- let id = self.id;
-
- let inboxes = blocking(pool, move |conn| {
- CommunityFollowerView::for_community(conn, id)
- })
- .await??;
- let inboxes = inboxes
- .into_iter()
- .filter(|i| !i.user_local)
- .map(|u| -> Result<Url, LemmyError> {
- let url = Url::parse(&u.user_actor_id)?;
- let domain = url.domain().context(location_info!())?;
- let port = if let Some(port) = url.port() {
- format!(":{}", port)
- } else {
- "".to_string()
- };
- Ok(Url::parse(&format!(
- "{}://{}{}/inbox",
- Settings::get().get_protocol_string(),
- domain,
- port,
- ))?)
- })
- .filter_map(Result::ok)
- // Don't send to blocked instances
- .filter(|inbox| check_is_apub_id_valid(inbox).is_ok())
- .unique()
- .collect();
-
- Ok(inboxes)
- }
-
- async fn send_follow(
- &self,
- _follow_actor_id: &Url,
- _context: &LemmyContext,
- ) -> Result<(), LemmyError> {
- unimplemented!()
- }
-
- async fn send_unfollow(
- &self,
- _follow_actor_id: &Url,
- _context: &LemmyContext,
- ) -> Result<(), LemmyError> {
- unimplemented!()
- }
-
- fn user_id(&self) -> i32 {
- self.creator_id
- }
-}
-
-#[async_trait::async_trait(?Send)]
-impl FromApub for CommunityForm {
- type ApubType = GroupExt;
-
- /// Parse an ActivityPub group received from another instance into a Lemmy community.
- async fn from_apub(
- group: &GroupExt,
- context: &LemmyContext,
- expected_domain: Option<Url>,
- ) -> Result<Self, LemmyError> {
- let creator_and_moderator_uris = group.inner.attributed_to().context(location_info!())?;
- let creator_uri = creator_and_moderator_uris
- .as_many()
- .context(location_info!())?
- .iter()
- .next()
- .context(location_info!())?
- .as_xsd_any_uri()
- .context(location_info!())?;
-
- let creator = get_or_fetch_and_upsert_user(creator_uri, context).await?;
- let name = group
- .inner
- .name()
- .context(location_info!())?
- .as_one()
- .context(location_info!())?
- .as_xsd_string()
- .context(location_info!())?
- .to_string();
- let title = group
- .inner
- .preferred_username()
- .context(location_info!())?
- .to_string();
- // TODO: should be parsed as html and tags like <script> removed (or use markdown source)
- // -> same for post.content etc
- let description = group
- .inner
- .content()
- .map(|s| s.as_single_xsd_string())
- .flatten()
- .map(|s| s.to_string());
- check_slurs(&name)?;
- check_slurs(&title)?;
- check_slurs_opt(&description)?;
-
- let icon = match group.icon() {
- Some(any_image) => Some(
- Image::from_any_base(any_image.as_one().context(location_info!())?.clone())
- .context(location_info!())?
- .context(location_info!())?
- .url()
- .context(location_info!())?
- .as_single_xsd_any_uri()
- .map(|u| u.to_string()),
- ),
- None => None,
- };
-
- let banner = match group.image() {
- Some(any_image) => Some(
- Image::from_any_base(any_image.as_one().context(location_info!())?.clone())
- .context(location_info!())?
- .context(location_info!())?
- .url()
- .context(location_info!())?
- .as_single_xsd_any_uri()
- .map(|u| u.to_string()),
- ),
- None => None,
- };
-
- Ok(CommunityForm {
- name,
- title,
- description,
- category_id: group.ext_one.category.identifier.parse::<i32>()?,
- creator_id: creator.id,
- removed: None,
- published: group.inner.published().map(|u| u.to_owned().naive_local()),
- updated: group.inner.updated().map(|u| u.to_owned().naive_local()),
- deleted: None,
- nsfw: group.ext_one.sensitive,
- actor_id: Some(check_actor_domain(group, expected_domain)?),
- local: false,
- private_key: None,
- public_key: Some(group.ext_two.to_owned().public_key.public_key_pem),
- last_refreshed_at: Some(naive_now()),
- icon,
- banner,
- })
- }
-}
-
-/// Return the community json over HTTP.
-pub async fn get_apub_community_http(
- 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??;
-
- if !community.deleted {
- let apub = community.to_apub(context.pool()).await?;
-
- Ok(create_apub_response(&apub))
- } else {
- Ok(create_apub_tombstone_response(&community.to_tombstone()?))
- }
-}
-
-/// Returns an empty followers collection, only populating the size (for privacy).
-pub async fn get_apub_community_followers(
- 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??;
-
- let community_id = community.id;
- let community_followers = blocking(context.pool(), move |conn| {
- CommunityFollowerView::for_community(&conn, community_id)
- })
- .await??;
-
- let mut collection = UnorderedCollection::new();
- collection
- .set_context(activitystreams::context())
- .set_id(community.get_followers_url()?)
- .set_total_items(community_followers.len() as u64);
- Ok(create_apub_response(&collection))
-}
-
-pub async fn get_apub_community_outbox(
- 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??;
-
- let community_id = community.id;
- let posts = blocking(context.pool(), move |conn| {
- Post::list_for_community(conn, community_id)
- })
- .await??;
-
- let mut pages: Vec<AnyBase> = vec![];
- for p in posts {
- pages.push(p.to_apub(context.pool()).await?.into_any_base()?);
- }
-
- let len = pages.len();
- let mut collection = OrderedCollection::new();
- collection
- .set_many_items(pages)
- .set_context(activitystreams::context())
- .set_id(community.get_outbox_url()?)
- .set_total_items(len as u64);
- Ok(create_apub_response(&collection))
-}
-
-pub async fn do_announce(
- activity: AnyBase,
- community: &Community,
- sender: &User_,
- context: &LemmyContext,
-) -> Result<(), LemmyError> {
- let mut announce = Announce::new(community.actor_id.to_owned(), activity);
- announce
- .set_context(activitystreams::context())
- .set_id(generate_activity_id(AnnounceType::Announce)?)
- .set_to(public())
- .set_many_ccs(vec![community.get_followers_url()?]);
-
- send_to_community_followers(
- announce,
- community,
- context,
- Some(sender.get_shared_inbox_url()?),
- )
- .await?;
-
- Ok(())
-}
/// Fetch any type of ActivityPub object, handling things like HTTP headers, deserialisation,
/// timeouts etc.
-pub async fn fetch_remote_object<Response>(
- client: &Client,
- url: &Url,
-) -> Result<Response, LemmyError>
+async fn fetch_remote_object<Response>(client: &Client, url: &Url) -> Result<Response, LemmyError>
where
Response: for<'de> Deserialize<'de>,
{
Ok(response)
}
-pub async fn get_or_fetch_and_upsert_actor(
+pub(crate) async fn get_or_fetch_and_upsert_actor(
apub_id: &Url,
context: &LemmyContext,
) -> Result<Box<dyn ActorType>, LemmyError> {
}
/// Check if a remote user exists, create if not found, if its too old update it.Fetch a user, insert/update it in the database and return the user.
-pub async fn get_or_fetch_and_upsert_user(
+pub(crate) async fn get_or_fetch_and_upsert_user(
apub_id: &Url,
context: &LemmyContext,
) -> Result<User_, LemmyError> {
}
/// Check if a remote community exists, create if not found, if its too old update it.Fetch a community, insert/update it in the database and return the community.
-pub async fn get_or_fetch_and_upsert_community(
+pub(crate) async fn get_or_fetch_and_upsert_community(
apub_id: &Url,
context: &LemmyContext,
) -> Result<Community, LemmyError> {
Ok(community)
}
-pub async fn get_or_fetch_and_insert_post(
+pub(crate) async fn get_or_fetch_and_insert_post(
post_ap_id: &Url,
context: &LemmyContext,
) -> Result<Post, LemmyError> {
}
}
-pub async fn get_or_fetch_and_insert_comment(
+pub(crate) async fn get_or_fetch_and_insert_comment(
comment_ap_id: &Url,
context: &LemmyContext,
) -> Result<Comment, LemmyError> {
--- /dev/null
+use crate::{
+ http::{create_apub_response, create_apub_tombstone_response},
+ ToApub,
+};
+use actix_web::{body::Body, web, web::Path, HttpResponse};
+use diesel::result::Error::NotFound;
+use lemmy_db::{comment::Comment, Crud};
+use lemmy_structs::blocking;
+use lemmy_utils::LemmyError;
+use lemmy_websocket::LemmyContext;
+use serde::Deserialize;
+
+#[derive(Deserialize)]
+pub struct CommentQuery {
+ comment_id: String,
+}
+
+/// Return the post json over HTTP.
+pub async fn get_apub_comment(
+ info: Path<CommentQuery>,
+ context: web::Data<LemmyContext>,
+) -> Result<HttpResponse<Body>, LemmyError> {
+ let id = info.comment_id.parse::<i32>()?;
+ let comment = blocking(context.pool(), move |conn| Comment::read(conn, id)).await??;
+ if !comment.local {
+ return Err(NotFound.into());
+ }
+
+ if !comment.deleted {
+ Ok(create_apub_response(
+ &comment.to_apub(context.pool()).await?,
+ ))
+ } else {
+ Ok(create_apub_tombstone_response(&comment.to_tombstone()?))
+ }
+}
--- /dev/null
+use crate::{
+ http::{create_apub_response, create_apub_tombstone_response},
+ ActorType,
+ ToApub,
+};
+use activitystreams::{
+ base::{AnyBase, BaseExt, ExtendsExt},
+ collection::{CollectionExt, OrderedCollection, UnorderedCollection},
+};
+use actix_web::{body::Body, web, HttpResponse};
+use lemmy_db::{community::Community, community_view::CommunityFollowerView, post::Post};
+use lemmy_structs::blocking;
+use lemmy_utils::LemmyError;
+use lemmy_websocket::LemmyContext;
+use serde::Deserialize;
+
+#[derive(Deserialize)]
+pub struct CommunityQuery {
+ community_name: String,
+}
+
+/// Return the community json over HTTP.
+pub async fn get_apub_community_http(
+ 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??;
+
+ if !community.deleted {
+ let apub = community.to_apub(context.pool()).await?;
+
+ Ok(create_apub_response(&apub))
+ } else {
+ Ok(create_apub_tombstone_response(&community.to_tombstone()?))
+ }
+}
+
+/// Returns an empty followers collection, only populating the size (for privacy).
+pub async fn get_apub_community_followers(
+ 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??;
+
+ let community_id = community.id;
+ let community_followers = blocking(context.pool(), move |conn| {
+ CommunityFollowerView::for_community(&conn, community_id)
+ })
+ .await??;
+
+ let mut collection = UnorderedCollection::new();
+ collection
+ .set_context(activitystreams::context())
+ .set_id(community.get_followers_url()?)
+ .set_total_items(community_followers.len() as u64);
+ Ok(create_apub_response(&collection))
+}
+
+pub async fn get_apub_community_outbox(
+ 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??;
+
+ let community_id = community.id;
+ let posts = blocking(context.pool(), move |conn| {
+ Post::list_for_community(conn, community_id)
+ })
+ .await??;
+
+ let mut pages: Vec<AnyBase> = vec![];
+ for p in posts {
+ pages.push(p.to_apub(context.pool()).await?.into_any_base()?);
+ }
+
+ let len = pages.len();
+ let mut collection = OrderedCollection::new();
+ collection
+ .set_many_items(pages)
+ .set_context(activitystreams::context())
+ .set_id(community.get_outbox_url()?)
+ .set_total_items(len as u64);
+ Ok(create_apub_response(&collection))
+}
--- /dev/null
+use crate::APUB_JSON_CONTENT_TYPE;
+use actix_web::{body::Body, HttpResponse};
+use serde::Serialize;
+
+pub mod comment;
+pub mod community;
+pub mod post;
+pub mod user;
+
+/// Convert the data to json and turn it into an HTTP Response with the correct ActivityPub
+/// headers.
+fn create_apub_response<T>(data: &T) -> HttpResponse<Body>
+where
+ T: Serialize,
+{
+ HttpResponse::Ok()
+ .content_type(APUB_JSON_CONTENT_TYPE)
+ .json(data)
+}
+
+fn create_apub_tombstone_response<T>(data: &T) -> HttpResponse<Body>
+where
+ T: Serialize,
+{
+ HttpResponse::Gone()
+ .content_type(APUB_JSON_CONTENT_TYPE)
+ .json(data)
+}
--- /dev/null
+use crate::{
+ http::{create_apub_response, create_apub_tombstone_response},
+ ToApub,
+};
+use actix_web::{body::Body, web, HttpResponse};
+use diesel::result::Error::NotFound;
+use lemmy_db::post::Post;
+use lemmy_structs::blocking;
+use lemmy_utils::LemmyError;
+use lemmy_websocket::LemmyContext;
+use serde::Deserialize;
+
+#[derive(Deserialize)]
+pub struct PostQuery {
+ post_id: String,
+}
+
+/// Return the post json over HTTP.
+pub async fn get_apub_post(
+ info: web::Path<PostQuery>,
+ context: web::Data<LemmyContext>,
+) -> Result<HttpResponse<Body>, LemmyError> {
+ let id = info.post_id.parse::<i32>()?;
+ let post = blocking(context.pool(), move |conn| Post::read(conn, id)).await??;
+ if !post.local {
+ return Err(NotFound.into());
+ }
+
+ if !post.deleted {
+ Ok(create_apub_response(&post.to_apub(context.pool()).await?))
+ } else {
+ Ok(create_apub_tombstone_response(&post.to_tombstone()?))
+ }
+}
--- /dev/null
+use crate::{http::create_apub_response, ToApub};
+use actix_web::{body::Body, web, HttpResponse};
+use lemmy_db::user::User_;
+use lemmy_structs::blocking;
+use lemmy_utils::LemmyError;
+use lemmy_websocket::LemmyContext;
+use serde::Deserialize;
+
+#[derive(Deserialize)]
+pub struct UserQuery {
+ user_name: String,
+}
+
+/// Return the user json over HTTP.
+pub async fn get_apub_user_http(
+ info: web::Path<UserQuery>,
+ context: web::Data<LemmyContext>,
+) -> Result<HttpResponse<Body>, LemmyError> {
+ let user_name = info.into_inner().user_name;
+ let user = blocking(context.pool(), move |conn| {
+ User_::find_by_email_or_username(conn, &user_name)
+ })
+ .await??;
+ let u = user.to_apub(context.pool()).await?;
+ Ok(create_apub_response(&u))
+}
+++ /dev/null
-pub mod announce;
-pub mod create;
-pub mod delete;
-pub mod dislike;
-pub mod like;
-pub mod remove;
-pub mod undo;
-pub mod update;
+++ /dev/null
-use crate::{
- fetcher::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post},
- inbox::shared_inbox::{
- announce_if_community_is_local,
- get_user_from_activity,
- receive_unhandled_activity,
- },
- ActorType,
- FromApub,
- GroupExt,
- PageExt,
-};
-use activitystreams::{
- activity::*,
- base::{AnyBase, AsBase},
- object::Note,
- prelude::*,
-};
-use actix_web::HttpResponse;
-use anyhow::{anyhow, Context};
-use lemmy_db::{
- comment::{Comment, CommentForm, CommentLike},
- comment_view::CommentView,
- community::{Community, CommunityForm},
- community_view::CommunityView,
- naive_now,
- post::{Post, PostForm, PostLike},
- post_view::PostView,
- Crud,
- Likeable,
-};
-use lemmy_structs::{
- blocking,
- comment::CommentResponse,
- community::CommunityResponse,
- post::PostResponse,
-};
-use lemmy_utils::{location_info, LemmyError};
-use lemmy_websocket::{
- messages::{SendComment, SendCommunityRoomMessage, SendPost},
- LemmyContext,
- UserOperation,
-};
-
-pub async fn receive_undo(
- activity: AnyBase,
- context: &LemmyContext,
-) -> Result<HttpResponse, LemmyError> {
- let undo = Undo::from_any_base(activity)?.context(location_info!())?;
- match undo.object().as_single_kind_str() {
- Some("Delete") => receive_undo_delete(undo, context).await,
- Some("Remove") => receive_undo_remove(undo, context).await,
- Some("Like") => receive_undo_like(undo, context).await,
- Some("Dislike") => receive_undo_dislike(undo, context).await,
- _ => receive_unhandled_activity(undo),
- }
-}
-
-fn check_is_undo_valid<T, A>(outer_activity: &Undo, inner_activity: &T) -> Result<(), LemmyError>
-where
- T: AsBase<A> + ActorAndObjectRef,
-{
- let outer_actor = outer_activity.actor()?;
- let outer_actor_uri = outer_actor
- .as_single_xsd_any_uri()
- .context(location_info!())?;
-
- let inner_actor = inner_activity.actor()?;
- let inner_actor_uri = inner_actor
- .as_single_xsd_any_uri()
- .context(location_info!())?;
-
- if outer_actor_uri.domain() != inner_actor_uri.domain() {
- Err(anyhow!("Cant undo activities from a different instance").into())
- } else {
- Ok(())
- }
-}
-
-async fn receive_undo_delete(
- undo: Undo,
- context: &LemmyContext,
-) -> Result<HttpResponse, LemmyError> {
- let delete = Delete::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
- .context(location_info!())?;
- check_is_undo_valid(&undo, &delete)?;
- let type_ = delete
- .object()
- .as_single_kind_str()
- .context(location_info!())?;
- match type_ {
- "Note" => receive_undo_delete_comment(undo, &delete, context).await,
- "Page" => receive_undo_delete_post(undo, &delete, context).await,
- "Group" => receive_undo_delete_community(undo, &delete, context).await,
- d => Err(anyhow!("Undo Delete type {} not supported", d).into()),
- }
-}
-
-async fn receive_undo_remove(
- undo: Undo,
- context: &LemmyContext,
-) -> Result<HttpResponse, LemmyError> {
- let remove = Remove::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
- .context(location_info!())?;
- check_is_undo_valid(&undo, &remove)?;
-
- let type_ = remove
- .object()
- .as_single_kind_str()
- .context(location_info!())?;
- match type_ {
- "Note" => receive_undo_remove_comment(undo, &remove, context).await,
- "Page" => receive_undo_remove_post(undo, &remove, context).await,
- "Group" => receive_undo_remove_community(undo, &remove, context).await,
- d => Err(anyhow!("Undo Delete type {} not supported", d).into()),
- }
-}
-
-async fn receive_undo_like(undo: Undo, context: &LemmyContext) -> Result<HttpResponse, LemmyError> {
- let like = Like::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
- .context(location_info!())?;
- check_is_undo_valid(&undo, &like)?;
-
- let type_ = like
- .object()
- .as_single_kind_str()
- .context(location_info!())?;
- match type_ {
- "Note" => receive_undo_like_comment(undo, &like, context).await,
- "Page" => receive_undo_like_post(undo, &like, context).await,
- d => Err(anyhow!("Undo Delete type {} not supported", d).into()),
- }
-}
-
-async fn receive_undo_dislike(
- undo: Undo,
- context: &LemmyContext,
-) -> Result<HttpResponse, LemmyError> {
- let dislike = Dislike::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
- .context(location_info!())?;
- check_is_undo_valid(&undo, &dislike)?;
-
- let type_ = dislike
- .object()
- .as_single_kind_str()
- .context(location_info!())?;
- match type_ {
- "Note" => receive_undo_dislike_comment(undo, &dislike, context).await,
- "Page" => receive_undo_dislike_post(undo, &dislike, context).await,
- d => Err(anyhow!("Undo Delete type {} not supported", d).into()),
- }
-}
-
-async fn receive_undo_delete_comment(
- undo: Undo,
- delete: &Delete,
- context: &LemmyContext,
-) -> Result<HttpResponse, LemmyError> {
- let user = get_user_from_activity(delete, context).await?;
- let note = Note::from_any_base(delete.object().to_owned().one().context(location_info!())?)?
- .context(location_info!())?;
-
- let comment_ap_id = CommentForm::from_apub(¬e, context, Some(user.actor_id()?))
- .await?
- .get_ap_id()?;
-
- let comment = get_or_fetch_and_insert_comment(&comment_ap_id, context).await?;
-
- let comment_form = CommentForm {
- content: comment.content.to_owned(),
- parent_id: comment.parent_id,
- post_id: comment.post_id,
- creator_id: comment.creator_id,
- removed: None,
- deleted: Some(false),
- read: None,
- published: None,
- updated: Some(naive_now()),
- ap_id: Some(comment.ap_id),
- local: comment.local,
- };
- let comment_id = comment.id;
- blocking(context.pool(), move |conn| {
- Comment::update(conn, comment_id, &comment_form)
- })
- .await??;
-
- // Refetch the view
- let comment_id = comment.id;
- let comment_view = blocking(context.pool(), move |conn| {
- CommentView::read(conn, comment_id, None)
- })
- .await??;
-
- // TODO get those recipient actor ids from somewhere
- let recipient_ids = vec![];
- let res = CommentResponse {
- comment: comment_view,
- recipient_ids,
- form_id: None,
- };
-
- context.chat_server().do_send(SendComment {
- op: UserOperation::EditComment,
- comment: res,
- websocket_id: None,
- });
-
- announce_if_community_is_local(undo, &user, context).await?;
- Ok(HttpResponse::Ok().finish())
-}
-
-async fn receive_undo_remove_comment(
- undo: Undo,
- remove: &Remove,
- context: &LemmyContext,
-) -> Result<HttpResponse, LemmyError> {
- let mod_ = get_user_from_activity(remove, context).await?;
- let note = Note::from_any_base(remove.object().to_owned().one().context(location_info!())?)?
- .context(location_info!())?;
-
- let comment_ap_id = CommentForm::from_apub(¬e, context, None)
- .await?
- .get_ap_id()?;
-
- let comment = get_or_fetch_and_insert_comment(&comment_ap_id, context).await?;
-
- let comment_form = CommentForm {
- content: comment.content.to_owned(),
- parent_id: comment.parent_id,
- post_id: comment.post_id,
- creator_id: comment.creator_id,
- removed: Some(false),
- deleted: None,
- read: None,
- published: None,
- updated: Some(naive_now()),
- ap_id: Some(comment.ap_id),
- local: comment.local,
- };
- let comment_id = comment.id;
- blocking(context.pool(), move |conn| {
- Comment::update(conn, comment_id, &comment_form)
- })
- .await??;
-
- // Refetch the view
- let comment_id = comment.id;
- let comment_view = blocking(context.pool(), move |conn| {
- CommentView::read(conn, comment_id, None)
- })
- .await??;
-
- // TODO get those recipient actor ids from somewhere
- let recipient_ids = vec![];
- let res = CommentResponse {
- comment: comment_view,
- recipient_ids,
- form_id: None,
- };
-
- context.chat_server().do_send(SendComment {
- op: UserOperation::EditComment,
- comment: res,
- websocket_id: None,
- });
-
- announce_if_community_is_local(undo, &mod_, context).await?;
- Ok(HttpResponse::Ok().finish())
-}
-
-async fn receive_undo_delete_post(
- undo: Undo,
- delete: &Delete,
- context: &LemmyContext,
-) -> Result<HttpResponse, LemmyError> {
- let user = get_user_from_activity(delete, context).await?;
- let page = PageExt::from_any_base(delete.object().to_owned().one().context(location_info!())?)?
- .context(location_info!())?;
-
- let post_ap_id = PostForm::from_apub(&page, context, Some(user.actor_id()?))
- .await?
- .get_ap_id()?;
-
- let post = get_or_fetch_and_insert_post(&post_ap_id, context).await?;
-
- let post_form = PostForm {
- name: post.name.to_owned(),
- url: post.url.to_owned(),
- body: post.body.to_owned(),
- creator_id: post.creator_id.to_owned(),
- community_id: post.community_id,
- removed: None,
- deleted: Some(false),
- nsfw: post.nsfw,
- locked: None,
- stickied: None,
- updated: Some(naive_now()),
- embed_title: post.embed_title,
- embed_description: post.embed_description,
- embed_html: post.embed_html,
- thumbnail_url: post.thumbnail_url,
- ap_id: Some(post.ap_id),
- local: post.local,
- published: None,
- };
- let post_id = post.id;
- blocking(context.pool(), move |conn| {
- Post::update(conn, post_id, &post_form)
- })
- .await??;
-
- // Refetch the view
- let post_id = post.id;
- let post_view = blocking(context.pool(), move |conn| {
- PostView::read(conn, post_id, None)
- })
- .await??;
-
- let res = PostResponse { post: post_view };
-
- context.chat_server().do_send(SendPost {
- op: UserOperation::EditPost,
- post: res,
- websocket_id: None,
- });
-
- announce_if_community_is_local(undo, &user, context).await?;
- Ok(HttpResponse::Ok().finish())
-}
-
-async fn receive_undo_remove_post(
- undo: Undo,
- remove: &Remove,
- context: &LemmyContext,
-) -> Result<HttpResponse, LemmyError> {
- let mod_ = get_user_from_activity(remove, context).await?;
- let page = PageExt::from_any_base(remove.object().to_owned().one().context(location_info!())?)?
- .context(location_info!())?;
-
- let post_ap_id = PostForm::from_apub(&page, context, None)
- .await?
- .get_ap_id()?;
-
- let post = get_or_fetch_and_insert_post(&post_ap_id, context).await?;
-
- let post_form = PostForm {
- name: post.name.to_owned(),
- url: post.url.to_owned(),
- body: post.body.to_owned(),
- creator_id: post.creator_id.to_owned(),
- community_id: post.community_id,
- removed: Some(false),
- deleted: None,
- nsfw: post.nsfw,
- locked: None,
- stickied: None,
- updated: Some(naive_now()),
- embed_title: post.embed_title,
- embed_description: post.embed_description,
- embed_html: post.embed_html,
- thumbnail_url: post.thumbnail_url,
- ap_id: Some(post.ap_id),
- local: post.local,
- published: None,
- };
- let post_id = post.id;
- blocking(context.pool(), move |conn| {
- Post::update(conn, post_id, &post_form)
- })
- .await??;
-
- // Refetch the view
- let post_id = post.id;
- let post_view = blocking(context.pool(), move |conn| {
- PostView::read(conn, post_id, None)
- })
- .await??;
-
- let res = PostResponse { post: post_view };
-
- context.chat_server().do_send(SendPost {
- op: UserOperation::EditPost,
- post: res,
- websocket_id: None,
- });
-
- announce_if_community_is_local(undo, &mod_, context).await?;
- Ok(HttpResponse::Ok().finish())
-}
-
-async fn receive_undo_delete_community(
- undo: Undo,
- delete: &Delete,
- context: &LemmyContext,
-) -> Result<HttpResponse, LemmyError> {
- let user = get_user_from_activity(delete, context).await?;
- let group = GroupExt::from_any_base(delete.object().to_owned().one().context(location_info!())?)?
- .context(location_info!())?;
-
- let community_actor_id = CommunityForm::from_apub(&group, context, Some(user.actor_id()?))
- .await?
- .actor_id
- .context(location_info!())?;
-
- let community = blocking(context.pool(), move |conn| {
- Community::read_from_actor_id(conn, &community_actor_id)
- })
- .await??;
-
- let community_form = CommunityForm {
- name: community.name.to_owned(),
- title: community.title.to_owned(),
- description: community.description.to_owned(),
- category_id: community.category_id, // Note: need to keep this due to foreign key constraint
- creator_id: community.creator_id, // Note: need to keep this due to foreign key constraint
- removed: None,
- published: None,
- updated: Some(naive_now()),
- deleted: Some(false),
- nsfw: community.nsfw,
- actor_id: Some(community.actor_id),
- local: community.local,
- private_key: community.private_key,
- public_key: community.public_key,
- last_refreshed_at: None,
- icon: Some(community.icon.to_owned()),
- banner: Some(community.banner.to_owned()),
- };
-
- let community_id = community.id;
- blocking(context.pool(), move |conn| {
- Community::update(conn, community_id, &community_form)
- })
- .await??;
-
- let community_id = community.id;
- let res = CommunityResponse {
- community: blocking(context.pool(), move |conn| {
- CommunityView::read(conn, community_id, None)
- })
- .await??,
- };
-
- let community_id = res.community.id;
-
- context.chat_server().do_send(SendCommunityRoomMessage {
- op: UserOperation::EditCommunity,
- response: res,
- community_id,
- websocket_id: None,
- });
-
- announce_if_community_is_local(undo, &user, context).await?;
- Ok(HttpResponse::Ok().finish())
-}
-
-async fn receive_undo_remove_community(
- undo: Undo,
- remove: &Remove,
- context: &LemmyContext,
-) -> Result<HttpResponse, LemmyError> {
- let mod_ = get_user_from_activity(remove, context).await?;
- let group = GroupExt::from_any_base(remove.object().to_owned().one().context(location_info!())?)?
- .context(location_info!())?;
-
- let community_actor_id = CommunityForm::from_apub(&group, context, Some(mod_.actor_id()?))
- .await?
- .actor_id
- .context(location_info!())?;
-
- let community = blocking(context.pool(), move |conn| {
- Community::read_from_actor_id(conn, &community_actor_id)
- })
- .await??;
-
- let community_form = CommunityForm {
- name: community.name.to_owned(),
- title: community.title.to_owned(),
- description: community.description.to_owned(),
- category_id: community.category_id, // Note: need to keep this due to foreign key constraint
- creator_id: community.creator_id, // Note: need to keep this due to foreign key constraint
- removed: Some(false),
- published: None,
- updated: Some(naive_now()),
- deleted: None,
- nsfw: community.nsfw,
- actor_id: Some(community.actor_id),
- local: community.local,
- private_key: community.private_key,
- public_key: community.public_key,
- last_refreshed_at: None,
- icon: Some(community.icon.to_owned()),
- banner: Some(community.banner.to_owned()),
- };
-
- let community_id = community.id;
- blocking(context.pool(), move |conn| {
- Community::update(conn, community_id, &community_form)
- })
- .await??;
-
- let community_id = community.id;
- let res = CommunityResponse {
- community: blocking(context.pool(), move |conn| {
- CommunityView::read(conn, community_id, None)
- })
- .await??,
- };
-
- let community_id = res.community.id;
-
- context.chat_server().do_send(SendCommunityRoomMessage {
- op: UserOperation::EditCommunity,
- response: res,
- community_id,
- websocket_id: None,
- });
-
- announce_if_community_is_local(undo, &mod_, context).await?;
- Ok(HttpResponse::Ok().finish())
-}
-
-async fn receive_undo_like_comment(
- undo: Undo,
- like: &Like,
- context: &LemmyContext,
-) -> Result<HttpResponse, LemmyError> {
- let user = get_user_from_activity(like, context).await?;
- let note = Note::from_any_base(like.object().to_owned().one().context(location_info!())?)?
- .context(location_info!())?;
-
- let comment = CommentForm::from_apub(¬e, context, None).await?;
-
- let comment_id = get_or_fetch_and_insert_comment(&comment.get_ap_id()?, context)
- .await?
- .id;
-
- let user_id = user.id;
- blocking(context.pool(), move |conn| {
- CommentLike::remove(conn, user_id, comment_id)
- })
- .await??;
-
- // Refetch the view
- let comment_view = blocking(context.pool(), move |conn| {
- CommentView::read(conn, comment_id, None)
- })
- .await??;
-
- // TODO get those recipient actor ids from somewhere
- let recipient_ids = vec![];
- let res = CommentResponse {
- comment: comment_view,
- recipient_ids,
- form_id: None,
- };
-
- context.chat_server().do_send(SendComment {
- op: UserOperation::CreateCommentLike,
- comment: res,
- websocket_id: None,
- });
-
- announce_if_community_is_local(undo, &user, context).await?;
- Ok(HttpResponse::Ok().finish())
-}
-
-async fn receive_undo_like_post(
- undo: Undo,
- like: &Like,
- context: &LemmyContext,
-) -> Result<HttpResponse, LemmyError> {
- let user = get_user_from_activity(like, context).await?;
- let page = PageExt::from_any_base(like.object().to_owned().one().context(location_info!())?)?
- .context(location_info!())?;
-
- let post = PostForm::from_apub(&page, context, None).await?;
-
- let post_id = get_or_fetch_and_insert_post(&post.get_ap_id()?, context)
- .await?
- .id;
-
- let user_id = user.id;
- blocking(context.pool(), move |conn| {
- PostLike::remove(conn, user_id, post_id)
- })
- .await??;
-
- // Refetch the view
- let post_view = blocking(context.pool(), move |conn| {
- PostView::read(conn, post_id, None)
- })
- .await??;
-
- let res = PostResponse { post: post_view };
-
- context.chat_server().do_send(SendPost {
- op: UserOperation::CreatePostLike,
- post: res,
- websocket_id: None,
- });
-
- announce_if_community_is_local(undo, &user, context).await?;
- Ok(HttpResponse::Ok().finish())
-}
-
-async fn receive_undo_dislike_comment(
- undo: Undo,
- dislike: &Dislike,
- context: &LemmyContext,
-) -> Result<HttpResponse, LemmyError> {
- let user = get_user_from_activity(dislike, context).await?;
- let note = Note::from_any_base(
- dislike
- .object()
- .to_owned()
- .one()
- .context(location_info!())?,
- )?
- .context(location_info!())?;
-
- let comment = CommentForm::from_apub(¬e, context, None).await?;
-
- let comment_id = get_or_fetch_and_insert_comment(&comment.get_ap_id()?, context)
- .await?
- .id;
-
- let user_id = user.id;
- blocking(context.pool(), move |conn| {
- CommentLike::remove(conn, user_id, comment_id)
- })
- .await??;
-
- // Refetch the view
- let comment_view = blocking(context.pool(), move |conn| {
- CommentView::read(conn, comment_id, None)
- })
- .await??;
-
- // TODO get those recipient actor ids from somewhere
- let recipient_ids = vec![];
- let res = CommentResponse {
- comment: comment_view,
- recipient_ids,
- form_id: None,
- };
-
- context.chat_server().do_send(SendComment {
- op: UserOperation::CreateCommentLike,
- comment: res,
- websocket_id: None,
- });
-
- announce_if_community_is_local(undo, &user, context).await?;
- Ok(HttpResponse::Ok().finish())
-}
-
-async fn receive_undo_dislike_post(
- undo: Undo,
- dislike: &Dislike,
- context: &LemmyContext,
-) -> Result<HttpResponse, LemmyError> {
- let user = get_user_from_activity(dislike, context).await?;
- let page = PageExt::from_any_base(
- dislike
- .object()
- .to_owned()
- .one()
- .context(location_info!())?,
- )?
- .context(location_info!())?;
-
- let post = PostForm::from_apub(&page, context, None).await?;
-
- let post_id = get_or_fetch_and_insert_post(&post.get_ap_id()?, context)
- .await?
- .id;
-
- let user_id = user.id;
- blocking(context.pool(), move |conn| {
- PostLike::remove(conn, user_id, post_id)
- })
- .await??;
-
- // Refetch the view
- let post_view = blocking(context.pool(), move |conn| {
- PostView::read(conn, post_id, None)
- })
- .await??;
-
- let res = PostResponse { post: post_view };
-
- context.chat_server().do_send(SendPost {
- op: UserOperation::CreatePostLike,
- post: res,
- websocket_id: None,
- });
-
- announce_if_community_is_local(undo, &user, context).await?;
- Ok(HttpResponse::Ok().finish())
-}
pub type AcceptedActivities = ActorAndObject<ValidTypes>;
-/// Handler for all incoming activities to community inboxes.
+/// Handler for all incoming receive to community inboxes.
pub async fn community_inbox(
request: HttpRequest,
input: web::Json<AcceptedActivities>,
-pub mod activities;
pub mod community_inbox;
pub mod shared_inbox;
pub mod user_inbox;
use crate::{
- check_is_apub_id_valid,
- community::do_announce,
- extensions::signatures::verify,
- fetcher::{
- get_or_fetch_and_upsert_actor,
- get_or_fetch_and_upsert_community,
- get_or_fetch_and_upsert_user,
- },
- inbox::activities::{
+ activities::receive::{
announce::receive_announce,
create::receive_create,
delete::receive_delete,
undo::receive_undo,
update::receive_update,
},
+ check_is_apub_id_valid,
+ extensions::signatures::verify,
+ fetcher::{get_or_fetch_and_upsert_actor, get_or_fetch_and_upsert_user},
insert_activity,
};
use activitystreams::{
activity::{ActorAndObject, ActorAndObjectRef},
- base::{AsBase, Extends},
+ base::AsBase,
object::AsObject,
prelude::*,
};
Announce,
}
-// TODO: this isnt entirely correct, cause some of these activities are not ActorAndObject,
+// TODO: this isnt entirely correct, cause some of these receive are not ActorAndObject,
// but it might still work due to the anybase conversion
pub type AcceptedActivities = ActorAndObject<ValidTypes>;
-/// Handler for all incoming activities to user inboxes.
+/// Handler for all incoming receive to user inboxes.
pub async fn shared_inbox(
request: HttpRequest,
input: web::Json<AcceptedActivities>,
res
}
-pub(in crate::inbox) fn receive_unhandled_activity<A>(
- activity: A,
-) -> Result<HttpResponse, LemmyError>
-where
- A: Debug,
-{
- debug!("received unhandled activity type: {:?}", activity);
- Ok(HttpResponse::NotImplemented().finish())
-}
-
-pub(in crate::inbox) async fn get_user_from_activity<T, A>(
+pub(in crate) async fn get_user_from_activity<T, A>(
activity: &T,
context: &LemmyContext,
) -> Result<User_, LemmyError>
get_or_fetch_and_upsert_user(&user_uri, context).await
}
-pub(in crate::inbox) fn get_community_id_from_activity<T, A>(
- activity: &T,
-) -> Result<Url, LemmyError>
+pub(in crate) fn get_community_id_from_activity<T, A>(activity: &T) -> Result<Url, LemmyError>
where
T: AsBase<A> + ActorAndObjectRef + AsObject<A>,
{
.to_owned(),
)
}
-
-pub(in crate::inbox) async fn announce_if_community_is_local<T, Kind>(
- activity: T,
- user: &User_,
- context: &LemmyContext,
-) -> Result<(), LemmyError>
-where
- T: AsObject<Kind>,
- T: Extends<Kind>,
- Kind: Serialize,
- <T as Extends<Kind>>::Error: From<serde_json::Error> + Send + Sync + 'static,
-{
- let cc = activity.cc().context(location_info!())?;
- let cc = cc.as_many().context(location_info!())?;
- let community_followers_uri = cc
- .first()
- .context(location_info!())?
- .as_xsd_any_uri()
- .context(location_info!())?;
- // TODO: this is hacky but seems to be the only way to get the community ID
- let community_uri = community_followers_uri
- .to_string()
- .replace("/followers", "");
- let community = get_or_fetch_and_upsert_community(&Url::parse(&community_uri)?, context).await?;
-
- if community.local {
- do_announce(activity.into_any_base()?, &community, &user, context).await?;
- }
- Ok(())
-}
pub type AcceptedActivities = ActorAndObject<ValidTypes>;
-/// Handler for all incoming activities to user inboxes.
+/// Handler for all incoming receive to user inboxes.
pub async fn user_inbox(
request: HttpRequest,
input: web::Json<AcceptedActivities>,
#[macro_use]
extern crate lazy_static;
+pub mod activities;
pub mod activity_queue;
-pub mod comment;
-pub mod community;
pub mod extensions;
pub mod fetcher;
+pub mod http;
pub mod inbox;
-pub mod post;
-pub mod private_message;
-pub mod user;
+pub mod objects;
use crate::extensions::{
group_extensions::GroupExtension,
use activitystreams::{
activity::Follow,
actor::{ApActor, Group, Person},
- base::AsBase,
+ base::{AnyBase, AsBase},
markers::Base,
object::{Page, Tombstone},
prelude::*,
};
use activitystreams_ext::{Ext1, Ext2};
-use actix_web::{body::Body, HttpResponse};
use anyhow::{anyhow, Context};
-use chrono::NaiveDateTime;
use lemmy_db::{activity::do_insert_activity, user::User_, DbPool};
-use lemmy_structs::{blocking, WebFingerResponse};
-use lemmy_utils::{
- location_info,
- request::{retry, RecvError},
- settings::Settings,
- utils::{convert_datetime, MentionData},
- LemmyError,
-};
+use lemmy_structs::blocking;
+use lemmy_utils::{location_info, settings::Settings, LemmyError};
use lemmy_websocket::LemmyContext;
-use log::debug;
-use reqwest::Client;
use serde::Serialize;
use url::{ParseError, Url};
-use uuid::Uuid;
type GroupExt = Ext2<ApActor<Group>, GroupExtension, PublicKeyExtension>;
type PersonExt = Ext1<ApActor<Person>, PublicKeyExtension>;
pub static APUB_JSON_CONTENT_TYPE: &str = "application/activity+json";
-/// Convert the data to json and turn it into an HTTP Response with the correct ActivityPub
-/// headers.
-fn create_apub_response<T>(data: &T) -> HttpResponse<Body>
-where
- T: Serialize,
-{
- HttpResponse::Ok()
- .content_type(APUB_JSON_CONTENT_TYPE)
- .json(data)
-}
-
-fn create_apub_tombstone_response<T>(data: &T) -> HttpResponse<Body>
-where
- T: Serialize,
-{
- HttpResponse::Gone()
- .content_type(APUB_JSON_CONTENT_TYPE)
- .json(data)
-}
-
// Checks if the ID has a valid format, correct scheme, and is in the allowed instance list.
fn check_is_apub_id_valid(apub_id: &Url) -> Result<(), LemmyError> {
let settings = Settings::get();
if allowed_instances.is_empty() && blocked_instances.is_empty() {
Ok(())
} else if !allowed_instances.is_empty() {
- // need to allow this explicitly because apub activities might contain objects from our local
+ // need to allow this explicitly because apub receive might contain objects from our local
// instance. split is needed to remove the port in our federation test setup.
allowed_instances.push(local_instance);
fn to_tombstone(&self) -> Result<Tombstone, LemmyError>;
}
-/// Updated is actually the deletion time
-fn create_tombstone<T>(
- deleted: bool,
- object_id: &str,
- updated: Option<NaiveDateTime>,
- former_type: T,
-) -> Result<Tombstone, LemmyError>
-where
- T: ToString,
-{
- if deleted {
- if let Some(updated) = updated {
- let mut tombstone = Tombstone::new();
- tombstone.set_id(object_id.parse()?);
- tombstone.set_former_type(former_type.to_string());
- tombstone.set_deleted(convert_datetime(updated));
- Ok(tombstone)
- } else {
- Err(anyhow!("Cant convert to tombstone because updated time was None.").into())
- }
- } else {
- Err(anyhow!("Cant convert object to tombstone if it wasnt deleted").into())
- }
-}
-
#[async_trait::async_trait(?Send)]
pub trait FromApub {
type ApubType;
async fn send_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
async fn send_undo_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
+ async fn send_announce(
+ &self,
+ activity: AnyBase,
+ sender: &User_,
+ context: &LemmyContext,
+ ) -> Result<(), LemmyError>;
+
/// For a given community, returns the inboxes of all followers.
async fn get_follower_inboxes(&self, pool: &DbPool) -> Result<Vec<Url>, LemmyError>;
}
}
-pub async fn fetch_webfinger_url(
- mention: &MentionData,
- client: &Client,
-) -> Result<Url, LemmyError> {
- let fetch_url = format!(
- "{}://{}/.well-known/webfinger?resource=acct:{}@{}",
- Settings::get().get_protocol_string(),
- mention.domain,
- mention.name,
- mention.domain
- );
- debug!("Fetching webfinger url: {}", &fetch_url);
-
- let response = retry(|| client.get(&fetch_url).send()).await?;
-
- let res: WebFingerResponse = response
- .json()
- .await
- .map_err(|e| RecvError(e.to_string()))?;
-
- let link = res
- .links
- .iter()
- .find(|l| l.type_.eq(&Some("application/activity+json".to_string())))
- .ok_or_else(|| anyhow!("No application/activity+json link found."))?;
- link
- .href
- .to_owned()
- .map(|u| Url::parse(&u))
- .transpose()?
- .ok_or_else(|| anyhow!("No href found.").into())
-}
-
pub async fn insert_activity<T>(
user_id: i32,
data: T,
.await??;
Ok(())
}
-
-pub(in crate) fn generate_activity_id<T>(kind: T) -> Result<Url, ParseError>
-where
- T: ToString,
-{
- let id = format!(
- "{}/activities/{}/{}",
- Settings::get().get_protocol_and_hostname(),
- kind.to_string().to_lowercase(),
- Uuid::new_v4()
- );
- Url::parse(&id)
-}
--- /dev/null
+use crate::{
+ check_actor_domain,
+ fetcher::{
+ get_or_fetch_and_insert_comment,
+ get_or_fetch_and_insert_post,
+ get_or_fetch_and_upsert_user,
+ },
+ objects::create_tombstone,
+ FromApub,
+ ToApub,
+};
+use activitystreams::{
+ object::{kind::NoteType, Note, Tombstone},
+ prelude::*,
+};
+use anyhow::Context;
+use lemmy_db::{
+ comment::{Comment, CommentForm},
+ community::Community,
+ post::Post,
+ user::User_,
+ Crud,
+ DbPool,
+};
+use lemmy_structs::blocking;
+use lemmy_utils::{
+ location_info,
+ utils::{convert_datetime, remove_slurs},
+ LemmyError,
+};
+use lemmy_websocket::LemmyContext;
+use url::Url;
+
+#[async_trait::async_trait(?Send)]
+impl ToApub for Comment {
+ type Response = Note;
+
+ async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> {
+ let mut comment = Note::new();
+
+ let creator_id = self.creator_id;
+ let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??;
+
+ let post_id = self.post_id;
+ let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
+
+ let community_id = post.community_id;
+ let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
+
+ // Add a vector containing some important info to the "in_reply_to" field
+ // [post_ap_id, Option(parent_comment_ap_id)]
+ let mut in_reply_to_vec = vec![post.ap_id];
+
+ if let Some(parent_id) = self.parent_id {
+ let parent_comment = blocking(pool, move |conn| Comment::read(conn, parent_id)).await??;
+
+ in_reply_to_vec.push(parent_comment.ap_id);
+ }
+
+ comment
+ // Not needed when the Post is embedded in a collection (like for community outbox)
+ .set_context(activitystreams::context())
+ .set_id(Url::parse(&self.ap_id)?)
+ .set_published(convert_datetime(self.published))
+ .set_to(community.actor_id)
+ .set_many_in_reply_tos(in_reply_to_vec)
+ .set_content(self.content.to_owned())
+ .set_attributed_to(creator.actor_id);
+
+ if let Some(u) = self.updated {
+ comment.set_updated(convert_datetime(u));
+ }
+
+ Ok(comment)
+ }
+
+ fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
+ create_tombstone(self.deleted, &self.ap_id, self.updated, NoteType::Note)
+ }
+}
+
+#[async_trait::async_trait(?Send)]
+impl FromApub for CommentForm {
+ type ApubType = Note;
+
+ /// Parse an ActivityPub note received from another instance into a Lemmy comment
+ async fn from_apub(
+ note: &Note,
+ context: &LemmyContext,
+ expected_domain: Option<Url>,
+ ) -> Result<CommentForm, LemmyError> {
+ let creator_actor_id = ¬e
+ .attributed_to()
+ .context(location_info!())?
+ .as_single_xsd_any_uri()
+ .context(location_info!())?;
+
+ let creator = get_or_fetch_and_upsert_user(creator_actor_id, context).await?;
+
+ let mut in_reply_tos = note
+ .in_reply_to()
+ .as_ref()
+ .context(location_info!())?
+ .as_many()
+ .context(location_info!())?
+ .iter()
+ .map(|i| i.as_xsd_any_uri().context(""));
+ let post_ap_id = in_reply_tos.next().context(location_info!())??;
+
+ // This post, or the parent comment might not yet exist on this server yet, fetch them.
+ let post = get_or_fetch_and_insert_post(&post_ap_id, context).await?;
+
+ // The 2nd item, if it exists, is the parent comment apub_id
+ // For deeply nested comments, FromApub automatically gets called recursively
+ let parent_id: Option<i32> = match in_reply_tos.next() {
+ Some(parent_comment_uri) => {
+ let parent_comment_ap_id = &parent_comment_uri?;
+ let parent_comment =
+ get_or_fetch_and_insert_comment(&parent_comment_ap_id, context).await?;
+
+ Some(parent_comment.id)
+ }
+ None => None,
+ };
+ let content = note
+ .content()
+ .context(location_info!())?
+ .as_single_xsd_string()
+ .context(location_info!())?
+ .to_string();
+ let content_slurs_removed = remove_slurs(&content);
+
+ Ok(CommentForm {
+ creator_id: creator.id,
+ post_id: post.id,
+ parent_id,
+ content: content_slurs_removed,
+ removed: None,
+ read: None,
+ published: note.published().map(|u| u.to_owned().naive_local()),
+ updated: note.updated().map(|u| u.to_owned().naive_local()),
+ deleted: None,
+ ap_id: Some(check_actor_domain(note, expected_domain)?),
+ local: false,
+ })
+ }
+}
--- /dev/null
+use crate::{
+ check_actor_domain,
+ extensions::group_extensions::GroupExtension,
+ fetcher::get_or_fetch_and_upsert_user,
+ objects::create_tombstone,
+ ActorType,
+ FromApub,
+ GroupExt,
+ ToApub,
+};
+use activitystreams::{
+ actor::{kind::GroupType, ApActor, Endpoints, Group},
+ base::BaseExt,
+ object::{Image, Tombstone},
+ prelude::*,
+};
+use activitystreams_ext::Ext2;
+use anyhow::Context;
+use lemmy_db::{
+ community::{Community, CommunityForm},
+ community_view::CommunityModeratorView,
+ naive_now,
+ DbPool,
+};
+use lemmy_structs::blocking;
+use lemmy_utils::{
+ location_info,
+ utils::{check_slurs, check_slurs_opt, convert_datetime},
+ LemmyError,
+};
+use lemmy_websocket::LemmyContext;
+use url::Url;
+
+#[async_trait::async_trait(?Send)]
+impl ToApub for Community {
+ type Response = GroupExt;
+
+ // Turn a Lemmy Community into an ActivityPub group that can be sent out over the network.
+ 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??;
+ let moderators: Vec<String> = moderators.into_iter().map(|m| m.user_actor_id).collect();
+
+ let mut group = Group::new();
+ group
+ .set_context(activitystreams::context())
+ .set_id(Url::parse(&self.actor_id)?)
+ .set_name(self.name.to_owned())
+ .set_published(convert_datetime(self.published))
+ .set_many_attributed_tos(moderators);
+
+ if let Some(u) = self.updated.to_owned() {
+ group.set_updated(convert_datetime(u));
+ }
+ if let Some(d) = self.description.to_owned() {
+ // TODO: this should be html, also add source field with raw markdown
+ // -> same for post.content and others
+ group.set_content(d);
+ }
+
+ if let Some(icon) = &self.icon {
+ let mut image = Image::new();
+ image.set_url(icon.to_owned());
+ group.set_icon(image.into_any_base()?);
+ }
+
+ if let Some(banner_url) = &self.banner {
+ let mut image = Image::new();
+ image.set_url(banner_url.to_owned());
+ group.set_image(image.into_any_base()?);
+ }
+
+ let mut ap_actor = ApActor::new(self.get_inbox_url()?, group);
+ ap_actor
+ .set_preferred_username(self.title.to_owned())
+ .set_outbox(self.get_outbox_url()?)
+ .set_followers(self.get_followers_url()?)
+ .set_endpoints(Endpoints {
+ shared_inbox: Some(self.get_shared_inbox_url()?),
+ ..Default::default()
+ });
+
+ let nsfw = self.nsfw;
+ let category_id = self.category_id;
+ let group_extension = blocking(pool, move |conn| {
+ GroupExtension::new(conn, category_id, nsfw)
+ })
+ .await??;
+
+ Ok(Ext2::new(
+ ap_actor,
+ group_extension,
+ self.get_public_key_ext()?,
+ ))
+ }
+
+ fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
+ create_tombstone(self.deleted, &self.actor_id, self.updated, GroupType::Group)
+ }
+}
+#[async_trait::async_trait(?Send)]
+impl FromApub for CommunityForm {
+ type ApubType = GroupExt;
+
+ /// Parse an ActivityPub group received from another instance into a Lemmy community.
+ async fn from_apub(
+ group: &GroupExt,
+ context: &LemmyContext,
+ expected_domain: Option<Url>,
+ ) -> Result<Self, LemmyError> {
+ let creator_and_moderator_uris = group.inner.attributed_to().context(location_info!())?;
+ let creator_uri = creator_and_moderator_uris
+ .as_many()
+ .context(location_info!())?
+ .iter()
+ .next()
+ .context(location_info!())?
+ .as_xsd_any_uri()
+ .context(location_info!())?;
+
+ let creator = get_or_fetch_and_upsert_user(creator_uri, context).await?;
+ let name = group
+ .inner
+ .name()
+ .context(location_info!())?
+ .as_one()
+ .context(location_info!())?
+ .as_xsd_string()
+ .context(location_info!())?
+ .to_string();
+ let title = group
+ .inner
+ .preferred_username()
+ .context(location_info!())?
+ .to_string();
+ // TODO: should be parsed as html and tags like <script> removed (or use markdown source)
+ // -> same for post.content etc
+ let description = group
+ .inner
+ .content()
+ .map(|s| s.as_single_xsd_string())
+ .flatten()
+ .map(|s| s.to_string());
+ check_slurs(&name)?;
+ check_slurs(&title)?;
+ check_slurs_opt(&description)?;
+
+ let icon = match group.icon() {
+ Some(any_image) => Some(
+ Image::from_any_base(any_image.as_one().context(location_info!())?.clone())
+ .context(location_info!())?
+ .context(location_info!())?
+ .url()
+ .context(location_info!())?
+ .as_single_xsd_any_uri()
+ .map(|u| u.to_string()),
+ ),
+ None => None,
+ };
+
+ let banner = match group.image() {
+ Some(any_image) => Some(
+ Image::from_any_base(any_image.as_one().context(location_info!())?.clone())
+ .context(location_info!())?
+ .context(location_info!())?
+ .url()
+ .context(location_info!())?
+ .as_single_xsd_any_uri()
+ .map(|u| u.to_string()),
+ ),
+ None => None,
+ };
+
+ Ok(CommunityForm {
+ name,
+ title,
+ description,
+ category_id: group.ext_one.category.identifier.parse::<i32>()?,
+ creator_id: creator.id,
+ removed: None,
+ published: group.inner.published().map(|u| u.to_owned().naive_local()),
+ updated: group.inner.updated().map(|u| u.to_owned().naive_local()),
+ deleted: None,
+ nsfw: group.ext_one.sensitive,
+ actor_id: Some(check_actor_domain(group, expected_domain)?),
+ local: false,
+ private_key: None,
+ public_key: Some(group.ext_two.to_owned().public_key.public_key_pem),
+ last_refreshed_at: Some(naive_now()),
+ icon,
+ banner,
+ })
+ }
+}
--- /dev/null
+use activitystreams::{
+ base::BaseExt,
+ object::{Tombstone, TombstoneExt},
+};
+use anyhow::anyhow;
+use chrono::NaiveDateTime;
+use lemmy_utils::{utils::convert_datetime, LemmyError};
+
+pub mod comment;
+pub mod community;
+pub mod post;
+pub mod private_message;
+pub mod user;
+
+/// Updated is actually the deletion time
+fn create_tombstone<T>(
+ deleted: bool,
+ object_id: &str,
+ updated: Option<NaiveDateTime>,
+ former_type: T,
+) -> Result<Tombstone, LemmyError>
+where
+ T: ToString,
+{
+ if deleted {
+ if let Some(updated) = updated {
+ let mut tombstone = Tombstone::new();
+ tombstone.set_id(object_id.parse()?);
+ tombstone.set_former_type(former_type.to_string());
+ tombstone.set_deleted(convert_datetime(updated));
+ Ok(tombstone)
+ } else {
+ Err(anyhow!("Cant convert to tombstone because updated time was None.").into())
+ }
+ } else {
+ Err(anyhow!("Cant convert object to tombstone if it wasnt deleted").into())
+ }
+}
--- /dev/null
+use crate::{
+ check_actor_domain,
+ extensions::page_extension::PageExtension,
+ fetcher::{get_or_fetch_and_upsert_community, get_or_fetch_and_upsert_user},
+ objects::create_tombstone,
+ FromApub,
+ PageExt,
+ ToApub,
+};
+use activitystreams::{
+ object::{kind::PageType, Image, Page, Tombstone},
+ prelude::*,
+};
+use activitystreams_ext::Ext1;
+use anyhow::Context;
+use lemmy_db::{
+ community::Community,
+ post::{Post, PostForm},
+ user::User_,
+ Crud,
+ DbPool,
+};
+use lemmy_structs::blocking;
+use lemmy_utils::{
+ location_info,
+ request::fetch_iframely_and_pictrs_data,
+ utils::{check_slurs, convert_datetime, remove_slurs},
+ LemmyError,
+};
+use lemmy_websocket::LemmyContext;
+use url::Url;
+
+#[async_trait::async_trait(?Send)]
+impl ToApub for Post {
+ type Response = PageExt;
+
+ // Turn a Lemmy post into an ActivityPub page that can be sent out over the network.
+ async fn to_apub(&self, pool: &DbPool) -> Result<PageExt, LemmyError> {
+ let mut page = Page::new();
+
+ let creator_id = self.creator_id;
+ let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??;
+
+ let community_id = self.community_id;
+ let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
+
+ page
+ // Not needed when the Post is embedded in a collection (like for community outbox)
+ // TODO: need to set proper context defining sensitive/commentsEnabled fields
+ // https://git.asonix.dog/Aardwolf/activitystreams/issues/5
+ .set_context(activitystreams::context())
+ .set_id(self.ap_id.parse::<Url>()?)
+ // Use summary field to be consistent with mastodon content warning.
+ // https://mastodon.xyz/@Louisa/103987265222901387.json
+ .set_summary(self.name.to_owned())
+ .set_published(convert_datetime(self.published))
+ .set_to(community.actor_id)
+ .set_attributed_to(creator.actor_id);
+
+ if let Some(body) = &self.body {
+ page.set_content(body.to_owned());
+ }
+
+ // TODO: hacky code because we get self.url == Some("")
+ // https://github.com/LemmyNet/lemmy/issues/602
+ let url = self.url.as_ref().filter(|u| !u.is_empty());
+ if let Some(u) = url {
+ page.set_url(u.to_owned());
+ }
+
+ if let Some(thumbnail_url) = &self.thumbnail_url {
+ let mut image = Image::new();
+ image.set_url(thumbnail_url.to_string());
+ page.set_image(image.into_any_base()?);
+ }
+
+ if let Some(u) = self.updated {
+ page.set_updated(convert_datetime(u));
+ }
+
+ let ext = PageExtension {
+ comments_enabled: !self.locked,
+ sensitive: self.nsfw,
+ stickied: self.stickied,
+ };
+ Ok(Ext1::new(page, ext))
+ }
+
+ fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
+ create_tombstone(self.deleted, &self.ap_id, self.updated, PageType::Page)
+ }
+}
+
+#[async_trait::async_trait(?Send)]
+impl FromApub for PostForm {
+ type ApubType = PageExt;
+
+ /// Parse an ActivityPub page received from another instance into a Lemmy post.
+ async fn from_apub(
+ page: &PageExt,
+ context: &LemmyContext,
+ expected_domain: Option<Url>,
+ ) -> Result<PostForm, LemmyError> {
+ let ext = &page.ext_one;
+ let creator_actor_id = page
+ .inner
+ .attributed_to()
+ .as_ref()
+ .context(location_info!())?
+ .as_single_xsd_any_uri()
+ .context(location_info!())?;
+
+ let creator = get_or_fetch_and_upsert_user(creator_actor_id, context).await?;
+
+ let community_actor_id = page
+ .inner
+ .to()
+ .as_ref()
+ .context(location_info!())?
+ .as_single_xsd_any_uri()
+ .context(location_info!())?;
+
+ let community = get_or_fetch_and_upsert_community(community_actor_id, context).await?;
+
+ let thumbnail_url = match &page.inner.image() {
+ Some(any_image) => Image::from_any_base(
+ any_image
+ .to_owned()
+ .as_one()
+ .context(location_info!())?
+ .to_owned(),
+ )?
+ .context(location_info!())?
+ .url()
+ .context(location_info!())?
+ .as_single_xsd_any_uri()
+ .map(|u| u.to_string()),
+ None => None,
+ };
+ let url = page
+ .inner
+ .url()
+ .map(|u| u.as_single_xsd_any_uri())
+ .flatten()
+ .map(|s| s.to_string());
+
+ let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
+ if let Some(url) = &url {
+ fetch_iframely_and_pictrs_data(context.client(), Some(url.to_owned())).await
+ } else {
+ (None, None, None, thumbnail_url)
+ };
+
+ let name = page
+ .inner
+ .summary()
+ .as_ref()
+ .context(location_info!())?
+ .as_single_xsd_string()
+ .context(location_info!())?
+ .to_string();
+ let body = page
+ .inner
+ .content()
+ .as_ref()
+ .map(|c| c.as_single_xsd_string())
+ .flatten()
+ .map(|s| s.to_string());
+ check_slurs(&name)?;
+ let body_slurs_removed = body.map(|b| remove_slurs(&b));
+ Ok(PostForm {
+ name,
+ url,
+ body: body_slurs_removed,
+ creator_id: creator.id,
+ community_id: community.id,
+ removed: None,
+ locked: Some(!ext.comments_enabled),
+ published: page
+ .inner
+ .published()
+ .as_ref()
+ .map(|u| u.to_owned().naive_local()),
+ updated: page
+ .inner
+ .updated()
+ .as_ref()
+ .map(|u| u.to_owned().naive_local()),
+ deleted: None,
+ nsfw: ext.sensitive,
+ stickied: Some(ext.stickied),
+ embed_title: iframely_title,
+ embed_description: iframely_description,
+ embed_html: iframely_html,
+ thumbnail_url: pictrs_thumbnail,
+ ap_id: Some(check_actor_domain(page, expected_domain)?),
+ local: false,
+ })
+ }
+}
--- /dev/null
+use crate::{
+ check_actor_domain,
+ check_is_apub_id_valid,
+ fetcher::get_or_fetch_and_upsert_user,
+ objects::create_tombstone,
+ FromApub,
+ ToApub,
+};
+use activitystreams::{
+ object::{kind::NoteType, Note, Tombstone},
+ prelude::*,
+};
+use anyhow::Context;
+use lemmy_db::{
+ private_message::{PrivateMessage, PrivateMessageForm},
+ user::User_,
+ Crud,
+ DbPool,
+};
+use lemmy_structs::blocking;
+use lemmy_utils::{location_info, utils::convert_datetime, LemmyError};
+use lemmy_websocket::LemmyContext;
+use url::Url;
+
+#[async_trait::async_trait(?Send)]
+impl ToApub for PrivateMessage {
+ type Response = Note;
+
+ async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> {
+ let mut private_message = Note::new();
+
+ let creator_id = self.creator_id;
+ let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??;
+
+ let recipient_id = self.recipient_id;
+ let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??;
+
+ private_message
+ .set_context(activitystreams::context())
+ .set_id(Url::parse(&self.ap_id.to_owned())?)
+ .set_published(convert_datetime(self.published))
+ .set_content(self.content.to_owned())
+ .set_to(recipient.actor_id)
+ .set_attributed_to(creator.actor_id);
+
+ if let Some(u) = self.updated {
+ private_message.set_updated(convert_datetime(u));
+ }
+
+ Ok(private_message)
+ }
+
+ fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
+ create_tombstone(self.deleted, &self.ap_id, self.updated, NoteType::Note)
+ }
+}
+
+#[async_trait::async_trait(?Send)]
+impl FromApub for PrivateMessageForm {
+ type ApubType = Note;
+
+ /// Parse an ActivityPub note received from another instance into a Lemmy Private message
+ async fn from_apub(
+ note: &Note,
+ context: &LemmyContext,
+ expected_domain: Option<Url>,
+ ) -> Result<PrivateMessageForm, LemmyError> {
+ let creator_actor_id = note
+ .attributed_to()
+ .context(location_info!())?
+ .clone()
+ .single_xsd_any_uri()
+ .context(location_info!())?;
+
+ let creator = get_or_fetch_and_upsert_user(&creator_actor_id, context).await?;
+ let recipient_actor_id = note
+ .to()
+ .context(location_info!())?
+ .clone()
+ .single_xsd_any_uri()
+ .context(location_info!())?;
+ let recipient = get_or_fetch_and_upsert_user(&recipient_actor_id, context).await?;
+ let ap_id = note.id_unchecked().context(location_info!())?.to_string();
+ check_is_apub_id_valid(&Url::parse(&ap_id)?)?;
+
+ Ok(PrivateMessageForm {
+ creator_id: creator.id,
+ recipient_id: recipient.id,
+ content: note
+ .content()
+ .context(location_info!())?
+ .as_single_xsd_string()
+ .context(location_info!())?
+ .to_string(),
+ published: note.published().map(|u| u.to_owned().naive_local()),
+ updated: note.updated().map(|u| u.to_owned().naive_local()),
+ deleted: None,
+ read: None,
+ ap_id: Some(check_actor_domain(note, expected_domain)?),
+ local: false,
+ })
+ }
+}
-use crate::{
- activity_queue::send_activity_single_dest,
- check_actor_domain,
- create_apub_response,
- fetcher::get_or_fetch_and_upsert_actor,
- generate_activity_id,
- ActorType,
- FromApub,
- PersonExt,
- ToApub,
-};
+use crate::{check_actor_domain, ActorType, FromApub, PersonExt, ToApub};
use activitystreams::{
- activity::{
- kind::{FollowType, UndoType},
- Follow,
- Undo,
- },
actor::{ApActor, Endpoints, Person},
object::{Image, Tombstone},
prelude::*,
};
use activitystreams_ext::Ext1;
-use actix_web::{body::Body, web, HttpResponse};
use anyhow::Context;
use lemmy_db::{
naive_now,
user::{UserForm, User_},
DbPool,
};
-use lemmy_structs::blocking;
use lemmy_utils::{
location_info,
utils::{check_slurs, check_slurs_opt, convert_datetime},
LemmyError,
};
use lemmy_websocket::LemmyContext;
-use serde::Deserialize;
use url::Url;
-#[derive(Deserialize)]
-pub struct UserQuery {
- user_name: String,
-}
-
#[async_trait::async_trait(?Send)]
impl ToApub for User_ {
type Response = PersonExt;
}
}
-#[async_trait::async_trait(?Send)]
-impl ActorType for User_ {
- fn actor_id_str(&self) -> String {
- self.actor_id.to_owned()
- }
-
- fn public_key(&self) -> Option<String> {
- self.public_key.to_owned()
- }
-
- fn private_key(&self) -> Option<String> {
- self.private_key.to_owned()
- }
-
- /// As a given local user, send out a follow request to a remote community.
- async fn send_follow(
- &self,
- follow_actor_id: &Url,
- context: &LemmyContext,
- ) -> Result<(), LemmyError> {
- let mut follow = Follow::new(self.actor_id.to_owned(), follow_actor_id.as_str());
- follow
- .set_context(activitystreams::context())
- .set_id(generate_activity_id(FollowType::Follow)?);
- let follow_actor = get_or_fetch_and_upsert_actor(follow_actor_id, context).await?;
- let to = follow_actor.get_inbox_url()?;
-
- send_activity_single_dest(follow, self, to, context).await?;
- Ok(())
- }
-
- async fn send_unfollow(
- &self,
- follow_actor_id: &Url,
- context: &LemmyContext,
- ) -> Result<(), LemmyError> {
- let mut follow = Follow::new(self.actor_id.to_owned(), follow_actor_id.as_str());
- follow
- .set_context(activitystreams::context())
- .set_id(generate_activity_id(FollowType::Follow)?);
- let follow_actor = get_or_fetch_and_upsert_actor(follow_actor_id, context).await?;
-
- let to = follow_actor.get_inbox_url()?;
-
- // Undo that fake activity
- let mut undo = Undo::new(Url::parse(&self.actor_id)?, follow.into_any_base()?);
- undo
- .set_context(activitystreams::context())
- .set_id(generate_activity_id(UndoType::Undo)?);
-
- send_activity_single_dest(undo, self, to, context).await?;
- Ok(())
- }
-
- async fn send_delete(&self, _creator: &User_, _context: &LemmyContext) -> Result<(), LemmyError> {
- unimplemented!()
- }
-
- async fn send_undo_delete(
- &self,
- _creator: &User_,
- _context: &LemmyContext,
- ) -> Result<(), LemmyError> {
- unimplemented!()
- }
-
- async fn send_remove(&self, _creator: &User_, _context: &LemmyContext) -> Result<(), LemmyError> {
- unimplemented!()
- }
-
- async fn send_undo_remove(
- &self,
- _creator: &User_,
- _context: &LemmyContext,
- ) -> Result<(), LemmyError> {
- unimplemented!()
- }
-
- async fn send_accept_follow(
- &self,
- _follow: Follow,
- _context: &LemmyContext,
- ) -> Result<(), LemmyError> {
- unimplemented!()
- }
-
- async fn get_follower_inboxes(&self, _pool: &DbPool) -> Result<Vec<Url>, LemmyError> {
- unimplemented!()
- }
-
- fn user_id(&self) -> i32 {
- self.id
- }
-}
-
#[async_trait::async_trait(?Send)]
impl FromApub for UserForm {
type ApubType = PersonExt;
})
}
}
-
-/// Return the user json over HTTP.
-pub async fn get_apub_user_http(
- info: web::Path<UserQuery>,
- context: web::Data<LemmyContext>,
-) -> Result<HttpResponse<Body>, LemmyError> {
- let user_name = info.into_inner().user_name;
- let user = blocking(context.pool(), move |conn| {
- User_::find_by_email_or_username(conn, &user_name)
- })
- .await??;
- let u = user.to_apub(context.pool()).await?;
- Ok(create_apub_response(&u))
-}
use actix_web::*;
use http_signature_normalization_actix::digest::middleware::VerifyDigest;
use lemmy_apub::{
- comment::get_apub_comment,
- community::*,
+ http::{
+ comment::get_apub_comment,
+ community::{get_apub_community_followers, get_apub_community_http, get_apub_community_outbox},
+ post::get_apub_post,
+ user::get_apub_user_http,
+ },
inbox::{community_inbox::community_inbox, shared_inbox::shared_inbox, user_inbox::user_inbox},
- post::get_apub_post,
- user::*,
APUB_JSON_CONTENT_TYPE,
};
use lemmy_utils::settings::Settings;