diff --git a/Cargo.lock b/Cargo.lock index 53c4a659f..ee05a46ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,12 +81,10 @@ dependencies = [ "ahash 0.7.4", "base64 0.13.0", "bitflags", - "brotli2", "bytes", "bytestring", "derive_more", "encoding_rs", - "flate2", "futures-core", "futures-util", "h2", @@ -108,7 +106,6 @@ dependencies = [ "smallvec", "time 0.2.27", "tokio", - "zstd", ] [[package]] @@ -398,7 +395,6 @@ dependencies = [ "base64 0.13.0", "bytes", "cfg-if", - "cookie", "derive_more", "futures-core", "itoa", @@ -565,26 +561,6 @@ dependencies = [ "opaque-debug 0.3.0", ] -[[package]] -name = "brotli-sys" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4445dea95f4c2b41cde57cc9fee236ae4dbae88d8fcbdb4750fc1bb5d86aaecd" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "brotli2" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cb036c3eade309815c15ddbacec5b22c4d1f3983a774ab2eac2e3e9ea85568e" -dependencies = [ - "brotli-sys", - "libc", -] - [[package]] name = "bumpalo" version = "3.7.0" @@ -649,9 +625,6 @@ name = "cc" version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a72c244c1ff497a746a7e1fb3d14bd08420ecda70c8f25c7112f2781652d787" -dependencies = [ - "jobserver", -] [[package]] name = "cfg-if" @@ -729,9 +702,9 @@ checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" [[package]] name = "cookie" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdf8865bac3d9a3bde5bde9088ca431b11f5d37c7a578b8086af77248b76627" +checksum = "d5f1c7727e460397e56abc4bddc1d49e07a1ad78fc98eb2e1c8f032a58a2f80d" dependencies = [ "percent-encoding", "time 0.2.27", @@ -1367,9 +1340,9 @@ dependencies = [ [[package]] name = "http-signature-normalization-actix" -version = "0.5.0-beta.6" +version = "0.5.0-beta.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fddb5f0de5059c337a00cfe5b224768e1821327da9df4b6f27762cbf305126d" +checksum = "aa7cf7b03512ba7341b4252794751c5ff46635e0ff33eb864a929a5b7381e17a" dependencies = [ "actix-web", "awc", @@ -1531,15 +1504,6 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" -[[package]] -name = "jobserver" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "972f5ae5d1cb9c6ae417789196c803205313edde988685da5e3aae0827b9e7fd" -dependencies = [ - "libc", -] - [[package]] name = "jpeg-decoder" version = "0.1.22" @@ -1751,6 +1715,7 @@ dependencies = [ "lemmy_utils", "lemmy_websocket", "serde", + "serde_json", "url", ] @@ -3657,32 +3622,3 @@ name = "xdg" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d089681aa106a86fade1b0128fb5daf07d5867a509ab036d99988dec80429a57" - -[[package]] -name = "zstd" -version = "0.7.0+zstd.1.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9428752481d8372e15b1bf779ea518a179ad6c771cca2d2c60e4fbff3cc2cd52" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "3.1.0+zstd.1.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa1926623ad7fe406e090555387daf73db555b948134b4d73eac5eb08fb666d" -dependencies = [ - "libc", - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "1.5.0+zstd.1.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e6c094340240369025fc6b731b054ee2a834328fa584310ac96aa4baebdc465" -dependencies = [ - "cc", - "libc", -] diff --git a/Cargo.toml b/Cargo.toml index 0259f69dd..b41020843 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,7 +51,7 @@ env_logger = "0.8.4" strum = "0.21.0" url = { version = "2.2.2", features = ["serde"] } openssl = "0.10.35" -http-signature-normalization-actix = { version = "0.5.0-beta.6", default-features = false, features = ["sha-2"] } +http-signature-normalization-actix = { version = "0.5.0-beta.7", default-features = false, features = ["sha-2"] } tokio = { version = "1.8.0", features = ["sync"] } anyhow = "1.0.41" reqwest = { version = "0.11.4", features = ["json"] } diff --git a/RELEASES.md b/RELEASES.md index 978898752..43a6affc8 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,3 +1,68 @@ +# Lemmy v0.11.3 Release (2021-07-30) + +## Changes + +Since our last release, we've had [~30](https://github.com/LemmyNet/lemmy/compare/0.11.0...main) commits to Lemmy, and [~60](https://github.com/LemmyNet/lemmy-ui/compare/0.11.0...main) to Lemmy UI. + +### Lemmy Server + +- Blank out extra info for deleted or removed content. Fixes [#1679](https://github.com/LemmyNet/Lemmy/issues/1679) +- Add show_new_posts_notifs setting. Fixes [#1664](https://github.com/LemmyNet/Lemmy/issues/1664) +- Fix issue with protocol string in actor id generation [#1668](https://github.com/LemmyNet/Lemmy/issues/1668) +- Adding shortname fetching for users and communities. Fixes [#1662](https://github.com/LemmyNet/Lemmy/issues/1662) +- Make captcha case-insensitive. +- Remove tracking params from post url (fixes [#768](https://github.com/LemmyNet/Lemmy/issues/768)) +- Upgrade pictrs. Fixes [#1599](https://github.com/LemmyNet/Lemmy/issues/1599) +- Invalidate current logins on account deletion. Fixes [#1602](https://github.com/LemmyNet/Lemmy/issues/1602) +- Fix nsfw posts showing for non-logged in users. Fixes [#1614](https://github.com/LemmyNet/Lemmy/issues/1614) +- Add additional slurs configuration option. Closes [#1464](https://github.com/LemmyNet/Lemmy/issues/1464). +- Updating to rust 1.51.0 + +### Lemmy UI + +- Have setting to disable notifs for new posts. Fixes [#132](https://github.com/LemmyNet/lemmy-ui/issues/132) +- Remove max length constraints on actors. Fixes [#350](https://github.com/LemmyNet/lemmy-ui/issues/350) +- Fix captcha replay bug. Fixes [#348](https://github.com/LemmyNet/lemmy-ui/issues/348) +- Removing community and user routes in favor of shortnames. Fixes [#317](https://github.com/LemmyNet/lemmy-ui/issues/317) +- Add front end helpers 1 [(#346](https://github.com/LemmyNet/lemmy-ui/issues/346)) +- Don't use default subscribed for communities page. +- Adding Listing type to communities page, default local. [#190](https://github.com/LemmyNet/lemmy-ui/issues/190) +- Fix language bug on mobile browsers. +- Collapse sidebar on mobile. Fixes [#335](https://github.com/LemmyNet/lemmy-ui/issues/335) +- Re-organized components folder. [(#339](https://github.com/LemmyNet/lemmy-ui/issues/339)) +- Moving comment link to top bar. Fixes [#307](https://github.com/LemmyNet/lemmy-ui/issues/307) +- Make spinner bigger. Fixes [#203](https://github.com/LemmyNet/lemmy-ui/issues/203) +- Fix preview description html. Fixes [#110](https://github.com/LemmyNet/lemmy-ui/issues/110) +- Update darkly, make danger darker. Fixes [#16](https://github.com/LemmyNet/lemmy-ui/issues/16) +- Always show previous paginator, extract paginator component. +- Use better comment collapse icon, and add text. Fixes [#318](https://github.com/LemmyNet/lemmy-ui/issues/318) +- Fix symbols issue. Fixes [#319](https://github.com/LemmyNet/lemmy-ui/issues/319) +- Don't restore scroll position on page refresh. Fixes [#186](https://github.com/LemmyNet/lemmy-ui/issues/186) +- Insert triple backticks for 'code' button when multiple lines are selected. [(#311](https://github.com/LemmyNet/lemmy-ui/issues/311)) +- Adding a comment here placeholder. Fixes [#301](https://github.com/LemmyNet/lemmy-ui/issues/301) +- Fix non-local community and person links. Fixes [#290](https://github.com/LemmyNet/lemmy-ui/issues/290) +- Fix navbar bug. Fixes [#289](https://github.com/LemmyNet/lemmy-ui/issues/289) +- Hide names of mods / admins without priveleges. Fixes [#285](https://github.com/LemmyNet/lemmy-ui/issues/285) +- Adding URL search type. Fixes [#286](https://github.com/LemmyNet/lemmy-ui/issues/286) +- Add a link to joinlemmy on lemmy.ml signup. Fixes [#235](https://github.com/LemmyNet/lemmy-ui/issues/235) +- Fix duped site description. Fixes [#281](https://github.com/LemmyNet/lemmy-ui/issues/281) + +### API + +- Added `show_new_posts_notifs` boolean to `SaveUserSettings`, and `LocalUserSettings`. +- A full list of the API changes can be seen on this diff of [lemmy-js-client: 0.11.0 -> 0.11.3](https://github.com/LemmyNet/lemmy-js-client/compare/0.11.0...0.11.3-rc.4) . + +### Federation + +- No changes in this release, but there will be many soon. + +## Upgrade notes + +To upgrade your instance to `0.11.3`, simply follow the instructions in the documentation: + +- [Upgrade with manual Docker installation](https://join-lemmy.org/docs/en/administration/install_docker.html#updating) +- [Upgrade with Ansible installation](https://join-lemmy.org/docs/en/administration/install_ansible.html) + # Lemmy v0.11.0 Release (2021-04-27) ## Changes @@ -49,7 +114,6 @@ Since our last release this month, we've had [~60](https://github.com/LemmyNet/l - Fix html notif bug. Fixes [#254](https://github.com/LemmyNet/lemmy-ui/issues/254) - Fixing issue with debounce. Fixes [#236](https://github.com/LemmyNet/lemmy-ui/issues/236) - ## Upgrade notes ### Servers diff --git a/ansible/VERSION b/ansible/VERSION index bc859cbd6..1a96df19c 100644 --- a/ansible/VERSION +++ b/ansible/VERSION @@ -1 +1 @@ -0.11.2 +0.11.3 diff --git a/api_tests/src/comment.spec.ts b/api_tests/src/comment.spec.ts index b0ca55d0d..68dbcf7c1 100644 --- a/api_tests/src/comment.spec.ts +++ b/api_tests/src/comment.spec.ts @@ -115,6 +115,7 @@ test('Delete a comment', async () => { commentRes.comment_view.comment.id ); expect(deleteCommentRes.comment_view.comment.deleted).toBe(true); + expect(deleteCommentRes.comment_view.comment.content).toBe(""); // Make sure that comment is undefined on beta let searchBeta = await searchComment(beta, commentRes.comment_view.comment); @@ -149,6 +150,7 @@ test('Remove a comment from admin and community on the same instance', async () // The beta admin removes it (the community lives on beta) let removeCommentRes = await removeComment(beta, true, betaCommentId); expect(removeCommentRes.comment_view.comment.removed).toBe(true); + expect(removeCommentRes.comment_view.comment.content).toBe(""); // Make sure that comment is removed on alpha (it gets pushed since an admin from beta removed it) let refetchedPost = await getPost(alpha, postRes.post_view.post.id); diff --git a/api_tests/src/community.spec.ts b/api_tests/src/community.spec.ts index a86c1259a..3d4a3a350 100644 --- a/api_tests/src/community.spec.ts +++ b/api_tests/src/community.spec.ts @@ -77,6 +77,7 @@ test('Delete community', async () => { communityRes.community_view.community.id ); expect(deleteCommunityRes.community_view.community.deleted).toBe(true); + expect(deleteCommunityRes.community_view.community.title).toBe(""); // Make sure it got deleted on A let communityOnAlphaDeleted = await getCommunity( @@ -128,6 +129,7 @@ test('Remove community', async () => { communityRes.community_view.community.id ); expect(removeCommunityRes.community_view.community.removed).toBe(true); + expect(removeCommunityRes.community_view.community.title).toBe(""); // Make sure it got Removed on A let communityOnAlphaRemoved = await getCommunity( diff --git a/api_tests/src/post.spec.ts b/api_tests/src/post.spec.ts index 36e5ac3fa..c5630f336 100644 --- a/api_tests/src/post.spec.ts +++ b/api_tests/src/post.spec.ts @@ -210,6 +210,7 @@ test('Delete a post', async () => { let deletedPost = await deletePost(alpha, true, postRes.post_view.post); expect(deletedPost.post_view.post.deleted).toBe(true); + expect(deletedPost.post_view.post.name).toBe(""); // Make sure lemmy beta sees post is deleted let searchBeta = await searchPost(beta, postRes.post_view.post); @@ -237,6 +238,7 @@ test('Remove a post from admin and community on different instance', async () => let removedPost = await removePost(alpha, true, postRes.post_view.post); expect(removedPost.post_view.post.removed).toBe(true); + expect(removedPost.post_view.post.name).toBe(""); // Make sure lemmy beta sees post is NOT removed let searchBeta = await searchPost(beta, postRes.post_view.post); diff --git a/api_tests/src/private_message.spec.ts b/api_tests/src/private_message.spec.ts index 11b41cb17..0051f2c77 100644 --- a/api_tests/src/private_message.spec.ts +++ b/api_tests/src/private_message.spec.ts @@ -64,6 +64,7 @@ test('Delete a private message', async () => { pmRes.private_message_view.private_message.id ); expect(deletedPmRes.private_message_view.private_message.deleted).toBe(true); + expect(deletedPmRes.private_message_view.private_message.content).toBe(""); // The GetPrivateMessages filters out deleted, // even though they are in the actual database. diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml index e3ce05375..2f497f514 100644 --- a/crates/api/Cargo.toml +++ b/crates/api/Cargo.toml @@ -35,7 +35,7 @@ lazy_static = "1.4.0" url = { version = "2.2.2", features = ["serde"] } openssl = "0.10.35" http = "0.2.4" -http-signature-normalization-actix = { version = "0.5.0-beta.6", default-features = false, features = ["sha-2"] } +http-signature-normalization-actix = { version = "0.5.0-beta.7", default-features = false, features = ["sha-2"] } base64 = "0.13.0" tokio = "1.8.0" futures = "0.3.15" diff --git a/crates/api/src/comment.rs b/crates/api/src/comment.rs index ff1010fb1..f92e679f7 100644 --- a/crates/api/src/comment.rs +++ b/crates/api/src/comment.rs @@ -7,12 +7,19 @@ use lemmy_api_common::{ comment::*, get_local_user_view_from_jwt, }; -use lemmy_apub::ApubLikeableType; +use lemmy_apub::{ + activities::voting::{ + undo_vote::UndoVote, + vote::{Vote, VoteType}, + }, + PostOrComment, +}; use lemmy_db_queries::{source::comment::Comment_, Likeable, Saveable}; use lemmy_db_schema::{source::comment::*, LocalUserId}; use lemmy_db_views::{comment_view::CommentView, local_user_view::LocalUserView}; use lemmy_utils::{ApiError, ConnectionId, LemmyError}; use lemmy_websocket::{messages::SendComment, LemmyContext, UserOperation}; +use std::convert::TryInto; #[async_trait::async_trait(?Send)] impl Perform for MarkCommentAsRead { @@ -170,6 +177,7 @@ impl Perform for CreateCommentLike { // Only add the like if the score isnt 0 let comment = orig_comment.comment; + let object = PostOrComment::Comment(Box::new(comment)); let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1); if do_add { let like_form2 = like_form.clone(); @@ -178,17 +186,24 @@ impl Perform for CreateCommentLike { return Err(ApiError::err("couldnt_like_comment").into()); } - if like_form.score == 1 { - comment.send_like(&local_user_view.person, context).await?; - } else if like_form.score == -1 { - comment - .send_dislike(&local_user_view.person, context) - .await?; - } + Vote::send( + &object, + &local_user_view.person, + orig_comment.community.id, + like_form.score.try_into()?, + context, + ) + .await?; } else { - comment - .send_undo_like(&local_user_view.person, context) - .await?; + // API doesn't distinguish between Undo/Like and Undo/Dislike + UndoVote::send( + &object, + &local_user_view.person, + orig_comment.community.id, + VoteType::Like, + context, + ) + .await?; } // Have to refetch the comment to get the current state diff --git a/crates/api/src/post.rs b/crates/api/src/post.rs index 4a55b7e05..684f64798 100644 --- a/crates/api/src/post.rs +++ b/crates/api/src/post.rs @@ -9,12 +9,23 @@ use lemmy_api_common::{ mark_post_as_read, post::*, }; -use lemmy_apub::{ApubLikeableType, ApubObjectType}; +use lemmy_apub::{ + activities::{ + post::create_or_update::CreateOrUpdatePost, + voting::{ + undo_vote::UndoVote, + vote::{Vote, VoteType}, + }, + CreateOrUpdateType, + }, + PostOrComment, +}; use lemmy_db_queries::{source::post::Post_, Crud, Likeable, Saveable}; use lemmy_db_schema::source::{moderator::*, post::*}; use lemmy_db_views::post_view::PostView; use lemmy_utils::{ApiError, ConnectionId, LemmyError}; use lemmy_websocket::{messages::SendPost, LemmyContext, UserOperation}; +use std::convert::TryInto; #[async_trait::async_trait(?Send)] impl Perform for CreatePostLike { @@ -50,6 +61,9 @@ impl Perform for CreatePostLike { }) .await??; + let community_id = post.community_id; + let object = PostOrComment::Post(Box::new(post)); + // Only add the like if the score isnt 0 let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1); if do_add { @@ -59,15 +73,24 @@ impl Perform for CreatePostLike { return Err(ApiError::err("couldnt_like_post").into()); } - if like_form.score == 1 { - post.send_like(&local_user_view.person, context).await?; - } else if like_form.score == -1 { - post.send_dislike(&local_user_view.person, context).await?; - } + Vote::send( + &object, + &local_user_view.person, + community_id, + like_form.score.try_into()?, + context, + ) + .await?; } else { - post - .send_undo_like(&local_user_view.person, context) - .await?; + // API doesn't distinguish between Undo/Like and Undo/Dislike + UndoVote::send( + &object, + &local_user_view.person, + community_id, + VoteType::Like, + context, + ) + .await?; } // Mark the post as read @@ -140,9 +163,13 @@ impl Perform for LockPost { blocking(context.pool(), move |conn| ModLockPost::create(conn, &form)).await??; // apub updates - updated_post - .send_update(&local_user_view.person, context) - .await?; + CreateOrUpdatePost::send( + &updated_post, + &local_user_view.person, + CreateOrUpdateType::Update, + context, + ) + .await?; // Refetch the post let post_id = data.post_id; @@ -214,9 +241,13 @@ impl Perform for StickyPost { // Apub updates // TODO stickied should pry work like locked for ease of use - updated_post - .send_update(&local_user_view.person, context) - .await?; + CreateOrUpdatePost::send( + &updated_post, + &local_user_view.person, + CreateOrUpdateType::Update, + context, + ) + .await?; // Refetch the post let post_id = data.post_id; diff --git a/crates/api/src/site.rs b/crates/api/src/site.rs index f9d7962be..d67c11815 100644 --- a/crates/api/src/site.rs +++ b/crates/api/src/site.rs @@ -15,6 +15,7 @@ use lemmy_db_queries::{ from_opt_str_to_opt_enum, source::site::Site_, Crud, + DeleteableOrRemoveable, ListingType, SearchType, SortType, @@ -332,6 +333,28 @@ impl Perform for Search { } }; + // Blank out deleted or removed info + for cv in comments + .iter_mut() + .filter(|cv| cv.comment.deleted || cv.comment.removed) + { + cv.comment = cv.to_owned().comment.blank_out_deleted_or_removed_info(); + } + + for cv in communities + .iter_mut() + .filter(|cv| cv.community.deleted || cv.community.removed) + { + cv.community = cv.to_owned().community.blank_out_deleted_or_removed_info(); + } + + for pv in posts + .iter_mut() + .filter(|p| p.post.deleted || p.post.removed) + { + pv.post = pv.to_owned().post.blank_out_deleted_or_removed_info(); + } + // Return the jwt Ok(SearchResponse { type_: search_type.to_string(), diff --git a/crates/api_common/Cargo.toml b/crates/api_common/Cargo.toml index 22839643b..e68793b52 100644 --- a/crates/api_common/Cargo.toml +++ b/crates/api_common/Cargo.toml @@ -18,7 +18,7 @@ lemmy_utils = { path = "../utils" } serde = { version = "1.0.126", features = ["derive"] } log = "0.4.14" diesel = "1.4.7" -actix-web = "4.0.0-beta.8" +actix-web = {version = "4.0.0-beta.8", default-features = false, features = ["cookies"] } chrono = { version = "0.4.19", features = ["serde"] } serde_json = { version = "1.0.64", features = ["preserve_order"] } url = "2.2.2" diff --git a/crates/api_crud/Cargo.toml b/crates/api_crud/Cargo.toml index ebc382f83..e64d50b11 100644 --- a/crates/api_crud/Cargo.toml +++ b/crates/api_crud/Cargo.toml @@ -30,7 +30,7 @@ lazy_static = "1.4.0" url = { version = "2.2.2", features = ["serde"] } openssl = "0.10.35" http = "0.2.4" -http-signature-normalization-actix = { version = "0.5.0-beta.6", default-features = false, features = ["sha-2"] } +http-signature-normalization-actix = { version = "0.5.0-beta.7", default-features = false, features = ["sha-2"] } base64 = "0.13.0" tokio = "1.8.0" futures = "0.3.15" diff --git a/crates/api_crud/src/comment/create.rs b/crates/api_crud/src/comment/create.rs index 7923b0836..5a6fec81c 100644 --- a/crates/api_crud/src/comment/create.rs +++ b/crates/api_crud/src/comment/create.rs @@ -8,7 +8,16 @@ use lemmy_api_common::{ get_post, send_local_notifs, }; -use lemmy_apub::{generate_apub_endpoint, ApubLikeableType, ApubObjectType, EndpointType}; +use lemmy_apub::{ + activities::{ + comment::create_or_update::CreateOrUpdateComment, + voting::vote::{Vote, VoteType}, + CreateOrUpdateType, + }, + generate_apub_endpoint, + EndpointType, + PostOrComment, +}; use lemmy_db_queries::{source::comment::Comment_, Crud, Likeable}; use lemmy_db_schema::source::comment::*; use lemmy_db_views::comment_view::CommentView; @@ -37,8 +46,9 @@ impl PerformCrud for CreateComment { // Check for a community ban let post_id = data.post_id; let post = get_post(post_id, context.pool()).await?; + let community_id = post.community_id; - check_community_ban(local_user_view.person.id, post.community_id, context.pool()).await?; + check_community_ban(local_user_view.person.id, community_id, context.pool()).await?; // Check if post is locked, no new comments if post.locked { @@ -83,9 +93,13 @@ impl PerformCrud for CreateComment { .await? .map_err(|_| ApiError::err("couldnt_create_comment"))?; - updated_comment - .send_create(&local_user_view.person, context) - .await?; + CreateOrUpdateComment::send( + &updated_comment, + &local_user_view.person, + CreateOrUpdateType::Create, + context, + ) + .await?; // Scan the comment for user mentions, add those rows let post_id = post.id; @@ -113,9 +127,15 @@ impl PerformCrud for CreateComment { return Err(ApiError::err("couldnt_like_comment").into()); } - updated_comment - .send_like(&local_user_view.person, context) - .await?; + let object = PostOrComment::Comment(Box::new(updated_comment)); + Vote::send( + &object, + &local_user_view.person, + community_id, + VoteType::Like, + context, + ) + .await?; let person_id = local_user_view.person.id; let mut comment_view = blocking(context.pool(), move |conn| { diff --git a/crates/api_crud/src/comment/delete.rs b/crates/api_crud/src/comment/delete.rs index f58f3b0e4..e2ec3080e 100644 --- a/crates/api_crud/src/comment/delete.rs +++ b/crates/api_crud/src/comment/delete.rs @@ -9,7 +9,7 @@ use lemmy_api_common::{ send_local_notifs, }; use lemmy_apub::ApubObjectType; -use lemmy_db_queries::{source::comment::Comment_, Crud}; +use lemmy_db_queries::{source::comment::Comment_, Crud, DeleteableOrRemoveable}; use lemmy_db_schema::source::{comment::*, moderator::*}; use lemmy_db_views::comment_view::CommentView; use lemmy_utils::{ApiError, ConnectionId, LemmyError}; @@ -47,7 +47,7 @@ impl PerformCrud for DeleteComment { // Do the delete let deleted = data.deleted; - let updated_comment = blocking(context.pool(), move |conn| { + let mut updated_comment = blocking(context.pool(), move |conn| { Comment::update_deleted(conn, comment_id, deleted) }) .await? @@ -55,6 +55,7 @@ impl PerformCrud for DeleteComment { // Send the apub message if deleted { + updated_comment = updated_comment.blank_out_deleted_or_removed_info(); updated_comment .send_delete(&local_user_view.person, context) .await?; @@ -67,11 +68,16 @@ impl PerformCrud for DeleteComment { // Refetch it let comment_id = data.comment_id; let person_id = local_user_view.person.id; - let comment_view = blocking(context.pool(), move |conn| { + let mut comment_view = blocking(context.pool(), move |conn| { CommentView::read(conn, comment_id, Some(person_id)) }) .await??; + // Blank out deleted or removed info + if deleted { + comment_view.comment = comment_view.comment.blank_out_deleted_or_removed_info(); + } + // Build the recipients let comment_view_2 = comment_view.clone(); let mentions = vec![]; @@ -136,7 +142,7 @@ impl PerformCrud for RemoveComment { // Do the remove let removed = data.removed; - let updated_comment = blocking(context.pool(), move |conn| { + let mut updated_comment = blocking(context.pool(), move |conn| { Comment::update_removed(conn, comment_id, removed) }) .await? @@ -156,6 +162,7 @@ impl PerformCrud for RemoveComment { // Send the apub message if removed { + updated_comment = updated_comment.blank_out_deleted_or_removed_info(); updated_comment .send_remove(&local_user_view.person, context) .await?; @@ -168,11 +175,16 @@ impl PerformCrud for RemoveComment { // Refetch it let comment_id = data.comment_id; let person_id = local_user_view.person.id; - let comment_view = blocking(context.pool(), move |conn| { + let mut comment_view = blocking(context.pool(), move |conn| { CommentView::read(conn, comment_id, Some(person_id)) }) .await??; + // Blank out deleted or removed info + if removed { + comment_view.comment = comment_view.comment.blank_out_deleted_or_removed_info(); + } + // Build the recipients let comment_view_2 = comment_view.clone(); diff --git a/crates/api_crud/src/comment/read.rs b/crates/api_crud/src/comment/read.rs index 47394402c..fc4bd1e35 100644 --- a/crates/api_crud/src/comment/read.rs +++ b/crates/api_crud/src/comment/read.rs @@ -2,7 +2,7 @@ use crate::PerformCrud; use actix_web::web::Data; use lemmy_api_common::{blocking, comment::*, get_local_user_view_from_jwt_opt}; use lemmy_apub::{build_actor_id_from_shortname, EndpointType}; -use lemmy_db_queries::{from_opt_str_to_opt_enum, ListingType, SortType}; +use lemmy_db_queries::{from_opt_str_to_opt_enum, DeleteableOrRemoveable, ListingType, SortType}; use lemmy_db_views::comment_view::CommentQueryBuilder; use lemmy_utils::{ApiError, ConnectionId, LemmyError}; use lemmy_websocket::LemmyContext; @@ -36,7 +36,7 @@ impl PerformCrud for GetComments { let saved_only = data.saved_only; let page = data.page; let limit = data.limit; - let comments = blocking(context.pool(), move |conn| { + let mut comments = blocking(context.pool(), move |conn| { CommentQueryBuilder::create(conn) .listing_type(listing_type) .sort(sort) @@ -52,6 +52,14 @@ impl PerformCrud for GetComments { .await? .map_err(|_| ApiError::err("couldnt_get_comments"))?; + // Blank out deleted or removed info + for cv in comments + .iter_mut() + .filter(|cv| cv.comment.deleted || cv.comment.removed) + { + cv.comment = cv.to_owned().comment.blank_out_deleted_or_removed_info(); + } + Ok(GetCommentsResponse { comments }) } } diff --git a/crates/api_crud/src/comment/update.rs b/crates/api_crud/src/comment/update.rs index e6ed6ab59..d0a0e3971 100644 --- a/crates/api_crud/src/comment/update.rs +++ b/crates/api_crud/src/comment/update.rs @@ -7,8 +7,11 @@ use lemmy_api_common::{ get_local_user_view_from_jwt, send_local_notifs, }; -use lemmy_apub::ApubObjectType; -use lemmy_db_queries::source::comment::Comment_; +use lemmy_apub::activities::{ + comment::create_or_update::CreateOrUpdateComment, + CreateOrUpdateType, +}; +use lemmy_db_queries::{source::comment::Comment_, DeleteableOrRemoveable}; use lemmy_db_schema::source::comment::*; use lemmy_db_views::comment_view::CommentView; use lemmy_utils::{ @@ -59,9 +62,13 @@ impl PerformCrud for EditComment { .map_err(|_| ApiError::err("couldnt_update_comment"))?; // Send the apub update - updated_comment - .send_update(&local_user_view.person, context) - .await?; + CreateOrUpdateComment::send( + &updated_comment, + &local_user_view.person, + CreateOrUpdateType::Update, + context, + ) + .await?; // Do the mentions / recipients let updated_comment_content = updated_comment.content.to_owned(); @@ -78,11 +85,16 @@ impl PerformCrud for EditComment { let comment_id = data.comment_id; let person_id = local_user_view.person.id; - let comment_view = blocking(context.pool(), move |conn| { + let mut comment_view = blocking(context.pool(), move |conn| { CommentView::read(conn, comment_id, Some(person_id)) }) .await??; + // Blank out deleted or removed info + if comment_view.comment.deleted || comment_view.comment.removed { + comment_view.comment = comment_view.comment.blank_out_deleted_or_removed_info(); + } + let res = CommentResponse { comment_view, recipient_ids, diff --git a/crates/api_crud/src/community/delete.rs b/crates/api_crud/src/community/delete.rs index 2208a348b..62fb0f38f 100644 --- a/crates/api_crud/src/community/delete.rs +++ b/crates/api_crud/src/community/delete.rs @@ -2,7 +2,7 @@ use crate::{community::send_community_websocket, PerformCrud}; use actix_web::web::Data; use lemmy_api_common::{blocking, community::*, get_local_user_view_from_jwt, is_admin}; use lemmy_apub::CommunityType; -use lemmy_db_queries::{source::community::Community_, Crud}; +use lemmy_db_queries::{source::community::Community_, Crud, DeleteableOrRemoveable}; use lemmy_db_schema::source::{ community::*, moderator::{ModRemoveCommunity, ModRemoveCommunityForm}, @@ -50,6 +50,7 @@ impl PerformCrud for DeleteCommunity { // Send apub messages if deleted { updated_community + .blank_out_deleted_or_removed_info() .send_delete(local_user_view.person.to_owned(), context) .await?; } else { @@ -60,11 +61,16 @@ impl PerformCrud for DeleteCommunity { let community_id = data.community_id; let person_id = local_user_view.person.id; - let community_view = blocking(context.pool(), move |conn| { + let mut community_view = blocking(context.pool(), move |conn| { CommunityView::read(conn, community_id, Some(person_id)) }) .await??; + // Blank out deleted or removed info + if deleted { + community_view.community = community_view.community.blank_out_deleted_or_removed_info(); + } + let res = CommunityResponse { community_view }; send_community_websocket( @@ -118,18 +124,26 @@ impl PerformCrud for RemoveCommunity { // Apub messages if removed { - updated_community.send_remove(context).await?; + updated_community + .blank_out_deleted_or_removed_info() + .send_remove(context) + .await?; } else { updated_community.send_undo_remove(context).await?; } let community_id = data.community_id; let person_id = local_user_view.person.id; - let community_view = blocking(context.pool(), move |conn| { + let mut community_view = blocking(context.pool(), move |conn| { CommunityView::read(conn, community_id, Some(person_id)) }) .await??; + // Blank out deleted or removed info + if removed { + community_view.community = community_view.community.blank_out_deleted_or_removed_info(); + } + let res = CommunityResponse { community_view }; send_community_websocket( diff --git a/crates/api_crud/src/community/read.rs b/crates/api_crud/src/community/read.rs index 0cca08df8..94e4ce7f4 100644 --- a/crates/api_crud/src/community/read.rs +++ b/crates/api_crud/src/community/read.rs @@ -2,7 +2,13 @@ use crate::PerformCrud; use actix_web::web::Data; use lemmy_api_common::{blocking, community::*, get_local_user_view_from_jwt_opt}; use lemmy_apub::{build_actor_id_from_shortname, EndpointType}; -use lemmy_db_queries::{from_opt_str_to_opt_enum, ApubObject, ListingType, SortType}; +use lemmy_db_queries::{ + from_opt_str_to_opt_enum, + ApubObject, + DeleteableOrRemoveable, + ListingType, + SortType, +}; use lemmy_db_schema::source::community::*; use lemmy_db_views_actor::{ community_moderator_view::CommunityModeratorView, @@ -39,12 +45,17 @@ impl PerformCrud for GetCommunity { } }; - let community_view = blocking(context.pool(), move |conn| { + let mut community_view = blocking(context.pool(), move |conn| { CommunityView::read(conn, community_id, person_id) }) .await? .map_err(|_| ApiError::err("couldnt_find_community"))?; + // Blank out deleted or removed info + if community_view.community.deleted || community_view.community.removed { + community_view.community = community_view.community.blank_out_deleted_or_removed_info(); + } + let moderators: Vec = blocking(context.pool(), move |conn| { CommunityModeratorView::for_community(conn, community_id) }) @@ -93,7 +104,7 @@ impl PerformCrud for ListCommunities { let page = data.page; let limit = data.limit; - let communities = blocking(context.pool(), move |conn| { + let mut communities = blocking(context.pool(), move |conn| { CommunityQueryBuilder::create(conn) .listing_type(listing_type) .sort(sort) @@ -105,6 +116,14 @@ impl PerformCrud for ListCommunities { }) .await??; + // Blank out deleted or removed info + for cv in communities + .iter_mut() + .filter(|cv| cv.community.deleted || cv.community.removed) + { + cv.community = cv.to_owned().community.blank_out_deleted_or_removed_info(); + } + // Return the jwt Ok(ListCommunitiesResponse { communities }) } diff --git a/crates/api_crud/src/community/update.rs b/crates/api_crud/src/community/update.rs index b17a223ee..49de1cfd7 100644 --- a/crates/api_crud/src/community/update.rs +++ b/crates/api_crud/src/community/update.rs @@ -6,7 +6,7 @@ use lemmy_api_common::{ get_local_user_view_from_jwt, }; use lemmy_apub::CommunityType; -use lemmy_db_queries::{diesel_option_overwrite_to_url, Crud}; +use lemmy_db_queries::{diesel_option_overwrite_to_url, Crud, DeleteableOrRemoveable}; use lemmy_db_schema::{ naive_now, source::community::{Community, CommunityForm}, @@ -78,11 +78,16 @@ impl PerformCrud for EditCommunity { let community_id = data.community_id; let person_id = local_user_view.person.id; - let community_view = blocking(context.pool(), move |conn| { + let mut community_view = blocking(context.pool(), move |conn| { CommunityView::read(conn, community_id, Some(person_id)) }) .await??; + // Blank out deleted or removed info + if community_view.community.deleted || community_view.community.removed { + community_view.community = community_view.community.blank_out_deleted_or_removed_info(); + } + let res = CommunityResponse { community_view }; send_community_websocket( diff --git a/crates/api_crud/src/post/create.rs b/crates/api_crud/src/post/create.rs index f2e97740e..f041a00b1 100644 --- a/crates/api_crud/src/post/create.rs +++ b/crates/api_crud/src/post/create.rs @@ -7,7 +7,16 @@ use lemmy_api_common::{ mark_post_as_read, post::*, }; -use lemmy_apub::{generate_apub_endpoint, ApubLikeableType, ApubObjectType, EndpointType}; +use lemmy_apub::{ + activities::{ + post::create_or_update::CreateOrUpdatePost, + voting::vote::{Vote, VoteType}, + CreateOrUpdateType, + }, + generate_apub_endpoint, + EndpointType, + PostOrComment, +}; use lemmy_db_queries::{source::post::Post_, Crud, Likeable}; use lemmy_db_schema::source::post::*; use lemmy_db_views::post_view::PostView; @@ -45,6 +54,9 @@ impl PerformCrud for CreatePost { let data_url = data.url.as_ref(); let (iframely_response, pictrs_thumbnail) = fetch_iframely_and_pictrs_data(context.client(), data_url).await?; + let (embed_title, embed_description, embed_html) = iframely_response + .map(|u| (u.title, u.description, u.html)) + .unwrap_or((None, None, None)); let post_form = PostForm { name: data.name.trim().to_owned(), @@ -53,9 +65,9 @@ impl PerformCrud for CreatePost { community_id: data.community_id, creator_id: local_user_view.person.id, nsfw: data.nsfw, - embed_title: iframely_response.title, - embed_description: iframely_response.description, - embed_html: iframely_response.html, + embed_title, + embed_description, + embed_html, thumbnail_url: pictrs_thumbnail.map(|u| u.into()), ..PostForm::default() }; @@ -82,9 +94,13 @@ impl PerformCrud for CreatePost { .await? .map_err(|_| ApiError::err("couldnt_create_post"))?; - updated_post - .send_create(&local_user_view.person, context) - .await?; + CreateOrUpdatePost::send( + &updated_post, + &local_user_view.person, + CreateOrUpdateType::Create, + context, + ) + .await?; // They like their own post by default let person_id = local_user_view.person.id; @@ -103,9 +119,15 @@ impl PerformCrud for CreatePost { // Mark the post as read mark_post_as_read(person_id, post_id, context.pool()).await?; - updated_post - .send_like(&local_user_view.person, context) - .await?; + let object = PostOrComment::Post(Box::new(updated_post)); + Vote::send( + &object, + &local_user_view.person, + inserted_post.community_id, + VoteType::Like, + context, + ) + .await?; // Refetch the view let inserted_post_id = inserted_post.id; diff --git a/crates/api_crud/src/post/delete.rs b/crates/api_crud/src/post/delete.rs index e7fcb2fd6..cf48757d3 100644 --- a/crates/api_crud/src/post/delete.rs +++ b/crates/api_crud/src/post/delete.rs @@ -8,7 +8,7 @@ use lemmy_api_common::{ post::*, }; use lemmy_apub::ApubObjectType; -use lemmy_db_queries::{source::post::Post_, Crud}; +use lemmy_db_queries::{source::post::Post_, Crud, DeleteableOrRemoveable}; use lemmy_db_schema::source::{moderator::*, post::*}; use lemmy_db_views::post_view::PostView; use lemmy_utils::{ApiError, ConnectionId, LemmyError}; @@ -52,6 +52,7 @@ impl PerformCrud for DeletePost { // apub updates if deleted { updated_post + .blank_out_deleted_or_removed_info() .send_delete(&local_user_view.person, context) .await?; } else { @@ -62,11 +63,15 @@ impl PerformCrud for DeletePost { // Refetch the post let post_id = data.post_id; - let post_view = blocking(context.pool(), move |conn| { + let mut post_view = blocking(context.pool(), move |conn| { PostView::read(conn, post_id, Some(local_user_view.person.id)) }) .await??; + if deleted { + post_view.post = post_view.post.blank_out_deleted_or_removed_info(); + } + let res = PostResponse { post_view }; context.chat_server().do_send(SendPost { @@ -132,6 +137,7 @@ impl PerformCrud for RemovePost { // apub updates if removed { updated_post + .blank_out_deleted_or_removed_info() .send_remove(&local_user_view.person, context) .await?; } else { @@ -143,11 +149,16 @@ impl PerformCrud for RemovePost { // Refetch the post let post_id = data.post_id; let person_id = local_user_view.person.id; - let post_view = blocking(context.pool(), move |conn| { + let mut post_view = blocking(context.pool(), move |conn| { PostView::read(conn, post_id, Some(person_id)) }) .await??; + // Blank out deleted or removed info + if removed { + post_view.post = post_view.post.blank_out_deleted_or_removed_info(); + } + let res = PostResponse { post_view }; context.chat_server().do_send(SendPost { diff --git a/crates/api_crud/src/post/read.rs b/crates/api_crud/src/post/read.rs index 77e7a2163..d651a0577 100644 --- a/crates/api_crud/src/post/read.rs +++ b/crates/api_crud/src/post/read.rs @@ -2,7 +2,7 @@ use crate::PerformCrud; use actix_web::web::Data; use lemmy_api_common::{blocking, get_local_user_view_from_jwt_opt, mark_post_as_read, post::*}; use lemmy_apub::{build_actor_id_from_shortname, EndpointType}; -use lemmy_db_queries::{from_opt_str_to_opt_enum, ListingType, SortType}; +use lemmy_db_queries::{from_opt_str_to_opt_enum, DeleteableOrRemoveable, ListingType, SortType}; use lemmy_db_views::{ comment_view::CommentQueryBuilder, post_view::{PostQueryBuilder, PostView}, @@ -32,19 +32,24 @@ impl PerformCrud for GetPost { let person_id = local_user_view.map(|u| u.person.id); let id = data.id; - let post_view = blocking(context.pool(), move |conn| { + let mut post_view = blocking(context.pool(), move |conn| { PostView::read(conn, id, person_id) }) .await? .map_err(|_| ApiError::err("couldnt_find_post"))?; + // Blank out deleted info + if post_view.post.deleted || post_view.post.removed { + post_view.post = post_view.post.blank_out_deleted_or_removed_info(); + } + // Mark the post as read if let Some(person_id) = person_id { mark_post_as_read(person_id, id, context.pool()).await?; } let id = data.id; - let comments = blocking(context.pool(), move |conn| { + let mut comments = blocking(context.pool(), move |conn| { CommentQueryBuilder::create(conn) .my_person_id(person_id) .show_bot_accounts(show_bot_accounts) @@ -54,6 +59,14 @@ impl PerformCrud for GetPost { }) .await??; + // Blank out deleted or removed info + for cv in comments + .iter_mut() + .filter(|cv| cv.comment.deleted || cv.comment.removed) + { + cv.comment = cv.to_owned().comment.blank_out_deleted_or_removed_info(); + } + let community_id = post_view.community.id; let moderators = blocking(context.pool(), move |conn| { CommunityModeratorView::for_community(conn, community_id) @@ -61,12 +74,17 @@ impl PerformCrud for GetPost { .await??; // Necessary for the sidebar - let community_view = blocking(context.pool(), move |conn| { + let mut community_view = blocking(context.pool(), move |conn| { CommunityView::read(conn, community_id, person_id) }) .await? .map_err(|_| ApiError::err("couldnt_find_community"))?; + // Blank out deleted or removed info + if community_view.community.deleted || community_view.community.removed { + community_view.community = community_view.community.blank_out_deleted_or_removed_info(); + } + let online = context .chat_server() .send(GetPostUsersOnline { post_id: data.id }) @@ -119,7 +137,7 @@ impl PerformCrud for GetPosts { .unwrap_or(None); let saved_only = data.saved_only; - let posts = blocking(context.pool(), move |conn| { + let mut posts = blocking(context.pool(), move |conn| { PostQueryBuilder::create(conn) .listing_type(listing_type) .sort(sort) @@ -137,6 +155,14 @@ impl PerformCrud for GetPosts { .await? .map_err(|_| ApiError::err("couldnt_get_posts"))?; + // Blank out deleted or removed info + for pv in posts + .iter_mut() + .filter(|p| p.post.deleted || p.post.removed) + { + pv.post = pv.to_owned().post.blank_out_deleted_or_removed_info(); + } + Ok(GetPostsResponse { posts }) } } diff --git a/crates/api_crud/src/post/update.rs b/crates/api_crud/src/post/update.rs index dc00dbc92..c9fe7e332 100644 --- a/crates/api_crud/src/post/update.rs +++ b/crates/api_crud/src/post/update.rs @@ -1,8 +1,8 @@ use crate::PerformCrud; use actix_web::web::Data; use lemmy_api_common::{blocking, check_community_ban, get_local_user_view_from_jwt, post::*}; -use lemmy_apub::ApubObjectType; -use lemmy_db_queries::{source::post::Post_, Crud}; +use lemmy_apub::activities::{post::create_or_update::CreateOrUpdatePost, CreateOrUpdateType}; +use lemmy_db_queries::{source::post::Post_, Crud, DeleteableOrRemoveable}; use lemmy_db_schema::{naive_now, source::post::*}; use lemmy_db_views::post_view::PostView; use lemmy_utils::{ @@ -54,6 +54,9 @@ impl PerformCrud for EditPost { let data_url = data.url.as_ref(); let (iframely_response, pictrs_thumbnail) = fetch_iframely_and_pictrs_data(context.client(), data_url).await?; + let (embed_title, embed_description, embed_html) = iframely_response + .map(|u| (u.title, u.description, u.html)) + .unwrap_or((None, None, None)); let post_form = PostForm { creator_id: orig_post.creator_id.to_owned(), @@ -63,9 +66,9 @@ impl PerformCrud for EditPost { body: data.body.to_owned(), nsfw: data.nsfw, updated: Some(naive_now()), - embed_title: iframely_response.title, - embed_description: iframely_response.description, - embed_html: iframely_response.html, + embed_title, + embed_description, + embed_html, thumbnail_url: pictrs_thumbnail.map(|u| u.into()), ..PostForm::default() }; @@ -89,16 +92,25 @@ impl PerformCrud for EditPost { }; // Send apub update - updated_post - .send_update(&local_user_view.person, context) - .await?; + CreateOrUpdatePost::send( + &updated_post, + &local_user_view.person, + CreateOrUpdateType::Update, + context, + ) + .await?; let post_id = data.post_id; - let post_view = blocking(context.pool(), move |conn| { + let mut post_view = blocking(context.pool(), move |conn| { PostView::read(conn, post_id, Some(local_user_view.person.id)) }) .await??; + // Blank out deleted info + if post_view.post.deleted || post_view.post.removed { + post_view.post = post_view.post.blank_out_deleted_or_removed_info(); + } + let res = PostResponse { post_view }; context.chat_server().do_send(SendPost { diff --git a/crates/api_crud/src/private_message/create.rs b/crates/api_crud/src/private_message/create.rs index 3e3074ad8..1da6466df 100644 --- a/crates/api_crud/src/private_message/create.rs +++ b/crates/api_crud/src/private_message/create.rs @@ -6,7 +6,14 @@ use lemmy_api_common::{ person::{CreatePrivateMessage, PrivateMessageResponse}, send_email_to_user, }; -use lemmy_apub::{generate_apub_endpoint, ApubObjectType, EndpointType}; +use lemmy_apub::{ + activities::{ + private_message::create_or_update::CreateOrUpdatePrivateMessage, + CreateOrUpdateType, + }, + generate_apub_endpoint, + EndpointType, +}; use lemmy_db_queries::{source::private_message::PrivateMessage_, Crud}; use lemmy_db_schema::source::private_message::{PrivateMessage, PrivateMessageForm}; use lemmy_db_views::{local_user_view::LocalUserView, private_message_view::PrivateMessageView}; @@ -63,9 +70,13 @@ impl PerformCrud for CreatePrivateMessage { .await? .map_err(|_| ApiError::err("couldnt_create_private_message"))?; - updated_private_message - .send_create(&local_user_view.person, context) - .await?; + CreateOrUpdatePrivateMessage::send( + &updated_private_message, + &local_user_view.person, + CreateOrUpdateType::Create, + context, + ) + .await?; let private_message_view = blocking(context.pool(), move |conn| { PrivateMessageView::read(conn, inserted_private_message.id) diff --git a/crates/api_crud/src/private_message/delete.rs b/crates/api_crud/src/private_message/delete.rs index 6befb75da..757596cd1 100644 --- a/crates/api_crud/src/private_message/delete.rs +++ b/crates/api_crud/src/private_message/delete.rs @@ -6,7 +6,7 @@ use lemmy_api_common::{ person::{DeletePrivateMessage, PrivateMessageResponse}, }; use lemmy_apub::ApubObjectType; -use lemmy_db_queries::{source::private_message::PrivateMessage_, Crud}; +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}; use lemmy_utils::{ApiError, ConnectionId, LemmyError}; @@ -46,6 +46,7 @@ impl PerformCrud for DeletePrivateMessage { // Send the apub update if data.deleted { updated_private_message + .blank_out_deleted_or_removed_info() .send_delete(&local_user_view.person, context) .await?; } else { @@ -55,11 +56,18 @@ impl PerformCrud for DeletePrivateMessage { } let private_message_id = data.private_message_id; - let private_message_view = blocking(context.pool(), move |conn| { + let mut private_message_view = blocking(context.pool(), move |conn| { PrivateMessageView::read(conn, private_message_id) }) .await??; + // Blank out deleted or removed info + if deleted { + private_message_view.private_message = private_message_view + .private_message + .blank_out_deleted_or_removed_info(); + } + let res = PrivateMessageResponse { private_message_view, }; diff --git a/crates/api_crud/src/private_message/read.rs b/crates/api_crud/src/private_message/read.rs index 6706bf419..3acbaa392 100644 --- a/crates/api_crud/src/private_message/read.rs +++ b/crates/api_crud/src/private_message/read.rs @@ -5,6 +5,7 @@ use lemmy_api_common::{ get_local_user_view_from_jwt, person::{GetPrivateMessages, PrivateMessagesResponse}, }; +use lemmy_db_queries::DeleteableOrRemoveable; use lemmy_db_views::private_message_view::PrivateMessageQueryBuilder; use lemmy_utils::{ConnectionId, LemmyError}; use lemmy_websocket::LemmyContext; @@ -25,7 +26,7 @@ impl PerformCrud for GetPrivateMessages { let page = data.page; let limit = data.limit; let unread_only = data.unread_only; - let messages = blocking(context.pool(), move |conn| { + let mut messages = blocking(context.pool(), move |conn| { PrivateMessageQueryBuilder::create(conn, person_id) .page(page) .limit(limit) @@ -34,6 +35,17 @@ impl PerformCrud for GetPrivateMessages { }) .await??; + // Blank out deleted or removed info + for pmv in messages + .iter_mut() + .filter(|pmv| pmv.private_message.deleted) + { + pmv.private_message = pmv + .to_owned() + .private_message + .blank_out_deleted_or_removed_info(); + } + Ok(PrivateMessagesResponse { private_messages: messages, }) diff --git a/crates/api_crud/src/private_message/update.rs b/crates/api_crud/src/private_message/update.rs index 35a9806a8..5f5f7b845 100644 --- a/crates/api_crud/src/private_message/update.rs +++ b/crates/api_crud/src/private_message/update.rs @@ -5,8 +5,11 @@ use lemmy_api_common::{ get_local_user_view_from_jwt, person::{EditPrivateMessage, PrivateMessageResponse}, }; -use lemmy_apub::ApubObjectType; -use lemmy_db_queries::{source::private_message::PrivateMessage_, Crud}; +use lemmy_apub::activities::{ + private_message::create_or_update::CreateOrUpdatePrivateMessage, + CreateOrUpdateType, +}; +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}; use lemmy_utils::{utils::remove_slurs, ApiError, ConnectionId, LemmyError}; @@ -44,16 +47,27 @@ impl PerformCrud for EditPrivateMessage { .map_err(|_| ApiError::err("couldnt_update_private_message"))?; // Send the apub update - updated_private_message - .send_update(&local_user_view.person, context) - .await?; + CreateOrUpdatePrivateMessage::send( + &updated_private_message, + &local_user_view.person, + CreateOrUpdateType::Update, + context, + ) + .await?; let private_message_id = data.private_message_id; - let private_message_view = blocking(context.pool(), move |conn| { + let mut private_message_view = blocking(context.pool(), move |conn| { PrivateMessageView::read(conn, private_message_id) }) .await??; + // Blank out deleted or removed info + if private_message_view.private_message.deleted { + private_message_view.private_message = private_message_view + .private_message + .blank_out_deleted_or_removed_info(); + } + let res = PrivateMessageResponse { private_message_view, }; diff --git a/crates/apub/Cargo.toml b/crates/apub/Cargo.toml index ac4453bbc..40cab9903 100644 --- a/crates/apub/Cargo.toml +++ b/crates/apub/Cargo.toml @@ -36,7 +36,7 @@ url = { version = "2.2.2", features = ["serde"] } percent-encoding = "2.1.0" openssl = "0.10.35" http = "0.2.4" -http-signature-normalization-actix = { version = "0.5.0-beta.6", default-features = false, features = ["sha-2"] } +http-signature-normalization-actix = { version = "0.5.0-beta.7", default-features = false, features = ["sha-2"] } http-signature-normalization-reqwest = { version = "0.2.0", default-features = false, features = ["sha-2"] } base64 = "0.13.0" tokio = "1.8.0" diff --git a/crates/apub/src/activities/comment/create.rs b/crates/apub/src/activities/comment/create.rs deleted file mode 100644 index 84dab1071..000000000 --- a/crates/apub/src/activities/comment/create.rs +++ /dev/null @@ -1,71 +0,0 @@ -use crate::{ - activities::{ - comment::{get_notif_recipients, send_websocket_message}, - verify_activity, - verify_person_in_community, - }, - objects::FromApub, - NoteExt, -}; -use activitystreams::{activity::kind::CreateType, base::BaseExt}; -use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler, PublicUrl}; -use lemmy_db_schema::source::comment::Comment; -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 CreateComment { - to: PublicUrl, - object: NoteExt, - cc: Vec, - #[serde(rename = "type")] - kind: CreateType, - #[serde(flatten)] - common: ActivityCommonFields, -} - -#[async_trait::async_trait(?Send)] -impl ActivityHandler for CreateComment { - async fn verify( - &self, - context: &LemmyContext, - request_counter: &mut i32, - ) -> Result<(), LemmyError> { - verify_activity(self.common())?; - verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; - verify_domains_match_opt(&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) - Ok(()) - } - - async fn receive( - &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 recipients = - get_notif_recipients(&self.common.actor, &comment, context, request_counter).await?; - send_websocket_message( - comment.id, - recipients, - UserOperationCrud::CreateComment, - context, - ) - .await - } - - fn common(&self) -> &ActivityCommonFields { - &self.common - } -} diff --git a/crates/apub/src/activities/comment/create_or_update.rs b/crates/apub/src/activities/comment/create_or_update.rs new file mode 100644 index 000000000..4cafafbaa --- /dev/null +++ b/crates/apub/src/activities/comment/create_or_update.rs @@ -0,0 +1,131 @@ +use crate::{ + activities::{ + comment::{collect_non_local_mentions, get_notif_recipients, send_websocket_message}, + community::announce::AnnouncableActivities, + extract_community, + generate_activity_id, + verify_activity, + verify_person_in_community, + CreateOrUpdateType, + }, + activity_queue::send_to_community_new, + extensions::context::lemmy_context, + objects::{comment::Note, FromApub, ToApub}, + ActorType, +}; +use activitystreams::link::Mention; +use lemmy_api_common::blocking; +use lemmy_apub_lib::{ + values::PublicUrl, + verify_domains_match, + ActivityCommonFields, + ActivityHandler, +}; +use lemmy_db_queries::Crud; +use lemmy_db_schema::source::{comment::Comment, community::Community, person::Person, post::Post}; +use lemmy_utils::LemmyError; +use lemmy_websocket::{LemmyContext, UserOperationCrud}; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateOrUpdateComment { + to: PublicUrl, + object: Note, + cc: Vec, + tag: Vec, + #[serde(rename = "type")] + kind: CreateOrUpdateType, + #[serde(flatten)] + common: ActivityCommonFields, +} + +impl CreateOrUpdateComment { + pub async fn send( + comment: &Comment, + actor: &Person, + kind: CreateOrUpdateType, + context: &LemmyContext, + ) -> Result<(), LemmyError> { + // TODO: might be helpful to add a comment method to retrieve community directly + let post_id = comment.post_id; + let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; + let community_id = post.community_id; + let community = blocking(context.pool(), move |conn| { + Community::read(conn, community_id) + }) + .await??; + + let id = generate_activity_id(kind.clone())?; + let maa = collect_non_local_mentions(comment, &community, context).await?; + + let create_or_update = CreateOrUpdateComment { + to: PublicUrl::Public, + object: comment.to_apub(context.pool()).await?, + cc: maa.ccs, + tag: maa.tags, + kind, + common: ActivityCommonFields { + context: lemmy_context(), + id: id.clone(), + actor: actor.actor_id(), + unparsed: Default::default(), + }, + }; + + let activity = AnnouncableActivities::CreateOrUpdateComment(create_or_update); + send_to_community_new(activity, &id, actor, &community, maa.inboxes, context).await + } +} + +#[async_trait::async_trait(?Send)] +impl ActivityHandler for CreateOrUpdateComment { + async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + let community = extract_community(&self.cc, context, request_counter).await?; + + verify_activity(self.common())?; + verify_person_in_community( + &self.common.actor, + &community.actor_id(), + context, + request_counter, + ) + .await?; + verify_domains_match(&self.common.actor, &self.object.id)?; + // 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?; + Ok(()) + } + + async fn receive( + &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 recipients = + get_notif_recipients(&self.common.actor, &comment, context, request_counter).await?; + let notif_type = match self.kind { + CreateOrUpdateType::Create => UserOperationCrud::CreateComment, + CreateOrUpdateType::Update => UserOperationCrud::EditComment, + }; + send_websocket_message(comment.id, recipients, notif_type, context).await + } + + fn common(&self) -> &ActivityCommonFields { + &self.common + } +} diff --git a/crates/apub/src/activities/comment/mod.rs b/crates/apub/src/activities/comment/mod.rs index 292d57bf4..e7499718c 100644 --- a/crates/apub/src/activities/comment/mod.rs +++ b/crates/apub/src/activities/comment/mod.rs @@ -1,18 +1,30 @@ -use crate::fetcher::person::get_or_fetch_and_upsert_person; -use lemmy_api_common::{blocking, comment::CommentResponse, send_local_notifs}; -use lemmy_db_queries::Crud; +use crate::{fetcher::person::get_or_fetch_and_upsert_person, ActorType}; +use activitystreams::{ + base::BaseExt, + link::{LinkExt, Mention}, +}; +use anyhow::anyhow; +use itertools::Itertools; +use lemmy_api_common::{blocking, comment::CommentResponse, send_local_notifs, WebFingerResponse}; +use lemmy_db_queries::{Crud, DbPool}; use lemmy_db_schema::{ - source::{comment::Comment, post::Post}, + source::{comment::Comment, community::Community, person::Person, post::Post}, CommentId, LocalUserId, }; use lemmy_db_views::comment_view::CommentView; -use lemmy_utils::{utils::scrape_text_for_mentions, LemmyError}; +use lemmy_utils::{ + request::{retry, RecvError}, + settings::structs::Settings, + utils::{scrape_text_for_mentions, MentionData}, + LemmyError, +}; use lemmy_websocket::{messages::SendComment, LemmyContext}; +use log::debug; +use reqwest::Client; use url::Url; -pub mod create; -pub mod update; +pub mod create_or_update; async fn get_notif_recipients( actor: &Url, @@ -63,3 +75,104 @@ pub(crate) async fn send_websocket_message< Ok(()) } + +pub struct MentionsAndAddresses { + pub ccs: Vec, + pub inboxes: Vec, + pub tags: Vec, +} + +/// This takes a comment, and builds a list of to_addresses, inboxes, +/// and mention tags, so they know where to be sent to. +/// Addresses are the persons / addresses that go in the cc field. +pub async fn collect_non_local_mentions( + comment: &Comment, + community: &Community, + context: &LemmyContext, +) -> Result { + let parent_creator = get_comment_parent_creator(context.pool(), comment).await?; + let mut addressed_ccs = vec![community.actor_id(), parent_creator.actor_id()]; + // Note: dont include community inbox here, as we send to it separately with `send_to_community()` + let mut inboxes = vec![parent_creator.get_shared_inbox_or_inbox_url()]; + + // Add the mention tag + let mut tags = Vec::new(); + + // Get the person IDs for any mentions + let mentions = scrape_text_for_mentions(&comment.content) + .into_iter() + // Filter only the non-local ones + .filter(|m| !m.is_local()) + .collect::>(); + + for mention in &mentions { + // TODO should it be fetching it every time? + if let Ok(actor_id) = fetch_webfinger_url(mention, context.client()).await { + debug!("mention actor_id: {}", actor_id); + addressed_ccs.push(actor_id.to_owned().to_string().parse()?); + + let mention_person = get_or_fetch_and_upsert_person(&actor_id, context, &mut 0).await?; + inboxes.push(mention_person.get_shared_inbox_or_inbox_url()); + + let mut mention_tag = Mention::new(); + mention_tag.set_href(actor_id).set_name(mention.full_name()); + tags.push(mention_tag); + } + } + + let inboxes = inboxes.into_iter().unique().collect(); + + Ok(MentionsAndAddresses { + ccs: addressed_ccs, + inboxes, + tags, + }) +} + +/// Returns the apub ID of the person this comment is responding to. Meaning, in case this is a +/// top-level comment, the creator of the post, otherwise the creator of the parent comment. +async fn get_comment_parent_creator( + pool: &DbPool, + comment: &Comment, +) -> Result { + let parent_creator_id = if let Some(parent_comment_id) = comment.parent_id { + let parent_comment = + blocking(pool, move |conn| Comment::read(conn, parent_comment_id)).await??; + parent_comment.creator_id + } else { + let parent_post_id = comment.post_id; + let parent_post = blocking(pool, move |conn| Post::read(conn, parent_post_id)).await??; + parent_post.creator_id + }; + Ok(blocking(pool, move |conn| Person::read(conn, parent_creator_id)).await??) +} + +/// Turns a person id like `@name@example.com` into an apub ID, like `https://example.com/user/name`, +/// using webfinger. +async fn fetch_webfinger_url(mention: &MentionData, client: &Client) -> Result { + 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() + .ok_or_else(|| anyhow!("No href found.").into()) +} diff --git a/crates/apub/src/activities/comment/remove.rs b/crates/apub/src/activities/comment/remove.rs deleted file mode 100644 index d60e3f804..000000000 --- a/crates/apub/src/activities/comment/remove.rs +++ /dev/null @@ -1,56 +0,0 @@ -use crate::activities::{comment::send_websocket_message, verify_mod_action}; -use activitystreams::activity::kind::RemoveType; -use lemmy_api_common::blocking; -use crate::{check_is_apub_id_valid, fetcher::objects::get_or_fetch_and_insert_comment}; -use lemmy_apub_lib::{verify_domains_match, ActivityCommonFields, ActivityHandlerNew, PublicUrl}; -use lemmy_db_queries::source::comment::Comment_; -use lemmy_db_schema::source::comment::Comment; -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 RemoveComment { - to: PublicUrl, - pub(in crate::activities::comment) object: Url, - cc: [Url; 1], - #[serde(rename = "type")] - kind: RemoveType, - #[serde(flatten)] - common: ActivityCommonFields, -} - -#[async_trait::async_trait(?Send)] -impl ActivityHandlerNew for RemoveComment { - async fn verify(&self, context: &LemmyContext, _: &mut i32) -> Result<(), LemmyError> { - verify_domains_match(&self.common.actor, self.common.id_unchecked())?; - check_is_apub_id_valid(&self.common.actor, false)?; - verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await - } - - async fn receive( - &self, - context: &LemmyContext, - request_counter: &mut i32, - ) -> Result<(), LemmyError> { - let comment = get_or_fetch_and_insert_comment(&self.object, context, request_counter).await?; - - let removed_comment = blocking(context.pool(), move |conn| { - Comment::update_removed(conn, comment.id, true) - }) - .await??; - - send_websocket_message( - removed_comment.id, - vec![], - UserOperationCrud::EditComment, - context, - ) - .await - } - - fn common(&self) -> &ActivityCommonFields { - &self.common - } -} diff --git a/crates/apub/src/activities/comment/undo_remove.rs b/crates/apub/src/activities/comment/undo_remove.rs deleted file mode 100644 index f3ebdf385..000000000 --- a/crates/apub/src/activities/comment/undo_remove.rs +++ /dev/null @@ -1,65 +0,0 @@ -use crate::activities::{ - comment::{remove::RemoveComment, send_websocket_message}, - verify_mod_action, -}; -use activitystreams::activity::kind::UndoType; -use lemmy_api_common::blocking; -use crate::{check_is_apub_id_valid, fetcher::objects::get_or_fetch_and_insert_comment}; -use lemmy_apub_lib::{verify_domains_match, ActivityCommonFields, ActivityHandlerNew, PublicUrl}; -use lemmy_db_queries::source::comment::Comment_; -use lemmy_db_schema::source::comment::Comment; -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 UndoRemoveComment { - to: PublicUrl, - object: RemoveComment, - cc: [Url; 1], - #[serde(rename = "type")] - kind: UndoType, - #[serde(flatten)] - common: ActivityCommonFields, -} - -#[async_trait::async_trait(?Send)] -impl ActivityHandlerNew for UndoRemoveComment { - async fn verify( - &self, - context: &LemmyContext, - request_counter: &mut i32, - ) -> Result<(), LemmyError> { - verify_domains_match(&self.common.actor, self.common.id_unchecked())?; - check_is_apub_id_valid(&self.common.actor, false)?; - verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?; - self.object.verify(context, request_counter).await - } - - async fn receive( - &self, - context: &LemmyContext, - request_counter: &mut i32, - ) -> Result<(), LemmyError> { - let comment = - get_or_fetch_and_insert_comment(&self.object.object, context, request_counter).await?; - - let removed_comment = blocking(context.pool(), move |conn| { - Comment::update_removed(conn, comment.id, false) - }) - .await??; - - send_websocket_message( - removed_comment.id, - vec![], - UserOperationCrud::EditComment, - context, - ) - .await - } - - fn common(&self) -> &ActivityCommonFields { - &self.common - } -} diff --git a/crates/apub/src/activities/comment/update.rs b/crates/apub/src/activities/comment/update.rs deleted file mode 100644 index 142656f50..000000000 --- a/crates/apub/src/activities/comment/update.rs +++ /dev/null @@ -1,70 +0,0 @@ -use crate::{ - activities::{ - comment::{get_notif_recipients, send_websocket_message}, - verify_activity, - verify_person_in_community, - }, - objects::FromApub, - NoteExt, -}; -use activitystreams::{activity::kind::UpdateType, base::BaseExt}; -use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler, PublicUrl}; -use lemmy_db_schema::source::comment::Comment; -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 UpdateComment { - to: PublicUrl, - object: NoteExt, - cc: Vec, - #[serde(rename = "type")] - kind: UpdateType, - #[serde(flatten)] - common: ActivityCommonFields, -} - -#[async_trait::async_trait(?Send)] -impl ActivityHandler for UpdateComment { - async fn verify( - &self, - context: &LemmyContext, - request_counter: &mut i32, - ) -> Result<(), LemmyError> { - verify_activity(self.common())?; - verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; - verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?; - Ok(()) - } - - async fn receive( - &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 recipients = - get_notif_recipients(&self.common.actor, &comment, context, request_counter).await?; - send_websocket_message( - comment.id, - recipients, - UserOperationCrud::EditComment, - context, - ) - .await - } - - fn common(&self) -> &ActivityCommonFields { - &self.common - } -} diff --git a/crates/apub/src/activities/community/add_mod.rs b/crates/apub/src/activities/community/add_mod.rs index 2785856c1..0dcd9818b 100644 --- a/crates/apub/src/activities/community/add_mod.rs +++ b/crates/apub/src/activities/community/add_mod.rs @@ -10,7 +10,7 @@ use crate::{ }; use activitystreams::{activity::kind::AddType, base::AnyBase}; use lemmy_api_common::blocking; -use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl}; +use lemmy_apub_lib::{values::PublicUrl, ActivityCommonFields, ActivityHandler}; use lemmy_db_queries::{source::community::CommunityModerator_, Joinable}; use lemmy_db_schema::source::community::{CommunityModerator, CommunityModeratorForm}; use lemmy_utils::LemmyError; @@ -38,7 +38,7 @@ impl ActivityHandler for AddMod { request_counter: &mut i32, ) -> Result<(), LemmyError> { verify_activity(self.common())?; - verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; + verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?; verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?; verify_add_remove_moderator_target(&self.target, self.cc[0].clone())?; Ok(()) diff --git a/crates/apub/src/activities/community/announce.rs b/crates/apub/src/activities/community/announce.rs index 301ccc648..9cde8a43c 100644 --- a/crates/apub/src/activities/community/announce.rs +++ b/crates/apub/src/activities/community/announce.rs @@ -1,34 +1,36 @@ use crate::{ activities::{ - comment::{create::CreateComment, update::UpdateComment}, + comment::create_or_update::CreateOrUpdateComment, community::{ add_mod::AddMod, block_user::BlockUserFromCommunity, + list_community_follower_inboxes, undo_block_user::UndoBlockUserFromCommunity, }, deletion::{ delete::DeletePostCommentOrCommunity, undo_delete::UndoDeletePostCommentOrCommunity, }, - post::{create::CreatePost, update::UpdatePost}, + generate_activity_id, + post::create_or_update::CreateOrUpdatePost, removal::{ remove::RemovePostCommentCommunityOrMod, undo_remove::UndoRemovePostCommentOrCommunity, }, verify_activity, verify_community, - voting::{ - dislike::DislikePostOrComment, - like::LikePostOrComment, - undo_dislike::UndoDislikePostOrComment, - undo_like::UndoLikePostOrComment, - }, + voting::{undo_vote::UndoVote, vote::Vote}, }, + activity_queue::send_activity_new, + extensions::context::lemmy_context, http::is_activity_already_known, insert_activity, + ActorType, + CommunityType, }; use activitystreams::activity::kind::AnnounceType; -use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl}; +use lemmy_apub_lib::{values::PublicUrl, ActivityCommonFields, ActivityHandler}; +use lemmy_db_schema::source::community::Community; use lemmy_utils::LemmyError; use lemmy_websocket::LemmyContext; use serde::{Deserialize, Serialize}; @@ -37,14 +39,10 @@ use url::Url; #[derive(Clone, Debug, Deserialize, Serialize, ActivityHandler)] #[serde(untagged)] pub enum AnnouncableActivities { - CreateComment(CreateComment), - UpdateComment(UpdateComment), - CreatePost(CreatePost), - UpdatePost(UpdatePost), - LikePostOrComment(LikePostOrComment), - DislikePostOrComment(DislikePostOrComment), - UndoLikePostOrComment(UndoLikePostOrComment), - UndoDislikePostOrComment(UndoDislikePostOrComment), + CreateOrUpdateComment(CreateOrUpdateComment), + CreateOrUpdatePost(Box), + Vote(Vote), + UndoVote(UndoVote), DeletePostCommentOrCommunity(DeletePostCommentOrCommunity), UndoDeletePostCommentOrCommunity(UndoDeletePostCommentOrCommunity), RemovePostCommentCommunityOrMod(RemovePostCommentCommunityOrMod), @@ -66,6 +64,38 @@ pub struct AnnounceActivity { common: ActivityCommonFields, } +impl AnnounceActivity { + pub async fn send( + object: AnnouncableActivities, + community: &Community, + additional_inboxes: Vec, + context: &LemmyContext, + ) -> Result<(), LemmyError> { + let announce = AnnounceActivity { + to: PublicUrl::Public, + object, + cc: vec![community.followers_url()], + kind: AnnounceType::Announce, + common: ActivityCommonFields { + context: lemmy_context(), + id: generate_activity_id(&AnnounceType::Announce)?, + actor: community.actor_id(), + unparsed: Default::default(), + }, + }; + let inboxes = list_community_follower_inboxes(community, additional_inboxes, context).await?; + send_activity_new( + context, + &announce, + &announce.common.id, + community, + inboxes, + false, + ) + .await + } +} + #[async_trait::async_trait(?Send)] impl ActivityHandler for AnnounceActivity { async fn verify( diff --git a/crates/apub/src/activities/community/block_user.rs b/crates/apub/src/activities/community/block_user.rs index c20652b02..34909b2e6 100644 --- a/crates/apub/src/activities/community/block_user.rs +++ b/crates/apub/src/activities/community/block_user.rs @@ -4,7 +4,7 @@ use crate::{ }; use activitystreams::activity::kind::BlockType; use lemmy_api_common::blocking; -use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl}; +use lemmy_apub_lib::{values::PublicUrl, ActivityCommonFields, ActivityHandler}; use lemmy_db_queries::{Bannable, Followable}; use lemmy_db_schema::source::community::{ CommunityFollower, @@ -36,7 +36,7 @@ impl ActivityHandler for BlockUserFromCommunity { request_counter: &mut i32, ) -> Result<(), LemmyError> { verify_activity(self.common())?; - verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; + verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?; verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?; Ok(()) } diff --git a/crates/apub/src/activities/community/mod.rs b/crates/apub/src/activities/community/mod.rs index 81152d926..4151471ad 100644 --- a/crates/apub/src/activities/community/mod.rs +++ b/crates/apub/src/activities/community/mod.rs @@ -1,8 +1,11 @@ +use crate::{check_is_apub_id_valid, CommunityType}; +use itertools::Itertools; use lemmy_api_common::{blocking, community::CommunityResponse}; -use lemmy_db_schema::CommunityId; +use lemmy_db_schema::{source::community::Community, CommunityId}; use lemmy_db_views_actor::community_view::CommunityView; -use lemmy_utils::LemmyError; +use lemmy_utils::{settings::structs::Settings, LemmyError}; use lemmy_websocket::{messages::SendCommunityRoomMessage, LemmyContext}; +use url::Url; pub mod add_mod; pub mod announce; @@ -33,3 +36,23 @@ pub(crate) async fn send_websocket_message< Ok(()) } + +async fn list_community_follower_inboxes( + community: &Community, + additional_inboxes: Vec, + context: &LemmyContext, +) -> Result, LemmyError> { + Ok( + vec![ + community.get_follower_inboxes(context.pool()).await?, + additional_inboxes, + ] + .iter() + .flatten() + .unique() + .filter(|inbox| inbox.host_str() != Some(&Settings::get().hostname)) + .filter(|inbox| check_is_apub_id_valid(inbox, false).is_ok()) + .map(|inbox| inbox.to_owned()) + .collect(), + ) +} diff --git a/crates/apub/src/activities/community/undo_block_user.rs b/crates/apub/src/activities/community/undo_block_user.rs index 025c498a6..53b665db1 100644 --- a/crates/apub/src/activities/community/undo_block_user.rs +++ b/crates/apub/src/activities/community/undo_block_user.rs @@ -9,7 +9,7 @@ use crate::{ }; use activitystreams::activity::kind::UndoType; use lemmy_api_common::blocking; -use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl}; +use lemmy_apub_lib::{values::PublicUrl, ActivityCommonFields, ActivityHandler}; use lemmy_db_queries::Bannable; use lemmy_db_schema::source::community::{CommunityPersonBan, CommunityPersonBanForm}; use lemmy_utils::LemmyError; @@ -36,7 +36,7 @@ impl ActivityHandler for UndoBlockUserFromCommunity { request_counter: &mut i32, ) -> Result<(), LemmyError> { verify_activity(self.common())?; - verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; + verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?; verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?; self.object.verify(context, request_counter).await?; Ok(()) diff --git a/crates/apub/src/activities/community/update.rs b/crates/apub/src/activities/community/update.rs index cfee29da7..fd0bf2d45 100644 --- a/crates/apub/src/activities/community/update.rs +++ b/crates/apub/src/activities/community/update.rs @@ -10,7 +10,7 @@ use crate::{ }; use activitystreams::activity::kind::UpdateType; use lemmy_api_common::blocking; -use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl}; +use lemmy_apub_lib::{values::PublicUrl, ActivityCommonFields, ActivityHandler}; use lemmy_db_queries::{ApubObject, Crud}; use lemmy_db_schema::source::community::{Community, CommunityForm}; use lemmy_utils::LemmyError; @@ -39,7 +39,7 @@ impl ActivityHandler for UpdateCommunity { request_counter: &mut i32, ) -> Result<(), LemmyError> { verify_activity(self.common())?; - verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; + verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?; verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?; Ok(()) } diff --git a/crates/apub/src/activities/deletion/delete.rs b/crates/apub/src/activities/deletion/delete.rs index f0f4185a7..cabfcce86 100644 --- a/crates/apub/src/activities/deletion/delete.rs +++ b/crates/apub/src/activities/deletion/delete.rs @@ -18,7 +18,7 @@ use crate::{ }; use activitystreams::activity::kind::DeleteType; use lemmy_api_common::blocking; -use lemmy_apub_lib::{verify_urls_match, ActivityCommonFields, ActivityHandler, PublicUrl}; +use lemmy_apub_lib::{values::PublicUrl, verify_urls_match, ActivityCommonFields, ActivityHandler}; use lemmy_db_queries::{ source::{comment::Comment_, community::Community_, post::Post_}, Crud, @@ -64,7 +64,8 @@ impl ActivityHandler for DeletePostCommentOrCommunity { } // deleting a post or comment else { - verify_person_in_community(&self.common().actor, &self.cc, context, request_counter).await?; + verify_person_in_community(&self.common().actor, &self.cc[0], context, request_counter) + .await?; let object_creator = get_post_or_comment_actor_id(&self.object, context, request_counter).await?; verify_urls_match(&self.common.actor, &object_creator)?; @@ -83,7 +84,7 @@ impl ActivityHandler for DeletePostCommentOrCommunity { if let Ok(community) = object_community { if community.local { // repeat these checks just to be sure - verify_person_in_community(&self.common().actor, &self.cc, context, request_counter) + verify_person_in_community(&self.common().actor, &self.cc[0], context, request_counter) .await?; verify_mod_action(&self.common.actor, self.object.clone(), context).await?; let mod_ = diff --git a/crates/apub/src/activities/deletion/undo_delete.rs b/crates/apub/src/activities/deletion/undo_delete.rs index 1de5ca587..ea70e5f5e 100644 --- a/crates/apub/src/activities/deletion/undo_delete.rs +++ b/crates/apub/src/activities/deletion/undo_delete.rs @@ -18,7 +18,7 @@ use crate::{ }; use activitystreams::activity::kind::UndoType; use lemmy_api_common::blocking; -use lemmy_apub_lib::{verify_urls_match, ActivityCommonFields, ActivityHandler, PublicUrl}; +use lemmy_apub_lib::{values::PublicUrl, verify_urls_match, ActivityCommonFields, ActivityHandler}; use lemmy_db_queries::source::{comment::Comment_, community::Community_, post::Post_}; use lemmy_db_schema::source::{comment::Comment, community::Community, post::Post}; use lemmy_utils::LemmyError; @@ -54,7 +54,8 @@ impl ActivityHandler for UndoDeletePostCommentOrCommunity { } // restoring a post or comment else { - verify_person_in_community(&self.common().actor, &self.cc, context, request_counter).await?; + verify_person_in_community(&self.common().actor, &self.cc[0], context, request_counter) + .await?; verify_urls_match(&self.common.actor, &self.object.common().actor)?; } Ok(()) @@ -71,7 +72,7 @@ impl ActivityHandler for UndoDeletePostCommentOrCommunity { if let Ok(community) = object_community { if community.local { // repeat these checks just to be sure - verify_person_in_community(&self.common().actor, &self.cc, context, request_counter) + verify_person_in_community(&self.common().actor, &self.cc[0], context, request_counter) .await?; verify_mod_action(&self.common.actor, self.object.object.clone(), context).await?; let mod_ = diff --git a/crates/apub/src/activities/mod.rs b/crates/apub/src/activities/mod.rs index d9f3dcdde..afbad6c8b 100644 --- a/crates/apub/src/activities/mod.rs +++ b/crates/apub/src/activities/mod.rs @@ -13,9 +13,12 @@ use lemmy_db_schema::{ DbUrl, }; use lemmy_db_views_actor::community_view::CommunityView; -use lemmy_utils::LemmyError; +use lemmy_utils::{settings::structs::Settings, LemmyError}; use lemmy_websocket::LemmyContext; -use url::Url; +use serde::{Deserialize, Serialize}; +use strum_macros::ToString; +use url::{ParseError, Url}; +use uuid::Uuid; pub mod comment; pub mod community; @@ -27,6 +30,12 @@ pub mod removal; pub mod send; pub mod voting; +#[derive(Clone, Debug, ToString, Deserialize, Serialize)] +pub enum CreateOrUpdateType { + Create, + Update, +} + /// Checks that the specified Url actually identifies a Person (by fetching it), and that the person /// doesn't have a site ban. async fn verify_person( @@ -41,27 +50,34 @@ async fn verify_person( Ok(()) } -/// Fetches the person and community to verify their type, then checks if person is banned from site -/// or community. -async fn verify_person_in_community( - person_id: &Url, +pub(crate) async fn extract_community( cc: &[Url], context: &LemmyContext, request_counter: &mut i32, ) -> Result { - let person = get_or_fetch_and_upsert_person(person_id, context, request_counter).await?; let mut cc_iter = cc.iter(); - let community: Community = loop { + loop { if let Some(cid) = cc_iter.next() { if let Ok(c) = get_or_fetch_and_upsert_community(cid, context, request_counter).await { - break c; + break Ok(c); } } else { return Err(anyhow!("No community found in cc").into()); } - }; - check_community_or_site_ban(&person, community.id, context.pool()).await?; - Ok(community) + } +} + +/// Fetches the person and community to verify their type, then checks if person is banned from site +/// or community. +pub(crate) async fn verify_person_in_community( + person_id: &Url, + community_id: &Url, + context: &LemmyContext, + request_counter: &mut i32, +) -> Result<(), LemmyError> { + let community = get_or_fetch_and_upsert_community(community_id, context, request_counter).await?; + let person = get_or_fetch_and_upsert_person(person_id, context, request_counter).await?; + check_community_or_site_ban(&person, community.id, context.pool()).await } /// Simply check that the url actually refers to a valid group. @@ -80,13 +96,16 @@ fn verify_activity(common: &ActivityCommonFields) -> Result<(), LemmyError> { Ok(()) } -async fn verify_mod_action( +/// Verify that the actor is a community mod. This check is only run if the community is local, +/// because in case of remote communities, admins can also perform mod actions. As admin status +/// is not federated, we cant verify their actions remotely. +pub(crate) async fn verify_mod_action( actor_id: &Url, - activity_cc: Url, + community: Url, context: &LemmyContext, ) -> Result<(), LemmyError> { let community = blocking(context.pool(), move |conn| { - Community::read_from_apub_id(conn, &activity_cc.into()) + Community::read_from_apub_id(conn, &community.into()) }) .await??; @@ -120,3 +139,18 @@ fn verify_add_remove_moderator_target(target: &Url, community: Url) -> Result<() } Ok(()) } + +/// Generate a unique ID for an activity, in the format: +/// `http(s)://example.com/receive/create/202daf0a-1489-45df-8d2e-c8a3173fed36` +fn generate_activity_id(kind: T) -> Result +where + T: ToString, +{ + let id = format!( + "{}/activities/{}/{}", + Settings::get().get_protocol_and_hostname(), + kind.to_string().to_lowercase(), + Uuid::new_v4() + ); + Url::parse(&id) +} diff --git a/crates/apub/src/activities/post/create.rs b/crates/apub/src/activities/post/create.rs deleted file mode 100644 index a2ccf4ce5..000000000 --- a/crates/apub/src/activities/post/create.rs +++ /dev/null @@ -1,62 +0,0 @@ -use crate::{ - activities::{post::send_websocket_message, verify_activity, verify_person_in_community}, - fetcher::person::get_or_fetch_and_upsert_person, - objects::FromApub, - ActorType, - PageExt, -}; -use activitystreams::{activity::kind::CreateType, base::BaseExt}; -use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler, PublicUrl}; -use lemmy_db_schema::source::post::Post; -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 CreatePost { - to: PublicUrl, - object: PageExt, - cc: Vec, - #[serde(rename = "type")] - kind: CreateType, - #[serde(flatten)] - common: ActivityCommonFields, -} - -#[async_trait::async_trait(?Send)] -impl ActivityHandler for CreatePost { - async fn verify( - &self, - context: &LemmyContext, - request_counter: &mut i32, - ) -> Result<(), LemmyError> { - verify_activity(self.common())?; - verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; - verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?; - Ok(()) - } - - async fn receive( - &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?; - - send_websocket_message(post.id, UserOperationCrud::CreatePost, context).await - } - - fn common(&self) -> &ActivityCommonFields { - &self.common - } -} diff --git a/crates/apub/src/activities/post/create_or_update.rs b/crates/apub/src/activities/post/create_or_update.rs new file mode 100644 index 000000000..4af720753 --- /dev/null +++ b/crates/apub/src/activities/post/create_or_update.rs @@ -0,0 +1,142 @@ +use crate::{ + activities::{ + community::announce::AnnouncableActivities, + extract_community, + generate_activity_id, + post::send_websocket_message, + verify_activity, + verify_mod_action, + verify_person_in_community, + CreateOrUpdateType, + }, + activity_queue::send_to_community_new, + extensions::context::lemmy_context, + fetcher::person::get_or_fetch_and_upsert_person, + objects::{post::Page, FromApub, ToApub}, + ActorType, +}; +use anyhow::anyhow; +use lemmy_api_common::blocking; +use lemmy_apub_lib::{ + values::PublicUrl, + verify_domains_match, + verify_urls_match, + ActivityCommonFields, + ActivityHandler, +}; +use lemmy_db_queries::Crud; +use lemmy_db_schema::source::{community::Community, person::Person, post::Post}; +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 CreateOrUpdatePost { + to: PublicUrl, + object: Page, + cc: [Url; 1], + #[serde(rename = "type")] + kind: CreateOrUpdateType, + #[serde(flatten)] + common: ActivityCommonFields, +} + +impl CreateOrUpdatePost { + pub async fn send( + post: &Post, + actor: &Person, + kind: CreateOrUpdateType, + context: &LemmyContext, + ) -> Result<(), LemmyError> { + let community_id = post.community_id; + let community = blocking(context.pool(), move |conn| { + Community::read(conn, community_id) + }) + .await??; + + let id = generate_activity_id(kind.clone())?; + let create_or_update = CreateOrUpdatePost { + to: PublicUrl::Public, + object: post.to_apub(context.pool()).await?, + cc: [community.actor_id()], + kind, + common: ActivityCommonFields { + context: lemmy_context(), + id: id.clone(), + actor: actor.actor_id(), + unparsed: Default::default(), + }, + }; + + let activity = AnnouncableActivities::CreateOrUpdatePost(Box::new(create_or_update)); + send_to_community_new(activity, &id, actor, &community, vec![], context).await + } +} + +#[async_trait::async_trait(?Send)] +impl ActivityHandler for CreateOrUpdatePost { + async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + verify_activity(self.common())?; + let community = extract_community(&self.cc, context, request_counter).await?; + let community_id = community.actor_id(); + 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_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 + // locked/stickied value, so this check may fail. So only check if its a local community, + // because then we will definitely receive all create and update activities separately. + let is_stickied_or_locked = + self.object.stickied == Some(true) || self.object.comments_enabled == Some(false); + if community.local && is_stickied_or_locked { + return Err(anyhow!("New post cannot be stickied or locked").into()); + } + } + CreateOrUpdateType::Update => { + let is_mod_action = self.object.is_mod_action(context.pool()).await?; + 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_urls_match(&self.common.actor, &self.object.attributed_to)?; + } + } + } + self.object.verify(context, request_counter).await?; + Ok(()) + } + + async fn receive( + &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 notif_type = match self.kind { + CreateOrUpdateType::Create => UserOperationCrud::CreatePost, + CreateOrUpdateType::Update => UserOperationCrud::EditPost, + }; + send_websocket_message(post.id, notif_type, context).await + } + + fn common(&self) -> &ActivityCommonFields { + &self.common + } +} diff --git a/crates/apub/src/activities/post/mod.rs b/crates/apub/src/activities/post/mod.rs index a46620402..b60348ed0 100644 --- a/crates/apub/src/activities/post/mod.rs +++ b/crates/apub/src/activities/post/mod.rs @@ -4,8 +4,7 @@ use lemmy_db_views::post_view::PostView; use lemmy_utils::LemmyError; use lemmy_websocket::{messages::SendPost, LemmyContext}; -pub mod create; -pub mod update; +pub mod create_or_update; pub(crate) async fn send_websocket_message< OP: ToString + Send + lemmy_websocket::OperationType + 'static, diff --git a/crates/apub/src/activities/post/update.rs b/crates/apub/src/activities/post/update.rs deleted file mode 100644 index 13456dcd8..000000000 --- a/crates/apub/src/activities/post/update.rs +++ /dev/null @@ -1,96 +0,0 @@ -use crate::{ - activities::{ - post::send_websocket_message, - verify_activity, - verify_mod_action, - verify_person_in_community, - }, - objects::{FromApub, FromApubToForm}, - ActorType, - PageExt, -}; -use activitystreams::{activity::kind::UpdateType, base::BaseExt}; -use anyhow::Context; -use lemmy_api_common::blocking; -use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler, PublicUrl}; -use lemmy_db_queries::ApubObject; -use lemmy_db_schema::{ - source::post::{Post, PostForm}, - DbUrl, -}; -use lemmy_utils::{location_info, LemmyError}; -use lemmy_websocket::{LemmyContext, UserOperationCrud}; -use url::Url; - -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "camelCase")] -pub struct UpdatePost { - to: PublicUrl, - object: PageExt, - cc: Vec, - #[serde(rename = "type")] - kind: UpdateType, - #[serde(flatten)] - common: ActivityCommonFields, -} - -#[async_trait::async_trait(?Send)] -impl ActivityHandler for UpdatePost { - async fn verify( - &self, - context: &LemmyContext, - request_counter: &mut i32, - ) -> Result<(), LemmyError> { - verify_activity(self.common())?; - let community = - verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; - - let temp_post = PostForm::from_apub( - &self.object, - context, - self.common.actor.clone(), - request_counter, - true, - ) - .await?; - let post_id: DbUrl = temp_post.ap_id.context(location_info!())?; - let old_post = blocking(context.pool(), move |conn| { - Post::read_from_apub_id(conn, &post_id) - }) - .await??; - let stickied = temp_post.stickied.context(location_info!())?; - let locked = temp_post.locked.context(location_info!())?; - // community mod changed locked/sticky status - if (stickied != old_post.stickied) || (locked != old_post.locked) { - verify_mod_action(&self.common.actor, community.actor_id(), context).await?; - } - // user edited their own post - else { - verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?; - } - - Ok(()) - } - - async fn receive( - &self, - context: &LemmyContext, - request_counter: &mut i32, - ) -> Result<(), LemmyError> { - let post = Post::from_apub( - &self.object, - context, - self.common.actor.clone(), - request_counter, - // TODO: we already check here if the mod action is valid, can remove that check param - true, - ) - .await?; - - send_websocket_message(post.id, UserOperationCrud::EditPost, context).await - } - - fn common(&self) -> &ActivityCommonFields { - &self.common - } -} diff --git a/crates/apub/src/activities/private_message/create.rs b/crates/apub/src/activities/private_message/create.rs deleted file mode 100644 index 5e6db4ea8..000000000 --- a/crates/apub/src/activities/private_message/create.rs +++ /dev/null @@ -1,64 +0,0 @@ -use crate::{ - activities::{private_message::send_websocket_message, verify_activity, verify_person}, - objects::FromApub, - NoteExt, -}; -use activitystreams::{activity::kind::CreateType, base::BaseExt}; -use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler}; -use lemmy_db_schema::source::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 CreatePrivateMessage { - to: Url, - object: NoteExt, - #[serde(rename = "type")] - kind: CreateType, - #[serde(flatten)] - common: ActivityCommonFields, -} - -#[async_trait::async_trait(?Send)] -impl ActivityHandler for CreatePrivateMessage { - async fn verify( - &self, - context: &LemmyContext, - request_counter: &mut i32, - ) -> Result<(), LemmyError> { - verify_activity(self.common())?; - verify_person(&self.common.actor, context, request_counter).await?; - verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?; - Ok(()) - } - - async fn receive( - &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?; - - send_websocket_message( - private_message.id, - UserOperationCrud::CreatePrivateMessage, - context, - ) - .await?; - - Ok(()) - } - - fn common(&self) -> &ActivityCommonFields { - &self.common - } -} diff --git a/crates/apub/src/activities/private_message/create_or_update.rs b/crates/apub/src/activities/private_message/create_or_update.rs new file mode 100644 index 000000000..05f3c98f2 --- /dev/null +++ b/crates/apub/src/activities/private_message/create_or_update.rs @@ -0,0 +1,107 @@ +use crate::{ + activities::{ + generate_activity_id, + private_message::send_websocket_message, + verify_activity, + verify_person, + CreateOrUpdateType, + }, + activity_queue::send_activity_new, + extensions::context::lemmy_context, + objects::{private_message::Note, FromApub, ToApub}, + ActorType, +}; +use lemmy_api_common::blocking; +use lemmy_apub_lib::{verify_domains_match, ActivityCommonFields, ActivityHandler}; +use lemmy_db_queries::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 CreateOrUpdatePrivateMessage { + to: Url, + object: Note, + #[serde(rename = "type")] + kind: CreateOrUpdateType, + #[serde(flatten)] + common: ActivityCommonFields, +} + +impl CreateOrUpdatePrivateMessage { + pub async fn send( + private_message: &PrivateMessage, + actor: &Person, + kind: CreateOrUpdateType, + context: &LemmyContext, + ) -> Result<(), LemmyError> { + let recipient_id = private_message.recipient_id; + let recipient = + blocking(context.pool(), move |conn| Person::read(conn, recipient_id)).await??; + + let id = generate_activity_id(kind.clone())?; + let create_or_update = CreateOrUpdatePrivateMessage { + to: recipient.actor_id(), + object: private_message.to_apub(context.pool()).await?, + kind, + common: ActivityCommonFields { + context: lemmy_context(), + id: id.clone(), + actor: actor.actor_id(), + unparsed: Default::default(), + }, + }; + send_activity_new( + context, + &create_or_update, + &id, + actor, + vec![recipient.get_shared_inbox_or_inbox_url()], + true, + ) + .await + } +} +#[async_trait::async_trait(?Send)] +impl ActivityHandler for CreateOrUpdatePrivateMessage { + async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + verify_activity(self.common())?; + verify_person(&self.common.actor, context, request_counter).await?; + verify_domains_match(&self.common.actor, &self.object.id)?; + self.object.verify(context, request_counter).await?; + Ok(()) + } + + async fn receive( + &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 notif_type = match self.kind { + CreateOrUpdateType::Create => UserOperationCrud::CreatePrivateMessage, + CreateOrUpdateType::Update => UserOperationCrud::EditPrivateMessage, + }; + send_websocket_message(private_message.id, notif_type, context).await?; + + Ok(()) + } + + fn common(&self) -> &ActivityCommonFields { + &self.common + } +} diff --git a/crates/apub/src/activities/private_message/mod.rs b/crates/apub/src/activities/private_message/mod.rs index beb28b299..0c0557286 100644 --- a/crates/apub/src/activities/private_message/mod.rs +++ b/crates/apub/src/activities/private_message/mod.rs @@ -4,10 +4,9 @@ use lemmy_db_views::{local_user_view::LocalUserView, private_message_view::Priva use lemmy_utils::LemmyError; use lemmy_websocket::{messages::SendUserRoomMessage, LemmyContext, UserOperationCrud}; -pub mod create; +pub mod create_or_update; pub mod delete; pub mod undo_delete; -pub mod update; async fn send_websocket_message( private_message_id: PrivateMessageId, diff --git a/crates/apub/src/activities/private_message/update.rs b/crates/apub/src/activities/private_message/update.rs deleted file mode 100644 index 72ac0749a..000000000 --- a/crates/apub/src/activities/private_message/update.rs +++ /dev/null @@ -1,64 +0,0 @@ -use crate::{ - activities::{private_message::send_websocket_message, verify_activity, verify_person}, - objects::FromApub, - NoteExt, -}; -use activitystreams::{activity::kind::UpdateType, base::BaseExt}; -use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler}; -use lemmy_db_schema::source::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 UpdatePrivateMessage { - to: Url, - object: NoteExt, - #[serde(rename = "type")] - kind: UpdateType, - #[serde(flatten)] - common: ActivityCommonFields, -} - -#[async_trait::async_trait(?Send)] -impl ActivityHandler for UpdatePrivateMessage { - async fn verify( - &self, - context: &LemmyContext, - request_counter: &mut i32, - ) -> Result<(), LemmyError> { - verify_activity(self.common())?; - verify_person(&self.common.actor, context, request_counter).await?; - verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?; - Ok(()) - } - - async fn receive( - &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?; - - send_websocket_message( - private_message.id, - UserOperationCrud::EditPrivateMessage, - context, - ) - .await?; - - Ok(()) - } - - fn common(&self) -> &ActivityCommonFields { - &self.common - } -} diff --git a/crates/apub/src/activities/removal/remove.rs b/crates/apub/src/activities/removal/remove.rs index a30f23327..053ddadf8 100644 --- a/crates/apub/src/activities/removal/remove.rs +++ b/crates/apub/src/activities/removal/remove.rs @@ -19,7 +19,7 @@ use crate::{ use activitystreams::{activity::kind::RemoveType, base::AnyBase}; use anyhow::anyhow; use lemmy_api_common::blocking; -use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl}; +use lemmy_apub_lib::{values::PublicUrl, ActivityCommonFields, ActivityHandler}; use lemmy_db_queries::{ source::{comment::Comment_, community::Community_, post::Post_}, Joinable, @@ -64,13 +64,13 @@ impl ActivityHandler for RemovePostCommentCommunityOrMod { } // removing community mod else if let Some(target) = &self.target { - verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; + verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?; verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?; verify_add_remove_moderator_target(target, self.cc[0].clone())?; } // removing a post or comment else { - verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; + verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?; verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?; } Ok(()) diff --git a/crates/apub/src/activities/removal/undo_remove.rs b/crates/apub/src/activities/removal/undo_remove.rs index 997d527d8..db4518b27 100644 --- a/crates/apub/src/activities/removal/undo_remove.rs +++ b/crates/apub/src/activities/removal/undo_remove.rs @@ -17,7 +17,7 @@ use crate::{ use activitystreams::activity::kind::UndoType; use anyhow::anyhow; use lemmy_api_common::blocking; -use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl}; +use lemmy_apub_lib::{values::PublicUrl, ActivityCommonFields, ActivityHandler}; use lemmy_db_queries::source::{comment::Comment_, community::Community_, post::Post_}; use lemmy_db_schema::source::{comment::Comment, community::Community, post::Post}; use lemmy_utils::LemmyError; @@ -52,7 +52,7 @@ impl ActivityHandler for UndoRemovePostCommentOrCommunity { } // removing a post or comment else { - verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; + verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?; verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?; } self.object.verify(context, request_counter).await?; diff --git a/crates/apub/src/activities/send/comment.rs b/crates/apub/src/activities/send/comment.rs index b93f9e1c9..8b446860a 100644 --- a/crates/apub/src/activities/send/comment.rs +++ b/crates/apub/src/activities/send/comment.rs @@ -1,114 +1,28 @@ use crate::{ - activities::send::generate_activity_id, - activity_queue::{send_comment_mentions, send_to_community}, + activities::generate_activity_id, + activity_queue::send_to_community, extensions::context::lemmy_context, - fetcher::person::get_or_fetch_and_upsert_person, - objects::ToApub, ActorType, - ApubLikeableType, ApubObjectType, }; use activitystreams::{ activity::{ - kind::{CreateType, DeleteType, DislikeType, LikeType, RemoveType, UndoType, UpdateType}, - Create, + kind::{DeleteType, RemoveType, UndoType}, Delete, - Dislike, - Like, Remove, Undo, - Update, }, - base::AnyBase, - link::Mention, prelude::*, public, }; -use anyhow::anyhow; -use itertools::Itertools; -use lemmy_api_common::{blocking, WebFingerResponse}; -use lemmy_db_queries::{Crud, DbPool}; +use lemmy_api_common::blocking; +use lemmy_db_queries::Crud; use lemmy_db_schema::source::{comment::Comment, community::Community, person::Person, post::Post}; -use lemmy_utils::{ - request::{retry, RecvError}, - settings::structs::Settings, - utils::{scrape_text_for_mentions, MentionData}, - LemmyError, -}; +use lemmy_utils::LemmyError; use lemmy_websocket::LemmyContext; -use log::debug; -use reqwest::Client; -use serde_json::Error; -use url::Url; #[async_trait::async_trait(?Send)] impl ApubObjectType for Comment { - /// Send out information about a newly created comment, to the followers of the community and - /// mentioned persons. - async fn send_create(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> { - let note = self.to_apub(context.pool()).await?; - - let post_id = self.post_id; - let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; - - let community_id = post.community_id; - let community = blocking(context.pool(), move |conn| { - Community::read(conn, community_id) - }) - .await??; - - let maa = collect_non_local_mentions(self, &community, context).await?; - - let mut create = Create::new( - creator.actor_id.to_owned().into_inner(), - note.into_any_base()?, - ); - create - .set_many_contexts(lemmy_context()?) - .set_id(generate_activity_id(CreateType::Create)?) - .set_to(public()) - .set_many_ccs(maa.ccs.to_owned()) - // Set the mention tags - .set_many_tags(maa.get_tags()?); - - send_to_community(create.clone(), creator, &community, None, context).await?; - send_comment_mentions(creator, maa.inboxes, create, context).await?; - Ok(()) - } - - /// Send out information about an edited post, to the followers of the community and mentioned - /// persons. - async fn send_update(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> { - let note = self.to_apub(context.pool()).await?; - - let post_id = self.post_id; - let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; - - let community_id = post.community_id; - let community = blocking(context.pool(), move |conn| { - Community::read(conn, community_id) - }) - .await??; - - let maa = collect_non_local_mentions(self, &community, context).await?; - - let mut update = Update::new( - creator.actor_id.to_owned().into_inner(), - note.into_any_base()?, - ); - update - .set_many_contexts(lemmy_context()?) - .set_id(generate_activity_id(UpdateType::Update)?) - .set_to(public()) - .set_many_ccs(maa.ccs.to_owned()) - // Set the mention tags - .set_many_tags(maa.get_tags()?); - - send_to_community(update.clone(), creator, &community, None, context).await?; - send_comment_mentions(creator, maa.inboxes, update, context).await?; - Ok(()) - } - async fn send_delete(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> { let post_id = self.post_id; let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; @@ -124,7 +38,7 @@ impl ApubObjectType for Comment { self.ap_id.to_owned().into_inner(), ); delete - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(generate_activity_id(DeleteType::Delete)?) .set_to(public()) .set_many_ccs(vec![community.actor_id()]); @@ -153,7 +67,7 @@ impl ApubObjectType for Comment { self.ap_id.to_owned().into_inner(), ); delete - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(generate_activity_id(DeleteType::Delete)?) .set_to(public()) .set_many_ccs(vec![community.actor_id()]); @@ -164,7 +78,7 @@ impl ApubObjectType for Comment { delete.into_any_base()?, ); undo - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(generate_activity_id(UndoType::Undo)?) .set_to(public()) .set_many_ccs(vec![community.actor_id()]); @@ -188,7 +102,7 @@ impl ApubObjectType for Comment { self.ap_id.to_owned().into_inner(), ); remove - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(generate_activity_id(RemoveType::Remove)?) .set_to(public()) .set_many_ccs(vec![community.actor_id()]); @@ -217,7 +131,7 @@ impl ApubObjectType for Comment { self.ap_id.to_owned().into_inner(), ); remove - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(generate_activity_id(RemoveType::Remove)?) .set_to(public()) .set_many_ccs(vec![community.actor_id()]); @@ -228,7 +142,7 @@ impl ApubObjectType for Comment { remove.into_any_base()?, ); undo - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(generate_activity_id(UndoType::Undo)?) .set_to(public()) .set_many_ccs(vec![community.actor_id()]); @@ -237,204 +151,3 @@ impl ApubObjectType for Comment { Ok(()) } } - -#[async_trait::async_trait(?Send)] -impl ApubLikeableType for Comment { - async fn send_like(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> { - let post_id = self.post_id; - let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; - - let community_id = post.community_id; - let community = blocking(context.pool(), move |conn| { - Community::read(conn, community_id) - }) - .await??; - - let mut like = Like::new( - creator.actor_id.to_owned().into_inner(), - self.ap_id.to_owned().into_inner(), - ); - like - .set_many_contexts(lemmy_context()?) - .set_id(generate_activity_id(LikeType::Like)?) - .set_to(public()) - .set_many_ccs(vec![community.actor_id()]); - - send_to_community(like, creator, &community, None, context).await?; - Ok(()) - } - - async fn send_dislike(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> { - let post_id = self.post_id; - let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; - - let community_id = post.community_id; - let community = blocking(context.pool(), move |conn| { - Community::read(conn, community_id) - }) - .await??; - - let mut dislike = Dislike::new( - creator.actor_id.to_owned().into_inner(), - self.ap_id.to_owned().into_inner(), - ); - dislike - .set_many_contexts(lemmy_context()?) - .set_id(generate_activity_id(DislikeType::Dislike)?) - .set_to(public()) - .set_many_ccs(vec![community.actor_id()]); - - send_to_community(dislike, creator, &community, None, context).await?; - Ok(()) - } - - async fn send_undo_like( - &self, - creator: &Person, - context: &LemmyContext, - ) -> Result<(), LemmyError> { - let post_id = self.post_id; - let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; - - let community_id = post.community_id; - let community = blocking(context.pool(), move |conn| { - Community::read(conn, community_id) - }) - .await??; - - let mut like = Like::new( - creator.actor_id.to_owned().into_inner(), - self.ap_id.to_owned().into_inner(), - ); - like - .set_many_contexts(lemmy_context()?) - .set_id(generate_activity_id(DislikeType::Dislike)?) - .set_to(public()) - .set_many_ccs(vec![community.actor_id()]); - - // Undo that fake activity - let mut undo = Undo::new( - creator.actor_id.to_owned().into_inner(), - like.into_any_base()?, - ); - undo - .set_many_contexts(lemmy_context()?) - .set_id(generate_activity_id(UndoType::Undo)?) - .set_to(public()) - .set_many_ccs(vec![community.actor_id()]); - - send_to_community(undo, creator, &community, None, context).await?; - Ok(()) - } -} - -struct MentionsAndAddresses { - ccs: Vec, - inboxes: Vec, - tags: Vec, -} - -impl MentionsAndAddresses { - fn get_tags(&self) -> Result, Error> { - self - .tags - .iter() - .map(|t| t.to_owned().into_any_base()) - .collect::, Error>>() - } -} - -/// This takes a comment, and builds a list of to_addresses, inboxes, -/// and mention tags, so they know where to be sent to. -/// Addresses are the persons / addresses that go in the cc field. -async fn collect_non_local_mentions( - comment: &Comment, - community: &Community, - context: &LemmyContext, -) -> Result { - let parent_creator = get_comment_parent_creator(context.pool(), comment).await?; - let mut addressed_ccs = vec![community.actor_id(), parent_creator.actor_id()]; - // Note: dont include community inbox here, as we send to it separately with `send_to_community()` - let mut inboxes = vec![parent_creator.get_shared_inbox_or_inbox_url()]; - - // Add the mention tag - let mut tags = Vec::new(); - - // Get the person IDs for any mentions - let mentions = scrape_text_for_mentions(&comment.content) - .into_iter() - // Filter only the non-local ones - .filter(|m| !m.is_local()) - .collect::>(); - - for mention in &mentions { - // TODO should it be fetching it every time? - if let Ok(actor_id) = fetch_webfinger_url(mention, context.client()).await { - debug!("mention actor_id: {}", actor_id); - addressed_ccs.push(actor_id.to_owned().to_string().parse()?); - - let mention_person = get_or_fetch_and_upsert_person(&actor_id, context, &mut 0).await?; - inboxes.push(mention_person.get_shared_inbox_or_inbox_url()); - - let mut mention_tag = Mention::new(); - mention_tag.set_href(actor_id).set_name(mention.full_name()); - tags.push(mention_tag); - } - } - - let inboxes = inboxes.into_iter().unique().collect(); - - Ok(MentionsAndAddresses { - ccs: addressed_ccs, - inboxes, - tags, - }) -} - -/// Returns the apub ID of the person this comment is responding to. Meaning, in case this is a -/// top-level comment, the creator of the post, otherwise the creator of the parent comment. -async fn get_comment_parent_creator( - pool: &DbPool, - comment: &Comment, -) -> Result { - let parent_creator_id = if let Some(parent_comment_id) = comment.parent_id { - let parent_comment = - blocking(pool, move |conn| Comment::read(conn, parent_comment_id)).await??; - parent_comment.creator_id - } else { - let parent_post_id = comment.post_id; - let parent_post = blocking(pool, move |conn| Post::read(conn, parent_post_id)).await??; - parent_post.creator_id - }; - Ok(blocking(pool, move |conn| Person::read(conn, parent_creator_id)).await??) -} - -/// Turns a person id like `@name@example.com` into an apub ID, like `https://example.com/user/name`, -/// using webfinger. -async fn fetch_webfinger_url(mention: &MentionData, client: &Client) -> Result { - 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() - .ok_or_else(|| anyhow!("No href found.").into()) -} diff --git a/crates/apub/src/activities/send/community.rs b/crates/apub/src/activities/send/community.rs index bf7514797..852410624 100644 --- a/crates/apub/src/activities/send/community.rs +++ b/crates/apub/src/activities/send/community.rs @@ -1,5 +1,5 @@ use crate::{ - activities::send::generate_activity_id, + activities::generate_activity_id, activity_queue::{send_activity_single_dest, send_to_community, send_to_community_followers}, check_is_apub_id_valid, extensions::context::lemmy_context, @@ -54,7 +54,7 @@ impl ActorType for Community { self.local } fn actor_id(&self) -> Url { - self.actor_id.to_owned().into_inner() + self.actor_id.to_owned().into() } fn name(&self) -> String { self.name.clone() @@ -78,7 +78,7 @@ impl ActorType for Community { #[async_trait::async_trait(?Send)] impl CommunityType for Community { fn followers_url(&self) -> Url { - self.followers_url.clone().into_inner() + self.followers_url.clone().into() } /// As a local community, accept the follow request from a remote person. @@ -98,7 +98,7 @@ impl CommunityType for Community { follow.into_any_base()?, ); accept - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(generate_activity_id(AcceptType::Accept)?) .set_to(person.actor_id()); @@ -117,7 +117,7 @@ impl CommunityType for Community { self.to_apub(context.pool()).await?.into_any_base()?, ); update - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(generate_activity_id(UpdateType::Update)?) .set_to(public()) .set_many_ccs(vec![self.actor_id()]); @@ -134,7 +134,7 @@ impl CommunityType for Community { if self.local { let mut delete = Delete::new(self.actor_id(), self.actor_id()); delete - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(generate_activity_id(DeleteType::Delete)?) .set_to(public()) .set_many_ccs(vec![self.followers_url()]); @@ -145,7 +145,7 @@ impl CommunityType for Community { else { let mut delete = Delete::new(mod_.actor_id(), self.actor_id()); delete - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(generate_activity_id(DeleteType::Delete)?) .set_to(public()) .set_many_ccs(vec![self.actor_id()]); @@ -163,14 +163,14 @@ impl CommunityType for Community { if self.local { let mut delete = Delete::new(self.actor_id(), self.actor_id()); delete - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(generate_activity_id(DeleteType::Delete)?) .set_to(public()) .set_many_ccs(vec![self.followers_url()]); let mut undo = Undo::new(self.actor_id(), delete.into_any_base()?); undo - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(generate_activity_id(UndoType::Undo)?) .set_to(public()) .set_many_ccs(vec![self.followers_url()]); @@ -181,14 +181,14 @@ impl CommunityType for Community { else { let mut delete = Delete::new(mod_.actor_id(), self.actor_id()); delete - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(generate_activity_id(DeleteType::Delete)?) .set_to(public()) .set_many_ccs(vec![self.actor_id()]); let mut undo = Undo::new(mod_.actor_id(), delete.into_any_base()?); undo - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(generate_activity_id(UndoType::Undo)?) .set_to(public()) .set_many_ccs(vec![self.actor_id()]); @@ -202,7 +202,7 @@ impl CommunityType for Community { async fn send_remove(&self, context: &LemmyContext) -> Result<(), LemmyError> { let mut remove = Remove::new(self.actor_id(), self.actor_id()); remove - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(generate_activity_id(RemoveType::Remove)?) .set_to(public()) .set_many_ccs(vec![self.followers_url()]); @@ -215,7 +215,7 @@ impl CommunityType for Community { async fn send_undo_remove(&self, context: &LemmyContext) -> Result<(), LemmyError> { let mut remove = Remove::new(self.actor_id(), self.actor_id()); remove - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(generate_activity_id(RemoveType::Remove)?) .set_to(public()) .set_many_ccs(vec![self.followers_url()]); @@ -223,7 +223,7 @@ impl CommunityType for Community { // Undo that fake activity let mut undo = Undo::new(self.actor_id(), remove.into_any_base()?); undo - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(generate_activity_id(LikeType::Like)?) .set_to(public()) .set_many_ccs(vec![self.followers_url()]); @@ -267,7 +267,7 @@ impl CommunityType for Community { } let mut announce = Announce::new(self.actor_id(), activity); announce - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(generate_activity_id(AnnounceType::Announce)?) .set_to(public()) .set_many_ccs(ccs); @@ -306,7 +306,7 @@ impl CommunityType for Community { ) -> Result<(), LemmyError> { let mut add = Add::new(actor.actor_id(), added_mod.actor_id()); add - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(generate_activity_id(AddType::Add)?) .set_to(public()) .set_many_ccs(vec![self.actor_id()]) @@ -324,7 +324,7 @@ impl CommunityType for Community { ) -> Result<(), LemmyError> { let mut remove = Remove::new(actor.actor_id(), removed_mod.actor_id()); remove - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(generate_activity_id(RemoveType::Remove)?) .set_to(public()) .set_many_ccs(vec![self.actor_id()]) @@ -342,7 +342,7 @@ impl CommunityType for Community { ) -> Result<(), LemmyError> { let mut block = Block::new(actor.actor_id(), blocked_user.actor_id()); block - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(generate_activity_id(BlockType::Block)?) .set_to(public()) .set_many_ccs(vec![self.actor_id()]); @@ -359,7 +359,7 @@ impl CommunityType for Community { ) -> Result<(), LemmyError> { let mut block = Block::new(actor.actor_id(), unblocked_user.actor_id()); block - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(generate_activity_id(BlockType::Block)?) .set_to(public()) .set_many_ccs(vec![self.actor_id()]); @@ -367,7 +367,7 @@ impl CommunityType for Community { // Undo that fake activity let mut undo = Undo::new(actor.actor_id(), block.into_any_base()?); undo - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(generate_activity_id(UndoType::Undo)?) .set_to(public()) .set_many_ccs(vec![self.actor_id()]); diff --git a/crates/apub/src/activities/send/mod.rs b/crates/apub/src/activities/send/mod.rs index 10dd8a263..65135bdda 100644 --- a/crates/apub/src/activities/send/mod.rs +++ b/crates/apub/src/activities/send/mod.rs @@ -1,24 +1,5 @@ -use lemmy_utils::settings::structs::Settings; -use url::{ParseError, Url}; -use uuid::Uuid; - pub(crate) mod comment; pub(crate) mod community; pub(crate) mod person; pub(crate) mod post; pub(crate) mod private_message; - -/// Generate a unique ID for an activity, in the format: -/// `http(s)://example.com/receive/create/202daf0a-1489-45df-8d2e-c8a3173fed36` -fn generate_activity_id(kind: T) -> Result -where - T: ToString, -{ - let id = format!( - "{}/activities/{}/{}", - Settings::get().get_protocol_and_hostname(), - kind.to_string().to_lowercase(), - Uuid::new_v4() - ); - Url::parse(&id) -} diff --git a/crates/apub/src/activities/send/person.rs b/crates/apub/src/activities/send/person.rs index b1fc0cd2b..b7b43c433 100644 --- a/crates/apub/src/activities/send/person.rs +++ b/crates/apub/src/activities/send/person.rs @@ -1,5 +1,5 @@ use crate::{ - activities::send::generate_activity_id, + activities::generate_activity_id, activity_queue::send_activity_single_dest, extensions::context::lemmy_context, ActorType, @@ -78,7 +78,7 @@ impl UserType for Person { let mut follow = Follow::new(self.actor_id(), community.actor_id()); follow - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(generate_activity_id(FollowType::Follow)?) .set_to(community.actor_id()); @@ -99,7 +99,7 @@ impl UserType for Person { let mut follow = Follow::new(self.actor_id(), community.actor_id()); follow - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(generate_activity_id(FollowType::Follow)?) .set_to(community.actor_id()); @@ -109,7 +109,7 @@ impl UserType for Person { follow.into_any_base()?, ); undo - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(generate_activity_id(UndoType::Undo)?) .set_to(community.actor_id()); diff --git a/crates/apub/src/activities/send/post.rs b/crates/apub/src/activities/send/post.rs index c51d6f2d6..b9713661b 100644 --- a/crates/apub/src/activities/send/post.rs +++ b/crates/apub/src/activities/send/post.rs @@ -1,22 +1,16 @@ use crate::{ - activities::send::generate_activity_id, + activities::generate_activity_id, activity_queue::send_to_community, extensions::context::lemmy_context, - objects::ToApub, ActorType, - ApubLikeableType, ApubObjectType, }; use activitystreams::{ activity::{ - kind::{CreateType, DeleteType, DislikeType, LikeType, RemoveType, UndoType, UpdateType}, - Create, + kind::{DeleteType, RemoveType, UndoType}, Delete, - Dislike, - Like, Remove, Undo, - Update, }, prelude::*, public, @@ -29,54 +23,6 @@ use lemmy_websocket::LemmyContext; #[async_trait::async_trait(?Send)] impl ApubObjectType for Post { - /// Send out information about a newly created post, to the followers of the community. - async fn send_create(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> { - let page = self.to_apub(context.pool()).await?; - - let community_id = self.community_id; - let community = blocking(context.pool(), move |conn| { - Community::read(conn, community_id) - }) - .await??; - - let mut create = Create::new( - creator.actor_id.to_owned().into_inner(), - page.into_any_base()?, - ); - create - .set_many_contexts(lemmy_context()?) - .set_id(generate_activity_id(CreateType::Create)?) - .set_to(public()) - .set_many_ccs(vec![community.actor_id()]); - - send_to_community(create, creator, &community, None, context).await?; - Ok(()) - } - - /// Send out information about an edited post, to the followers of the community. - async fn send_update(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> { - let page = self.to_apub(context.pool()).await?; - - let community_id = self.community_id; - let community = blocking(context.pool(), move |conn| { - Community::read(conn, community_id) - }) - .await??; - - let mut update = Update::new( - creator.actor_id.to_owned().into_inner(), - page.into_any_base()?, - ); - update - .set_many_contexts(lemmy_context()?) - .set_id(generate_activity_id(UpdateType::Update)?) - .set_to(public()) - .set_many_ccs(vec![community.actor_id()]); - - send_to_community(update, creator, &community, None, context).await?; - Ok(()) - } - async fn send_delete(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> { let community_id = self.community_id; let community = blocking(context.pool(), move |conn| { @@ -89,7 +35,7 @@ impl ApubObjectType for Post { self.ap_id.to_owned().into_inner(), ); delete - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(generate_activity_id(DeleteType::Delete)?) .set_to(public()) .set_many_ccs(vec![community.actor_id()]); @@ -114,7 +60,7 @@ impl ApubObjectType for Post { self.ap_id.to_owned().into_inner(), ); delete - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(generate_activity_id(DeleteType::Delete)?) .set_to(public()) .set_many_ccs(vec![community.actor_id()]); @@ -125,7 +71,7 @@ impl ApubObjectType for Post { delete.into_any_base()?, ); undo - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(generate_activity_id(UndoType::Undo)?) .set_to(public()) .set_many_ccs(vec![community.actor_id()]); @@ -146,7 +92,7 @@ impl ApubObjectType for Post { self.ap_id.to_owned().into_inner(), ); remove - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(generate_activity_id(RemoveType::Remove)?) .set_to(public()) .set_many_ccs(vec![community.actor_id()]); @@ -171,7 +117,7 @@ impl ApubObjectType for Post { self.ap_id.to_owned().into_inner(), ); remove - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(generate_activity_id(RemoveType::Remove)?) .set_to(public()) .set_many_ccs(vec![community.actor_id()]); @@ -182,7 +128,7 @@ impl ApubObjectType for Post { remove.into_any_base()?, ); undo - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(generate_activity_id(UndoType::Undo)?) .set_to(public()) .set_many_ccs(vec![community.actor_id()]); @@ -191,84 +137,3 @@ impl ApubObjectType for Post { Ok(()) } } - -#[async_trait::async_trait(?Send)] -impl ApubLikeableType for Post { - async fn send_like(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> { - let community_id = self.community_id; - let community = blocking(context.pool(), move |conn| { - Community::read(conn, community_id) - }) - .await??; - - let mut like = Like::new( - creator.actor_id.to_owned().into_inner(), - self.ap_id.to_owned().into_inner(), - ); - like - .set_many_contexts(lemmy_context()?) - .set_id(generate_activity_id(LikeType::Like)?) - .set_to(public()) - .set_many_ccs(vec![community.actor_id()]); - - send_to_community(like, creator, &community, None, context).await?; - Ok(()) - } - - async fn send_dislike(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> { - let community_id = self.community_id; - let community = blocking(context.pool(), move |conn| { - Community::read(conn, community_id) - }) - .await??; - - let mut dislike = Dislike::new( - creator.actor_id.to_owned().into_inner(), - self.ap_id.to_owned().into_inner(), - ); - dislike - .set_many_contexts(lemmy_context()?) - .set_id(generate_activity_id(DislikeType::Dislike)?) - .set_to(public()) - .set_many_ccs(vec![community.actor_id()]); - - send_to_community(dislike, creator, &community, None, context).await?; - Ok(()) - } - - async fn send_undo_like( - &self, - creator: &Person, - context: &LemmyContext, - ) -> Result<(), LemmyError> { - let community_id = self.community_id; - let community = blocking(context.pool(), move |conn| { - Community::read(conn, community_id) - }) - .await??; - - let mut like = Like::new( - creator.actor_id.to_owned().into_inner(), - self.ap_id.to_owned().into_inner(), - ); - like - .set_many_contexts(lemmy_context()?) - .set_id(generate_activity_id(LikeType::Like)?) - .set_to(public()) - .set_many_ccs(vec![community.actor_id()]); - - // Undo that fake activity - let mut undo = Undo::new( - creator.actor_id.to_owned().into_inner(), - like.into_any_base()?, - ); - undo - .set_many_contexts(lemmy_context()?) - .set_id(generate_activity_id(UndoType::Undo)?) - .set_to(public()) - .set_many_ccs(vec![community.actor_id()]); - - send_to_community(undo, creator, &community, None, context).await?; - Ok(()) - } -} diff --git a/crates/apub/src/activities/send/private_message.rs b/crates/apub/src/activities/send/private_message.rs index e5a30585b..edcff3771 100644 --- a/crates/apub/src/activities/send/private_message.rs +++ b/crates/apub/src/activities/send/private_message.rs @@ -1,20 +1,18 @@ use crate::{ - activities::send::generate_activity_id, + activities::generate_activity_id, activity_queue::send_activity_single_dest, extensions::context::lemmy_context, - objects::ToApub, ActorType, ApubObjectType, }; use activitystreams::{ activity::{ - kind::{CreateType, DeleteType, UndoType, UpdateType}, - Create, + kind::{DeleteType, UndoType}, Delete, Undo, - Update, }, - prelude::*, + base::{BaseExt, ExtendsExt}, + object::ObjectExt, }; use lemmy_api_common::blocking; use lemmy_db_queries::Crud; @@ -24,49 +22,6 @@ use lemmy_websocket::LemmyContext; #[async_trait::async_trait(?Send)] impl ApubObjectType for PrivateMessage { - /// Send out information about a newly created private message - async fn send_create(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> { - let note = self.to_apub(context.pool()).await?; - - let recipient_id = self.recipient_id; - let recipient = - blocking(context.pool(), move |conn| Person::read(conn, recipient_id)).await??; - - let mut create = Create::new( - creator.actor_id.to_owned().into_inner(), - note.into_any_base()?, - ); - - create - .set_many_contexts(lemmy_context()?) - .set_id(generate_activity_id(CreateType::Create)?) - .set_to(recipient.actor_id()); - - send_activity_single_dest(create, creator, recipient.inbox_url.into(), context).await?; - Ok(()) - } - - /// Send out information about an edited private message, to the followers of the community. - async fn send_update(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> { - let note = self.to_apub(context.pool()).await?; - - let recipient_id = self.recipient_id; - let recipient = - blocking(context.pool(), move |conn| Person::read(conn, recipient_id)).await??; - - let mut update = Update::new( - creator.actor_id.to_owned().into_inner(), - note.into_any_base()?, - ); - update - .set_many_contexts(lemmy_context()?) - .set_id(generate_activity_id(UpdateType::Update)?) - .set_to(recipient.actor_id()); - - send_activity_single_dest(update, creator, recipient.inbox_url.into(), context).await?; - Ok(()) - } - async fn send_delete(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> { let recipient_id = self.recipient_id; let recipient = @@ -77,7 +32,7 @@ impl ApubObjectType for PrivateMessage { self.ap_id.to_owned().into_inner(), ); delete - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(generate_activity_id(DeleteType::Delete)?) .set_to(recipient.actor_id()); @@ -99,7 +54,7 @@ impl ApubObjectType for PrivateMessage { self.ap_id.to_owned().into_inner(), ); delete - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(generate_activity_id(DeleteType::Delete)?) .set_to(recipient.actor_id()); @@ -109,7 +64,7 @@ impl ApubObjectType for PrivateMessage { delete.into_any_base()?, ); undo - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(generate_activity_id(UndoType::Undo)?) .set_to(recipient.actor_id()); diff --git a/crates/apub/src/activities/voting/dislike.rs b/crates/apub/src/activities/voting/dislike.rs deleted file mode 100644 index 18d72f394..000000000 --- a/crates/apub/src/activities/voting/dislike.rs +++ /dev/null @@ -1,54 +0,0 @@ -use crate::activities::{ - verify_activity, - verify_person_in_community, - voting::receive_like_or_dislike, -}; -use activitystreams::activity::kind::DislikeType; -use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl}; -use lemmy_utils::LemmyError; -use lemmy_websocket::LemmyContext; -use url::Url; - -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "camelCase")] -pub struct DislikePostOrComment { - to: PublicUrl, - pub(in crate::activities) object: Url, - cc: [Url; 1], - #[serde(rename = "type")] - kind: DislikeType, - #[serde(flatten)] - common: ActivityCommonFields, -} - -#[async_trait::async_trait(?Send)] -impl ActivityHandler for DislikePostOrComment { - async fn verify( - &self, - context: &LemmyContext, - request_counter: &mut i32, - ) -> Result<(), LemmyError> { - verify_activity(self.common())?; - verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; - Ok(()) - } - - async fn receive( - &self, - context: &LemmyContext, - request_counter: &mut i32, - ) -> Result<(), LemmyError> { - receive_like_or_dislike( - -1, - &self.common.actor, - &self.object, - context, - request_counter, - ) - .await - } - - fn common(&self) -> &ActivityCommonFields { - &self.common - } -} diff --git a/crates/apub/src/activities/voting/like.rs b/crates/apub/src/activities/voting/like.rs deleted file mode 100644 index ca899d3d3..000000000 --- a/crates/apub/src/activities/voting/like.rs +++ /dev/null @@ -1,54 +0,0 @@ -use crate::activities::{ - verify_activity, - verify_person_in_community, - voting::receive_like_or_dislike, -}; -use activitystreams::activity::kind::LikeType; -use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl}; -use lemmy_utils::LemmyError; -use lemmy_websocket::LemmyContext; -use url::Url; - -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "camelCase")] -pub struct LikePostOrComment { - to: PublicUrl, - pub(in crate::activities::voting) object: Url, - cc: [Url; 1], - #[serde(rename = "type")] - kind: LikeType, - #[serde(flatten)] - common: ActivityCommonFields, -} - -#[async_trait::async_trait(?Send)] -impl ActivityHandler for LikePostOrComment { - async fn verify( - &self, - context: &LemmyContext, - request_counter: &mut i32, - ) -> Result<(), LemmyError> { - verify_activity(self.common())?; - verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; - Ok(()) - } - - async fn receive( - &self, - context: &LemmyContext, - request_counter: &mut i32, - ) -> Result<(), LemmyError> { - receive_like_or_dislike( - 1, - &self.common.actor, - &self.object, - context, - request_counter, - ) - .await - } - - fn common(&self) -> &ActivityCommonFields { - &self.common - } -} diff --git a/crates/apub/src/activities/voting/mod.rs b/crates/apub/src/activities/voting/mod.rs index 739c43051..48d9446f8 100644 --- a/crates/apub/src/activities/voting/mod.rs +++ b/crates/apub/src/activities/voting/mod.rs @@ -1,62 +1,33 @@ -use crate::{ - activities::{ - comment::send_websocket_message as send_comment_message, - post::send_websocket_message as send_post_message, - }, - fetcher::{ - objects::get_or_fetch_and_insert_post_or_comment, - person::get_or_fetch_and_upsert_person, - }, - PostOrComment, +use crate::activities::{ + comment::send_websocket_message as send_comment_message, + post::send_websocket_message as send_post_message, + voting::vote::VoteType, }; use lemmy_api_common::blocking; use lemmy_db_queries::Likeable; use lemmy_db_schema::source::{ comment::{Comment, CommentLike, CommentLikeForm}, + person::Person, post::{Post, PostLike, PostLikeForm}, }; use lemmy_utils::LemmyError; use lemmy_websocket::{LemmyContext, UserOperation}; -use std::ops::Deref; -use url::Url; -pub mod dislike; -pub mod like; -pub mod undo_dislike; -pub mod undo_like; +pub mod undo_vote; +pub mod vote; -pub(in crate::activities::voting) async fn receive_like_or_dislike( - score: i16, - actor: &Url, - object: &Url, - context: &LemmyContext, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - match get_or_fetch_and_insert_post_or_comment(object, context, request_counter).await? { - PostOrComment::Post(p) => { - like_or_dislike_post(score, actor, p.deref(), context, request_counter).await - } - PostOrComment::Comment(c) => { - like_or_dislike_comment(score, actor, c.deref(), context, request_counter).await - } - } -} - -async fn like_or_dislike_comment( - score: i16, - actor: &Url, +async fn vote_comment( + vote_type: &VoteType, + actor: Person, comment: &Comment, context: &LemmyContext, - request_counter: &mut i32, ) -> Result<(), LemmyError> { - let actor = get_or_fetch_and_upsert_person(actor, context, request_counter).await?; - let comment_id = comment.id; let like_form = CommentLikeForm { comment_id, post_id: comment.post_id, person_id: actor.id, - score, + score: vote_type.into(), }; let person_id = actor.id; blocking(context.pool(), move |conn| { @@ -74,20 +45,17 @@ async fn like_or_dislike_comment( .await } -async fn like_or_dislike_post( - score: i16, - actor: &Url, +async fn vote_post( + vote_type: &VoteType, + actor: Person, post: &Post, context: &LemmyContext, - request_counter: &mut i32, ) -> Result<(), LemmyError> { - let actor = get_or_fetch_and_upsert_person(actor, context, request_counter).await?; - let post_id = post.id; let like_form = PostLikeForm { post_id: post.id, person_id: actor.id, - score, + score: vote_type.into(), }; let person_id = actor.id; blocking(context.pool(), move |conn| { @@ -99,30 +67,11 @@ async fn like_or_dislike_post( send_post_message(post.id, UserOperation::CreatePostLike, context).await } -pub(in crate::activities::voting) async fn receive_undo_like_or_dislike( - actor: &Url, - object: &Url, - context: &LemmyContext, - request_counter: &mut i32, -) -> Result<(), LemmyError> { - match get_or_fetch_and_insert_post_or_comment(object, context, request_counter).await? { - PostOrComment::Post(p) => { - undo_like_or_dislike_post(actor, p.deref(), context, request_counter).await - } - PostOrComment::Comment(c) => { - undo_like_or_dislike_comment(actor, c.deref(), context, request_counter).await - } - } -} - -async fn undo_like_or_dislike_comment( - actor: &Url, +async fn undo_vote_comment( + actor: Person, comment: &Comment, context: &LemmyContext, - request_counter: &mut i32, ) -> Result<(), LemmyError> { - let actor = get_or_fetch_and_upsert_person(actor, context, request_counter).await?; - let comment_id = comment.id; let person_id = actor.id; blocking(context.pool(), move |conn| { @@ -139,14 +88,11 @@ async fn undo_like_or_dislike_comment( .await } -async fn undo_like_or_dislike_post( - actor: &Url, +async fn undo_vote_post( + actor: Person, post: &Post, context: &LemmyContext, - request_counter: &mut i32, ) -> Result<(), LemmyError> { - let actor = get_or_fetch_and_upsert_person(actor, context, request_counter).await?; - let post_id = post.id; let person_id = actor.id; blocking(context.pool(), move |conn| { diff --git a/crates/apub/src/activities/voting/undo_dislike.rs b/crates/apub/src/activities/voting/undo_dislike.rs deleted file mode 100644 index 11871e791..000000000 --- a/crates/apub/src/activities/voting/undo_dislike.rs +++ /dev/null @@ -1,55 +0,0 @@ -use crate::activities::{ - verify_activity, - verify_person_in_community, - voting::{dislike::DislikePostOrComment, receive_undo_like_or_dislike}, -}; -use activitystreams::activity::kind::UndoType; -use lemmy_apub_lib::{verify_urls_match, ActivityCommonFields, ActivityHandler, PublicUrl}; -use lemmy_utils::LemmyError; -use lemmy_websocket::LemmyContext; -use url::Url; - -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "camelCase")] -pub struct UndoDislikePostOrComment { - to: PublicUrl, - object: DislikePostOrComment, - cc: [Url; 1], - #[serde(rename = "type")] - kind: UndoType, - #[serde(flatten)] - common: ActivityCommonFields, -} - -#[async_trait::async_trait(?Send)] -impl ActivityHandler for UndoDislikePostOrComment { - async fn verify( - &self, - context: &LemmyContext, - request_counter: &mut i32, - ) -> Result<(), LemmyError> { - verify_activity(self.common())?; - verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; - verify_urls_match(&self.common.actor, &self.object.common().actor)?; - self.object.verify(context, request_counter).await?; - Ok(()) - } - - async fn receive( - &self, - context: &LemmyContext, - request_counter: &mut i32, - ) -> Result<(), LemmyError> { - receive_undo_like_or_dislike( - &self.common.actor, - &self.object.object, - context, - request_counter, - ) - .await - } - - fn common(&self) -> &ActivityCommonFields { - &self.common - } -} diff --git a/crates/apub/src/activities/voting/undo_like.rs b/crates/apub/src/activities/voting/undo_like.rs deleted file mode 100644 index 07c3c4709..000000000 --- a/crates/apub/src/activities/voting/undo_like.rs +++ /dev/null @@ -1,55 +0,0 @@ -use crate::activities::{ - verify_activity, - verify_person_in_community, - voting::{like::LikePostOrComment, receive_undo_like_or_dislike}, -}; -use activitystreams::activity::kind::UndoType; -use lemmy_apub_lib::{verify_urls_match, ActivityCommonFields, ActivityHandler, PublicUrl}; -use lemmy_utils::LemmyError; -use lemmy_websocket::LemmyContext; -use url::Url; - -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "camelCase")] -pub struct UndoLikePostOrComment { - to: PublicUrl, - object: LikePostOrComment, - cc: [Url; 1], - #[serde(rename = "type")] - kind: UndoType, - #[serde(flatten)] - common: ActivityCommonFields, -} - -#[async_trait::async_trait(?Send)] -impl ActivityHandler for UndoLikePostOrComment { - async fn verify( - &self, - context: &LemmyContext, - request_counter: &mut i32, - ) -> Result<(), LemmyError> { - verify_activity(self.common())?; - verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; - verify_urls_match(&self.common.actor, &self.object.common().actor)?; - self.object.verify(context, request_counter).await?; - Ok(()) - } - - async fn receive( - &self, - context: &LemmyContext, - request_counter: &mut i32, - ) -> Result<(), LemmyError> { - receive_undo_like_or_dislike( - &self.common.actor, - &self.object.object, - context, - request_counter, - ) - .await - } - - fn common(&self) -> &ActivityCommonFields { - &self.common - } -} diff --git a/crates/apub/src/activities/voting/undo_vote.rs b/crates/apub/src/activities/voting/undo_vote.rs new file mode 100644 index 000000000..8c3137e87 --- /dev/null +++ b/crates/apub/src/activities/voting/undo_vote.rs @@ -0,0 +1,122 @@ +use crate::{ + activities::{ + community::announce::AnnouncableActivities, + generate_activity_id, + verify_activity, + verify_person_in_community, + voting::{ + undo_vote_comment, + undo_vote_post, + vote::{Vote, VoteType}, + }, + }, + activity_queue::send_to_community_new, + extensions::context::lemmy_context, + fetcher::{ + objects::get_or_fetch_and_insert_post_or_comment, + person::get_or_fetch_and_upsert_person, + }, + ActorType, + PostOrComment, +}; +use activitystreams::activity::kind::UndoType; +use lemmy_api_common::blocking; +use lemmy_apub_lib::{values::PublicUrl, verify_urls_match, ActivityCommonFields, ActivityHandler}; +use lemmy_db_queries::Crud; +use lemmy_db_schema::{ + source::{community::Community, person::Person}, + CommunityId, +}; +use lemmy_utils::LemmyError; +use lemmy_websocket::LemmyContext; +use std::ops::Deref; +use url::Url; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UndoVote { + to: PublicUrl, + object: Vote, + cc: [Url; 1], + #[serde(rename = "type")] + kind: UndoType, + #[serde(flatten)] + common: ActivityCommonFields, +} + +impl UndoVote { + pub async fn send( + object: &PostOrComment, + actor: &Person, + community_id: CommunityId, + kind: VoteType, + context: &LemmyContext, + ) -> Result<(), LemmyError> { + let community = blocking(context.pool(), move |conn| { + Community::read(conn, community_id) + }) + .await??; + let id = generate_activity_id(UndoType::Undo)?; + + let undo_vote = UndoVote { + to: PublicUrl::Public, + object: Vote { + to: PublicUrl::Public, + object: object.ap_id(), + cc: [community.actor_id()], + kind: kind.clone(), + common: ActivityCommonFields { + context: lemmy_context(), + id: generate_activity_id(kind)?, + actor: actor.actor_id(), + unparsed: Default::default(), + }, + }, + cc: [community.actor_id()], + kind: UndoType::Undo, + common: ActivityCommonFields { + context: lemmy_context(), + id: id.clone(), + actor: actor.actor_id(), + unparsed: Default::default(), + }, + }; + let activity = AnnouncableActivities::UndoVote(undo_vote); + send_to_community_new(activity, &id, actor, &community, vec![], context).await + } +} + +#[async_trait::async_trait(?Send)] +impl ActivityHandler for UndoVote { + async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + verify_activity(self.common())?; + verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?; + verify_urls_match(&self.common.actor, &self.object.common().actor)?; + self.object.verify(context, request_counter).await?; + Ok(()) + } + + async fn receive( + &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 object = + get_or_fetch_and_insert_post_or_comment(&self.object.object, context, request_counter) + .await?; + match object { + PostOrComment::Post(p) => undo_vote_post(actor, p.deref(), context).await, + PostOrComment::Comment(c) => undo_vote_comment(actor, c.deref(), context).await, + } + } + + fn common(&self) -> &ActivityCommonFields { + &self.common + } +} diff --git a/crates/apub/src/activities/voting/vote.rs b/crates/apub/src/activities/voting/vote.rs new file mode 100644 index 000000000..0f5a2b5c4 --- /dev/null +++ b/crates/apub/src/activities/voting/vote.rs @@ -0,0 +1,133 @@ +use crate::{ + activities::{ + community::announce::AnnouncableActivities, + generate_activity_id, + verify_activity, + verify_person_in_community, + voting::{vote_comment, vote_post}, + }, + activity_queue::send_to_community_new, + extensions::context::lemmy_context, + fetcher::{ + objects::get_or_fetch_and_insert_post_or_comment, + person::get_or_fetch_and_upsert_person, + }, + ActorType, + PostOrComment, +}; +use anyhow::anyhow; +use lemmy_api_common::blocking; +use lemmy_apub_lib::{values::PublicUrl, ActivityCommonFields, ActivityHandler}; +use lemmy_db_queries::Crud; +use lemmy_db_schema::{ + source::{community::Community, person::Person}, + CommunityId, +}; +use lemmy_utils::LemmyError; +use lemmy_websocket::LemmyContext; +use serde::{Deserialize, Serialize}; +use std::{convert::TryFrom, ops::Deref}; +use strum_macros::ToString; +use url::Url; + +#[derive(Clone, Debug, ToString, Deserialize, Serialize)] +pub enum VoteType { + Like, + Dislike, +} + +impl TryFrom for VoteType { + type Error = LemmyError; + + fn try_from(value: i16) -> Result { + match value { + 1 => Ok(VoteType::Like), + -1 => Ok(VoteType::Dislike), + _ => Err(anyhow!("invalid vote value").into()), + } + } +} + +impl From<&VoteType> for i16 { + fn from(value: &VoteType) -> i16 { + match value { + VoteType::Like => 1, + VoteType::Dislike => -1, + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Vote { + pub(in crate::activities::voting) to: PublicUrl, + pub(in crate::activities::voting) object: Url, + pub(in crate::activities::voting) cc: [Url; 1], + #[serde(rename = "type")] + pub(in crate::activities::voting) kind: VoteType, + #[serde(flatten)] + pub(in crate::activities::voting) common: ActivityCommonFields, +} + +impl Vote { + pub async fn send( + object: &PostOrComment, + actor: &Person, + community_id: CommunityId, + kind: VoteType, + context: &LemmyContext, + ) -> Result<(), LemmyError> { + let community = blocking(context.pool(), move |conn| { + Community::read(conn, community_id) + }) + .await??; + let id = generate_activity_id(kind.clone())?; + + let vote = Vote { + to: PublicUrl::Public, + object: object.ap_id(), + cc: [community.actor_id()], + kind, + common: ActivityCommonFields { + context: lemmy_context(), + id: id.clone(), + actor: actor.actor_id(), + unparsed: Default::default(), + }, + }; + let activity = AnnouncableActivities::Vote(vote); + send_to_community_new(activity, &id, actor, &community, vec![], context).await + } +} + +#[async_trait::async_trait(?Send)] +impl ActivityHandler for Vote { + async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + verify_activity(self.common())?; + verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?; + Ok(()) + } + + async fn receive( + &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 object = + get_or_fetch_and_insert_post_or_comment(&self.object, context, request_counter).await?; + match object { + PostOrComment::Post(p) => vote_post(&self.kind, actor, p.deref(), context).await, + PostOrComment::Comment(c) => vote_comment(&self.kind, actor, c.deref(), context).await, + } + } + + fn common(&self) -> &ActivityCommonFields { + &self.common + } +} diff --git a/crates/apub/src/activity_queue.rs b/crates/apub/src/activity_queue.rs index 6ff3d0334..c7ff0a81d 100644 --- a/crates/apub/src/activity_queue.rs +++ b/crates/apub/src/activity_queue.rs @@ -1,4 +1,5 @@ use crate::{ + activities::community::announce::{AnnouncableActivities, AnnounceActivity}, check_is_apub_id_valid, extensions::signatures::sign_and_send, insert_activity, @@ -24,7 +25,7 @@ use itertools::Itertools; use lemmy_db_schema::source::{community::Community, person::Person}; use lemmy_utils::{location_info, settings::structs::Settings, LemmyError}; use lemmy_websocket::LemmyContext; -use log::{debug, warn}; +use log::{debug, info, warn}; use reqwest::Client; use serde::{Deserialize, Serialize}; use std::{collections::BTreeMap, env, fmt::Debug, future::Future, pin::Pin}; @@ -137,37 +138,77 @@ where Ok(()) } -/// Sends notification to any persons mentioned in a comment -/// -/// * `creator` person who created the comment -/// * `mentions` list of inboxes of persons which are mentioned in the comment -/// * `activity` either a `Create/Note` or `Update/Note` -pub(crate) async fn send_comment_mentions( - creator: &Person, - mentions: Vec, - activity: T, +pub(crate) async fn send_to_community_new( + activity: AnnouncableActivities, + activity_id: &Url, + actor: &dyn ActorType, + community: &Community, + additional_inboxes: Vec, context: &LemmyContext, +) -> Result<(), LemmyError> { + // if this is a local community, we need to do an announce from the community instead + if community.local { + insert_activity(activity_id, activity.clone(), true, false, context.pool()).await?; + AnnounceActivity::send(activity, community, additional_inboxes, context).await?; + } else { + let mut inboxes = additional_inboxes; + inboxes.push(community.get_shared_inbox_or_inbox_url()); + send_activity_new(context, &activity, activity_id, actor, inboxes, false).await?; + } + + Ok(()) +} + +pub(crate) async fn send_activity_new( + context: &LemmyContext, + activity: &T, + activity_id: &Url, + actor: &dyn ActorType, + inboxes: Vec, + sensitive: bool, ) -> Result<(), LemmyError> where - T: AsObject + Extends + Debug + BaseExt, - Kind: Serialize, - >::Error: From + Send + Sync + 'static, + T: Serialize, { - debug!( - "Sending mentions activity {:?} to {:?}", - &activity.id_unchecked(), - &mentions - ); - let mentions = mentions + if !Settings::get().federation.enabled || inboxes.is_empty() { + return Ok(()); + } + + info!("Sending activity {}", activity_id.to_string()); + + // Don't send anything to ourselves + // TODO: this should be a debug assert + let hostname = Settings::get().get_hostname_without_port()?; + let inboxes: Vec<&Url> = inboxes .iter() - .filter(|inbox| check_is_apub_id_valid(inbox, false).is_ok()) - .map(|i| i.to_owned()) + .filter(|i| i.domain().expect("valid inbox url") != hostname) .collect(); - send_activity_internal( - context, activity, creator, mentions, false, // Don't create a new DB row - false, + + let serialised_activity = serde_json::to_string(&activity)?; + + insert_activity( + activity_id, + serialised_activity.clone(), + true, + sensitive, + context.pool(), ) .await?; + + for i in inboxes { + let message = SendActivityTask { + activity: serialised_activity.to_owned(), + inbox: i.to_owned(), + actor_id: actor.actor_id(), + private_key: actor.private_key().context(location_info!())?, + }; + if env::var("LEMMY_TEST_SEND_SYNC").is_ok() { + do_send(message, &Client::default()).await?; + } else { + context.activity_queue.queue::(message)?; + } + } + Ok(()) } diff --git a/crates/apub/src/extensions/context.rs b/crates/apub/src/extensions/context.rs index 08491dcbf..1d6f83bd7 100644 --- a/crates/apub/src/extensions/context.rs +++ b/crates/apub/src/extensions/context.rs @@ -1,9 +1,8 @@ -use activitystreams::{base::AnyBase, context}; -use lemmy_utils::LemmyError; +use activitystreams::{base::AnyBase, context, primitives::OneOrMany}; use serde_json::json; use url::Url; -pub fn lemmy_context() -> Result, LemmyError> { +pub fn lemmy_context() -> OneOrMany { let context_ext = AnyBase::from_arbitrary_json(json!( { "sc": "http://schema.org#", @@ -19,10 +18,11 @@ pub fn lemmy_context() -> Result, LemmyError> { "type": "sc:Text", "id": "as:alsoKnownAs" }, - }))?; - Ok(vec![ + })) + .expect("parse context"); + OneOrMany::from(vec![ AnyBase::from(context()), context_ext, - AnyBase::from(Url::parse("https://w3id.org/security/v1")?), + AnyBase::from(Url::parse("https://w3id.org/security/v1").expect("parse context")), ]) } diff --git a/crates/apub/src/extensions/mod.rs b/crates/apub/src/extensions/mod.rs index 19e37894d..781e89e6e 100644 --- a/crates/apub/src/extensions/mod.rs +++ b/crates/apub/src/extensions/mod.rs @@ -1,5 +1,4 @@ pub mod context; pub(crate) mod group_extension; -pub(crate) mod page_extension; pub(crate) mod person_extension; pub mod signatures; diff --git a/crates/apub/src/extensions/page_extension.rs b/crates/apub/src/extensions/page_extension.rs deleted file mode 100644 index 752fa2b4b..000000000 --- a/crates/apub/src/extensions/page_extension.rs +++ /dev/null @@ -1,36 +0,0 @@ -use activitystreams::unparsed::UnparsedMutExt; -use activitystreams_ext::UnparsedExtension; -use serde::{Deserialize, Serialize}; - -/// Activitystreams extension to allow (de)serializing additional Post fields -/// `comemnts_enabled` (called 'locked' in Lemmy), -/// `sensitive` (called 'nsfw') and `stickied`. -#[derive(Clone, Debug, Default, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct PageExtension { - pub comments_enabled: Option, - pub sensitive: Option, - pub stickied: Option, -} - -impl UnparsedExtension for PageExtension -where - U: UnparsedMutExt, -{ - type Error = serde_json::Error; - - fn try_from_unparsed(unparsed_mut: &mut U) -> Result { - Ok(PageExtension { - comments_enabled: unparsed_mut.remove("commentsEnabled")?, - sensitive: unparsed_mut.remove("sensitive")?, - stickied: unparsed_mut.remove("stickied")?, - }) - } - - fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> { - unparsed_mut.insert("commentsEnabled", self.comments_enabled)?; - unparsed_mut.insert("sensitive", self.sensitive)?; - unparsed_mut.insert("stickied", self.stickied)?; - Ok(()) - } -} diff --git a/crates/apub/src/fetcher/objects.rs b/crates/apub/src/fetcher/objects.rs index af8a59f76..a06b99d63 100644 --- a/crates/apub/src/fetcher/objects.rs +++ b/crates/apub/src/fetcher/objects.rs @@ -1,8 +1,6 @@ use crate::{ fetcher::fetch::fetch_remote_object, - objects::FromApub, - NoteExt, - PageExt, + objects::{comment::Note, post::Page, FromApub}, PostOrComment, }; use anyhow::anyhow; @@ -35,7 +33,7 @@ pub async fn get_or_fetch_and_insert_post( Err(NotFound {}) => { debug!("Fetching and creating remote post: {}", post_ap_id); let page = - fetch_remote_object::(context.client(), post_ap_id, recursion_counter).await?; + fetch_remote_object::(context.client(), post_ap_id, recursion_counter).await?; let post = Post::from_apub( &page, context, @@ -74,7 +72,7 @@ pub async fn get_or_fetch_and_insert_comment( comment_ap_id ); let comment = - fetch_remote_object::(context.client(), comment_ap_id, recursion_counter).await?; + fetch_remote_object::(context.client(), comment_ap_id, recursion_counter).await?; let comment = Comment::from_apub( &comment, context, diff --git a/crates/apub/src/fetcher/search.rs b/crates/apub/src/fetcher/search.rs index 5a09fd430..647d057a6 100644 --- a/crates/apub/src/fetcher/search.rs +++ b/crates/apub/src/fetcher/search.rs @@ -6,11 +6,9 @@ use crate::{ is_deleted, }, find_object_by_id, - objects::FromApub, + objects::{comment::Note, post::Page, FromApub}, GroupExt, - NoteExt, Object, - PageExt, PersonExt, }; use activitystreams::base::BaseExt; @@ -46,8 +44,8 @@ use url::Url; enum SearchAcceptedObjects { Person(Box), Group(Box), - Page(Box), - Comment(Box), + Page(Box), + Comment(Box), } /// Attempt to parse the query as URL, and fetch an ActivityPub object from it. diff --git a/crates/apub/src/http/community.rs b/crates/apub/src/http/community.rs index 3173bca12..587c6cfb8 100644 --- a/crates/apub/src/http/community.rs +++ b/crates/apub/src/http/community.rs @@ -81,7 +81,7 @@ pub(crate) async fn get_apub_community_followers( let mut collection = UnorderedCollection::new(); collection - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(community.followers_url.into()) .set_total_items(community_followers.len() as u64); Ok(create_apub_response(&collection)) @@ -112,7 +112,7 @@ pub(crate) async fn get_apub_community_outbox( let mut collection = OrderedCollection::new(); collection .set_many_items(activities) - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(community.get_outbox_url()?) .set_total_items(len as u64); Ok(create_apub_response(&collection)) @@ -130,7 +130,7 @@ pub(crate) async fn get_apub_community_inbox( let mut collection = OrderedCollection::new(); collection .set_id(community.inbox_url.into()) - .set_many_contexts(lemmy_context()?); + .set_many_contexts(lemmy_context()); Ok(create_apub_response(&collection)) } @@ -155,13 +155,13 @@ pub(crate) async fn get_apub_community_moderators( let moderators: Vec = moderators .into_iter() - .map(|m| m.moderator.actor_id.into_inner()) + .map(|m| m.moderator.actor_id.into()) .collect(); let mut collection = OrderedCollection::new(); collection .set_id(generate_moderators_url(&community.actor_id)?.into()) .set_total_items(moderators.len() as u64) .set_many_items(moderators) - .set_many_contexts(lemmy_context()?); + .set_many_contexts(lemmy_context()); Ok(create_apub_response(&collection)) } diff --git a/crates/apub/src/http/inbox_enums.rs b/crates/apub/src/http/inbox_enums.rs index 0f31f534e..034048777 100644 --- a/crates/apub/src/http/inbox_enums.rs +++ b/crates/apub/src/http/inbox_enums.rs @@ -1,5 +1,5 @@ use crate::activities::{ - comment::{create::CreateComment, update::UpdateComment}, + comment::create_or_update::CreateOrUpdateComment, community::{ add_mod::AddMod, announce::AnnounceActivity, @@ -9,23 +9,17 @@ use crate::activities::{ }, deletion::{delete::DeletePostCommentOrCommunity, undo_delete::UndoDeletePostCommentOrCommunity}, following::{accept::AcceptFollowCommunity, follow::FollowCommunity, undo::UndoFollowCommunity}, - post::{create::CreatePost, update::UpdatePost}, + post::create_or_update::CreateOrUpdatePost, private_message::{ - create::CreatePrivateMessage, + create_or_update::CreateOrUpdatePrivateMessage, delete::DeletePrivateMessage, undo_delete::UndoDeletePrivateMessage, - update::UpdatePrivateMessage, }, removal::{ remove::RemovePostCommentCommunityOrMod, undo_remove::UndoRemovePostCommentOrCommunity, }, - voting::{ - dislike::DislikePostOrComment, - like::LikePostOrComment, - undo_dislike::UndoDislikePostOrComment, - undo_like::UndoLikePostOrComment, - }, + voting::{undo_vote::UndoVote, vote::Vote}, }; use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler}; use lemmy_utils::LemmyError; @@ -36,8 +30,7 @@ use serde::{Deserialize, Serialize}; #[serde(untagged)] pub enum PersonInboxActivities { AcceptFollowCommunity(AcceptFollowCommunity), - CreatePrivateMessage(CreatePrivateMessage), - UpdatePrivateMessage(UpdatePrivateMessage), + CreateOrUpdatePrivateMessage(CreateOrUpdatePrivateMessage), DeletePrivateMessage(DeletePrivateMessage), UndoDeletePrivateMessage(UndoDeletePrivateMessage), AnnounceActivity(Box), @@ -48,14 +41,10 @@ pub enum PersonInboxActivities { pub enum GroupInboxActivities { FollowCommunity(FollowCommunity), UndoFollowCommunity(UndoFollowCommunity), - CreateComment(CreateComment), - UpdateComment(UpdateComment), - CreatePost(CreatePost), - UpdatePost(UpdatePost), - LikePostOrComment(LikePostOrComment), - DislikePostOrComment(DislikePostOrComment), - UndoLikePostOrComment(UndoLikePostOrComment), - UndoDislikePostOrComment(UndoDislikePostOrComment), + CreateOrUpdateComment(CreateOrUpdateComment), + CreateOrUpdatePost(Box), + Vote(Vote), + UndoVote(UndoVote), DeletePostCommentOrCommunity(DeletePostCommentOrCommunity), UndoDeletePostCommentOrCommunity(UndoDeletePostCommentOrCommunity), RemovePostCommentOrCommunity(RemovePostCommentCommunityOrMod), @@ -72,14 +61,10 @@ pub enum SharedInboxActivities { // received by group FollowCommunity(FollowCommunity), UndoFollowCommunity(UndoFollowCommunity), - CreateComment(CreateComment), - UpdateComment(UpdateComment), - CreatePost(CreatePost), - UpdatePost(UpdatePost), - LikePostOrComment(LikePostOrComment), - DislikePostOrComment(DislikePostOrComment), - UndoDislikePostOrComment(UndoDislikePostOrComment), - UndoLikePostOrComment(UndoLikePostOrComment), + CreateOrUpdateComment(CreateOrUpdateComment), + CreateOrUpdatePost(Box), + Vote(Vote), + UndoVote(UndoVote), DeletePostCommentOrCommunity(DeletePostCommentOrCommunity), UndoDeletePostCommentOrCommunity(UndoDeletePostCommentOrCommunity), RemovePostCommentOrCommunity(RemovePostCommentCommunityOrMod), @@ -92,8 +77,7 @@ pub enum SharedInboxActivities { AcceptFollowCommunity(AcceptFollowCommunity), // Note, pm activities need to be at the end, otherwise comments will end up here. We can probably // avoid this problem by replacing createpm.object with our own struct, instead of NoteExt. - CreatePrivateMessage(CreatePrivateMessage), - UpdatePrivateMessage(UpdatePrivateMessage), + CreateOrUpdatePrivateMessage(CreateOrUpdatePrivateMessage), DeletePrivateMessage(DeletePrivateMessage), UndoDeletePrivateMessage(UndoDeletePrivateMessage), AnnounceActivity(Box), diff --git a/crates/apub/src/http/person.rs b/crates/apub/src/http/person.rs index 2c96aafd8..42f25bb62 100644 --- a/crates/apub/src/http/person.rs +++ b/crates/apub/src/http/person.rs @@ -71,7 +71,7 @@ pub(crate) async fn get_apub_person_outbox( let mut collection = OrderedCollection::new(); collection .set_many_items(Vec::::new()) - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(person.get_outbox_url()?) .set_total_items(0_u64); Ok(create_apub_response(&collection)) @@ -89,6 +89,6 @@ pub(crate) async fn get_apub_person_inbox( let mut collection = OrderedCollection::new(); collection .set_id(person.inbox_url.into()) - .set_many_contexts(lemmy_context()?); + .set_many_contexts(lemmy_context()); Ok(create_apub_response(&collection)) } diff --git a/crates/apub/src/lib.rs b/crates/apub/src/lib.rs index af3e4e38e..60d7c4871 100644 --- a/crates/apub/src/lib.rs +++ b/crates/apub/src/lib.rs @@ -11,7 +11,6 @@ pub mod objects; use crate::{ extensions::{ group_extension::GroupExtension, - page_extension::PageExtension, person_extension::PersonExtension, signatures::{PublicKey, PublicKeyExtension}, }, @@ -21,9 +20,9 @@ use activitystreams::{ activity::Follow, actor, base::AnyBase, - object::{ApObject, AsObject, Note, ObjectExt, Page}, + object::{ApObject, AsObject, ObjectExt}, }; -use activitystreams_ext::{Ext1, Ext2}; +use activitystreams_ext::Ext2; use anyhow::{anyhow, Context}; use diesel::NotFound; use lemmy_api_common::blocking; @@ -54,9 +53,6 @@ pub type GroupExt = type PersonExt = Ext2>>, PersonExtension, PublicKeyExtension>; pub type SiteExt = actor::ApActor>; -/// Activitystreams type for post -pub type PageExt = Ext1, PageExtension>; -pub type NoteExt = ApObject; #[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize, PartialEq)] pub enum UserTypes { @@ -133,10 +129,6 @@ pub fn check_is_apub_id_valid(apub_id: &Url, use_strict_allowlist: bool) -> Resu /// and actors in Lemmy. #[async_trait::async_trait(?Send)] pub trait ApubObjectType { - async fn send_create(&self, creator: &DbPerson, context: &LemmyContext) - -> Result<(), LemmyError>; - async fn send_update(&self, creator: &DbPerson, context: &LemmyContext) - -> Result<(), LemmyError>; async fn send_delete(&self, creator: &DbPerson, context: &LemmyContext) -> Result<(), LemmyError>; async fn send_undo_delete( @@ -152,21 +144,6 @@ pub trait ApubObjectType { ) -> Result<(), LemmyError>; } -#[async_trait::async_trait(?Send)] -pub trait ApubLikeableType { - async fn send_like(&self, creator: &DbPerson, context: &LemmyContext) -> Result<(), LemmyError>; - async fn send_dislike( - &self, - creator: &DbPerson, - context: &LemmyContext, - ) -> Result<(), LemmyError>; - async fn send_undo_like( - &self, - creator: &DbPerson, - context: &LemmyContext, - ) -> Result<(), LemmyError>; -} - /// Common methods provided by ActivityPub actors (community and person). Not all methods are /// implemented by all actors. pub trait ActorType { @@ -314,7 +291,7 @@ pub fn generate_inbox_url(actor_id: &DbUrl) -> Result { } pub fn generate_shared_inbox_url(actor_id: &DbUrl) -> Result { - let actor_id = actor_id.clone().into_inner(); + let actor_id: Url = actor_id.clone().into(); let url = format!( "{}://{}{}/inbox", &actor_id.scheme(), @@ -377,6 +354,16 @@ pub enum PostOrComment { Post(Box), } +impl PostOrComment { + pub(crate) fn ap_id(&self) -> Url { + match self { + PostOrComment::Post(p) => p.ap_id.clone(), + PostOrComment::Comment(c) => c.ap_id.clone(), + } + .into() + } +} + /// 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. diff --git a/crates/apub/src/objects/comment.rs b/crates/apub/src/objects/comment.rs index 7b181eff4..c97ea994c 100644 --- a/crates/apub/src/objects/comment.rs +++ b/crates/apub/src/objects/comment.rs @@ -1,29 +1,24 @@ use crate::{ + activities::verify_person_in_community, extensions::context::lemmy_context, fetcher::objects::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post}, - get_community_from_to_or_cc, - objects::{ - check_object_domain, - check_object_for_community_or_site_ban, - create_tombstone, - get_object_from_apub, - get_or_fetch_and_upsert_person, - get_source_markdown_value, - set_content_and_source, - FromApub, - FromApubToForm, - ToApub, - }, - NoteExt, + objects::{create_tombstone, get_or_fetch_and_upsert_person, FromApub, Source, ToApub}, + ActorType, }; use activitystreams::{ - object::{kind::NoteType, ApObject, Note, Tombstone}, - prelude::*, - public, + base::AnyBase, + object::{kind::NoteType, Tombstone}, + primitives::OneOrMany, + unparsed::Unparsed, }; use anyhow::{anyhow, Context}; +use chrono::{DateTime, FixedOffset}; use lemmy_api_common::blocking; -use lemmy_db_queries::{Crud, DbPool}; +use lemmy_apub_lib::{ + values::{MediaTypeHtml, MediaTypeMarkdown, PublicUrl}, + verify_domains_match, +}; +use lemmy_db_queries::{ApubObject, Crud, DbPool}; use lemmy_db_schema::{ source::{ comment::{Comment, CommentForm}, @@ -39,24 +34,103 @@ use lemmy_utils::{ LemmyError, }; use lemmy_websocket::LemmyContext; +use serde::{Deserialize, Serialize}; use url::Url; +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Note { + #[serde(rename = "@context")] + context: OneOrMany, + r#type: NoteType, + pub(crate) 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 + /// the post in [`in_reply_to`]). + to: PublicUrl, + content: String, + media_type: MediaTypeHtml, + source: Source, + in_reply_to: Vec, + published: DateTime, + updated: Option>, + #[serde(flatten)] + unparsed: Unparsed, +} + +impl Note { + async fn get_parents( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(Post, Option), LemmyError> { + // This post, or the parent comment might not yet exist on this server yet, fetch them. + let post_id = self.in_reply_to.get(0).context(location_info!())?; + let post = Box::pin(get_or_fetch_and_insert_post( + post_id, + context, + request_counter, + )) + .await?; + + // The 2nd item, if it exists, is the parent comment apub_id + // Nested comments will automatically get fetched recursively + let parent_id: Option = match self.in_reply_to.get(1) { + Some(parent_comment_uri) => { + let parent_comment = Box::pin(get_or_fetch_and_insert_comment( + parent_comment_uri, + context, + request_counter, + )) + .await?; + + Some(parent_comment.id) + } + None => None, + }; + + Ok((post, parent_id)) + } + + pub(crate) async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + let (post, _parent_comment_id) = self.get_parents(context, request_counter).await?; + let community_id = post.community_id; + let community = blocking(context.pool(), move |conn| { + Community::read(conn, community_id) + }) + .await??; + + if post.locked { + return Err(anyhow!("Post is locked").into()); + } + verify_domains_match(&self.attributed_to, &self.id)?; + verify_person_in_community( + &self.attributed_to, + &community.actor_id(), + context, + request_counter, + ) + .await?; + Ok(()) + } +} + #[async_trait::async_trait(?Send)] impl ToApub for Comment { - type ApubType = NoteExt; - - async fn to_apub(&self, pool: &DbPool) -> Result { - let mut comment = ApObject::new(Note::new()); + type ApubType = Note; + async fn to_apub(&self, pool: &DbPool) -> Result { let creator_id = self.creator_id; let creator = blocking(pool, move |conn| Person::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.into_inner()]; @@ -67,23 +141,25 @@ impl ToApub for Comment { in_reply_to_vec.push(parent_comment.ap_id.into_inner()); } - comment - // Not needed when the Post is embedded in a collection (like for community outbox) - .set_many_contexts(lemmy_context()?) - .set_id(self.ap_id.to_owned().into_inner()) - .set_published(convert_datetime(self.published)) - // NOTE: included community id for compatibility with lemmy v0.9.9 - .set_many_tos(vec![community.actor_id.into_inner(), public()]) - .set_many_in_reply_tos(in_reply_to_vec) - .set_attributed_to(creator.actor_id.into_inner()); + let note = Note { + context: lemmy_context(), + r#type: NoteType::Note, + id: self.ap_id.to_owned().into_inner(), + attributed_to: creator.actor_id.into_inner(), + to: PublicUrl::Public, + content: self.content.clone(), + media_type: MediaTypeHtml::Html, + source: Source { + content: self.content.clone(), + media_type: MediaTypeMarkdown::Markdown, + }, + in_reply_to: in_reply_to_vec, + published: convert_datetime(self.published), + updated: self.updated.map(convert_datetime), + unparsed: Default::default(), + }; - set_content_and_source(&mut comment, &self.content)?; - - if let Some(u) = self.updated { - comment.set_updated(convert_datetime(u)); - } - - Ok(comment) + Ok(note) } fn to_tombstone(&self) -> Result { @@ -98,108 +174,38 @@ impl ToApub for Comment { #[async_trait::async_trait(?Send)] impl FromApub for Comment { - type ApubType = NoteExt; + type ApubType = Note; /// Converts a `Note` to `Comment`. /// /// If the parent community, post and comment(s) are not known locally, these are also fetched. async fn from_apub( - note: &NoteExt, + note: &Note, context: &LemmyContext, - expected_domain: Url, - request_counter: &mut i32, - mod_action_allowed: bool, - ) -> Result { - let comment: Comment = get_object_from_apub( - note, - context, - expected_domain, - request_counter, - mod_action_allowed, - ) - .await?; - - let post_id = comment.post_id; - let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; - check_object_for_community_or_site_ban(note, post.community_id, context, request_counter) - .await?; - Ok(comment) - } -} - -#[async_trait::async_trait(?Send)] -impl FromApubToForm for CommentForm { - async fn from_apub( - note: &NoteExt, - context: &LemmyContext, - expected_domain: Url, + _expected_domain: Url, request_counter: &mut i32, _mod_action_allowed: bool, - ) -> Result { - let community = get_community_from_to_or_cc(note, context, request_counter).await?; - let ap_id = Some(check_object_domain(note, expected_domain, community.local)?); - let creator_actor_id = ¬e - .attributed_to() - .context(location_info!())? - .as_single_xsd_any_uri() - .context(location_info!())?; - + ) -> Result { let creator = - get_or_fetch_and_upsert_person(creator_actor_id, context, request_counter).await?; + 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?; - 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!())??; + let content = ¬e.source.content; + let content_slurs_removed = remove_slurs(content); - // This post, or the parent comment might not yet exist on this server yet, fetch them. - let post = Box::pin(get_or_fetch_and_insert_post( - post_ap_id, - context, - request_counter, - )) - .await?; - if post.locked { - return Err(anyhow!("Post is locked").into()); - } - - // 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 = match in_reply_tos.next() { - Some(parent_comment_uri) => { - let parent_comment_ap_id = &parent_comment_uri?; - let parent_comment = Box::pin(get_or_fetch_and_insert_comment( - parent_comment_ap_id, - context, - request_counter, - )) - .await?; - - Some(parent_comment.id) - } - None => None, - }; - - let content = get_source_markdown_value(note)?.context(location_info!())?; - let content_slurs_removed = remove_slurs(&content); - - Ok(CommentForm { + let form = CommentForm { creator_id: creator.id, post_id: post.id, - parent_id, + parent_id: parent_comment_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()), + published: Some(note.published.naive_local()), + updated: note.updated.map(|u| u.to_owned().naive_local()), deleted: None, - ap_id, + ap_id: Some(note.id.clone().into()), local: Some(false), - }) + }; + Ok(blocking(context.pool(), move |conn| Comment::upsert(conn, &form)).await??) } } diff --git a/crates/apub/src/objects/community.rs b/crates/apub/src/objects/community.rs index 34a15793b..f9948588d 100644 --- a/crates/apub/src/objects/community.rs +++ b/crates/apub/src/objects/community.rs @@ -55,7 +55,7 @@ impl ToApub for Community { let mut group = ApObject::new(Group::new()); group - .set_many_contexts(lemmy_context()?) + .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)) diff --git a/crates/apub/src/objects/mod.rs b/crates/apub/src/objects/mod.rs index f065719b9..0a29f29a3 100644 --- a/crates/apub/src/objects/mod.rs +++ b/crates/apub/src/objects/mod.rs @@ -1,8 +1,4 @@ -use crate::{ - check_community_or_site_ban, - check_is_apub_id_valid, - fetcher::person::get_or_fetch_and_upsert_person, -}; +use crate::{check_is_apub_id_valid, fetcher::person::get_or_fetch_and_upsert_person}; use activitystreams::{ base::{AsBase, BaseExt, ExtendsExt}, markers::Base, @@ -12,8 +8,9 @@ use activitystreams::{ use anyhow::{anyhow, Context}; 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::{CommunityId, DbUrl}; +use lemmy_db_schema::DbUrl; use lemmy_utils::{ location_info, settings::structs::Settings, @@ -70,6 +67,13 @@ pub trait FromApubToForm { Self: Sized; } +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Source { + content: String, + media_type: MediaTypeMarkdown, +} + /// Updated is actually the deletion time fn create_tombstone( deleted: bool, @@ -211,21 +215,3 @@ where Ok(to) } } - -pub(in crate::objects) async fn check_object_for_community_or_site_ban( - object: &T, - community_id: CommunityId, - context: &LemmyContext, - request_counter: &mut i32, -) -> Result<(), LemmyError> -where - T: ObjectExt, -{ - let person_id = object - .attributed_to() - .context(location_info!())? - .as_single_xsd_any_uri() - .context(location_info!())?; - let person = get_or_fetch_and_upsert_person(person_id, context, request_counter).await?; - check_community_or_site_ban(&person, community_id, context.pool()).await -} diff --git a/crates/apub/src/objects/person.rs b/crates/apub/src/objects/person.rs index b4cb57d95..cbc80b505 100644 --- a/crates/apub/src/objects/person.rs +++ b/crates/apub/src/objects/person.rs @@ -50,7 +50,7 @@ impl ToApub for DbPerson { let mut person = ApObject::new(actor); person - .set_many_contexts(lemmy_context()?) + .set_many_contexts(lemmy_context()) .set_id(self.actor_id.to_owned().into_inner()) .set_published(convert_datetime(self.published)); diff --git a/crates/apub/src/objects/post.rs b/crates/apub/src/objects/post.rs index 3e72bd15e..2fdf8d67b 100644 --- a/crates/apub/src/objects/post.rs +++ b/crates/apub/src/objects/post.rs @@ -1,30 +1,27 @@ use crate::{ - check_is_apub_id_valid, - extensions::{context::lemmy_context, page_extension::PageExtension}, + activities::{extract_community, verify_person_in_community}, + extensions::context::lemmy_context, fetcher::person::get_or_fetch_and_upsert_person, - get_community_from_to_or_cc, - objects::{ - check_object_domain, - check_object_for_community_or_site_ban, - create_tombstone, - get_object_from_apub, - get_source_markdown_value, - set_content_and_source, - FromApub, - FromApubToForm, - ToApub, - }, - PageExt, + objects::{create_tombstone, FromApub, Source, ToApub}, + ActorType, }; use activitystreams::{ - object::{kind::PageType, ApObject, Image, Page, Tombstone}, - prelude::*, + base::AnyBase, + object::{ + kind::{ImageType, PageType}, + Tombstone, + }, + primitives::OneOrMany, public, + unparsed::Unparsed, }; -use activitystreams_ext::Ext1; -use anyhow::Context; +use chrono::{DateTime, FixedOffset}; use lemmy_api_common::blocking; -use lemmy_db_queries::{Crud, DbPool}; +use lemmy_apub_lib::{ + values::{MediaTypeHtml, MediaTypeMarkdown}, + verify_domains_match, +}; +use lemmy_db_queries::{ApubObject, Crud, DbPool}; use lemmy_db_schema::{ self, source::{ @@ -34,66 +31,125 @@ use lemmy_db_schema::{ }, }; use lemmy_utils::{ - location_info, request::fetch_iframely_and_pictrs_data, - utils::{check_slurs, convert_datetime, remove_slurs}, + utils::{check_slurs, convert_datetime, markdown_to_html, remove_slurs}, LemmyError, }; use lemmy_websocket::LemmyContext; +use serde::{Deserialize, Serialize}; use url::Url; +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Page { + #[serde(rename = "@context")] + context: OneOrMany, + r#type: PageType, + pub(crate) id: Url, + pub(crate) attributed_to: Url, + to: [Url; 2], + name: String, + content: Option, + media_type: MediaTypeHtml, + source: Option, + url: Option, + image: Option, + pub(crate) comments_enabled: Option, + sensitive: Option, + pub(crate) stickied: Option, + published: DateTime, + updated: Option>, + #[serde(flatten)] + unparsed: Unparsed, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ImageObject { + content: ImageType, + url: Url, +} + +impl Page { + /// 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. + /// + /// Both stickied and locked need to be false on a newly created post (verified in [[CreatePost]]. + pub(crate) async fn is_mod_action(&self, pool: &DbPool) -> Result { + let post_id = self.id.clone(); + let old_post = blocking(pool, move |conn| { + Post::read_from_apub_id(conn, &post_id.into()) + }) + .await?; + + let is_mod_action = if let Ok(old_post) = old_post { + self.stickied != Some(old_post.stickied) || self.comments_enabled != Some(!old_post.locked) + } else { + false + }; + Ok(is_mod_action) + } + + pub(crate) async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + let community = extract_community(&self.to, context, request_counter).await?; + + check_slurs(&self.name)?; + verify_domains_match(&self.attributed_to, &self.id)?; + verify_person_in_community( + &self.attributed_to, + &community.actor_id(), + context, + request_counter, + ) + .await?; + Ok(()) + } +} + #[async_trait::async_trait(?Send)] impl ToApub for Post { - type ApubType = PageExt; + type ApubType = Page; // Turn a Lemmy post into an ActivityPub page that can be sent out over the network. - async fn to_apub(&self, pool: &DbPool) -> Result { - let mut page = ApObject::new(Page::new()); - + async fn to_apub(&self, pool: &DbPool) -> Result { let creator_id = self.creator_id; let creator = blocking(pool, move |conn| Person::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_many_contexts(lemmy_context()?) - .set_id(self.ap_id.to_owned().into_inner()) - .set_name(self.name.to_owned()) - // `summary` field for compatibility with lemmy v0.9.9 and older, - // TODO: remove this after some time - .set_summary(self.name.to_owned()) - .set_published(convert_datetime(self.published)) - .set_many_tos(vec![community.actor_id.into_inner(), public()]) - .set_attributed_to(creator.actor_id.into_inner()); + let source = self.body.clone().map(|body| Source { + content: body, + media_type: MediaTypeMarkdown::Markdown, + }); + let image = self.thumbnail_url.clone().map(|thumb| ImageObject { + content: ImageType::Image, + url: thumb.into(), + }); - if let Some(body) = &self.body { - set_content_and_source(&mut page, body)?; - } - - if let Some(url) = &self.url { - page.set_url::(url.to_owned().into()); - } - - if let Some(thumbnail_url) = &self.thumbnail_url { - let mut image = Image::new(); - image.set_url::(thumbnail_url.to_owned().into()); - page.set_image(image.into_any_base()?); - } - - if let Some(u) = self.updated { - page.set_updated(convert_datetime(u)); - } - - let ext = PageExtension { + let page = Page { + context: lemmy_context(), + r#type: PageType::Page, + id: self.ap_id.clone().into(), + attributed_to: creator.actor_id.into(), + to: [community.actor_id.into(), public()], + name: self.name.clone(), + content: self.body.as_ref().map(|b| markdown_to_html(b)), + media_type: MediaTypeHtml::Html, + source, + url: self.url.clone().map(|u| u.into()), + image, comments_enabled: Some(!self.locked), sensitive: Some(self.nsfw), stickied: Some(self.stickied), + published: convert_datetime(self.published), + updated: self.updated.map(convert_datetime), + unparsed: Default::default(), }; - Ok(Ext1::new(page, ext)) + Ok(page) } fn to_tombstone(&self) -> Result { @@ -108,136 +164,50 @@ impl ToApub for Post { #[async_trait::async_trait(?Send)] impl FromApub for Post { - type ApubType = PageExt; + type ApubType = Page; - /// Converts a `PageExt` to `PostForm`. - /// - /// If the post's community or creator are not known locally, these are also fetched. async fn from_apub( - page: &PageExt, + page: &Page, context: &LemmyContext, - expected_domain: Url, + _expected_domain: Url, request_counter: &mut i32, - mod_action_allowed: bool, + _mod_action_allowed: bool, ) -> Result { - let post: Post = get_object_from_apub( - page, - context, - expected_domain, - request_counter, - mod_action_allowed, - ) - .await?; - check_object_for_community_or_site_ban(page, post.community_id, context, request_counter) - .await?; - Ok(post) - } -} - -#[async_trait::async_trait(?Send)] -impl FromApubToForm for PostForm { - async fn from_apub( - page: &PageExt, - context: &LemmyContext, - expected_domain: Url, - request_counter: &mut i32, - mod_action_allowed: bool, - ) -> Result { - let community = get_community_from_to_or_cc(page, context, request_counter).await?; - let ap_id = if mod_action_allowed { - let id = page.id_unchecked().context(location_info!())?; - check_is_apub_id_valid(id, community.local)?; - id.to_owned().into() - } else { - check_object_domain(page, expected_domain, community.local)? - }; - 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_person(creator_actor_id, context, request_counter).await?; + get_or_fetch_and_upsert_person(&page.attributed_to, context, request_counter).await?; + let community = extract_community(&page.to, context, request_counter).await?; - let thumbnail_url: Option = 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(|url| url.to_owned()), - None => None, - }; - let url = page - .inner - .url() - .map(|u| u.as_single_xsd_any_uri()) - .flatten() - .map(|u| u.to_owned()); - - let (embed_title, embed_description, embed_html, pictrs_thumbnail) = if let Some(url) = &url { - let (embed, thumb) = fetch_iframely_and_pictrs_data(context.client(), Some(url)).await?; - (embed.title, embed.description, embed.html, thumb) + let thumbnail_url: Option = page.image.clone().map(|i| i.url); + let (iframely_response, pictrs_thumbnail) = if let Some(url) = &page.url { + fetch_iframely_and_pictrs_data(context.client(), Some(url)).await? } else { - (None, None, None, thumbnail_url) + (None, thumbnail_url) }; + let (embed_title, embed_description, embed_html) = iframely_response + .map(|u| (u.title, u.description, u.html)) + .unwrap_or((None, None, None)); - let name = page - .inner - .name() - // The following is for compatibility with lemmy v0.9.9 and older - // TODO: remove it after some time (along with the map above) - .or_else(|| page.inner.summary()) - .context(location_info!())? - .as_single_xsd_string() - .context(location_info!())? - .to_string(); - let body = get_source_markdown_value(page)?; - - // TODO: expected_domain is wrong in this case, because it simply takes the domain of the actor - // maybe we need to take id_unchecked() if the activity is from community to user? - // why did this work before? -> i dont think it did? - // -> try to make expected_domain optional and set it null if it is a mod action - - check_slurs(&name)?; - let body_slurs_removed = body.map(|b| remove_slurs(&b)); - Ok(PostForm { - name, - url: url.map(|u| u.into()), + let body_slurs_removed = page.source.as_ref().map(|s| remove_slurs(&s.content)); + let form = PostForm { + name: page.name.clone(), + url: page.url.clone().map(|u| u.into()), body: body_slurs_removed, creator_id: creator.id, community_id: community.id, removed: None, - locked: ext.comments_enabled.map(|e| !e), - 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()), + locked: page.comments_enabled.map(|e| !e), + published: Some(page.published.naive_local()), + updated: page.updated.map(|u| u.naive_local()), deleted: None, - nsfw: ext.sensitive, - stickied: ext.stickied, + nsfw: page.sensitive, + stickied: page.stickied, embed_title, embed_description, embed_html, thumbnail_url: pictrs_thumbnail.map(|u| u.into()), - ap_id: Some(ap_id), + ap_id: Some(page.id.clone().into()), local: Some(false), - }) + }; + Ok(blocking(context.pool(), move |conn| Post::upsert(conn, &form)).await??) } } diff --git a/crates/apub/src/objects/private_message.rs b/crates/apub/src/objects/private_message.rs index 6bb4820f4..2b284a2da 100644 --- a/crates/apub/src/objects/private_message.rs +++ b/crates/apub/src/objects/private_message.rs @@ -1,60 +1,93 @@ use crate::{ extensions::context::lemmy_context, fetcher::person::get_or_fetch_and_upsert_person, - objects::{ - check_object_domain, - create_tombstone, - get_object_from_apub, - get_source_markdown_value, - set_content_and_source, - FromApub, - FromApubToForm, - ToApub, - }, - NoteExt, + objects::{create_tombstone, FromApub, Source, ToApub}, }; use activitystreams::{ - object::{kind::NoteType, ApObject, Note, Tombstone}, - prelude::*, + base::AnyBase, + object::{kind::NoteType, Tombstone}, + primitives::OneOrMany, + unparsed::Unparsed, }; -use anyhow::Context; +use anyhow::anyhow; +use chrono::{DateTime, FixedOffset}; use lemmy_api_common::blocking; -use lemmy_db_queries::{Crud, DbPool}; +use lemmy_apub_lib::{ + values::{MediaTypeHtml, MediaTypeMarkdown}, + verify_domains_match, +}; +use lemmy_db_queries::{ApubObject, Crud, DbPool}; use lemmy_db_schema::source::{ person::Person, private_message::{PrivateMessage, PrivateMessageForm}, }; -use lemmy_utils::{location_info, utils::convert_datetime, LemmyError}; +use lemmy_utils::{utils::convert_datetime, LemmyError}; use lemmy_websocket::LemmyContext; +use serde::{Deserialize, Serialize}; use url::Url; +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Note { + #[serde(rename = "@context")] + context: OneOrMany, + r#type: NoteType, + pub(crate) id: Url, + pub(crate) attributed_to: Url, + to: Url, + content: String, + media_type: MediaTypeHtml, + source: Source, + published: DateTime, + updated: Option>, + #[serde(flatten)] + unparsed: Unparsed, +} + +impl Note { + pub(crate) async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + verify_domains_match(&self.attributed_to, &self.id)?; + let person = + get_or_fetch_and_upsert_person(&self.attributed_to, context, request_counter).await?; + if person.banned { + return Err(anyhow!("Person is banned from site").into()); + } + Ok(()) + } +} + #[async_trait::async_trait(?Send)] impl ToApub for PrivateMessage { - type ApubType = NoteExt; - - async fn to_apub(&self, pool: &DbPool) -> Result { - let mut private_message = ApObject::new(Note::new()); + type ApubType = Note; + async fn to_apub(&self, pool: &DbPool) -> Result { let creator_id = self.creator_id; let creator = blocking(pool, move |conn| Person::read(conn, creator_id)).await??; let recipient_id = self.recipient_id; let recipient = blocking(pool, move |conn| Person::read(conn, recipient_id)).await??; - private_message - .set_many_contexts(lemmy_context()?) - .set_id(self.ap_id.to_owned().into_inner()) - .set_published(convert_datetime(self.published)) - .set_to(recipient.actor_id.into_inner()) - .set_attributed_to(creator.actor_id.into_inner()); - - set_content_and_source(&mut private_message, &self.content)?; - - if let Some(u) = self.updated { - private_message.set_updated(convert_datetime(u)); - } - - Ok(private_message) + let note = Note { + context: lemmy_context(), + r#type: NoteType::Note, + id: self.ap_id.clone().into(), + attributed_to: creator.actor_id.into_inner(), + to: recipient.actor_id.into(), + content: self.content.clone(), + media_type: MediaTypeHtml::Html, + source: Source { + content: self.content.clone(), + media_type: MediaTypeMarkdown::Markdown, + }, + published: convert_datetime(self.published), + updated: self.updated.map(convert_datetime), + unparsed: Default::default(), + }; + Ok(note) } fn to_tombstone(&self) -> Result { @@ -69,66 +102,35 @@ impl ToApub for PrivateMessage { #[async_trait::async_trait(?Send)] impl FromApub for PrivateMessage { - type ApubType = NoteExt; + type ApubType = Note; async fn from_apub( - note: &NoteExt, + note: &Note, context: &LemmyContext, - expected_domain: Url, - request_counter: &mut i32, - mod_action_allowed: bool, - ) -> Result { - get_object_from_apub( - note, - context, - expected_domain, - request_counter, - mod_action_allowed, - ) - .await - } -} - -#[async_trait::async_trait(?Send)] -impl FromApubToForm for PrivateMessageForm { - async fn from_apub( - note: &NoteExt, - context: &LemmyContext, - expected_domain: Url, + _expected_domain: Url, request_counter: &mut i32, _mod_action_allowed: bool, - ) -> Result { - let creator_actor_id = note - .attributed_to() - .context(location_info!())? - .clone() - .single_xsd_any_uri() - .context(location_info!())?; - + ) -> Result { let creator = - get_or_fetch_and_upsert_person(&creator_actor_id, context, request_counter).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_person(&recipient_actor_id, context, request_counter).await?; - let ap_id = Some(check_object_domain(note, expected_domain, false)?); + 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?; - let content = get_source_markdown_value(note)?.context(location_info!())?; - - Ok(PrivateMessageForm { + let form = PrivateMessageForm { creator_id: creator.id, recipient_id: recipient.id, - content, - published: note.published().map(|u| u.to_owned().naive_local()), - updated: note.updated().map(|u| u.to_owned().naive_local()), + content: note.source.content.clone(), + published: Some(note.published.naive_local()), + updated: note.updated.map(|u| u.to_owned().naive_local()), deleted: None, read: None, - ap_id, + ap_id: Some(note.id.clone().into()), local: Some(false), - }) + }; + Ok( + blocking(context.pool(), move |conn| { + PrivateMessage::upsert(conn, &form) + }) + .await??, + ) } } diff --git a/crates/apub_lib/Cargo.toml b/crates/apub_lib/Cargo.toml index 327670b50..6f8c84170 100644 --- a/crates/apub_lib/Cargo.toml +++ b/crates/apub_lib/Cargo.toml @@ -12,3 +12,4 @@ activitystreams-ext = "0.1.0-alpha.2" serde = { version = "1.0.123", features = ["derive"] } async-trait = "0.1.42" url = { version = "2.2.1", features = ["serde"] } +serde_json = { version = "1.0.64", features = ["preserve_order"] } diff --git a/crates/apub_lib/src/lib.rs b/crates/apub_lib/src/lib.rs index 66bba9f43..13fc4025d 100644 --- a/crates/apub_lib/src/lib.rs +++ b/crates/apub_lib/src/lib.rs @@ -1,3 +1,5 @@ +pub mod values; + use activitystreams::{ base::AnyBase, error::DomainError, @@ -9,18 +11,12 @@ use lemmy_utils::LemmyError; use lemmy_websocket::LemmyContext; use url::Url; -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -pub enum PublicUrl { - #[serde(rename = "https://www.w3.org/ns/activitystreams#Public")] - Public, -} - #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct ActivityCommonFields { #[serde(rename = "@context")] pub context: OneOrMany, - id: Url, + pub id: Url, pub actor: Url, // unparsed fields diff --git a/crates/apub_lib/src/values/mod.rs b/crates/apub_lib/src/values/mod.rs new file mode 100644 index 000000000..2c9f65ffe --- /dev/null +++ b/crates/apub_lib/src/values/mod.rs @@ -0,0 +1,61 @@ +//! The enums here serve to limit a json string value to a single, hardcoded value which can be +//! verified at compilation time. When using it as the type of a struct field, the struct can only +//! be constructed or deserialized if the field has the exact same value. +//! +//! If we used String as the field type, any value would be accepted, and we would have to check +//! manually at runtime that it contains the expected value. +//! +//! The enums in [`activitystreams::activity::kind`] work in the same way, and can be used to +//! distinguish different activity types. +//! +//! In the example below, `MyObject` can only be constructed or +//! deserialized if `media_type` is `text/markdown`, but not if it is `text/html`. +//! +//! ``` +//! use lemmy_apub_lib::values::MediaTypeMarkdown; +//! use serde_json::from_str; +//! use serde::{Deserialize, Serialize}; +//! +//! #[derive(Deserialize, Serialize)] +//! struct MyObject { +//! content: String, +//! media_type: MediaTypeMarkdown, +//! } +//! +//! let markdown_json = r#"{"content": "**test**", "media_type": "text/markdown"}"#; +//! let from_markdown = from_str::(markdown_json); +//! assert!(from_markdown.is_ok()); +//! +//! let markdown_html = r#"{"content": "test", "media_type": "text/html"}"#; +//! let from_html = from_str::(markdown_html); +//! assert!(from_html.is_err()); +//! ``` + +use serde::{Deserialize, Serialize}; + +/// The identifier used to address activities to the public. +/// +/// +#[derive(Debug, Clone, Deserialize, Serialize)] +pub enum PublicUrl { + #[serde(rename = "https://www.w3.org/ns/activitystreams#Public")] + Public, +} + +/// Media type for markdown text. +/// +/// +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum MediaTypeMarkdown { + #[serde(rename = "text/markdown")] + Markdown, +} + +/// Media type for HTML text/ +/// +/// +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum MediaTypeHtml { + #[serde(rename = "text/html")] + Html, +} diff --git a/crates/db_queries/src/lib.rs b/crates/db_queries/src/lib.rs index fbd508c15..adc0717ac 100644 --- a/crates/db_queries/src/lib.rs +++ b/crates/db_queries/src/lib.rs @@ -117,6 +117,10 @@ pub trait Reportable
{ Self: Sized; } +pub trait DeleteableOrRemoveable { + fn blank_out_deleted_or_removed_info(self) -> Self; +} + pub trait ApubObject { fn read_from_apub_id(conn: &PgConnection, object_id: &DbUrl) -> Result where diff --git a/crates/db_queries/src/source/comment.rs b/crates/db_queries/src/source/comment.rs index 1eec0c987..d0171cb8d 100644 --- a/crates/db_queries/src/source/comment.rs +++ b/crates/db_queries/src/source/comment.rs @@ -1,4 +1,4 @@ -use crate::{ApubObject, Crud, Likeable, Saveable}; +use crate::{ApubObject, Crud, DeleteableOrRemoveable, Likeable, Saveable}; use diesel::{dsl::*, result::Error, *}; use lemmy_db_schema::{ naive_now, @@ -228,6 +228,13 @@ impl Saveable for CommentSaved { } } +impl DeleteableOrRemoveable for Comment { + fn blank_out_deleted_or_removed_info(mut self) -> Self { + self.content = "".into(); + self + } +} + #[cfg(test)] mod tests { use crate::{establish_unpooled_connection, Crud, Likeable, Saveable}; diff --git a/crates/db_queries/src/source/community.rs b/crates/db_queries/src/source/community.rs index 3f1f04767..2a6219339 100644 --- a/crates/db_queries/src/source/community.rs +++ b/crates/db_queries/src/source/community.rs @@ -1,4 +1,4 @@ -use crate::{ApubObject, Bannable, Crud, Followable, Joinable}; +use crate::{ApubObject, Bannable, Crud, DeleteableOrRemoveable, Followable, Joinable}; use diesel::{dsl::*, result::Error, *}; use lemmy_db_schema::{ naive_now, @@ -11,6 +11,7 @@ use lemmy_db_schema::{ CommunityModeratorForm, CommunityPersonBan, CommunityPersonBanForm, + CommunitySafe, }, CommunityId, DbUrl, @@ -199,6 +200,26 @@ impl Joinable for CommunityModerator { } } +impl DeleteableOrRemoveable for CommunitySafe { + fn blank_out_deleted_or_removed_info(mut self) -> Self { + self.title = "".into(); + self.description = None; + self.icon = None; + self.banner = None; + self + } +} + +impl DeleteableOrRemoveable for Community { + fn blank_out_deleted_or_removed_info(mut self) -> Self { + self.title = "".into(); + self.description = None; + self.icon = None; + self.banner = None; + self + } +} + pub trait CommunityModerator_ { fn delete_for_community( conn: &PgConnection, diff --git a/crates/db_queries/src/source/post.rs b/crates/db_queries/src/source/post.rs index 9f2d2f9b3..f0cf10935 100644 --- a/crates/db_queries/src/source/post.rs +++ b/crates/db_queries/src/source/post.rs @@ -1,4 +1,4 @@ -use crate::{ApubObject, Crud, Likeable, Readable, Saveable}; +use crate::{ApubObject, Crud, DeleteableOrRemoveable, Likeable, Readable, Saveable}; use diesel::{dsl::*, result::Error, *}; use lemmy_db_schema::{ naive_now, @@ -260,6 +260,20 @@ impl Readable for PostRead { } } +impl DeleteableOrRemoveable for Post { + fn blank_out_deleted_or_removed_info(mut self) -> Self { + self.name = "".into(); + self.url = None; + self.body = None; + self.embed_title = None; + self.embed_description = None; + self.embed_html = None; + self.thumbnail_url = None; + + self + } +} + #[cfg(test)] mod tests { use crate::{establish_unpooled_connection, source::post::*}; diff --git a/crates/db_queries/src/source/private_message.rs b/crates/db_queries/src/source/private_message.rs index 98c8e08ce..da1c5abd5 100644 --- a/crates/db_queries/src/source/private_message.rs +++ b/crates/db_queries/src/source/private_message.rs @@ -1,4 +1,4 @@ -use crate::{ApubObject, Crud}; +use crate::{ApubObject, Crud, DeleteableOrRemoveable}; use diesel::{dsl::*, result::Error, *}; use lemmy_db_schema::{naive_now, source::private_message::*, DbUrl, PersonId, PrivateMessageId}; @@ -137,6 +137,13 @@ impl PrivateMessage_ for PrivateMessage { } } +impl DeleteableOrRemoveable for PrivateMessage { + fn blank_out_deleted_or_removed_info(mut self) -> Self { + self.content = "".into(); + self + } +} + #[cfg(test)] mod tests { use crate::{establish_unpooled_connection, source::private_message::PrivateMessage_, Crud}; diff --git a/crates/db_schema/src/lib.rs b/crates/db_schema/src/lib.rs index 4efa983fe..3c8abaf79 100644 --- a/crates/db_schema/src/lib.rs +++ b/crates/db_schema/src/lib.rs @@ -92,6 +92,7 @@ where } impl DbUrl { + // TODO: remove this method and just use into() pub fn into_inner(self) -> Url { self.0 } @@ -99,7 +100,7 @@ impl DbUrl { impl Display for DbUrl { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - self.to_owned().into_inner().fmt(f) + self.to_owned().0.fmt(f) } } diff --git a/crates/utils/src/request.rs b/crates/utils/src/request.rs index 27b7a6cd9..c59180c55 100644 --- a/crates/utils/src/request.rs +++ b/crates/utils/src/request.rs @@ -47,7 +47,7 @@ where response.expect("retry http request") } -#[derive(Deserialize, Debug, Default)] +#[derive(Deserialize, Debug)] pub struct IframelyResponse { pub title: Option, pub description: Option, @@ -70,7 +70,7 @@ pub(crate) async fn fetch_iframely( .map_err(|e| RecvError(e.to_string()))?; Ok(res) } else { - Ok(IframelyResponse::default()) + Err(anyhow!("Missing Iframely URL in config.").into()) } } @@ -119,18 +119,23 @@ pub(crate) async fn fetch_pictrs( pub async fn fetch_iframely_and_pictrs_data( client: &Client, url: Option<&Url>, -) -> Result<(IframelyResponse, Option), LemmyError> { +) -> Result<(Option, Option), LemmyError> { match &url { Some(url) => { // Fetch iframely data - let iframely_response = fetch_iframely(client, url).await?; + let iframely_res_option = fetch_iframely(client, url).await.ok(); // Fetch pictrs thumbnail - let pictrs_hash = match &iframely_response.thumbnail_url { - Some(iframely_thumbnail_url) => fetch_pictrs(client, iframely_thumbnail_url) - .await? - .map(|r| r.files[0].file.to_owned()), - // Try to generate a small thumbnail if iframely is not supported + let pictrs_hash = match &iframely_res_option { + Some(iframely_res) => match &iframely_res.thumbnail_url { + Some(iframely_thumbnail_url) => fetch_pictrs(client, iframely_thumbnail_url) + .await? + .map(|r| r.files[0].file.to_owned()), + // Try to generate a small thumbnail if iframely is not supported + None => fetch_pictrs(client, url) + .await? + .map(|r| r.files[0].file.to_owned()), + }, None => fetch_pictrs(client, url) .await? .map(|r| r.files[0].file.to_owned()), @@ -147,22 +152,10 @@ pub async fn fetch_iframely_and_pictrs_data( .ok() }) .flatten(); - /* - let pictrs_thumbnail = if let Some(pictrs_hash) = pictrs_hash { - Some(Url::parse(&format!( - "{}/pictrs/image/{}", - Settings::get().get_protocol_and_hostname(), - pictrs_hash - ))?) - } else { - None - }; - */ - - Ok((iframely_response, pictrs_thumbnail)) + Ok((iframely_res_option, pictrs_thumbnail)) } - None => Ok((IframelyResponse::default(), None)), + None => Ok((None, None)), } } diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml index 5d8bf5680..807faa115 100644 --- a/docker/prod/docker-compose.yml +++ b/docker/prod/docker-compose.yml @@ -12,7 +12,7 @@ services: restart: always lemmy: - image: dessalines/lemmy:0.11.2 + image: dessalines/lemmy:0.11.3 ports: - "127.0.0.1:8536:8536" restart: always @@ -26,7 +26,7 @@ services: - iframely lemmy-ui: - image: dessalines/lemmy-ui:0.11.2 + image: dessalines/lemmy-ui:0.11.3 ports: - "127.0.0.1:1235:1234" restart: always diff --git a/migrations/2021-08-02-002342_comment_count_fixes/down.sql b/migrations/2021-08-02-002342_comment_count_fixes/down.sql new file mode 100644 index 000000000..3a895f70b --- /dev/null +++ b/migrations/2021-08-02-002342_comment_count_fixes/down.sql @@ -0,0 +1,28 @@ +drop trigger post_aggregates_comment_set_deleted on comment; +drop function post_aggregates_comment_deleted; + +create or replace function post_aggregates_comment_count() +returns trigger language plpgsql +as $$ +begin + IF (TG_OP = 'INSERT') THEN + update post_aggregates pa + set comments = comments + 1, + newest_comment_time = NEW.published + where pa.post_id = NEW.post_id; + + -- A 2 day necro-bump limit + update post_aggregates pa + set newest_comment_time_necro = NEW.published + where pa.post_id = NEW.post_id + and published > ('now'::timestamp - '2 days'::interval); + ELSIF (TG_OP = 'DELETE') THEN + -- Join to post because that post may not exist anymore + update post_aggregates pa + set comments = comments - 1 + from post p + where pa.post_id = p.id + and pa.post_id = OLD.post_id; + END IF; + return null; +end $$; diff --git a/migrations/2021-08-02-002342_comment_count_fixes/up.sql b/migrations/2021-08-02-002342_comment_count_fixes/up.sql new file mode 100644 index 000000000..b65f60089 --- /dev/null +++ b/migrations/2021-08-02-002342_comment_count_fixes/up.sql @@ -0,0 +1,61 @@ +-- Creating a new trigger for when comment.deleted is updated + +create or replace function post_aggregates_comment_deleted() +returns trigger language plpgsql +as $$ +begin + IF NEW.deleted = TRUE THEN + update post_aggregates pa + set comments = comments - 1 + where pa.post_id = NEW.post_id; + ELSE + update post_aggregates pa + set comments = comments + 1 + where pa.post_id = NEW.post_id; + END IF; + return null; +end $$; + +create trigger post_aggregates_comment_set_deleted +after update of deleted on comment +for each row +execute procedure post_aggregates_comment_deleted(); + +-- Fix issue with being able to necro-bump your own post +create or replace function post_aggregates_comment_count() +returns trigger language plpgsql +as $$ +begin + IF (TG_OP = 'INSERT') THEN + update post_aggregates pa + set comments = comments + 1, + newest_comment_time = NEW.published + where pa.post_id = NEW.post_id; + + -- A 2 day necro-bump limit + update post_aggregates pa + set newest_comment_time_necro = NEW.published + from post p + where pa.post_id = p.id + and pa.post_id = NEW.post_id + -- Fix issue with being able to necro-bump your own post + and NEW.creator_id != p.creator_id + and pa.published > ('now'::timestamp - '2 days'::interval); + + ELSIF (TG_OP = 'DELETE') THEN + -- Join to post because that post may not exist anymore + update post_aggregates pa + set comments = comments - 1 + from post p + where pa.post_id = p.id + and pa.post_id = OLD.post_id; + ELSIF (TG_OP = 'UPDATE') THEN + -- Join to post because that post may not exist anymore + update post_aggregates pa + set comments = comments - 1 + from post p + where pa.post_id = p.id + and pa.post_id = OLD.post_id; + END IF; + return null; +end $$;