diff --git a/Cargo.lock b/Cargo.lock index d6558f4d1..f2463c19c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1239,9 +1239,9 @@ dependencies = [ [[package]] name = "diesel" -version = "2.2.4" +version = "2.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "158fe8e2e68695bd615d7e4f3227c0727b151330d3e253b525086c348d055d5e" +checksum = "cbf9649c05e0a9dbd6d0b0b8301db5182b972d0fd02f0a7c6736cf632d7c0fd5" dependencies = [ "bitflags 2.6.0", "byteorder", @@ -1255,9 +1255,9 @@ dependencies = [ [[package]] name = "diesel-async" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c5c6ec8d5c7b8444d19a47161797cbe361e0fb1ee40c6a8124ec915b64a4125" +checksum = "51a307ac00f7c23f526a04a77761a0519b9f0eb2838ebf5b905a58580095bdcb" dependencies = [ "async-trait", "deadpool", @@ -2582,6 +2582,7 @@ dependencies = [ "moka", "pretty_assertions", "reqwest 0.12.8", + "semver", "serde", "serde_json", "serde_with", @@ -2879,7 +2880,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] diff --git a/api_tests/src/comment.spec.ts b/api_tests/src/comment.spec.ts index c3f4b3efe..edc588db7 100644 --- a/api_tests/src/comment.spec.ts +++ b/api_tests/src/comment.spec.ts @@ -156,7 +156,6 @@ 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 deleted on beta await waitUntil( @@ -254,7 +253,6 @@ test("Remove a comment from admin and community on different instance", async () betaComment.comment.id, ); expect(removeCommentRes.comment_view.comment.removed).toBe(true); - expect(removeCommentRes.comment_view.comment.content).toBe(""); // Comment text is also hidden from list let listComments = await getComments( @@ -263,7 +261,6 @@ test("Remove a comment from admin and community on different instance", async () ); expect(listComments.comments.length).toBe(1); expect(listComments.comments[0].comment.removed).toBe(true); - expect(listComments.comments[0].comment.content).toBe(""); // Make sure its not removed on alpha let refetchedPostComments = await getComments( diff --git a/crates/api_common/src/oauth_provider.rs b/crates/api_common/src/oauth_provider.rs index 36fef3b18..2f3344802 100644 --- a/crates/api_common/src/oauth_provider.rs +++ b/crates/api_common/src/oauth_provider.rs @@ -25,6 +25,8 @@ pub struct CreateOAuthProvider { #[cfg_attr(feature = "full", ts(optional))] pub account_linking_enabled: Option, #[cfg_attr(feature = "full", ts(optional))] + pub use_pkce: Option, + #[cfg_attr(feature = "full", ts(optional))] pub enabled: Option, } @@ -54,6 +56,8 @@ pub struct EditOAuthProvider { #[cfg_attr(feature = "full", ts(optional))] pub account_linking_enabled: Option, #[cfg_attr(feature = "full", ts(optional))] + pub use_pkce: Option, + #[cfg_attr(feature = "full", ts(optional))] pub enabled: Option, } @@ -82,4 +86,6 @@ pub struct AuthenticateWithOauth { /// An answer is mandatory if require application is enabled on the server #[cfg_attr(feature = "full", ts(optional))] pub answer: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub pkce_code_verifier: Option, } diff --git a/crates/api_crud/Cargo.toml b/crates/api_crud/Cargo.toml index 3f1a00ccd..a05a4deed 100644 --- a/crates/api_crud/Cargo.toml +++ b/crates/api_crud/Cargo.toml @@ -29,6 +29,7 @@ anyhow.workspace = true chrono.workspace = true webmention = "0.6.0" accept-language = "3.1.0" +regex = { workspace = true } serde_json = { workspace = true } serde = { workspace = true } serde_with = { workspace = true } diff --git a/crates/api_crud/src/oauth_provider/create.rs b/crates/api_crud/src/oauth_provider/create.rs index fe44ae56e..c1e30066a 100644 --- a/crates/api_crud/src/oauth_provider/create.rs +++ b/crates/api_crud/src/oauth_provider/create.rs @@ -35,6 +35,7 @@ pub async fn create_oauth_provider( scopes: data.scopes.to_string(), auto_verify_email: data.auto_verify_email, account_linking_enabled: data.account_linking_enabled, + use_pkce: data.use_pkce, enabled: data.enabled, }; let oauth_provider = OAuthProvider::create(&mut context.pool(), &oauth_provider_form).await?; diff --git a/crates/api_crud/src/oauth_provider/update.rs b/crates/api_crud/src/oauth_provider/update.rs index 29ba19b49..f8631a487 100644 --- a/crates/api_crud/src/oauth_provider/update.rs +++ b/crates/api_crud/src/oauth_provider/update.rs @@ -33,6 +33,7 @@ pub async fn update_oauth_provider( auto_verify_email: data.auto_verify_email, account_linking_enabled: data.account_linking_enabled, enabled: data.enabled, + use_pkce: data.use_pkce, updated: Some(Some(Utc::now())), }; diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs index deb65ec38..16156abe4 100644 --- a/crates/api_crud/src/user/create.rs +++ b/crates/api_crud/src/user/create.rs @@ -45,9 +45,10 @@ use lemmy_utils::{ validation::is_valid_actor_name, }, }; +use regex::Regex; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; -use std::collections::HashSet; +use std::{collections::HashSet, sync::LazyLock}; #[skip_serializing_none] #[derive(Debug, Serialize, Deserialize, Clone, Default)] @@ -225,6 +226,11 @@ pub async fn authenticate_with_oauth( Err(LemmyErrorType::OauthAuthorizationInvalid)? } + // validate the PKCE challenge + if let Some(code_verifier) = &data.pkce_code_verifier { + check_code_verifier(code_verifier)?; + } + // Fetch the OAUTH provider and make sure it's enabled let oauth_provider_id = data.oauth_provider_id; let oauth_provider = OAuthProvider::read(&mut context.pool(), oauth_provider_id) @@ -236,9 +242,14 @@ pub async fn authenticate_with_oauth( return Err(LemmyErrorType::OauthAuthorizationInvalid)?; } - let token_response = - oauth_request_access_token(&context, &oauth_provider, &data.code, redirect_uri.as_str()) - .await?; + let token_response = oauth_request_access_token( + &context, + &oauth_provider, + &data.code, + data.pkce_code_verifier.as_deref(), + redirect_uri.as_str(), + ) + .await?; let user_info = oidc_get_user_info( &context, @@ -533,20 +544,27 @@ async fn oauth_request_access_token( context: &Data, oauth_provider: &OAuthProvider, code: &str, + pkce_code_verifier: Option<&str>, redirect_uri: &str, ) -> LemmyResult { + let mut form = vec![ + ("client_id", &*oauth_provider.client_id), + ("client_secret", &*oauth_provider.client_secret), + ("code", code), + ("grant_type", "authorization_code"), + ("redirect_uri", redirect_uri), + ]; + + if let Some(code_verifier) = pkce_code_verifier { + form.push(("code_verifier", code_verifier)); + } + // Request an Access Token from the OAUTH provider let response = context .client() .post(oauth_provider.token_endpoint.as_str()) .header("Accept", "application/json") - .form(&[ - ("grant_type", "authorization_code"), - ("code", code), - ("redirect_uri", redirect_uri), - ("client_id", &oauth_provider.client_id), - ("client_secret", &oauth_provider.client_secret), - ]) + .form(&form[..]) .send() .await .with_lemmy_type(LemmyErrorType::OauthLoginFailed)? @@ -596,3 +614,17 @@ fn read_user_info(user_info: &serde_json::Value, key: &str) -> LemmyResult LemmyResult<()> { + static VALID_CODE_VERIFIER_REGEX: LazyLock = + LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9\-._~]{43,128}$").expect("compile regex")); + + let check = VALID_CODE_VERIFIER_REGEX.is_match(code_verifier); + + if check { + Ok(()) + } else { + Err(LemmyErrorType::InvalidCodeVerifier.into()) + } +} diff --git a/crates/apub/Cargo.toml b/crates/apub/Cargo.toml index 057e9bd38..8124fda01 100644 --- a/crates/apub/Cargo.toml +++ b/crates/apub/Cargo.toml @@ -45,6 +45,7 @@ html2md = "0.2.14" html2text = "0.12.6" stringreader = "0.1.1" enum_delegate = "0.2.0" +semver = "1.0.23" [dev-dependencies] serial_test = { workspace = true } diff --git a/crates/apub/assets/lemmy/activities/create_or_update/create_note.json b/crates/apub/assets/lemmy/activities/create_or_update/create_comment.json similarity index 100% rename from crates/apub/assets/lemmy/activities/create_or_update/create_note.json rename to crates/apub/assets/lemmy/activities/create_or_update/create_comment.json diff --git a/crates/apub/assets/lemmy/activities/create_or_update/create_private_message.json b/crates/apub/assets/lemmy/activities/create_or_update/create_private_message.json index 54ee39350..e7dbdd0f9 100644 --- a/crates/apub/assets/lemmy/activities/create_or_update/create_private_message.json +++ b/crates/apub/assets/lemmy/activities/create_or_update/create_private_message.json @@ -3,7 +3,7 @@ "actor": "http://enterprise.lemmy.ml/u/lemmy_beta", "to": ["http://ds9.lemmy.ml/u/lemmy_alpha"], "object": { - "type": "ChatMessage", + "type": "Note", "id": "http://enterprise.lemmy.ml/private_message/1", "attributedTo": "http://enterprise.lemmy.ml/u/lemmy_beta", "to": ["http://ds9.lemmy.ml/u/lemmy_alpha"], diff --git a/crates/apub/assets/lemmy/objects/note.json b/crates/apub/assets/lemmy/objects/comment.json similarity index 100% rename from crates/apub/assets/lemmy/objects/note.json rename to crates/apub/assets/lemmy/objects/comment.json diff --git a/crates/apub/assets/lemmy/objects/chat_message.json b/crates/apub/assets/lemmy/objects/private_message.json similarity index 93% rename from crates/apub/assets/lemmy/objects/chat_message.json rename to crates/apub/assets/lemmy/objects/private_message.json index 95b37322e..a3579523e 100644 --- a/crates/apub/assets/lemmy/objects/chat_message.json +++ b/crates/apub/assets/lemmy/objects/private_message.json @@ -1,6 +1,6 @@ { "id": "https://enterprise.lemmy.ml/private_message/1621", - "type": "ChatMessage", + "type": "Note", "attributedTo": "https://enterprise.lemmy.ml/u/picard", "to": ["https://queer.hacktivis.me/users/lanodan"], "content": "

Hello hello, testing

\n", diff --git a/crates/apub/assets/mastodon/activities/private_message.json b/crates/apub/assets/mastodon/activities/private_message.json new file mode 100644 index 000000000..b542859b5 --- /dev/null +++ b/crates/apub/assets/mastodon/activities/private_message.json @@ -0,0 +1,49 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + { + "ostatus": "http://ostatus.org#", + "atomUri": "ostatus:atomUri", + "inReplyToAtomUri": "ostatus:inReplyToAtomUri", + "conversation": "ostatus:conversation", + "sensitive": "as:sensitive", + "toot": "http://joinmastodon.org/ns#", + "votersCount": "toot:votersCount" + } + ], + "id": "https://mastodon.world/users/nutomic/statuses/110854468010322301", + "type": "Note", + "summary": null, + "inReplyTo": "https://mastodon.world/users/nutomic/statuses/110854464248188528", + "published": "2023-08-08T14:29:04Z", + "url": "https://mastodon.world/@nutomic/110854468010322301", + "attributedTo": "https://mastodon.world/users/nutomic", + "to": ["https://ds9.lemmy.ml/u/nutomic"], + "cc": [], + "sensitive": false, + "atomUri": "https://mastodon.world/users/nutomic/statuses/110854468010322301", + "inReplyToAtomUri": "https://mastodon.world/users/nutomic/statuses/110854464248188528", + "conversation": "tag:mastodon.world,2023-08-08:objectId=121377096:objectType=Conversation", + "content": "

@nutomic@ds9.lemmy.ml 444

", + "contentMap": { + "es": "

@nutomic@ds9.lemmy.ml 444

" + }, + "attachment": [], + "tag": [ + { + "type": "Mention", + "href": "https://ds9.lemmy.ml/u/nutomic", + "name": "@nutomic@ds9.lemmy.ml" + } + ], + "replies": { + "id": "https://mastodon.world/users/nutomic/statuses/110854468010322301/replies", + "type": "Collection", + "first": { + "type": "CollectionPage", + "next": "https://mastodon.world/users/nutomic/statuses/110854468010322301/replies?only_other_accounts=true&page=true", + "partOf": "https://mastodon.world/users/nutomic/statuses/110854468010322301/replies", + "items": [] + } + } +} diff --git a/crates/apub/src/activities/community/announce.rs b/crates/apub/src/activities/community/announce.rs index 950f4861d..9d714e304 100644 --- a/crates/apub/src/activities/community/announce.rs +++ b/crates/apub/src/activities/community/announce.rs @@ -64,16 +64,17 @@ impl ActivityHandler for RawAnnouncableActivities { // verify and receive activity activity.verify(context).await?; - activity.clone().receive(context).await?; + let actor_id = activity.actor().clone().into(); + activity.receive(context).await?; // if community is local, send activity to followers if let Some(community) = community { if community.local { - let actor_id = activity.actor().clone().into(); verify_person_in_community(&actor_id, &community, context).await?; AnnounceActivity::send(self, &community, context).await?; } } + Ok(()) } } diff --git a/crates/apub/src/activities/create_or_update/comment.rs b/crates/apub/src/activities/create_or_update/comment.rs index 9f64e805b..72dae48b7 100644 --- a/crates/apub/src/activities/create_or_update/comment.rs +++ b/crates/apub/src/activities/create_or_update/comment.rs @@ -43,6 +43,7 @@ use lemmy_utils::{ error::{LemmyError, LemmyResult}, utils::mention::scrape_text_for_mentions, }; +use serde_json::{from_value, to_value}; use url::Url; impl CreateOrUpdateNote { @@ -98,7 +99,11 @@ impl CreateOrUpdateNote { inboxes.add_inbox(person.shared_inbox_or_inbox()); } - let activity = AnnouncableActivities::CreateOrUpdateComment(create_or_update); + // AnnouncableActivities doesnt contain Comment activity but only NoteWrapper, + // to be able to handle both comment and private message. So to send this out we need + // to convert this to NoteWrapper, by serializing and then deserializing again. + let converted = from_value(to_value(create_or_update)?)?; + let activity = AnnouncableActivities::CreateOrUpdateNoteWrapper(converted); send_activity_in_community(activity, &person, &community, inboxes, false, &context).await } } diff --git a/crates/apub/src/activities/create_or_update/mod.rs b/crates/apub/src/activities/create_or_update/mod.rs index c69e00e91..b442e5fa3 100644 --- a/crates/apub/src/activities/create_or_update/mod.rs +++ b/crates/apub/src/activities/create_or_update/mod.rs @@ -1,3 +1,4 @@ pub mod comment; +pub(crate) mod note_wrapper; pub mod post; pub mod private_message; diff --git a/crates/apub/src/activities/create_or_update/note_wrapper.rs b/crates/apub/src/activities/create_or_update/note_wrapper.rs new file mode 100644 index 000000000..ca79b45d2 --- /dev/null +++ b/crates/apub/src/activities/create_or_update/note_wrapper.rs @@ -0,0 +1,74 @@ +use crate::{ + objects::community::ApubCommunity, + protocol::{ + activities::create_or_update::{ + note::CreateOrUpdateNote, + note_wrapper::CreateOrUpdateNoteWrapper, + private_message::CreateOrUpdatePrivateMessage, + }, + InCommunity, + }, +}; +use activitypub_federation::{config::Data, traits::ActivityHandler}; +use lemmy_api_common::context::LemmyContext; +use lemmy_utils::error::{LemmyError, LemmyResult}; +use serde_json::{from_value, to_value}; +use url::Url; + +/// In Activitypub, both private messages and comments are represented by `type: Note` which +/// makes it difficult to distinguish them. This wrapper handles receiving of both types, and +/// routes them to the correct handler. +#[async_trait::async_trait] +impl ActivityHandler for CreateOrUpdateNoteWrapper { + type DataType = LemmyContext; + type Error = LemmyError; + + fn id(&self) -> &Url { + &self.id + } + + fn actor(&self) -> &Url { + &self.actor + } + + #[tracing::instrument(skip_all)] + async fn verify(&self, _context: &Data) -> LemmyResult<()> { + // Do everything in receive to avoid extra checks. + Ok(()) + } + + #[tracing::instrument(skip_all)] + async fn receive(self, context: &Data) -> LemmyResult<()> { + // Use serde to convert NoteWrapper either into Comment or PrivateMessage, + // depending on conditions below. This works because NoteWrapper keeps all + // additional data in field `other: Map`. + let val = to_value(self)?; + + // Convert self to a comment and get the community. If the conversion is + // successful and a community is returned, this is a comment. + let comment = from_value::(val.clone()); + if let Ok(comment) = comment { + if comment.community(context).await.is_ok() { + CreateOrUpdateNote::verify(&comment, context).await?; + CreateOrUpdateNote::receive(comment, context).await?; + return Ok(()); + } + } + + // If any of the previous checks failed, we are dealing with a private message. + let private_message = from_value(val)?; + CreateOrUpdatePrivateMessage::verify(&private_message, context).await?; + CreateOrUpdatePrivateMessage::receive(private_message, context).await?; + Ok(()) + } +} + +#[async_trait::async_trait] +impl InCommunity for CreateOrUpdateNoteWrapper { + async fn community(&self, context: &Data) -> LemmyResult { + // Same logic as in receive. In case this is a private message, an error is returned. + let val = to_value(self)?; + let comment: CreateOrUpdateNote = from_value(val.clone())?; + comment.community(context).await + } +} diff --git a/crates/apub/src/activities/create_or_update/private_message.rs b/crates/apub/src/activities/create_or_update/private_message.rs index 6bba4e374..b6e7478ef 100644 --- a/crates/apub/src/activities/create_or_update/private_message.rs +++ b/crates/apub/src/activities/create_or_update/private_message.rs @@ -3,7 +3,7 @@ use crate::{ insert_received_activity, objects::{person::ApubPerson, private_message::ApubPrivateMessage}, protocol::activities::{ - create_or_update::chat_message::CreateOrUpdateChatMessage, + create_or_update::private_message::CreateOrUpdatePrivateMessage, CreateOrUpdateType, }, }; @@ -30,7 +30,7 @@ pub(crate) async fn send_create_or_update_pm( kind.clone(), &context.settings().get_protocol_and_hostname(), )?; - let create_or_update = CreateOrUpdateChatMessage { + let create_or_update = CreateOrUpdatePrivateMessage { id: id.clone(), actor: actor.id().into(), to: [recipient.id().into()], @@ -44,7 +44,7 @@ pub(crate) async fn send_create_or_update_pm( } #[async_trait::async_trait] -impl ActivityHandler for CreateOrUpdateChatMessage { +impl ActivityHandler for CreateOrUpdatePrivateMessage { type DataType = LemmyContext; type Error = LemmyError; diff --git a/crates/apub/src/activities/deletion/mod.rs b/crates/apub/src/activities/deletion/mod.rs index 15118a476..941ac4237 100644 --- a/crates/apub/src/activities/deletion/mod.rs +++ b/crates/apub/src/activities/deletion/mod.rs @@ -36,7 +36,7 @@ use lemmy_db_schema::{ community::{Community, CommunityUpdateForm}, person::Person, post::{Post, PostUpdateForm}, - private_message::{PrivateMessage, PrivateMessageUpdateForm}, + private_message::{PrivateMessage as DbPrivateMessage, PrivateMessageUpdateForm}, }, traits::Crud, }; @@ -82,7 +82,7 @@ pub(crate) async fn send_apub_delete_in_community( #[tracing::instrument(skip_all)] pub(crate) async fn send_apub_delete_private_message( actor: &ApubPerson, - pm: PrivateMessage, + pm: DbPrivateMessage, deleted: bool, context: Data, ) -> LemmyResult<()> { @@ -298,7 +298,7 @@ async fn receive_delete_action( } } DeletableObjects::PrivateMessage(pm) => { - PrivateMessage::update( + DbPrivateMessage::update( &mut context.pool(), pm.id, &PrivateMessageUpdateForm { diff --git a/crates/apub/src/activity_lists.rs b/crates/apub/src/activity_lists.rs index 7ed1d8baf..0a11e30a0 100644 --- a/crates/apub/src/activity_lists.rs +++ b/crates/apub/src/activity_lists.rs @@ -11,11 +11,7 @@ use crate::{ report::Report, update::UpdateCommunity, }, - create_or_update::{ - chat_message::CreateOrUpdateChatMessage, - note::CreateOrUpdateNote, - page::CreateOrUpdatePage, - }, + create_or_update::{note_wrapper::CreateOrUpdateNoteWrapper, page::CreateOrUpdatePage}, deletion::{delete::Delete, undo_delete::UndoDelete}, following::{ accept::AcceptFollow, @@ -48,47 +44,17 @@ pub enum SharedInboxActivities { AcceptFollow(AcceptFollow), RejectFollow(RejectFollow), UndoFollow(UndoFollow), - CreateOrUpdatePrivateMessage(CreateOrUpdateChatMessage), Report(Report), AnnounceActivity(AnnounceActivity), /// This is a catch-all and needs to be last RawAnnouncableActivities(RawAnnouncableActivities), } -/// List of activities which the group inbox can handle. -#[derive(Debug, Deserialize, Serialize)] -#[serde(untagged)] -#[enum_delegate::implement(ActivityHandler)] -pub enum GroupInboxActivities { - Follow(Follow), - UndoFollow(UndoFollow), - Report(Report), - /// This is a catch-all and needs to be last - AnnouncableActivities(RawAnnouncableActivities), -} - -/// List of activities which the person inbox can handle. -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(untagged)] -#[enum_delegate::implement(ActivityHandler)] -pub enum PersonInboxActivities { - Follow(Follow), - AcceptFollow(AcceptFollow), - RejectFollow(RejectFollow), - UndoFollow(UndoFollow), - CreateOrUpdatePrivateMessage(CreateOrUpdateChatMessage), - Delete(Delete), - UndoDelete(UndoDelete), - AnnounceActivity(AnnounceActivity), - /// User can also receive some "announcable" activities, eg a comment mention. - AnnouncableActivities(AnnouncableActivities), -} - #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(untagged)] #[enum_delegate::implement(ActivityHandler)] pub enum AnnouncableActivities { - CreateOrUpdateComment(CreateOrUpdateNote), + CreateOrUpdateNoteWrapper(CreateOrUpdateNoteWrapper), CreateOrUpdatePost(CreateOrUpdatePage), Vote(Vote), UndoVote(UndoVote), @@ -111,7 +77,7 @@ impl InCommunity for AnnouncableActivities { async fn community(&self, context: &Data) -> LemmyResult { use AnnouncableActivities::*; match self { - CreateOrUpdateComment(a) => a.community(context).await, + CreateOrUpdateNoteWrapper(a) => a.community(context).await, CreateOrUpdatePost(a) => a.community(context).await, Vote(a) => a.community(context).await, UndoVote(a) => a.community(context).await, @@ -133,40 +99,32 @@ impl InCommunity for AnnouncableActivities { mod tests { use crate::{ - activity_lists::{GroupInboxActivities, PersonInboxActivities, SharedInboxActivities}, + activity_lists::SharedInboxActivities, protocol::tests::{test_json, test_parse_lemmy_item}, }; use lemmy_utils::error::LemmyResult; - #[test] - fn test_group_inbox() -> LemmyResult<()> { - test_parse_lemmy_item::("assets/lemmy/activities/following/follow.json")?; - test_parse_lemmy_item::( - "assets/lemmy/activities/create_or_update/create_note.json", - )?; - Ok(()) - } - - #[test] - fn test_person_inbox() -> LemmyResult<()> { - test_parse_lemmy_item::( - "assets/lemmy/activities/following/accept.json", - )?; - test_parse_lemmy_item::( - "assets/lemmy/activities/create_or_update/create_note.json", - )?; - test_parse_lemmy_item::( - "assets/lemmy/activities/create_or_update/create_private_message.json", - )?; - test_json::("assets/mastodon/activities/follow.json")?; - Ok(()) - } - #[test] fn test_shared_inbox() -> LemmyResult<()> { test_parse_lemmy_item::( "assets/lemmy/activities/deletion/delete_user.json", )?; + test_parse_lemmy_item::( + "assets/lemmy/activities/following/accept.json", + )?; + test_parse_lemmy_item::( + "assets/lemmy/activities/create_or_update/create_comment.json", + )?; + test_parse_lemmy_item::( + "assets/lemmy/activities/create_or_update/create_private_message.json", + )?; + test_parse_lemmy_item::( + "assets/lemmy/activities/following/follow.json", + )?; + test_parse_lemmy_item::( + "assets/lemmy/activities/create_or_update/create_comment.json", + )?; + test_json::("assets/mastodon/activities/follow.json")?; Ok(()) } } diff --git a/crates/apub/src/objects/comment.rs b/crates/apub/src/objects/comment.rs index dc0721404..2c8ed9f9d 100644 --- a/crates/apub/src/objects/comment.rs +++ b/crates/apub/src/objects/comment.rs @@ -266,7 +266,7 @@ pub(crate) mod tests { let url = Url::parse("https://enterprise.lemmy.ml/comment/38741")?; let data = prepare_comment_test(&url, &context).await?; - let json: Note = file_to_json_object("assets/lemmy/objects/note.json")?; + let json: Note = file_to_json_object("assets/lemmy/objects/comment.json")?; ApubComment::verify(&json, &url, &context).await?; let comment = ApubComment::from_json(json.clone(), &context).await?; diff --git a/crates/apub/src/objects/private_message.rs b/crates/apub/src/objects/private_message.rs index 9ada5f657..ec3e16fac 100644 --- a/crates/apub/src/objects/private_message.rs +++ b/crates/apub/src/objects/private_message.rs @@ -4,7 +4,7 @@ use crate::{ fetcher::markdown_links::markdown_rewrite_remote_links, objects::read_from_string_or_source, protocol::{ - objects::chat_message::{ChatMessage, ChatMessageType}, + objects::private_message::{PrivateMessage, PrivateMessageType}, Source, }, }; @@ -25,10 +25,11 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ source::{ + instance::Instance, local_site::LocalSite, person::Person, person_block::PersonBlock, - private_message::{PrivateMessage, PrivateMessageInsertForm}, + private_message::{PrivateMessage as DbPrivateMessage, PrivateMessageInsertForm}, }, traits::Crud, }; @@ -37,21 +38,22 @@ use lemmy_utils::{ error::{FederationError, LemmyError, LemmyErrorType, LemmyResult}, utils::markdown::markdown_to_html, }; +use semver::{Version, VersionReq}; use std::ops::Deref; use url::Url; #[derive(Clone, Debug)] -pub struct ApubPrivateMessage(pub(crate) PrivateMessage); +pub struct ApubPrivateMessage(pub(crate) DbPrivateMessage); impl Deref for ApubPrivateMessage { - type Target = PrivateMessage; + type Target = DbPrivateMessage; fn deref(&self) -> &Self::Target { &self.0 } } -impl From for ApubPrivateMessage { - fn from(pm: PrivateMessage) -> Self { +impl From for ApubPrivateMessage { + fn from(pm: DbPrivateMessage) -> Self { ApubPrivateMessage(pm) } } @@ -59,7 +61,7 @@ impl From for ApubPrivateMessage { #[async_trait::async_trait] impl Object for ApubPrivateMessage { type DataType = LemmyContext; - type Kind = ChatMessage; + type Kind = PrivateMessage; type Error = LemmyError; fn last_refreshed_at(&self) -> Option> { @@ -72,7 +74,7 @@ impl Object for ApubPrivateMessage { context: &Data, ) -> LemmyResult> { Ok( - PrivateMessage::read_from_apub_id(&mut context.pool(), object_id) + DbPrivateMessage::read_from_apub_id(&mut context.pool(), object_id) .await? .map(Into::into), ) @@ -84,15 +86,26 @@ impl Object for ApubPrivateMessage { } #[tracing::instrument(skip_all)] - async fn into_json(self, context: &Data) -> LemmyResult { + async fn into_json(self, context: &Data) -> LemmyResult { let creator_id = self.creator_id; let creator = Person::read(&mut context.pool(), creator_id).await?; let recipient_id = self.recipient_id; let recipient = Person::read(&mut context.pool(), recipient_id).await?; - let note = ChatMessage { - r#type: ChatMessageType::ChatMessage, + let instance = Instance::read(&mut context.pool(), recipient.instance_id).await?; + let mut kind = PrivateMessageType::Note; + + // Deprecated: For Lemmy versions before 0.20, send private messages with old type + if let (Some(software), Some(version)) = (instance.software, &instance.version) { + let req = VersionReq::parse("<0.20")?; + if software == "lemmy" && req.matches(&Version::parse(version)?) { + kind = PrivateMessageType::ChatMessage + } + } + + let note = PrivateMessage { + kind, id: self.ap_id.clone().into(), attributed_to: creator.actor_id.into(), to: [recipient.actor_id.into()], @@ -107,7 +120,7 @@ impl Object for ApubPrivateMessage { #[tracing::instrument(skip_all)] async fn verify( - note: &ChatMessage, + note: &PrivateMessage, expected_domain: &Url, context: &Data, ) -> LemmyResult<()> { @@ -128,7 +141,7 @@ impl Object for ApubPrivateMessage { #[tracing::instrument(skip_all)] async fn from_json( - note: ChatMessage, + note: PrivateMessage, context: &Data, ) -> LemmyResult { let creator = note.attributed_to.dereference(context).await?; @@ -161,7 +174,7 @@ impl Object for ApubPrivateMessage { local: Some(false), }; let timestamp = note.updated.or(note.published).unwrap_or_else(Utc::now); - let pm = PrivateMessage::insert_apub(&mut context.pool(), timestamp, &form).await?; + let pm = DbPrivateMessage::insert_apub(&mut context.pool(), timestamp, &form).await?; Ok(pm.into()) } } @@ -213,7 +226,7 @@ mod tests { let context = LemmyContext::init_test_context().await; let url = Url::parse("https://enterprise.lemmy.ml/private_message/1621")?; let data = prepare_comment_test(&url, &context).await?; - let json: ChatMessage = file_to_json_object("assets/lemmy/objects/chat_message.json")?; + let json: PrivateMessage = file_to_json_object("assets/lemmy/objects/private_message.json")?; ApubPrivateMessage::verify(&json, &url, &context).await?; let pm = ApubPrivateMessage::from_json(json.clone(), &context).await?; @@ -225,7 +238,7 @@ mod tests { let to_apub = pm.into_json(&context).await?; assert_json_include!(actual: json, expected: to_apub); - PrivateMessage::delete(&mut context.pool(), pm_id).await?; + DbPrivateMessage::delete(&mut context.pool(), pm_id).await?; cleanup(data, &context).await?; Ok(()) } @@ -245,7 +258,7 @@ mod tests { assert_eq!(pm.content.len(), 3); assert_eq!(context.request_count(), 0); - PrivateMessage::delete(&mut context.pool(), pm.id).await?; + DbPrivateMessage::delete(&mut context.pool(), pm.id).await?; cleanup(data, &context).await?; Ok(()) } diff --git a/crates/apub/src/protocol/activities/create_or_update/mod.rs b/crates/apub/src/protocol/activities/create_or_update/mod.rs index 3d9dbbb1d..d34be93c8 100644 --- a/crates/apub/src/protocol/activities/create_or_update/mod.rs +++ b/crates/apub/src/protocol/activities/create_or_update/mod.rs @@ -1,14 +1,16 @@ -pub mod chat_message; pub mod note; +pub(crate) mod note_wrapper; pub mod page; +pub mod private_message; #[cfg(test)] mod tests { + use super::note_wrapper::CreateOrUpdateNoteWrapper; use crate::protocol::{ activities::create_or_update::{ - chat_message::CreateOrUpdateChatMessage, note::CreateOrUpdateNote, page::CreateOrUpdatePage, + private_message::CreateOrUpdatePrivateMessage, }, tests::test_parse_lemmy_item, }; @@ -23,9 +25,15 @@ mod tests { "assets/lemmy/activities/create_or_update/update_page.json", )?; test_parse_lemmy_item::( - "assets/lemmy/activities/create_or_update/create_note.json", + "assets/lemmy/activities/create_or_update/create_comment.json", )?; - test_parse_lemmy_item::( + test_parse_lemmy_item::( + "assets/lemmy/activities/create_or_update/create_private_message.json", + )?; + test_parse_lemmy_item::( + "assets/lemmy/activities/create_or_update/create_comment.json", + )?; + test_parse_lemmy_item::( "assets/lemmy/activities/create_or_update/create_private_message.json", )?; Ok(()) diff --git a/crates/apub/src/protocol/activities/create_or_update/note_wrapper.rs b/crates/apub/src/protocol/activities/create_or_update/note_wrapper.rs new file mode 100644 index 000000000..242bfe519 --- /dev/null +++ b/crates/apub/src/protocol/activities/create_or_update/note_wrapper.rs @@ -0,0 +1,26 @@ +use activitypub_federation::kinds::object::NoteType; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; +use url::Url; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateOrUpdateNoteWrapper { + pub(crate) object: NoteWrapper, + pub(crate) id: Url, + #[serde(default)] + pub(crate) to: Vec, + #[serde(default)] + pub(crate) cc: Vec, + pub(crate) actor: Url, + #[serde(flatten)] + other: Map, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct NoteWrapper { + pub(crate) r#type: NoteType, + #[serde(flatten)] + other: Map, +} diff --git a/crates/apub/src/protocol/activities/create_or_update/chat_message.rs b/crates/apub/src/protocol/activities/create_or_update/private_message.rs similarity index 75% rename from crates/apub/src/protocol/activities/create_or_update/chat_message.rs rename to crates/apub/src/protocol/activities/create_or_update/private_message.rs index 30e94a28d..0c08f3991 100644 --- a/crates/apub/src/protocol/activities/create_or_update/chat_message.rs +++ b/crates/apub/src/protocol/activities/create_or_update/private_message.rs @@ -1,6 +1,6 @@ use crate::{ objects::person::ApubPerson, - protocol::{activities::CreateOrUpdateType, objects::chat_message::ChatMessage}, + protocol::{activities::CreateOrUpdateType, objects::private_message::PrivateMessage}, }; use activitypub_federation::{fetch::object_id::ObjectId, protocol::helpers::deserialize_one}; use serde::{Deserialize, Serialize}; @@ -8,12 +8,12 @@ use url::Url; #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] -pub struct CreateOrUpdateChatMessage { +pub struct CreateOrUpdatePrivateMessage { pub(crate) id: Url, pub(crate) actor: ObjectId, #[serde(deserialize_with = "deserialize_one")] pub(crate) to: [ObjectId; 1], - pub(crate) object: ChatMessage, + pub(crate) object: PrivateMessage, #[serde(rename = "type")] pub(crate) kind: CreateOrUpdateType, } diff --git a/crates/apub/src/protocol/objects/mod.rs b/crates/apub/src/protocol/objects/mod.rs index 00fe26d2b..acc8c14dd 100644 --- a/crates/apub/src/protocol/objects/mod.rs +++ b/crates/apub/src/protocol/objects/mod.rs @@ -8,12 +8,12 @@ use lemmy_utils::error::LemmyResult; use serde::{Deserialize, Serialize}; use url::Url; -pub(crate) mod chat_message; pub(crate) mod group; pub(crate) mod instance; pub(crate) mod note; pub(crate) mod page; pub(crate) mod person; +pub(crate) mod private_message; pub(crate) mod tombstone; #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] @@ -102,13 +102,14 @@ impl LanguageTag { #[cfg(test)] mod tests { use crate::protocol::{ + activities::create_or_update::note_wrapper::NoteWrapper, objects::{ - chat_message::ChatMessage, group::Group, instance::Instance, note::Note, page::Page, person::Person, + private_message::PrivateMessage, tombstone::Tombstone, }, tests::{test_json, test_parse_lemmy_item}, @@ -121,8 +122,10 @@ mod tests { test_parse_lemmy_item::("assets/lemmy/objects/group.json")?; test_parse_lemmy_item::("assets/lemmy/objects/person.json")?; test_parse_lemmy_item::("assets/lemmy/objects/page.json")?; - test_parse_lemmy_item::("assets/lemmy/objects/note.json")?; - test_parse_lemmy_item::("assets/lemmy/objects/chat_message.json")?; + test_parse_lemmy_item::("assets/lemmy/objects/comment.json")?; + test_parse_lemmy_item::("assets/lemmy/objects/private_message.json")?; + test_parse_lemmy_item::("assets/lemmy/objects/comment.json")?; + test_parse_lemmy_item::("assets/lemmy/objects/private_message.json")?; test_parse_lemmy_item::("assets/lemmy/objects/tombstone.json")?; Ok(()) } @@ -131,7 +134,6 @@ mod tests { fn test_parse_objects_pleroma() -> LemmyResult<()> { test_json::("assets/pleroma/objects/person.json")?; test_json::("assets/pleroma/objects/note.json")?; - test_json::("assets/pleroma/objects/chat_message.json")?; Ok(()) } diff --git a/crates/apub/src/protocol/objects/chat_message.rs b/crates/apub/src/protocol/objects/private_message.rs similarity index 78% rename from crates/apub/src/protocol/objects/chat_message.rs rename to crates/apub/src/protocol/objects/private_message.rs index 8cb83e664..93b9ba39c 100644 --- a/crates/apub/src/protocol/objects/chat_message.rs +++ b/crates/apub/src/protocol/objects/private_message.rs @@ -16,8 +16,9 @@ use serde_with::skip_serializing_none; #[skip_serializing_none] #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] -pub struct ChatMessage { - pub(crate) r#type: ChatMessageType, +pub struct PrivateMessage { + #[serde(rename = "type")] + pub(crate) kind: PrivateMessageType, pub(crate) id: ObjectId, pub(crate) attributed_to: ObjectId, #[serde(deserialize_with = "deserialize_one")] @@ -31,8 +32,10 @@ pub struct ChatMessage { pub(crate) updated: Option>, } -/// https://docs.pleroma.social/backend/development/ap_extensions/#chatmessages #[derive(Clone, Debug, Deserialize, Serialize)] -pub enum ChatMessageType { +pub enum PrivateMessageType { + /// Deprecated, for compatibility with Lemmy 0.19 and earlier + /// https://docs.pleroma.social/backend/development/ap_extensions/#chatmessages ChatMessage, + Note, } diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index ca1aa1a21..a5e4400f2 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -668,6 +668,7 @@ diesel::table! { enabled -> Bool, published -> Timestamptz, updated -> Nullable, + use_pkce -> Bool, } } diff --git a/crates/db_schema/src/source/oauth_provider.rs b/crates/db_schema/src/source/oauth_provider.rs index a70405a5e..0a82ab9a9 100644 --- a/crates/db_schema/src/source/oauth_provider.rs +++ b/crates/db_schema/src/source/oauth_provider.rs @@ -62,6 +62,8 @@ pub struct OAuthProvider { pub published: DateTime, #[cfg_attr(feature = "full", ts(optional))] pub updated: Option>, + /// switch to enable or disable PKCE + pub use_pkce: bool, } #[derive(Clone, PartialEq, Eq, Debug, Deserialize)] @@ -83,6 +85,7 @@ impl Serialize for PublicOAuthProvider { state.serialize_field("authorization_endpoint", &self.0.authorization_endpoint)?; state.serialize_field("client_id", &self.0.client_id)?; state.serialize_field("scopes", &self.0.scopes)?; + state.serialize_field("use_pkce", &self.0.use_pkce)?; state.end() } } @@ -102,6 +105,7 @@ pub struct OAuthProviderInsertForm { pub scopes: String, pub auto_verify_email: Option, pub account_linking_enabled: Option, + pub use_pkce: Option, pub enabled: Option, } @@ -118,6 +122,7 @@ pub struct OAuthProviderUpdateForm { pub scopes: Option, pub auto_verify_email: Option, pub account_linking_enabled: Option, + pub use_pkce: Option, pub enabled: Option, pub updated: Option>>, } diff --git a/crates/db_views/src/comment_view.rs b/crates/db_views/src/comment_view.rs index 1037cf6ff..2cf751f9f 100644 --- a/crates/db_views/src/comment_view.rs +++ b/crates/db_views/src/comment_view.rs @@ -316,17 +316,14 @@ impl CommentView { comment_id: CommentId, my_local_user: Option<&'_ LocalUser>, ) -> Result { + let is_admin = my_local_user.map(|u| u.admin).unwrap_or(false); // If a person is given, then my_vote (res.9), if None, should be 0, not null // Necessary to differentiate between other person's votes - let res = queries().read(pool, (comment_id, my_local_user)).await?; - let mut new_view = res.clone(); + let mut res = queries().read(pool, (comment_id, my_local_user)).await?; if my_local_user.is_some() && res.my_vote.is_none() { - new_view.my_vote = Some(0); + res.my_vote = Some(0); } - if res.comment.deleted || res.comment.removed { - new_view.comment.content = String::new(); - } - Ok(new_view) + Ok(handle_deleted(res, is_admin)) } } @@ -350,22 +347,25 @@ pub struct CommentQuery<'a> { impl CommentQuery<'_> { pub async fn list(self, site: &Site, pool: &mut DbPool<'_>) -> Result, Error> { + let is_admin = self.local_user.map(|u| u.admin).unwrap_or(false); Ok( queries() .list(pool, (self, site)) .await? .into_iter() - .map(|mut c| { - if c.comment.deleted || c.comment.removed { - c.comment.content = String::new(); - } - c - }) + .map(|c| handle_deleted(c, is_admin)) .collect(), ) } } +fn handle_deleted(mut c: CommentView, is_admin: bool) -> CommentView { + if !is_admin && (c.comment.deleted || c.comment.removed) { + c.comment.content = String::new(); + } + c +} + #[cfg(test)] #[expect(clippy::indexing_slicing)] mod tests { @@ -1301,4 +1301,65 @@ mod tests { cleanup(data, pool).await } + + #[tokio::test] + #[serial] + async fn comment_removed() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let mut data = init_data(pool).await?; + + // Mark a comment as removed + let form = CommentUpdateForm { + removed: Some(true), + ..Default::default() + }; + Comment::update(pool, data.inserted_comment_0.id, &form).await?; + + // Read as normal user, content is cleared + data.timmy_local_user_view.local_user.admin = false; + let comment_view = CommentView::read( + pool, + data.inserted_comment_0.id, + Some(&data.timmy_local_user_view.local_user), + ) + .await?; + assert_eq!("", comment_view.comment.content); + let comment_listing = CommentQuery { + community_id: Some(data.inserted_community.id), + local_user: Some(&data.timmy_local_user_view.local_user), + sort: Some(CommentSortType::Old), + ..Default::default() + } + .list(&data.site, pool) + .await?; + assert_eq!("", comment_listing[0].comment.content); + + // Read as admin, content is returned + data.timmy_local_user_view.local_user.admin = true; + let comment_view = CommentView::read( + pool, + data.inserted_comment_0.id, + Some(&data.timmy_local_user_view.local_user), + ) + .await?; + assert_eq!( + data.inserted_comment_0.content, + comment_view.comment.content + ); + let comment_listing = CommentQuery { + community_id: Some(data.inserted_community.id), + local_user: Some(&data.timmy_local_user_view.local_user), + sort: Some(CommentSortType::Old), + ..Default::default() + } + .list(&data.site, pool) + .await?; + assert_eq!( + data.inserted_comment_0.content, + comment_listing[0].comment.content + ); + + cleanup(data, pool).await + } } diff --git a/crates/utils/src/error.rs b/crates/utils/src/error.rs index 40f878747..f45bc271f 100644 --- a/crates/utils/src/error.rs +++ b/crates/utils/src/error.rs @@ -76,6 +76,7 @@ pub enum LemmyErrorType { InvalidEmailAddress(String), RateLimitError, InvalidName, + InvalidCodeVerifier, InvalidDisplayName, InvalidMatrixId, InvalidPostTitle, diff --git a/migrations/2024-11-23-234637_oauth_pkce/down.sql b/migrations/2024-11-23-234637_oauth_pkce/down.sql new file mode 100644 index 000000000..50c09050a --- /dev/null +++ b/migrations/2024-11-23-234637_oauth_pkce/down.sql @@ -0,0 +1,3 @@ +ALTER TABLE oauth_provider + DROP COLUMN use_pkce; + diff --git a/migrations/2024-11-23-234637_oauth_pkce/up.sql b/migrations/2024-11-23-234637_oauth_pkce/up.sql new file mode 100644 index 000000000..b03d74f7f --- /dev/null +++ b/migrations/2024-11-23-234637_oauth_pkce/up.sql @@ -0,0 +1,3 @@ +ALTER TABLE oauth_provider + ADD COLUMN use_pkce boolean DEFAULT FALSE NOT NULL; +