source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858"
dependencies = [
- "darling_core",
- "darling_macro",
+ "darling_core 0.10.2",
+ "darling_macro 0.10.2",
+]
+
+[[package]]
+name = "darling"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "757c0ded2af11d8e739c4daea1ac623dd1624b06c844cf3f5a39f1bdbd99bb12"
+dependencies = [
+ "darling_core 0.13.0",
+ "darling_macro 0.13.0",
]
[[package]]
"ident_case",
"proc-macro2 1.0.27",
"quote 1.0.9",
- "strsim",
+ "strsim 0.9.3",
+ "syn 1.0.73",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c34d8efb62d0c2d7f60ece80f75e5c63c1588ba68032740494b0b9a996466e3"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2 1.0.27",
+ "quote 1.0.9",
+ "strsim 0.10.0",
"syn 1.0.73",
]
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72"
dependencies = [
- "darling_core",
+ "darling_core 0.10.2",
+ "quote 1.0.9",
+ "syn 1.0.73",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ade7bff147130fe5e6d39f089c6bd49ec0250f35d70b2eebf72afdfc919f15cc"
+dependencies = [
+ "darling_core 0.13.0",
"quote 1.0.9",
"syn 1.0.73",
]
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2658621297f2cf68762a6f7dc0bb7e1ff2cfd6583daef8ee0fed6f7ec468ec0"
dependencies = [
- "darling",
+ "darling 0.10.2",
"derive_builder_core",
"proc-macro2 1.0.27",
"quote 1.0.9",
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2791ea3e372c8495c0bc2033991d76b512cd799d07491fbd6890124db9458bef"
dependencies = [
- "darling",
+ "darling 0.10.2",
"proc-macro2 1.0.27",
"quote 1.0.9",
"syn 1.0.73",
version = "0.11.3"
dependencies = [
"activitystreams",
- "activitystreams-ext",
"actix",
"actix-rt",
"actix-web",
"reqwest",
"serde",
"serde_json",
+ "serde_with",
"sha2",
"strum",
"strum_macros",
"webpki",
]
+[[package]]
+name = "rustversion"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088"
+
[[package]]
name = "ryu"
version = "1.0.5"
"serde",
]
+[[package]]
+name = "serde_with"
+version = "1.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ad9fdbb69badc8916db738c25efd04f0a65297d26c2f8de4b62e57b8c12bc72"
+dependencies = [
+ "rustversion",
+ "serde",
+ "serde_with_macros",
+]
+
+[[package]]
+name = "serde_with_macros"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1569374bd54623ec8bd592cf22ba6e03c0f177ff55fbc8c29a49e296e7adecf"
+dependencies = [
+ "darling 0.13.0",
+ "proc-macro2 1.0.27",
+ "quote 1.0.9",
+ "syn 1.0.73",
+]
+
[[package]]
name = "serial_test"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c"
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
[[package]]
name = "strum"
version = "0.21.0"
get_local_user_view_from_jwt,
is_mod_or_admin,
};
-use lemmy_apub::{ActorType, CommunityType, UserType};
+use lemmy_apub::{
+ activities::following::{
+ follow::FollowCommunity as FollowCommunityApub,
+ undo::UndoFollowCommunity,
+ },
+ CommunityType,
+};
use lemmy_db_queries::{
source::{comment::Comment_, community::CommunityModerator_, post::Post_},
Bannable,
} else if data.follow {
// Dont actually add to the community followers here, because you need
// to wait for the accept
- local_user_view
- .person
- .send_follow(&community.actor_id(), context)
- .await?;
+ FollowCommunityApub::send(&local_user_view.person, &community, context).await?;
} else {
- local_user_view
- .person
- .send_unfollow(&community.actor_id(), context)
- .await?;
+ UndoFollowCommunity::send(&local_user_view.person, &community, context).await?;
let unfollow = move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
if blocking(context.pool(), unfollow).await?.is_err() {
return Err(ApiError::err("community_follower_already_exists").into());
get_local_user_view_from_jwt,
person::{DeletePrivateMessage, PrivateMessageResponse},
};
-use lemmy_apub::ApubObjectType;
+use lemmy_apub::activities::private_message::{
+ delete::DeletePrivateMessage as DeletePrivateMessageApub,
+ undo_delete::UndoDeletePrivateMessage,
+};
use lemmy_db_queries::{source::private_message::PrivateMessage_, Crud, DeleteableOrRemoveable};
use lemmy_db_schema::source::private_message::PrivateMessage;
use lemmy_db_views::{local_user_view::LocalUserView, private_message_view::PrivateMessageView};
// Send the apub update
if data.deleted {
- updated_private_message
- .blank_out_deleted_or_removed_info()
- .send_delete(&local_user_view.person, context)
- .await?;
+ DeletePrivateMessageApub::send(
+ &local_user_view.person,
+ &updated_private_message.blank_out_deleted_or_removed_info(),
+ context,
+ )
+ .await?;
} else {
- updated_private_message
- .send_undo_delete(&local_user_view.person, context)
+ UndoDeletePrivateMessage::send(&local_user_view.person, &updated_private_message, context)
.await?;
}
lemmy_websocket = { version = "=0.11.3", path = "../websocket" }
diesel = "1.4.7"
activitystreams = "0.7.0-alpha.11"
-activitystreams-ext = "0.1.0-alpha.2"
bcrypt = "0.10.0"
chrono = { version = "0.4.19", features = ["serde"] }
serde_json = { version = "1.0.64", features = ["preserve_order"] }
serde = { version = "1.0.126", features = ["derive"] }
+serde_with = "1.9.4"
actix = "0.12.0"
actix-web = { version = "4.0.0-beta.8", default-features = false }
actix-rt = { version = "2.2.0", default-features = false }
request_counter,
)
.await?;
- verify_domains_match(&self.common.actor, &self.object.id)?;
+ verify_domains_match(&self.common.actor, self.object.id_unchecked())?;
// TODO: should add a check that the correct community is in cc (probably needs changes to
// comment deserialization)
self.object.verify(context, request_counter).await?;
}
async fn receive(
- &self,
+ self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
- let comment = Comment::from_apub(
- &self.object,
- context,
- self.common.actor.clone(),
- request_counter,
- false,
- )
- .await?;
+ let comment =
+ Comment::from_apub(&self.object, context, &self.common.actor, request_counter).await?;
let recipients =
get_notif_recipients(&self.common.actor, &comment, context, request_counter).await?;
let notif_type = match self.kind {
}
async fn receive(
- &self,
+ self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
.await??;
}
if community.local {
- let anybase = AnyBase::from_arbitrary_json(serde_json::to_string(self)?)?;
+ let anybase = AnyBase::from_arbitrary_json(serde_json::to_string(&self)?)?;
community
.send_announce(anybase, Some(self.object.clone()), context)
.await?;
}
async fn receive(
- &self,
+ self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
}
async fn receive(
- &self,
+ self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
}
async fn receive(
- &self,
+ self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_mod_action,
verify_person_in_community,
},
- objects::FromApubToForm,
- GroupExt,
+ objects::community::Group,
};
use activitystreams::activity::kind::UpdateType;
use lemmy_api_common::blocking;
#[serde(rename_all = "camelCase")]
pub struct UpdateCommunity {
to: PublicUrl,
- object: GroupExt,
+ object: Group,
cc: [Url; 1],
#[serde(rename = "type")]
kind: UpdateType,
}
async fn receive(
- &self,
+ self,
context: &LemmyContext,
- request_counter: &mut i32,
+ _request_counter: &mut i32,
) -> Result<(), LemmyError> {
let cc = self.cc[0].clone().into();
let community = blocking(context.pool(), move |conn| {
})
.await??;
- let updated_community = CommunityForm::from_apub(
- &self.object,
- context,
- community.actor_id.clone().into(),
- request_counter,
- false,
- )
- .await?;
+ let updated_community =
+ Group::from_apub_to_form(&self.object, &community.actor_id.clone().into()).await?;
let cf = CommunityForm {
name: updated_community.name,
title: updated_community.title,
}
async fn receive(
- &self,
+ self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
}
async fn receive(
- &self,
+ self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
use crate::{
- activities::{following::follow::FollowCommunity, verify_activity, verify_community},
+ activities::{
+ following::follow::FollowCommunity,
+ generate_activity_id,
+ verify_activity,
+ verify_community,
+ },
+ activity_queue::send_activity_new,
+ extensions::context::lemmy_context,
fetcher::{community::get_or_fetch_and_upsert_community, person::get_or_fetch_and_upsert_person},
+ ActorType,
};
use activitystreams::activity::kind::AcceptType;
use lemmy_api_common::blocking;
use lemmy_apub_lib::{verify_urls_match, ActivityCommonFields, ActivityHandler};
-use lemmy_db_queries::Followable;
-use lemmy_db_schema::source::community::CommunityFollower;
+use lemmy_db_queries::{ApubObject, Followable};
+use lemmy_db_schema::source::{
+ community::{Community, CommunityFollower},
+ person::Person,
+};
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use url::Url;
common: ActivityCommonFields,
}
+impl AcceptFollowCommunity {
+ pub async fn send(follow: FollowCommunity, context: &LemmyContext) -> Result<(), LemmyError> {
+ let community_id = follow.object.clone();
+ let community = blocking(context.pool(), move |conn| {
+ Community::read_from_apub_id(conn, &community_id.into())
+ })
+ .await??;
+ let person_id = follow.common.actor.clone();
+ let person = blocking(context.pool(), move |conn| {
+ Person::read_from_apub_id(conn, &person_id.into())
+ })
+ .await??;
+
+ let id = generate_activity_id(AcceptType::Accept)?;
+ let accept = AcceptFollowCommunity {
+ to: person.actor_id(),
+ object: follow,
+ kind: AcceptType::Accept,
+ common: ActivityCommonFields {
+ context: lemmy_context(),
+ id: id.clone(),
+ actor: community.actor_id(),
+ unparsed: Default::default(),
+ },
+ };
+ let inbox = vec![person.inbox_url.into()];
+ send_activity_new(context, &accept, &id, &community, inbox, true).await
+ }
+}
/// Handle accepted follows
#[async_trait::async_trait(?Send)]
impl ActivityHandler for AcceptFollowCommunity {
}
async fn receive(
- &self,
+ self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
use crate::{
- activities::{verify_activity, verify_person},
+ activities::{
+ following::accept::AcceptFollowCommunity,
+ generate_activity_id,
+ verify_activity,
+ verify_person,
+ },
+ activity_queue::send_activity_new,
+ extensions::context::lemmy_context,
fetcher::{community::get_or_fetch_and_upsert_community, person::get_or_fetch_and_upsert_person},
- CommunityType,
+ ActorType,
};
-use activitystreams::{
- activity::{kind::FollowType, Follow},
- base::{AnyBase, ExtendsExt},
-};
-use anyhow::Context;
+use activitystreams::activity::kind::FollowType;
use lemmy_api_common::blocking;
use lemmy_apub_lib::{verify_urls_match, ActivityCommonFields, ActivityHandler};
use lemmy_db_queries::Followable;
-use lemmy_db_schema::source::community::{CommunityFollower, CommunityFollowerForm};
-use lemmy_utils::{location_info, LemmyError};
+use lemmy_db_schema::source::{
+ community::{Community, CommunityFollower, CommunityFollowerForm},
+ person::Person,
+};
+use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use url::Url;
pub(in crate::activities::following) to: Url,
pub(in crate::activities::following) object: Url,
#[serde(rename = "type")]
- kind: FollowType,
+ pub(in crate::activities::following) kind: FollowType,
#[serde(flatten)]
pub(in crate::activities::following) common: ActivityCommonFields,
}
+impl FollowCommunity {
+ pub async fn send(
+ actor: &Person,
+ community: &Community,
+ context: &LemmyContext,
+ ) -> Result<(), LemmyError> {
+ let community_follower_form = CommunityFollowerForm {
+ community_id: community.id,
+ person_id: actor.id,
+ pending: true,
+ };
+ blocking(context.pool(), move |conn| {
+ CommunityFollower::follow(conn, &community_follower_form).ok()
+ })
+ .await?;
+
+ let id = generate_activity_id(FollowType::Follow)?;
+ let follow = FollowCommunity {
+ to: community.actor_id(),
+ object: community.actor_id(),
+ kind: FollowType::Follow,
+ common: ActivityCommonFields {
+ context: lemmy_context(),
+ id: id.clone(),
+ actor: actor.actor_id(),
+ unparsed: Default::default(),
+ },
+ };
+ let inbox = vec![community.inbox_url.clone().into()];
+ send_activity_new(context, &follow, &id, actor, inbox, true).await
+ }
+}
+
#[async_trait::async_trait(?Send)]
impl ActivityHandler for FollowCommunity {
async fn verify(
}
async fn receive(
- &self,
+ self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
})
.await?;
- // TODO: avoid the conversion and pass our own follow struct directly
- let anybase = AnyBase::from_arbitrary_json(serde_json::to_string(self)?)?;
- let anybase = Follow::from_any_base(anybase)?.context(location_info!())?;
- community.send_accept_follow(anybase, context).await
+ AcceptFollowCommunity::send(self, context).await
}
fn common(&self) -> &ActivityCommonFields {
use crate::{
- activities::{following::follow::FollowCommunity, verify_activity, verify_person},
+ activities::{
+ following::follow::FollowCommunity,
+ generate_activity_id,
+ verify_activity,
+ verify_person,
+ },
+ activity_queue::send_activity_new,
+ extensions::context::lemmy_context,
fetcher::{community::get_or_fetch_and_upsert_community, person::get_or_fetch_and_upsert_person},
+ ActorType,
};
-use activitystreams::activity::kind::UndoType;
+use activitystreams::activity::kind::{FollowType, UndoType};
use lemmy_api_common::blocking;
use lemmy_apub_lib::{verify_urls_match, ActivityCommonFields, ActivityHandler};
use lemmy_db_queries::Followable;
-use lemmy_db_schema::source::community::{CommunityFollower, CommunityFollowerForm};
+use lemmy_db_schema::source::{
+ community::{Community, CommunityFollower, CommunityFollowerForm},
+ person::Person,
+};
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use url::Url;
common: ActivityCommonFields,
}
+impl UndoFollowCommunity {
+ pub async fn send(
+ actor: &Person,
+ community: &Community,
+ context: &LemmyContext,
+ ) -> Result<(), LemmyError> {
+ let id = generate_activity_id(UndoType::Undo)?;
+ let undo = UndoFollowCommunity {
+ to: community.actor_id(),
+ object: FollowCommunity {
+ to: community.actor_id(),
+ object: community.actor_id(),
+ kind: FollowType::Follow,
+ common: ActivityCommonFields {
+ context: lemmy_context(),
+ id: generate_activity_id(FollowType::Follow)?,
+ actor: actor.actor_id(),
+ unparsed: Default::default(),
+ },
+ },
+ kind: UndoType::Undo,
+ common: ActivityCommonFields {
+ context: lemmy_context(),
+ id: id.clone(),
+ actor: actor.actor_id(),
+ unparsed: Default::default(),
+ },
+ };
+ let inbox = vec![community.get_shared_inbox_or_inbox_url()];
+ send_activity_new(context, &undo, &id, actor, inbox, true).await
+ }
+}
+
#[async_trait::async_trait(?Send)]
impl ActivityHandler for UndoFollowCommunity {
async fn verify(
}
async fn receive(
- &self,
+ self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_person_in_community(&self.common.actor, &community_id, context, request_counter).await?;
match self.kind {
CreateOrUpdateType::Create => {
- verify_domains_match(&self.common.actor, &self.object.id)?;
+ verify_domains_match(&self.common.actor, self.object.id_unchecked())?;
verify_urls_match(&self.common.actor, &self.object.attributed_to)?;
// Check that the post isnt locked or stickied, as that isnt possible for newly created posts.
// However, when fetching a remote post we generate a new create activity with the current
if is_mod_action {
verify_mod_action(&self.common.actor, community_id, context).await?;
} else {
- verify_domains_match(&self.common.actor, &self.object.id)?;
+ verify_domains_match(&self.common.actor, self.object.id_unchecked())?;
verify_urls_match(&self.common.actor, &self.object.attributed_to)?;
}
}
}
async fn receive(
- &self,
+ self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let actor =
get_or_fetch_and_upsert_person(&self.common.actor, context, request_counter).await?;
- let post = Post::from_apub(
- &self.object,
- context,
- actor.actor_id(),
- request_counter,
- false,
- )
- .await?;
+ let post = Post::from_apub(&self.object, context, &actor.actor_id(), request_counter).await?;
let notif_type = match self.kind {
CreateOrUpdateType::Create => UserOperationCrud::CreatePost,
unparsed: Default::default(),
},
};
- send_activity_new(
- context,
- &create_or_update,
- &id,
- actor,
- vec![recipient.get_shared_inbox_or_inbox_url()],
- true,
- )
- .await
+ let inbox = vec![recipient.get_shared_inbox_or_inbox_url()];
+ send_activity_new(context, &create_or_update, &id, actor, inbox, true).await
}
}
#[async_trait::async_trait(?Send)]
) -> Result<(), LemmyError> {
verify_activity(self.common())?;
verify_person(&self.common.actor, context, request_counter).await?;
- verify_domains_match(&self.common.actor, &self.object.id)?;
+ verify_domains_match(&self.common.actor, self.object.id_unchecked())?;
self.object.verify(context, request_counter).await?;
Ok(())
}
async fn receive(
- &self,
+ self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
- let private_message = PrivateMessage::from_apub(
- &self.object,
- context,
- self.common.actor.clone(),
- request_counter,
- false,
- )
- .await?;
+ let private_message =
+ PrivateMessage::from_apub(&self.object, context, &self.common.actor, request_counter).await?;
let notif_type = match self.kind {
CreateOrUpdateType::Create => UserOperationCrud::CreatePrivateMessage,
-use crate::activities::{private_message::send_websocket_message, verify_activity, verify_person};
+use crate::{
+ activities::{
+ generate_activity_id,
+ private_message::send_websocket_message,
+ verify_activity,
+ verify_person,
+ },
+ activity_queue::send_activity_new,
+ extensions::context::lemmy_context,
+ ActorType,
+};
use activitystreams::activity::kind::DeleteType;
use lemmy_api_common::blocking;
use lemmy_apub_lib::{verify_domains_match, ActivityCommonFields, ActivityHandler};
-use lemmy_db_queries::{source::private_message::PrivateMessage_, ApubObject};
-use lemmy_db_schema::source::private_message::PrivateMessage;
+use lemmy_db_queries::{source::private_message::PrivateMessage_, ApubObject, Crud};
+use lemmy_db_schema::source::{person::Person, private_message::PrivateMessage};
use lemmy_utils::LemmyError;
use lemmy_websocket::{LemmyContext, UserOperationCrud};
use url::Url;
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DeletePrivateMessage {
- to: Url,
+ pub(in crate::activities::private_message) to: Url,
pub(in crate::activities::private_message) object: Url,
#[serde(rename = "type")]
- kind: DeleteType,
+ pub(in crate::activities::private_message) kind: DeleteType,
#[serde(flatten)]
pub(in crate::activities::private_message) common: ActivityCommonFields,
}
+impl DeletePrivateMessage {
+ pub async fn send(
+ actor: &Person,
+ pm: &PrivateMessage,
+ context: &LemmyContext,
+ ) -> Result<(), LemmyError> {
+ let recipient_id = pm.recipient_id;
+ let recipient =
+ blocking(context.pool(), move |conn| Person::read(conn, recipient_id)).await??;
+
+ let id = generate_activity_id(DeleteType::Delete)?;
+ let delete = DeletePrivateMessage {
+ to: actor.actor_id(),
+ object: pm.ap_id.clone().into(),
+ kind: DeleteType::Delete,
+ common: ActivityCommonFields {
+ context: lemmy_context(),
+ id: id.clone(),
+ actor: actor.actor_id(),
+ unparsed: Default::default(),
+ },
+ };
+ let inbox = vec![recipient.get_shared_inbox_or_inbox_url()];
+ send_activity_new(context, &delete, &id, actor, inbox, true).await
+ }
+}
+
#[async_trait::async_trait(?Send)]
impl ActivityHandler for DeletePrivateMessage {
async fn verify(
}
async fn receive(
- &self,
+ self,
context: &LemmyContext,
_request_counter: &mut i32,
) -> Result<(), LemmyError> {
-use crate::activities::{
- private_message::{delete::DeletePrivateMessage, send_websocket_message},
- verify_activity,
- verify_person,
+use crate::{
+ activities::{
+ generate_activity_id,
+ private_message::{delete::DeletePrivateMessage, send_websocket_message},
+ verify_activity,
+ verify_person,
+ },
+ activity_queue::send_activity_new,
+ extensions::context::lemmy_context,
+ ActorType,
};
-use activitystreams::activity::kind::UndoType;
+use activitystreams::activity::kind::{DeleteType, UndoType};
use lemmy_api_common::blocking;
use lemmy_apub_lib::{
verify_domains_match,
ActivityCommonFields,
ActivityHandler,
};
-use lemmy_db_queries::{source::private_message::PrivateMessage_, ApubObject};
-use lemmy_db_schema::source::private_message::PrivateMessage;
+use lemmy_db_queries::{source::private_message::PrivateMessage_, ApubObject, Crud};
+use lemmy_db_schema::source::{person::Person, private_message::PrivateMessage};
use lemmy_utils::LemmyError;
use lemmy_websocket::{LemmyContext, UserOperationCrud};
use url::Url;
common: ActivityCommonFields,
}
+impl UndoDeletePrivateMessage {
+ pub async fn send(
+ actor: &Person,
+ pm: &PrivateMessage,
+ context: &LemmyContext,
+ ) -> Result<(), LemmyError> {
+ let recipient_id = pm.recipient_id;
+ let recipient =
+ blocking(context.pool(), move |conn| Person::read(conn, recipient_id)).await??;
+
+ let object = DeletePrivateMessage {
+ to: recipient.actor_id(),
+ object: pm.ap_id.clone().into(),
+ kind: DeleteType::Delete,
+ common: ActivityCommonFields {
+ context: lemmy_context(),
+ id: generate_activity_id(DeleteType::Delete)?,
+ actor: actor.actor_id(),
+ unparsed: Default::default(),
+ },
+ };
+
+ let id = generate_activity_id(UndoType::Undo)?;
+ let undo = UndoDeletePrivateMessage {
+ to: recipient.actor_id(),
+ object,
+ kind: UndoType::Undo,
+ common: ActivityCommonFields {
+ context: lemmy_context(),
+ id: id.clone(),
+ actor: actor.actor_id(),
+ unparsed: Default::default(),
+ },
+ };
+ let inbox = vec![recipient.get_shared_inbox_or_inbox_url()];
+ send_activity_new(context, &undo, &id, actor, inbox, true).await
+ }
+}
+
#[async_trait::async_trait(?Send)]
impl ActivityHandler for UndoDeletePrivateMessage {
async fn verify(
}
async fn receive(
- &self,
+ self,
context: &LemmyContext,
_request_counter: &mut i32,
) -> Result<(), LemmyError> {
}
async fn receive(
- &self,
+ self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
CommunityModerator::leave(conn, &form)
})
.await??;
- let anybase = AnyBase::from_arbitrary_json(serde_json::to_string(self)?)?;
+ let anybase = AnyBase::from_arbitrary_json(serde_json::to_string(&self)?)?;
community
.send_announce(anybase, Some(self.object.clone()), context)
.await?;
}
async fn receive(
- &self,
+ self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
use crate::{
activities::generate_activity_id,
- activity_queue::{send_activity_single_dest, send_to_community, send_to_community_followers},
+ activity_queue::{send_to_community, send_to_community_followers},
check_is_apub_id_valid,
extensions::context::lemmy_context,
- fetcher::{get_or_fetch_and_upsert_actor, person::get_or_fetch_and_upsert_person},
+ fetcher::get_or_fetch_and_upsert_actor,
generate_moderators_url,
insert_activity,
objects::ToApub,
use activitystreams::{
activity::{
kind::{
- AcceptType,
AddType,
AnnounceType,
BlockType,
UndoType,
UpdateType,
},
- Accept,
- ActorAndObjectRefExt,
Add,
Announce,
Block,
Delete,
- Follow,
OptTargetRefExt,
Remove,
Undo,
self.followers_url.clone().into()
}
- /// As a local community, accept the follow request from a remote person.
- 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 person = get_or_fetch_and_upsert_person(actor_uri, context, &mut 0).await?;
-
- let mut accept = Accept::new(
- self.actor_id.to_owned().into_inner(),
- follow.into_any_base()?,
- );
- accept
- .set_many_contexts(lemmy_context())
- .set_id(generate_activity_id(AcceptType::Accept)?)
- .set_to(person.actor_id());
-
- send_activity_single_dest(accept, self, person.inbox_url.into(), context).await?;
- Ok(())
- }
-
/// If a remote community is updated by a local mod, send the updated info to the community's
/// instance.
async fn send_update(&self, mod_: Person, context: &LemmyContext) -> Result<(), LemmyError> {
} else {
let mut update = Update::new(
mod_.actor_id(),
- self.to_apub(context.pool()).await?.into_any_base()?,
+ AnyBase::from_arbitrary_json(self.to_apub(context.pool()).await?)?,
);
update
.set_many_contexts(lemmy_context())
pub(crate) mod community;
pub(crate) mod person;
pub(crate) mod post;
-pub(crate) mod private_message;
-use crate::{
- activities::generate_activity_id,
- activity_queue::send_activity_single_dest,
- extensions::context::lemmy_context,
- ActorType,
- UserType,
-};
-use activitystreams::{
- activity::{
- kind::{FollowType, UndoType},
- Follow,
- Undo,
- },
- base::{BaseExt, ExtendsExt},
- object::ObjectExt,
-};
-use lemmy_api_common::blocking;
-use lemmy_db_queries::{ApubObject, Followable};
-use lemmy_db_schema::source::{
- community::{Community, CommunityFollower, CommunityFollowerForm},
- person::Person,
-};
-use lemmy_utils::LemmyError;
-use lemmy_websocket::LemmyContext;
+use crate::ActorType;
+use lemmy_db_schema::source::person::Person;
use url::Url;
impl ActorType for Person {
.into()
}
}
-
-#[async_trait::async_trait(?Send)]
-impl UserType for Person {
- /// As a given local person, send out a follow request to a remote community.
- async fn send_follow(
- &self,
- follow_actor_id: &Url,
- context: &LemmyContext,
- ) -> Result<(), LemmyError> {
- let follow_actor_id = follow_actor_id.to_owned();
- let community = blocking(context.pool(), move |conn| {
- Community::read_from_apub_id(conn, &follow_actor_id.into())
- })
- .await??;
-
- let community_follower_form = CommunityFollowerForm {
- community_id: community.id,
- person_id: self.id,
- pending: true,
- };
- blocking(context.pool(), move |conn| {
- CommunityFollower::follow(conn, &community_follower_form).ok()
- })
- .await?;
-
- let mut follow = Follow::new(self.actor_id(), community.actor_id());
- follow
- .set_many_contexts(lemmy_context())
- .set_id(generate_activity_id(FollowType::Follow)?)
- .set_to(community.actor_id());
-
- send_activity_single_dest(follow, self, community.inbox_url.into(), context).await?;
- Ok(())
- }
-
- async fn send_unfollow(
- &self,
- follow_actor_id: &Url,
- context: &LemmyContext,
- ) -> Result<(), LemmyError> {
- let follow_actor_id = follow_actor_id.to_owned();
- let community = blocking(context.pool(), move |conn| {
- Community::read_from_apub_id(conn, &follow_actor_id.into())
- })
- .await??;
-
- let mut follow = Follow::new(self.actor_id(), community.actor_id());
- follow
- .set_many_contexts(lemmy_context())
- .set_id(generate_activity_id(FollowType::Follow)?)
- .set_to(community.actor_id());
-
- // Undo that fake activity
- let mut undo = Undo::new(
- self.actor_id.to_owned().into_inner(),
- follow.into_any_base()?,
- );
- undo
- .set_many_contexts(lemmy_context())
- .set_id(generate_activity_id(UndoType::Undo)?)
- .set_to(community.actor_id());
-
- send_activity_single_dest(undo, self, community.inbox_url.into(), context).await?;
- Ok(())
- }
-}
+++ /dev/null
-use crate::{
- activities::generate_activity_id,
- activity_queue::send_activity_single_dest,
- extensions::context::lemmy_context,
- ActorType,
- ApubObjectType,
-};
-use activitystreams::{
- activity::{
- kind::{DeleteType, UndoType},
- Delete,
- Undo,
- },
- base::{BaseExt, ExtendsExt},
- object::ObjectExt,
-};
-use lemmy_api_common::blocking;
-use lemmy_db_queries::Crud;
-use lemmy_db_schema::source::{person::Person, private_message::PrivateMessage};
-use lemmy_utils::LemmyError;
-use lemmy_websocket::LemmyContext;
-
-#[async_trait::async_trait(?Send)]
-impl ApubObjectType for PrivateMessage {
- async fn send_delete(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
- let recipient_id = self.recipient_id;
- let recipient =
- blocking(context.pool(), move |conn| Person::read(conn, recipient_id)).await??;
-
- let mut delete = Delete::new(
- creator.actor_id.to_owned().into_inner(),
- self.ap_id.to_owned().into_inner(),
- );
- delete
- .set_many_contexts(lemmy_context())
- .set_id(generate_activity_id(DeleteType::Delete)?)
- .set_to(recipient.actor_id());
-
- send_activity_single_dest(delete, creator, recipient.inbox_url.into(), context).await?;
- Ok(())
- }
-
- async fn send_undo_delete(
- &self,
- creator: &Person,
- context: &LemmyContext,
- ) -> Result<(), LemmyError> {
- let recipient_id = self.recipient_id;
- let recipient =
- blocking(context.pool(), move |conn| Person::read(conn, recipient_id)).await??;
-
- let mut delete = Delete::new(
- creator.actor_id.to_owned().into_inner(),
- self.ap_id.to_owned().into_inner(),
- );
- delete
- .set_many_contexts(lemmy_context())
- .set_id(generate_activity_id(DeleteType::Delete)?)
- .set_to(recipient.actor_id());
-
- // Undo that fake activity
- let mut undo = Undo::new(
- creator.actor_id.to_owned().into_inner(),
- delete.into_any_base()?,
- );
- undo
- .set_many_contexts(lemmy_context())
- .set_id(generate_activity_id(UndoType::Undo)?)
- .set_to(recipient.actor_id());
-
- send_activity_single_dest(undo, creator, recipient.inbox_url.into(), context).await?;
- Ok(())
- }
-
- async fn send_remove(&self, _mod_: &Person, _context: &LemmyContext) -> Result<(), LemmyError> {
- unimplemented!()
- }
-
- async fn send_undo_remove(
- &self,
- _mod_: &Person,
- _context: &LemmyContext,
- ) -> Result<(), LemmyError> {
- unimplemented!()
- }
-}
}
async fn receive(
- &self,
+ self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
}
async fn receive(
- &self,
+ self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
use std::{collections::BTreeMap, env, fmt::Debug, future::Future, pin::Pin};
use url::Url;
-/// Sends a local activity to a single, remote actor.
-///
-/// * `activity` the apub activity to be sent
-/// * `creator` the local actor which created the activity
-/// * `inbox` the inbox url where the activity should be delivered to
-pub(crate) async fn send_activity_single_dest<T, Kind>(
- activity: T,
- creator: &dyn ActorType,
- inbox: Url,
- context: &LemmyContext,
-) -> Result<(), LemmyError>
-where
- T: AsObject<Kind> + Extends<Kind> + Debug + BaseExt<Kind>,
- Kind: Serialize,
- <T as Extends<Kind>>::Error: From<serde_json::Error> + Send + Sync + 'static,
-{
- if check_is_apub_id_valid(&inbox, false).is_ok() {
- debug!(
- "Sending activity {:?} to {}",
- &activity.id_unchecked().map(ToString::to_string),
- &inbox
- );
- send_activity_internal(context, activity, creator, vec![inbox], true, true).await?;
- }
-
- Ok(())
-}
-
/// From a local community, send activity to all remote followers.
///
/// * `activity` the apub activity to send
+++ /dev/null
-use activitystreams::unparsed::UnparsedMutExt;
-use activitystreams_ext::UnparsedExtension;
-use lemmy_utils::LemmyError;
-use serde::{Deserialize, Serialize};
-use url::Url;
-
-/// Activitystreams extension to allow (de)serializing additional Community field
-/// `sensitive` (called 'nsfw' in Lemmy).
-#[derive(Clone, Debug, Default, Deserialize, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct GroupExtension {
- pub sensitive: Option<bool>,
- pub moderators: Option<Url>,
-}
-
-impl GroupExtension {
- pub fn new(sensitive: bool, moderators_url: Url) -> Result<GroupExtension, LemmyError> {
- Ok(GroupExtension {
- sensitive: Some(sensitive),
- moderators: Some(moderators_url),
- })
- }
-}
-
-impl<U> UnparsedExtension<U> for GroupExtension
-where
- U: UnparsedMutExt,
-{
- type Error = serde_json::Error;
-
- fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> {
- Ok(GroupExtension {
- sensitive: unparsed_mut.remove("sensitive")?,
- moderators: unparsed_mut.remove("moderators")?,
- })
- }
-
- fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
- unparsed_mut.insert("sensitive", self.sensitive)?;
- unparsed_mut.insert("moderators", self.moderators)?;
- Ok(())
- }
-}
pub mod context;
-pub(crate) mod group_extension;
-pub(crate) mod person_extension;
pub mod signatures;
+++ /dev/null
-use activitystreams::unparsed::UnparsedMutExt;
-use activitystreams_ext::UnparsedExtension;
-use lemmy_utils::LemmyError;
-use serde::{Deserialize, Serialize};
-
-/// Activitystreams extension to allow (de)serializing additional Person field
-/// `also_known_as` (used for Matrix profile link).
-#[derive(Clone, Debug, Default, Deserialize, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct PersonExtension {
- pub matrix_user_id: Option<String>,
-}
-
-impl PersonExtension {
- pub fn new(matrix_user_id: Option<String>) -> Result<PersonExtension, LemmyError> {
- Ok(PersonExtension { matrix_user_id })
- }
-}
-
-impl<U> UnparsedExtension<U> for PersonExtension
-where
- U: UnparsedMutExt,
-{
- type Error = serde_json::Error;
-
- fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> {
- Ok(PersonExtension {
- matrix_user_id: unparsed_mut.remove("matrix_user_id")?,
- })
- }
-
- fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
- unparsed_mut.insert("matrix_user_id", self.matrix_user_id)?;
- Ok(())
- }
-}
-use activitystreams::unparsed::UnparsedMutExt;
-use activitystreams_ext::UnparsedExtension;
use actix_web::HttpRequest;
use anyhow::anyhow;
use http::{header::HeaderName, HeaderMap, HeaderValue};
}
}
-/// Extension for actor public key, which is needed on person and community for HTTP signatures.
-///
-/// Taken from: https://docs.rs/activitystreams/0.5.0-alpha.17/activitystreams/ext/index.html
-#[derive(Clone, Debug, Deserialize, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct PublicKeyExtension {
- pub public_key: PublicKey,
-}
-
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PublicKey {
pub owner: Url,
pub public_key_pem: String,
}
-
-impl PublicKey {
- pub fn to_ext(&self) -> PublicKeyExtension {
- PublicKeyExtension {
- public_key: self.to_owned(),
- }
- }
-}
-
-impl<U> UnparsedExtension<U> for PublicKeyExtension
-where
- U: UnparsedMutExt,
-{
- type Error = serde_json::Error;
-
- fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> {
- Ok(PublicKeyExtension {
- public_key: unparsed_mut.remove("publicKey")?,
- })
- }
-
- fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
- unparsed_mut.insert("publicKey", self.public_key)?;
- Ok(())
- }
-}
person::get_or_fetch_and_upsert_person,
should_refetch_actor,
},
- objects::FromApub,
- GroupExt,
-};
-use activitystreams::{
- actor::ApActorExt,
- collection::{CollectionExt, OrderedCollection},
+ objects::{community::Group, FromApub},
};
+use activitystreams::collection::{CollectionExt, OrderedCollection};
use anyhow::Context;
use diesel::result::Error::NotFound;
use lemmy_api_common::blocking;
old_community: Option<Community>,
request_counter: &mut i32,
) -> Result<Community, LemmyError> {
- let group = fetch_remote_object::<GroupExt>(context.client(), apub_id, request_counter).await;
+ let group = fetch_remote_object::<Group>(context.client(), apub_id, request_counter).await;
if let Some(c) = old_community.to_owned() {
if is_deleted(&group) {
}
let group = group?;
- let community =
- Community::from_apub(&group, context, apub_id.to_owned(), request_counter, false).await?;
+ let community = Community::from_apub(&group, context, apub_id, request_counter).await?;
update_community_mods(&group, &community, context, request_counter).await?;
// only fetch outbox for new communities, otherwise this can create an infinite loop
if old_community.is_none() {
- let outbox = group.inner.outbox()?.context(location_info!())?;
- fetch_community_outbox(context, outbox, request_counter).await?
+ fetch_community_outbox(context, &group.outbox, request_counter).await?
}
Ok(community)
}
async fn update_community_mods(
- group: &GroupExt,
+ group: &Group,
community: &Community,
context: &LemmyContext,
request_counter: &mut i32,
pub(crate) async fn fetch_community_mods(
context: &LemmyContext,
- group: &GroupExt,
+ group: &Group,
recursion_counter: &mut i32,
) -> Result<Vec<Url>, LemmyError> {
- if let Some(mods_url) = &group.ext_one.moderators {
+ if let Some(mods_url) = &group.moderators {
let mods =
fetch_remote_object::<OrderedCollection>(context.client(), mods_url, recursion_counter)
.await?;
debug!("Fetching and creating remote post: {}", post_ap_id);
let page =
fetch_remote_object::<Page>(context.client(), post_ap_id, recursion_counter).await?;
- let post = Post::from_apub(
- &page,
- context,
- post_ap_id.to_owned(),
- recursion_counter,
- false,
- )
- .await?;
+ let post = Post::from_apub(&page, context, post_ap_id, recursion_counter).await?;
Ok(post)
}
);
let comment =
fetch_remote_object::<Note>(context.client(), comment_ap_id, recursion_counter).await?;
- let comment = Comment::from_apub(
- &comment,
- context,
- comment_ap_id.to_owned(),
- recursion_counter,
- false,
- )
- .await?;
+ let comment = Comment::from_apub(&comment, context, comment_ap_id, recursion_counter).await?;
let post_id = comment.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
use crate::{
fetcher::{fetch::fetch_remote_object, is_deleted, should_refetch_actor},
- objects::FromApub,
- PersonExt,
+ objects::{person::Person as ApubPerson, FromApub},
};
use anyhow::anyhow;
use diesel::result::Error::NotFound;
Ok(u) if !u.local && should_refetch_actor(u.last_refreshed_at) => {
debug!("Fetching and updating from remote person: {}", apub_id);
let person =
- fetch_remote_object::<PersonExt>(context.client(), apub_id, recursion_counter).await;
+ fetch_remote_object::<ApubPerson>(context.client(), apub_id, recursion_counter).await;
if is_deleted(&person) {
// TODO: use Person::update_deleted() once implemented
return Ok(u);
}
- let person = Person::from_apub(
- &person?,
- context,
- apub_id.to_owned(),
- recursion_counter,
- false,
- )
- .await?;
+ let person = Person::from_apub(&person?, context, apub_id, recursion_counter).await?;
let person_id = person.id;
blocking(context.pool(), move |conn| {
Err(NotFound {}) => {
debug!("Fetching and creating remote person: {}", apub_id);
let person =
- fetch_remote_object::<PersonExt>(context.client(), apub_id, recursion_counter).await?;
+ fetch_remote_object::<ApubPerson>(context.client(), apub_id, recursion_counter).await?;
- let person = Person::from_apub(
- &person,
- context,
- apub_id.to_owned(),
- recursion_counter,
- false,
- )
- .await?;
+ let person = Person::from_apub(&person, context, apub_id, recursion_counter).await?;
Ok(person)
}
is_deleted,
},
find_object_by_id,
- objects::{comment::Note, post::Page, FromApub},
- GroupExt,
+ objects::{comment::Note, community::Group, person::Person as ApubPerson, post::Page, FromApub},
Object,
- PersonExt,
};
-use activitystreams::base::BaseExt;
-use anyhow::{anyhow, Context};
+use anyhow::anyhow;
use lemmy_api_common::{blocking, site::SearchResponse};
use lemmy_db_queries::{
source::{
#[derive(serde::Deserialize, Debug)]
#[serde(untagged)]
enum SearchAcceptedObjects {
- Person(Box<PersonExt>),
- Group(Box<GroupExt>),
+ Person(Box<ApubPerson>),
+ Group(Box<Group>),
Page(Box<Page>),
Comment(Box<Note>),
}
recursion_counter: &mut i32,
context: &LemmyContext,
) -> Result<SearchResponse, LemmyError> {
- let domain = query_url.domain().context("url has no domain")?;
let mut response = SearchResponse {
type_: SearchType::All.to_string(),
comments: vec![],
match fetch_response {
SearchAcceptedObjects::Person(p) => {
- let person_uri = p.inner.id(domain)?.context("person has no id")?;
-
- let person = get_or_fetch_and_upsert_person(person_uri, context, recursion_counter).await?;
+ let person_id = p.id(&query_url)?;
+ let person = get_or_fetch_and_upsert_person(person_id, context, recursion_counter).await?;
response.users = vec![
blocking(context.pool(), move |conn| {
];
}
SearchAcceptedObjects::Group(g) => {
- let community_uri = g.inner.id(domain)?.context("group has no id")?;
-
+ let community_uri = g.id(&query_url)?;
let community =
get_or_fetch_and_upsert_community(community_uri, context, recursion_counter).await?;
];
}
SearchAcceptedObjects::Page(p) => {
- let p = Post::from_apub(&p, context, query_url, recursion_counter, false).await?;
+ let p = Post::from_apub(&p, context, &query_url, recursion_counter).await?;
response.posts =
vec![blocking(context.pool(), move |conn| PostView::read(conn, p.id, None)).await??];
}
SearchAcceptedObjects::Comment(c) => {
- let c = Comment::from_apub(&c, context, query_url, recursion_counter, false).await?;
+ let c = Comment::from_apub(&c, context, &query_url, recursion_counter).await?;
response.comments = vec![
blocking(context.pool(), move |conn| {
pub mod migrations;
pub mod objects;
-use crate::{
- extensions::{
- group_extension::GroupExtension,
- person_extension::PersonExtension,
- signatures::{PublicKey, PublicKeyExtension},
- },
- fetcher::community::get_or_fetch_and_upsert_community,
-};
-use activitystreams::{
- activity::Follow,
- actor,
- base::AnyBase,
- object::{ApObject, AsObject, ObjectExt},
-};
-use activitystreams_ext::Ext2;
+use crate::extensions::signatures::PublicKey;
+use activitystreams::base::AnyBase;
use anyhow::{anyhow, Context};
use diesel::NotFound;
use lemmy_api_common::blocking;
use std::net::IpAddr;
use url::{ParseError, Url};
-/// Activitystreams type for community
-pub type GroupExt =
- Ext2<actor::ApActor<ApObject<actor::Group>>, GroupExtension, PublicKeyExtension>;
-/// Activitystreams type for person
-type PersonExt =
- Ext2<actor::ApActor<ApObject<actor::Actor<UserTypes>>>, PersonExtension, PublicKeyExtension>;
-pub type SiteExt = actor::ApActor<ApObject<actor::Service>>;
-
-#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize, PartialEq)]
-pub enum UserTypes {
- Person,
- Service,
-}
-
pub static APUB_JSON_CONTENT_TYPE: &str = "application/activity+json";
/// Checks if the ID is allowed for sending or receiving.
/// - URL being in the allowlist (if it is active)
/// - URL not being in the blocklist (if it is active)
///
-pub fn check_is_apub_id_valid(apub_id: &Url, use_strict_allowlist: bool) -> Result<(), LemmyError> {
+pub(crate) fn check_is_apub_id_valid(
+ apub_id: &Url,
+ use_strict_allowlist: bool,
+) -> Result<(), LemmyError> {
let settings = Settings::get();
let domain = apub_id.domain().context(location_info!())?.to_string();
let local_instance = settings.get_hostname_without_port()?;
Ok(Url::parse(&format!("{}/outbox", &self.actor_id()))?)
}
- fn get_public_key_ext(&self) -> Result<PublicKeyExtension, LemmyError> {
- Ok(
- PublicKey {
- id: format!("{}#main-key", self.actor_id()),
- owner: self.actor_id(),
- public_key_pem: self.public_key().context(location_info!())?,
- }
- .to_ext(),
- )
+ fn get_public_key(&self) -> Result<PublicKey, LemmyError> {
+ Ok(PublicKey {
+ id: format!("{}#main-key", self.actor_id()),
+ owner: self.actor_id(),
+ public_key_pem: self.public_key().context(location_info!())?,
+ })
}
}
pub trait CommunityType {
fn followers_url(&self) -> Url;
async fn get_follower_inboxes(&self, pool: &DbPool) -> Result<Vec<Url>, LemmyError>;
- async fn send_accept_follow(
- &self,
- follow: Follow,
- context: &LemmyContext,
- ) -> Result<(), LemmyError>;
async fn send_update(&self, mod_: Person, context: &LemmyContext) -> Result<(), LemmyError>;
async fn send_delete(&self, mod_: Person, context: &LemmyContext) -> Result<(), LemmyError>;
) -> Result<(), LemmyError>;
}
-#[async_trait::async_trait(?Send)]
-pub trait UserType {
- async fn send_follow(
- &self,
- follow_actor_id: &Url,
- context: &LemmyContext,
- ) -> Result<(), LemmyError>;
- async fn send_unfollow(
- &self,
- follow_actor_id: &Url,
- context: &LemmyContext,
- ) -> Result<(), LemmyError>;
-}
-
pub enum EndpointType {
Community,
Person,
}
/// Generates an apub endpoint for a given domain, IE xyz.tld
-pub fn generate_apub_endpoint_for_domain(
+pub(crate) fn generate_apub_endpoint_for_domain(
endpoint_type: EndpointType,
name: &str,
domain: &str,
/// Store a sent or received activity in the database, for logging purposes. These records are not
/// persistent.
-pub async fn insert_activity<T>(
+pub(crate) async fn insert_activity<T>(
ap_id: &Url,
activity: T,
local: bool,
/// Tries to find a post or comment in the local database, without any network requests.
/// This is used to handle deletions and removals, because in case we dont have the object, we can
/// simply ignore the activity.
-pub async fn find_post_or_comment_by_id(
+pub(crate) async fn find_post_or_comment_by_id(
context: &LemmyContext,
apub_id: Url,
) -> Result<PostOrComment, LemmyError> {
PrivateMessage(Box<PrivateMessage>),
}
-pub async fn find_object_by_id(context: &LemmyContext, apub_id: Url) -> Result<Object, LemmyError> {
+pub(crate) async fn find_object_by_id(
+ context: &LemmyContext,
+ apub_id: Url,
+) -> Result<Object, LemmyError> {
let ap_id = apub_id.clone();
if let Ok(pc) = find_post_or_comment_by_id(context, ap_id.to_owned()).await {
return Ok(match pc {
Err(NotFound.into())
}
-pub async fn check_community_or_site_ban(
+pub(crate) async fn check_community_or_site_ban(
person: &Person,
community_id: CommunityId,
pool: &DbPool,
Ok(())
}
-
-pub fn get_activity_to_and_cc<T, Kind>(activity: &T) -> Vec<Url>
-where
- T: AsObject<Kind>,
-{
- let mut to_and_cc = vec![];
- if let Some(to) = activity.to() {
- let to = to.to_owned().unwrap_to_vec();
- let mut to = to
- .iter()
- .map(|t| t.as_xsd_any_uri())
- .flatten()
- .map(|t| t.to_owned())
- .collect();
- to_and_cc.append(&mut to);
- }
- if let Some(cc) = activity.cc() {
- let cc = cc.to_owned().unwrap_to_vec();
- let mut cc = cc
- .iter()
- .map(|c| c.as_xsd_any_uri())
- .flatten()
- .map(|c| c.to_owned())
- .collect();
- to_and_cc.append(&mut cc);
- }
- to_and_cc
-}
-
-pub async fn get_community_from_to_or_cc<T, Kind>(
- activity: &T,
- context: &LemmyContext,
- request_counter: &mut i32,
-) -> Result<Community, LemmyError>
-where
- T: AsObject<Kind>,
-{
- for cid in get_activity_to_and_cc(activity) {
- let community = get_or_fetch_and_upsert_community(&cid, context, request_counter).await;
- if community.is_ok() {
- return community;
- }
- }
- Err(NotFound.into())
-}
};
use lemmy_websocket::LemmyContext;
use serde::{Deserialize, Serialize};
+use serde_with::skip_serializing_none;
use std::ops::Deref;
use url::Url;
+#[skip_serializing_none]
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Note {
#[serde(rename = "@context")]
context: OneOrMany<AnyBase>,
r#type: NoteType,
- pub(crate) id: Url,
+ id: Url,
pub(crate) attributed_to: Url,
/// Indicates that the object is publicly readable. Unlike [`Post.to`], this one doesn't contain
/// the community ID, as it would be incompatible with Pleroma (and we can get the community from
}
impl Note {
+ pub(crate) fn id_unchecked(&self) -> &Url {
+ &self.id
+ }
+ pub(crate) fn id(&self, expected_domain: &Url) -> Result<&Url, LemmyError> {
+ verify_domains_match(&self.id, expected_domain)?;
+ Ok(&self.id)
+ }
+
async fn get_parents(
&self,
context: &LemmyContext,
async fn from_apub(
note: &Note,
context: &LemmyContext,
- _expected_domain: Url,
+ expected_domain: &Url,
request_counter: &mut i32,
- _mod_action_allowed: bool,
) -> Result<Comment, LemmyError> {
+ let ap_id = Some(note.id(expected_domain)?.clone().into());
let creator =
get_or_fetch_and_upsert_person(¬e.attributed_to, context, request_counter).await?;
let (post, parent_comment_id) = note.get_parents(context, request_counter).await?;
published: Some(note.published.naive_local()),
updated: note.updated.map(|u| u.to_owned().naive_local()),
deleted: None,
- ap_id: Some(note.id.clone().into()),
+ ap_id,
local: Some(false),
};
Ok(blocking(context.pool(), move |conn| Comment::upsert(conn, &form)).await??)
use crate::{
- extensions::{context::lemmy_context, group_extension::GroupExtension},
+ extensions::{context::lemmy_context, signatures::PublicKey},
fetcher::community::fetch_community_mods,
generate_moderators_url,
- objects::{
- check_object_domain,
- create_tombstone,
- get_object_from_apub,
- get_source_markdown_value,
- set_content_and_source,
- FromApub,
- FromApubToForm,
- ToApub,
- },
+ objects::{create_tombstone, FromApub, ImageObject, Source, ToApub},
ActorType,
- GroupExt,
};
use activitystreams::{
- actor::{kind::GroupType, ApActor, Endpoints, Group},
- base::BaseExt,
- object::{ApObject, Image, Tombstone},
- prelude::*,
+ actor::{kind::GroupType, Endpoints},
+ base::AnyBase,
+ object::{kind::ImageType, Tombstone},
+ primitives::OneOrMany,
+ unparsed::Unparsed,
};
-use activitystreams_ext::Ext2;
-use anyhow::Context;
+use chrono::{DateTime, FixedOffset};
use lemmy_api_common::blocking;
-use lemmy_db_queries::DbPool;
+use lemmy_apub_lib::{
+ values::{MediaTypeHtml, MediaTypeMarkdown},
+ verify_domains_match,
+};
+use lemmy_db_queries::{ApubObject, DbPool};
use lemmy_db_schema::{
naive_now,
source::community::{Community, CommunityForm},
};
-use lemmy_db_views_actor::community_moderator_view::CommunityModeratorView;
use lemmy_utils::{
- location_info,
- utils::{check_slurs, check_slurs_opt, convert_datetime},
+ utils::{check_slurs, check_slurs_opt, convert_datetime, markdown_to_html},
LemmyError,
};
use lemmy_websocket::LemmyContext;
+use serde::{Deserialize, Serialize};
+use serde_with::skip_serializing_none;
use url::Url;
-#[async_trait::async_trait(?Send)]
-impl ToApub for Community {
- type ApubType = GroupExt;
-
- async fn to_apub(&self, pool: &DbPool) -> Result<GroupExt, LemmyError> {
- let id = self.id;
- let moderators = blocking(pool, move |conn| {
- CommunityModeratorView::for_community(conn, id)
- })
- .await??;
- let moderators: Vec<Url> = moderators
- .into_iter()
- .map(|m| m.moderator.actor_id.into_inner())
- .collect();
-
- let mut group = ApObject::new(Group::new());
- group
- .set_many_contexts(lemmy_context())
- .set_id(self.actor_id.to_owned().into())
- .set_name(self.title.to_owned())
- .set_published(convert_datetime(self.published))
- // NOTE: included attritubed_to field for compatibility with lemmy v0.9.9
- .set_many_attributed_tos(moderators);
+#[skip_serializing_none]
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Group {
+ #[serde(rename = "@context")]
+ context: OneOrMany<AnyBase>,
+ #[serde(rename = "type")]
+ kind: GroupType,
+ id: Url,
+ /// username, set at account creation and can never be changed
+ preferred_username: String,
+ /// title (can be changed at any time)
+ name: String,
+ content: Option<String>,
+ media_type: Option<MediaTypeHtml>,
+ source: Option<Source>,
+ icon: Option<ImageObject>,
+ /// banner
+ image: Option<ImageObject>,
+ // lemmy extension
+ sensitive: Option<bool>,
+ // lemmy extension
+ pub(crate) moderators: Option<Url>,
+ inbox: Url,
+ pub(crate) outbox: Url,
+ followers: Url,
+ endpoints: Endpoints<Url>,
+ public_key: PublicKey,
+ published: DateTime<FixedOffset>,
+ updated: Option<DateTime<FixedOffset>>,
+ #[serde(flatten)]
+ unparsed: Unparsed,
+}
- if let Some(u) = self.updated.to_owned() {
- group.set_updated(convert_datetime(u));
- }
- if let Some(d) = self.description.to_owned() {
- set_content_and_source(&mut group, &d)?;
- }
+impl Group {
+ pub(crate) fn id(&self, expected_domain: &Url) -> Result<&Url, LemmyError> {
+ verify_domains_match(&self.id, expected_domain)?;
+ Ok(&self.id)
+ }
+ pub(crate) async fn from_apub_to_form(
+ group: &Group,
+ expected_domain: &Url,
+ ) -> Result<CommunityForm, LemmyError> {
+ let actor_id = Some(group.id(expected_domain)?.clone().into());
+ let name = group.preferred_username.clone();
+ let title = group.name.clone();
+ let description = group.source.clone().map(|s| s.content);
+ let shared_inbox = group.endpoints.shared_inbox.clone().map(|s| s.into());
- if let Some(icon_url) = &self.icon {
- let mut image = Image::new();
- image.set_url::<Url>(icon_url.to_owned().into());
- group.set_icon(image.into_any_base()?);
- }
+ check_slurs(&name)?;
+ check_slurs(&title)?;
+ check_slurs_opt(&description)?;
- if let Some(banner_url) = &self.banner {
- let mut image = Image::new();
- image.set_url::<Url>(banner_url.to_owned().into());
- group.set_image(image.into_any_base()?);
- }
+ Ok(CommunityForm {
+ name,
+ title,
+ description,
+ removed: None,
+ published: Some(group.published.naive_local()),
+ updated: group.updated.map(|u| u.naive_local()),
+ deleted: None,
+ nsfw: Some(group.sensitive.unwrap_or(false)),
+ actor_id,
+ local: Some(false),
+ private_key: None,
+ public_key: Some(group.public_key.public_key_pem.clone()),
+ last_refreshed_at: Some(naive_now()),
+ icon: Some(group.icon.clone().map(|i| i.url.into())),
+ banner: Some(group.image.clone().map(|i| i.url.into())),
+ followers_url: Some(group.followers.clone().into()),
+ inbox_url: Some(group.inbox.clone().into()),
+ shared_inbox_url: Some(shared_inbox),
+ })
+ }
+}
- let mut ap_actor = ApActor::new(self.inbox_url.clone().into(), group);
- ap_actor
- .set_preferred_username(self.name.to_owned())
- .set_outbox(self.get_outbox_url()?)
- .set_followers(self.followers_url.clone().into())
- .set_endpoints(Endpoints {
- shared_inbox: Some(self.get_shared_inbox_or_inbox_url()),
+#[async_trait::async_trait(?Send)]
+impl ToApub for Community {
+ type ApubType = Group;
+
+ async fn to_apub(&self, _pool: &DbPool) -> Result<Group, LemmyError> {
+ let source = self.description.clone().map(|bio| Source {
+ content: bio,
+ media_type: MediaTypeMarkdown::Markdown,
+ });
+ let icon = self.icon.clone().map(|url| ImageObject {
+ kind: ImageType::Image,
+ url: url.into(),
+ });
+ let image = self.banner.clone().map(|url| ImageObject {
+ kind: ImageType::Image,
+ url: url.into(),
+ });
+
+ let group = Group {
+ context: lemmy_context(),
+ kind: GroupType::Group,
+ id: self.actor_id(),
+ preferred_username: self.name.clone(),
+ name: self.title.clone(),
+ content: self.description.as_ref().map(|b| markdown_to_html(b)),
+ media_type: self.description.as_ref().map(|_| MediaTypeHtml::Html),
+ source,
+ icon,
+ image,
+ sensitive: Some(self.nsfw),
+ moderators: Some(generate_moderators_url(&self.actor_id)?.into()),
+ inbox: self.inbox_url.clone().into(),
+ outbox: self.get_outbox_url()?,
+ followers: self.followers_url.clone().into(),
+ endpoints: Endpoints {
+ shared_inbox: self.shared_inbox_url.clone().map(|s| s.into()),
..Default::default()
- });
-
- Ok(Ext2::new(
- ap_actor,
- GroupExtension::new(self.nsfw, generate_moderators_url(&self.actor_id)?.into())?,
- self.get_public_key_ext()?,
- ))
+ },
+ public_key: self.get_public_key()?,
+ published: convert_datetime(self.published),
+ updated: self.updated.map(convert_datetime),
+ unparsed: Default::default(),
+ };
+ Ok(group)
}
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
#[async_trait::async_trait(?Send)]
impl FromApub for Community {
- type ApubType = GroupExt;
+ type ApubType = Group;
/// Converts a `Group` to `Community`, inserts it into the database and updates moderators.
async fn from_apub(
- group: &GroupExt,
+ group: &Group,
context: &LemmyContext,
- expected_domain: Url,
+ expected_domain: &Url,
request_counter: &mut i32,
- mod_action_allowed: bool,
) -> Result<Community, LemmyError> {
- get_object_from_apub(
- group,
- context,
- expected_domain,
- request_counter,
- mod_action_allowed,
- )
- .await
- }
-}
-
-#[async_trait::async_trait(?Send)]
-impl FromApubToForm<GroupExt> for CommunityForm {
- async fn from_apub(
- group: &GroupExt,
- context: &LemmyContext,
- expected_domain: Url,
- request_counter: &mut i32,
- _mod_action_allowed: bool,
- ) -> Result<Self, LemmyError> {
fetch_community_mods(context, group, request_counter).await?;
+ let form = Group::from_apub_to_form(group, expected_domain).await?;
- let name = group
- .inner
- .preferred_username()
- .context(location_info!())?
- .to_string();
- let title = group
- .inner
- .name()
- .context(location_info!())?
- .as_one()
- .context(location_info!())?
- .as_xsd_string()
- .context(location_info!())?
- .to_string();
-
- let description = get_source_markdown_value(group)?;
-
- 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_owned().into()),
- ),
- 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_owned().into()),
- ),
- None => None,
- };
- let shared_inbox = group
- .inner
- .endpoints()?
- .map(|e| e.shared_inbox)
- .flatten()
- .map(|s| s.to_owned().into());
-
- Ok(CommunityForm {
- name,
- title,
- description,
- 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: Some(group.ext_one.sensitive.unwrap_or(false)),
- actor_id: Some(check_object_domain(group, expected_domain, true)?),
- local: Some(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,
- followers_url: Some(
- group
- .inner
- .followers()?
- .context(location_info!())?
- .to_owned()
- .into(),
- ),
- inbox_url: Some(group.inner.inbox()?.to_owned().into()),
- shared_inbox_url: Some(shared_inbox),
- })
+ let community = blocking(context.pool(), move |conn| Community::upsert(conn, &form)).await??;
+ Ok(community)
}
}
-use crate::{check_is_apub_id_valid, fetcher::person::get_or_fetch_and_upsert_person};
+use crate::fetcher::person::get_or_fetch_and_upsert_person;
use activitystreams::{
- base::{AsBase, BaseExt, ExtendsExt},
- markers::Base,
- mime::{FromStrError, Mime},
- object::{ApObjectExt, Object, ObjectExt, Tombstone, TombstoneExt},
+ base::BaseExt,
+ object::{kind::ImageType, Tombstone, TombstoneExt},
};
-use anyhow::{anyhow, Context};
+use anyhow::anyhow;
use chrono::NaiveDateTime;
-use lemmy_api_common::blocking;
use lemmy_apub_lib::values::MediaTypeMarkdown;
-use lemmy_db_queries::{ApubObject, Crud, DbPool};
-use lemmy_db_schema::DbUrl;
-use lemmy_utils::{
- location_info,
- settings::structs::Settings,
- utils::{convert_datetime, markdown_to_html},
- LemmyError,
-};
+use lemmy_db_queries::DbPool;
+use lemmy_utils::{utils::convert_datetime, LemmyError};
use lemmy_websocket::LemmyContext;
use url::Url;
async fn from_apub(
apub: &Self::ApubType,
context: &LemmyContext,
- expected_domain: Url,
+ expected_domain: &Url,
request_counter: &mut i32,
- mod_action_allowed: bool,
- ) -> Result<Self, LemmyError>
- where
- Self: Sized;
-}
-
-#[async_trait::async_trait(?Send)]
-pub trait FromApubToForm<ApubType> {
- async fn from_apub(
- apub: &ApubType,
- context: &LemmyContext,
- expected_domain: Url,
- request_counter: &mut i32,
- mod_action_allowed: bool,
) -> Result<Self, LemmyError>
where
Self: Sized;
media_type: MediaTypeMarkdown,
}
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ImageObject {
+ #[serde(rename = "type")]
+ kind: ImageType,
+ url: Url,
+}
+
/// Updated is actually the deletion time
fn create_tombstone<T>(
deleted: bool,
Err(anyhow!("Cant convert object to tombstone if it wasnt deleted").into())
}
}
-
-pub(in crate::objects) fn check_object_domain<T, Kind>(
- apub: &T,
- expected_domain: Url,
- use_strict_allowlist: bool,
-) -> Result<DbUrl, LemmyError>
-where
- T: Base + AsBase<Kind>,
-{
- let domain = expected_domain.domain().context(location_info!())?;
- let object_id = apub.id(domain)?.context(location_info!())?;
- check_is_apub_id_valid(object_id, use_strict_allowlist)?;
- Ok(object_id.to_owned().into())
-}
-
-pub(in crate::objects) fn set_content_and_source<T, Kind1, Kind2>(
- object: &mut T,
- markdown_text: &str,
-) -> Result<(), LemmyError>
-where
- T: ApObjectExt<Kind1> + ObjectExt<Kind2> + AsBase<Kind2>,
-{
- let mut source = Object::<()>::new_none_type();
- source
- .set_content(markdown_text)
- .set_media_type(mime_markdown()?);
- object.set_source(source.into_any_base()?);
-
- object.set_content(markdown_to_html(markdown_text));
- object.set_media_type(mime_html()?);
- Ok(())
-}
-
-pub(in crate::objects) fn get_source_markdown_value<T, Kind1, Kind2>(
- object: &T,
-) -> Result<Option<String>, LemmyError>
-where
- T: ApObjectExt<Kind1> + ObjectExt<Kind2> + AsBase<Kind2>,
-{
- let content = object
- .content()
- .map(|s| s.as_single_xsd_string().map(|s2| s2.to_string()))
- .flatten();
- if content.is_some() {
- let source = object.source().context(location_info!())?;
- let source = Object::<()>::from_any_base(source.to_owned())?.context(location_info!())?;
- check_is_markdown(source.media_type())?;
- let source_content = source
- .content()
- .map(|s| s.as_single_xsd_string().map(|s2| s2.to_string()))
- .flatten()
- .context(location_info!())?;
- return Ok(Some(source_content));
- }
- Ok(None)
-}
-
-fn mime_markdown() -> Result<Mime, FromStrError> {
- "text/markdown".parse()
-}
-
-fn mime_html() -> Result<Mime, FromStrError> {
- "text/html".parse()
-}
-
-pub(in crate::objects) fn check_is_markdown(mime: Option<&Mime>) -> Result<(), LemmyError> {
- let mime = mime.context(location_info!())?;
- if !mime.eq(&mime_markdown()?) {
- Err(LemmyError::from(anyhow!(
- "Lemmy only supports markdown content"
- )))
- } else {
- Ok(())
- }
-}
-
-/// Converts an ActivityPub object (eg `Note`) to a database object (eg `Comment`). If an object
-/// with the same ActivityPub ID already exists in the database, it is returned directly. Otherwise
-/// the apub object is parsed, inserted and returned.
-pub async fn get_object_from_apub<From, Kind, To, ToForm, IdType>(
- from: &From,
- context: &LemmyContext,
- expected_domain: Url,
- request_counter: &mut i32,
- is_mod_action: bool,
-) -> Result<To, LemmyError>
-where
- From: BaseExt<Kind>,
- To: ApubObject<ToForm> + Crud<ToForm, IdType> + Send + 'static,
- ToForm: FromApubToForm<From> + Send + 'static,
-{
- let object_id = from.id_unchecked().context(location_info!())?.to_owned();
- let domain = object_id.domain().context(location_info!())?;
-
- // if its a local object, return it directly from the database
- if Settings::get().hostname == domain {
- let object = blocking(context.pool(), move |conn| {
- To::read_from_apub_id(conn, &object_id.into())
- })
- .await??;
- Ok(object)
- }
- // otherwise parse and insert, assuring that it comes from the right domain
- else {
- let to_form = ToForm::from_apub(
- from,
- context,
- expected_domain,
- request_counter,
- is_mod_action,
- )
- .await?;
-
- let to = blocking(context.pool(), move |conn| To::upsert(conn, &to_form)).await??;
- Ok(to)
- }
-}
use crate::{
- extensions::{context::lemmy_context, person_extension::PersonExtension},
- objects::{
- check_object_domain,
- get_source_markdown_value,
- set_content_and_source,
- FromApub,
- FromApubToForm,
- ToApub,
- },
+ check_is_apub_id_valid,
+ extensions::{context::lemmy_context, signatures::PublicKey},
+ objects::{FromApub, ImageObject, Source, ToApub},
ActorType,
- PersonExt,
- UserTypes,
};
use activitystreams::{
- actor::{Actor, ApActor, ApActorExt, Endpoints},
- base::{BaseExt, ExtendsExt},
- object::{ApObject, Image, Object, ObjectExt, Tombstone},
+ actor::Endpoints,
+ base::AnyBase,
+ chrono::{DateTime, FixedOffset},
+ object::{kind::ImageType, Tombstone},
+ primitives::OneOrMany,
+ unparsed::Unparsed,
};
-use activitystreams_ext::Ext2;
-use anyhow::Context;
use lemmy_api_common::blocking;
+use lemmy_apub_lib::{
+ values::{MediaTypeHtml, MediaTypeMarkdown},
+ verify_domains_match,
+};
use lemmy_db_queries::{ApubObject, DbPool};
use lemmy_db_schema::{
naive_now,
source::person::{Person as DbPerson, PersonForm},
};
use lemmy_utils::{
- location_info,
- settings::structs::Settings,
- utils::{check_slurs, check_slurs_opt, convert_datetime},
+ utils::{check_slurs, check_slurs_opt, convert_datetime, markdown_to_html},
LemmyError,
};
use lemmy_websocket::LemmyContext;
+use serde::{Deserialize, Serialize};
+use serde_with::skip_serializing_none;
use url::Url;
+#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)]
+pub enum UserTypes {
+ Person,
+ Service,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Person {
+ #[serde(rename = "@context")]
+ context: OneOrMany<AnyBase>,
+ #[serde(rename = "type")]
+ kind: UserTypes,
+ id: Url,
+ /// username, set at account creation and can never be changed
+ preferred_username: String,
+ /// displayname (can be changed at any time)
+ name: Option<String>,
+ content: Option<String>,
+ media_type: Option<MediaTypeHtml>,
+ source: Option<Source>,
+ /// user avatar
+ icon: Option<ImageObject>,
+ /// user banner
+ image: Option<ImageObject>,
+ matrix_user_id: Option<String>,
+ inbox: Url,
+ /// mandatory field in activitypub, currently empty in lemmy
+ outbox: Url,
+ endpoints: Endpoints<Url>,
+ public_key: PublicKey,
+ published: DateTime<FixedOffset>,
+ updated: Option<DateTime<FixedOffset>>,
+ #[serde(flatten)]
+ unparsed: Unparsed,
+}
+
+// TODO: can generate this with a derive macro
+impl Person {
+ pub(crate) fn id(&self, expected_domain: &Url) -> Result<&Url, LemmyError> {
+ verify_domains_match(&self.id, expected_domain)?;
+ Ok(&self.id)
+ }
+}
+
#[async_trait::async_trait(?Send)]
impl ToApub for DbPerson {
- type ApubType = PersonExt;
+ type ApubType = Person;
- async fn to_apub(&self, _pool: &DbPool) -> Result<PersonExt, LemmyError> {
- let object = Object::<UserTypes>::new_none_type();
- let mut actor = Actor(object);
+ async fn to_apub(&self, _pool: &DbPool) -> Result<Person, LemmyError> {
let kind = if self.bot_account {
UserTypes::Service
} else {
UserTypes::Person
};
- actor.set_kind(kind);
- let mut person = ApObject::new(actor);
-
- person
- .set_many_contexts(lemmy_context())
- .set_id(self.actor_id.to_owned().into_inner())
- .set_published(convert_datetime(self.published));
-
- if let Some(u) = self.updated {
- person.set_updated(convert_datetime(u));
- }
-
- if let Some(avatar_url) = &self.avatar {
- let mut image = Image::new();
- image.set_url::<Url>(avatar_url.to_owned().into());
- person.set_icon(image.into_any_base()?);
- }
-
- if let Some(banner_url) = &self.banner {
- let mut image = Image::new();
- image.set_url::<Url>(banner_url.to_owned().into());
- person.set_image(image.into_any_base()?);
- }
-
- if let Some(bio) = &self.bio {
- set_content_and_source(&mut person, bio)?;
- }
-
- // In apub, the "name" is a display name
- if let Some(i) = self.display_name.to_owned() {
- person.set_name(i);
- }
-
- let mut ap_actor = ApActor::new(self.inbox_url.clone().into(), person);
- ap_actor
- .set_preferred_username(self.name.to_owned())
- .set_outbox(self.get_outbox_url()?)
- .set_endpoints(Endpoints {
- shared_inbox: Some(self.get_shared_inbox_or_inbox_url()),
+ let source = self.bio.clone().map(|bio| Source {
+ content: bio,
+ media_type: MediaTypeMarkdown::Markdown,
+ });
+ let icon = self.avatar.clone().map(|url| ImageObject {
+ kind: ImageType::Image,
+ url: url.into(),
+ });
+ let image = self.banner.clone().map(|url| ImageObject {
+ kind: ImageType::Image,
+ url: url.into(),
+ });
+
+ let person = Person {
+ context: lemmy_context(),
+ kind,
+ id: self.actor_id.to_owned().into_inner(),
+ preferred_username: self.name.clone(),
+ name: self.display_name.clone(),
+ content: self.bio.as_ref().map(|b| markdown_to_html(b)),
+ media_type: self.bio.as_ref().map(|_| MediaTypeHtml::Html),
+ source,
+ icon,
+ image,
+ matrix_user_id: self.matrix_user_id.clone(),
+ published: convert_datetime(self.published),
+ outbox: self.get_outbox_url()?,
+ endpoints: Endpoints {
+ shared_inbox: self.shared_inbox_url.clone().map(|s| s.into()),
..Default::default()
- });
-
- let person_ext = PersonExtension::new(self.matrix_user_id.to_owned())?;
- Ok(Ext2::new(ap_actor, person_ext, self.get_public_key_ext()?))
+ },
+ public_key: self.get_public_key()?,
+ updated: self.updated.map(convert_datetime),
+ unparsed: Default::default(),
+ inbox: self.inbox_url.clone().into(),
+ };
+ Ok(person)
}
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
unimplemented!()
#[async_trait::async_trait(?Send)]
impl FromApub for DbPerson {
- type ApubType = PersonExt;
+ type ApubType = Person;
async fn from_apub(
- person: &PersonExt,
+ person: &Person,
context: &LemmyContext,
- expected_domain: Url,
- request_counter: &mut i32,
- mod_action_allowed: bool,
- ) -> Result<DbPerson, LemmyError> {
- let person_id = person.id_unchecked().context(location_info!())?.to_owned();
- let domain = person_id.domain().context(location_info!())?;
- if domain == Settings::get().hostname {
- let person = blocking(context.pool(), move |conn| {
- DbPerson::read_from_apub_id(conn, &person_id.into())
- })
- .await??;
- Ok(person)
- } else {
- let person_form = PersonForm::from_apub(
- person,
- context,
- expected_domain,
- request_counter,
- mod_action_allowed,
- )
- .await?;
- let person = blocking(context.pool(), move |conn| {
- DbPerson::upsert(conn, &person_form)
- })
- .await??;
- Ok(person)
- }
- }
-}
-
-#[async_trait::async_trait(?Send)]
-impl FromApubToForm<PersonExt> for PersonForm {
- async fn from_apub(
- person: &PersonExt,
- _context: &LemmyContext,
- expected_domain: Url,
+ expected_domain: &Url,
_request_counter: &mut i32,
- _mod_action_allowed: bool,
- ) -> Result<Self, LemmyError> {
- let avatar = match person.icon() {
- Some(any_image) => Some(
- Image::from_any_base(any_image.as_one().context(location_info!())?.clone())?
- .context(location_info!())?
- .url()
- .context(location_info!())?
- .as_single_xsd_any_uri()
- .map(|url| url.to_owned()),
- ),
- None => None,
- };
-
- let banner = match person.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(|url| url.to_owned()),
- ),
- None => None,
+ ) -> Result<DbPerson, LemmyError> {
+ let actor_id = Some(person.id(expected_domain)?.clone().into());
+ let name = person.preferred_username.clone();
+ let display_name: Option<String> = person.name.clone();
+ let bio = person.source.clone().map(|s| s.content);
+ let shared_inbox = person.endpoints.shared_inbox.clone().map(|s| s.into());
+ let bot_account = match person.kind {
+ UserTypes::Person => false,
+ UserTypes::Service => true,
};
- let name: String = person
- .inner
- .preferred_username()
- .context(location_info!())?
- .to_string();
- let display_name: Option<String> = person
- .name()
- .map(|n| n.one())
- .flatten()
- .map(|n| n.to_owned().xsd_string())
- .flatten();
- let bio = get_source_markdown_value(person)?;
- let shared_inbox = person
- .inner
- .endpoints()?
- .map(|e| e.shared_inbox)
- .flatten()
- .map(|s| s.to_owned().into());
-
check_slurs(&name)?;
check_slurs_opt(&display_name)?;
check_slurs_opt(&bio)?;
+ check_is_apub_id_valid(&person.id, false)?;
- Ok(PersonForm {
+ let person_form = PersonForm {
name,
display_name: Some(display_name),
banned: None,
deleted: None,
- avatar: avatar.map(|o| o.map(|i| i.into())),
- banner: banner.map(|o| o.map(|i| i.into())),
- published: person.inner.published().map(|u| u.to_owned().naive_local()),
- updated: person.updated().map(|u| u.to_owned().naive_local()),
- actor_id: Some(check_object_domain(person, expected_domain, false)?),
+ avatar: Some(person.icon.clone().map(|i| i.url.into())),
+ banner: Some(person.image.clone().map(|i| i.url.into())),
+ published: Some(person.published.naive_local()),
+ updated: person.updated.map(|u| u.clone().naive_local()),
+ actor_id,
bio: Some(bio),
local: Some(false),
admin: Some(false),
- bot_account: Some(person.inner.is_kind(&UserTypes::Service)),
+ bot_account: Some(bot_account),
private_key: None,
- public_key: Some(Some(person.ext_two.public_key.to_owned().public_key_pem)),
+ public_key: Some(Some(person.public_key.public_key_pem.clone())),
last_refreshed_at: Some(naive_now()),
- inbox_url: Some(person.inner.inbox()?.to_owned().into()),
+ inbox_url: Some(person.inbox.to_owned().into()),
shared_inbox_url: Some(shared_inbox),
- matrix_user_id: Some(person.ext_one.matrix_user_id.to_owned()),
+ matrix_user_id: Some(person.matrix_user_id.clone()),
+ };
+ let person = blocking(context.pool(), move |conn| {
+ DbPerson::upsert(conn, &person_form)
})
+ .await??;
+ Ok(person)
}
}
activities::{extract_community, verify_person_in_community},
extensions::context::lemmy_context,
fetcher::person::get_or_fetch_and_upsert_person,
- objects::{create_tombstone, FromApub, Source, ToApub},
+ objects::{create_tombstone, FromApub, ImageObject, Source, ToApub},
ActorType,
};
use activitystreams::{
};
use lemmy_websocket::LemmyContext;
use serde::{Deserialize, Serialize};
+use serde_with::skip_serializing_none;
use url::Url;
+#[skip_serializing_none]
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Page {
#[serde(rename = "@context")]
context: OneOrMany<AnyBase>,
r#type: PageType,
- pub(crate) id: Url,
+ id: Url,
pub(crate) attributed_to: Url,
to: [Url; 2],
name: String,
unparsed: Unparsed,
}
-#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct ImageObject {
- content: ImageType,
- url: Url,
-}
-
impl Page {
+ pub(crate) fn id_unchecked(&self) -> &Url {
+ &self.id
+ }
+ pub(crate) fn id(&self, expected_domain: &Url) -> Result<&Url, LemmyError> {
+ verify_domains_match(&self.id, expected_domain)?;
+ Ok(&self.id)
+ }
+
/// Only mods can change the post's stickied/locked status. So if either of these is changed from
/// the current value, it is a mod action and needs to be verified as such.
///
media_type: MediaTypeMarkdown::Markdown,
});
let image = self.thumbnail_url.clone().map(|thumb| ImageObject {
- content: ImageType::Image,
+ kind: ImageType::Image,
url: thumb.into(),
});
async fn from_apub(
page: &Page,
context: &LemmyContext,
- _expected_domain: Url,
+ expected_domain: &Url,
request_counter: &mut i32,
- _mod_action_allowed: bool,
) -> Result<Post, LemmyError> {
+ // We can't verify the domain in case of mod action, because the mod may be on a different
+ // instance from the post author.
+ let ap_id = if page.is_mod_action(context.pool()).await? {
+ page.id_unchecked()
+ } else {
+ page.id(expected_domain)?
+ };
+ let ap_id = Some(ap_id.clone().into());
let creator =
get_or_fetch_and_upsert_person(&page.attributed_to, context, request_counter).await?;
let community = extract_community(&page.to, context, request_counter).await?;
embed_description,
embed_html,
thumbnail_url: pictrs_thumbnail.map(|u| u.into()),
- ap_id: Some(page.id.clone().into()),
+ ap_id,
local: Some(false),
};
Ok(blocking(context.pool(), move |conn| Post::upsert(conn, &form)).await??)
use lemmy_utils::{utils::convert_datetime, LemmyError};
use lemmy_websocket::LemmyContext;
use serde::{Deserialize, Serialize};
+use serde_with::skip_serializing_none;
use url::Url;
+#[skip_serializing_none]
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Note {
#[serde(rename = "@context")]
context: OneOrMany<AnyBase>,
r#type: NoteType,
- pub(crate) id: Url,
+ id: Url,
pub(crate) attributed_to: Url,
to: Url,
content: String,
}
impl Note {
+ pub(crate) fn id_unchecked(&self) -> &Url {
+ &self.id
+ }
+ pub(crate) fn id(&self, expected_domain: &Url) -> Result<&Url, LemmyError> {
+ verify_domains_match(&self.id, expected_domain)?;
+ Ok(&self.id)
+ }
+
pub(crate) async fn verify(
&self,
context: &LemmyContext,
async fn from_apub(
note: &Note,
context: &LemmyContext,
- _expected_domain: Url,
+ expected_domain: &Url,
request_counter: &mut i32,
- _mod_action_allowed: bool,
) -> Result<PrivateMessage, LemmyError> {
+ let ap_id = Some(note.id(expected_domain)?.clone().into());
let creator =
get_or_fetch_and_upsert_person(¬e.attributed_to, context, request_counter).await?;
let recipient = get_or_fetch_and_upsert_person(¬e.to, context, request_counter).await?;
updated: note.updated.map(|u| u.to_owned().naive_local()),
deleted: None,
read: None,
- ap_id: Some(note.id.clone().into()),
+ ap_id,
local: Some(false),
};
Ok(
) -> Result<(), LemmyError>;
async fn receive(
- &self,
+ self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError>;
}
}
async fn receive(
- &self,
+ self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {