From e35e84228db9dc29906647c1d30dd90749f6cc2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sun, 1 Sep 2024 11:26:01 +0200 Subject: [PATCH 01/25] Change scrobble external link param name to use snake case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- changelog.d/scrobbles.change | 1 + docs/development/API/pleroma_api.md | 1 + .../operations/pleroma_scrobble_operation.ex | 12 ++++--- lib/pleroma/web/common_api/activity_draft.ex | 3 +- .../controllers/scrobble_controller.ex | 4 +++ .../web/pleroma_api/views/scrobble_view.ex | 6 ++-- .../controllers/scrobble_controller_test.exs | 33 ++++++++++++++++--- 7 files changed, 48 insertions(+), 12 deletions(-) create mode 100644 changelog.d/scrobbles.change diff --git a/changelog.d/scrobbles.change b/changelog.d/scrobbles.change new file mode 100644 index 0000000000..ed1777b2d4 --- /dev/null +++ b/changelog.d/scrobbles.change @@ -0,0 +1 @@ +Change scrobble external link param name to use snake case \ No newline at end of file diff --git a/docs/development/API/pleroma_api.md b/docs/development/API/pleroma_api.md index 000d7d27d9..b17f61cbbb 100644 --- a/docs/development/API/pleroma_api.md +++ b/docs/development/API/pleroma_api.md @@ -671,6 +671,7 @@ Audio scrobbling in Pleroma is **deprecated**. "artist": "Some Artist", "album": "Some Album", "length": 180000, + "external_link": "https://www.last.fm/music/Some+Artist/_/Some+Title", "created_at": "2019-09-28T12:40:45.000Z" } ] diff --git a/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex index f595583b6b..6f77584a86 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex @@ -59,11 +59,15 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do album: %Schema{type: :string, description: "The album of the media playing"}, artist: %Schema{type: :string, description: "The artist of the media playing"}, length: %Schema{type: :integer, description: "The length of the media playing"}, - externalLink: %Schema{type: :string, description: "A URL referencing the media playing"}, + external_link: %Schema{type: :string, description: "A URL referencing the media playing"}, visibility: %Schema{ allOf: [VisibilityScope], default: "public", description: "Scrobble visibility" + }, + externalLink: %Schema{ + type: :string, + description: "Deprecated, use `external_link` instead" } }, example: %{ @@ -71,7 +75,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do "artist" => "Some Artist", "album" => "Some Album", "length" => 180_000, - "externalLink" => "https://www.last.fm/music/Some+Artist/_/Some+Title" + "external_link" => "https://www.last.fm/music/Some+Artist/_/Some+Title" } } end @@ -85,7 +89,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do title: %Schema{type: :string, description: "The title of the media playing"}, album: %Schema{type: :string, description: "The album of the media playing"}, artist: %Schema{type: :string, description: "The artist of the media playing"}, - externalLink: %Schema{type: :string, description: "A URL referencing the media playing"}, + external_link: %Schema{type: :string, description: "A URL referencing the media playing"}, length: %Schema{ type: :integer, description: "The length of the media playing", @@ -100,7 +104,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do "artist" => "Some Artist", "album" => "Some Album", "length" => 180_000, - "externalLink" => "https://www.last.fm/music/Some+Artist/_/Some+Title", + "external_link" => "https://www.last.fm/music/Some+Artist/_/Some+Title", "created_at" => "2019-09-28T12:40:45.000Z" } } diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index 8aa1e258d4..0268d3f48d 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -85,7 +85,8 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do defp listen_object(draft) do object = draft.params - |> Map.take([:album, :artist, :title, :length, :externalLink]) + |> Map.take([:album, :artist, :title, :length]) + |> Map.put(:externalLink, Map.get(draft.params, :external_link)) |> Map.new(fn {key, value} -> {to_string(key), value} end) |> Map.put("type", "Audio") |> Map.put("to", draft.to) diff --git a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex index bf6dc500c0..5f5f7643f7 100644 --- a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex @@ -24,6 +24,10 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleController do defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaScrobbleOperation def create(%{assigns: %{user: user}, body_params: params} = conn, _) do + params = + params + |> Map.put_new(:external_link, Map.get(params, :externalLink)) + with {:ok, activity} <- CommonAPI.listen(user, params) do render(conn, "show.json", activity: activity, for: user) else diff --git a/lib/pleroma/web/pleroma_api/views/scrobble_view.ex b/lib/pleroma/web/pleroma_api/views/scrobble_view.ex index edf0a23903..51828ad975 100644 --- a/lib/pleroma/web/pleroma_api/views/scrobble_view.ex +++ b/lib/pleroma/web/pleroma_api/views/scrobble_view.ex @@ -27,8 +27,10 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleView do title: object.data["title"] |> HTML.strip_tags(), artist: object.data["artist"] |> HTML.strip_tags(), album: object.data["album"] |> HTML.strip_tags(), - externalLink: object.data["externalLink"], - length: object.data["length"] + external_link: object.data["externalLink"], + length: object.data["length"], + # DEPRECATED + externalLink: object.data["externalLink"] } end diff --git a/test/pleroma/web/pleroma_api/controllers/scrobble_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/scrobble_controller_test.exs index be94a02ade..bcc25b83e5 100644 --- a/test/pleroma/web/pleroma_api/controllers/scrobble_controller_test.exs +++ b/test/pleroma/web/pleroma_api/controllers/scrobble_controller_test.exs @@ -19,10 +19,33 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleControllerTest do "artist" => "lain", "album" => "lain radio", "length" => "180000", - "externalLink" => "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+1" + "external_link" => "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+1" }) - assert %{"title" => "lain radio episode 1"} = json_response_and_validate_schema(conn, 200) + assert %{ + "title" => "lain radio episode 1", + "external_link" => "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+1" + } = json_response_and_validate_schema(conn, 200) + end + + test "external_link fallback" do + %{conn: conn} = oauth_access(["write"]) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/scrobble", %{ + "title" => "lain radio episode 2", + "artist" => "lain", + "album" => "lain radio", + "length" => "180000", + "externalLink" => "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+2" + }) + + assert %{ + "title" => "lain radio episode 2", + "external_link" => "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+2" + } = json_response_and_validate_schema(conn, 200) end end @@ -35,7 +58,7 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleControllerTest do title: "lain radio episode 1", artist: "lain", album: "lain radio", - externalLink: "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+1" + external_link: "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+1" }) {:ok, _activity} = @@ -43,7 +66,7 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleControllerTest do title: "lain radio episode 2", artist: "lain", album: "lain radio", - externalLink: "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+2" + external_link: "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+2" }) {:ok, _activity} = @@ -51,7 +74,7 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleControllerTest do title: "lain radio episode 3", artist: "lain", album: "lain radio", - externalLink: "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+3" + external_link: "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+3" }) conn = get(conn, "/api/v1/pleroma/accounts/#{user.id}/scrobbles") From 7b69e525643da749afbe4f6fa0bd59cbd6dcc923 Mon Sep 17 00:00:00 2001 From: tusooa Date: Sun, 23 Feb 2025 21:12:08 -0500 Subject: [PATCH 02/25] Fix AssignAppUser migration OOM --- changelog.d/assign-app-user-oom.fix | 1 + .../20240904142434_assign_app_user.exs | 18 +++++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) create mode 100644 changelog.d/assign-app-user-oom.fix diff --git a/changelog.d/assign-app-user-oom.fix b/changelog.d/assign-app-user-oom.fix new file mode 100644 index 0000000000..ac1de71597 --- /dev/null +++ b/changelog.d/assign-app-user-oom.fix @@ -0,0 +1 @@ +Fix AssignAppUser migration OOM diff --git a/priv/repo/migrations/20240904142434_assign_app_user.exs b/priv/repo/migrations/20240904142434_assign_app_user.exs index 11bec529bf..74740220d6 100644 --- a/priv/repo/migrations/20240904142434_assign_app_user.exs +++ b/priv/repo/migrations/20240904142434_assign_app_user.exs @@ -1,20 +1,24 @@ defmodule Pleroma.Repo.Migrations.AssignAppUser do use Ecto.Migration + import Ecto.Query + alias Pleroma.Repo alias Pleroma.Web.OAuth.App alias Pleroma.Web.OAuth.Token def up do - Repo.all(Token) - |> Enum.group_by(fn x -> Map.get(x, :app_id) end) - |> Enum.each(fn {_app_id, tokens} -> - token = - Enum.filter(tokens, fn x -> not is_nil(x.user_id) end) - |> List.first() - + Token + |> where([t], not is_nil(t.user_id)) + |> group_by([t], t.app_id) + |> select([t], %{app_id: t.app_id, id: min(t.id)}) + |> order_by(asc: :app_id) + |> Repo.stream() + |> Stream.each(fn %{id: id} -> + token = Token.Query.get_by_id(id) |> Repo.one() App.maybe_update_owner(token) end) + |> Stream.run() end def down, do: :ok From 890ac8ff86e28af464f56fc023d9d7e2f4bc2f1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sun, 30 Apr 2023 17:33:11 +0200 Subject: [PATCH 03/25] Expose markup configuration in InstanceView MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Nicole Mikołajczyk --- lib/pleroma/web/mastodon_api/views/instance_view.ex | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index 4b0480f66e..af6a63e92a 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -270,7 +270,8 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do post_formats: Config.get([:instance, :allowed_post_formats]), birthday_required: Config.get([:instance, :birthday_required]), birthday_min_age: Config.get([:instance, :birthday_min_age]), - translation: supported_languages() + translation: supported_languages(), + markup: markup() }, stats: %{mau: Pleroma.User.active_user_count()}, vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) @@ -321,4 +322,12 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do target_languages: target_languages } end + + defp markup() do + %{ + allow_inline_images: Config.get([:markup, :allow_inline_images]), + allow_headings: Config.get([:markup, :allow_headings]), + allow_tables: Config.get([:markup, :allow_tables]) + } + end end From d1b9d03302b4f8ea83174d0934f71360709d7585 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicole=20Miko=C5=82ajczyk?= Date: Fri, 28 Mar 2025 17:00:36 +0100 Subject: [PATCH 04/25] update changelog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Nicole Mikołajczyk --- changelog.d/expose-markup-configuration.add | 1 + lib/pleroma/web/mastodon_api/views/instance_view.ex | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/expose-markup-configuration.add diff --git a/changelog.d/expose-markup-configuration.add b/changelog.d/expose-markup-configuration.add new file mode 100644 index 0000000000..8c7f356976 --- /dev/null +++ b/changelog.d/expose-markup-configuration.add @@ -0,0 +1 @@ +Expose markup configuration in InstanceView diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index af6a63e92a..848bf1a225 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -323,7 +323,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do } end - defp markup() do + defp markup do %{ allow_inline_images: Config.get([:markup, :allow_inline_images]), allow_headings: Config.get([:markup, :allow_headings]), From ded40182b0aa6848b55febe73ec7e41eace1e0f6 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 5 May 2025 15:28:02 +0400 Subject: [PATCH 05/25] Public getting stripped from unlisted activity CC: Add possible tests --- test/fixtures/poast_unlisted.json | 65 +++++++++++++++++++ .../transmogrifier/note_handling_test.exs | 31 +++++++++ .../web/activity_pub/transmogrifier_test.exs | 37 +++++++++++ 3 files changed, 133 insertions(+) create mode 100644 test/fixtures/poast_unlisted.json diff --git a/test/fixtures/poast_unlisted.json b/test/fixtures/poast_unlisted.json new file mode 100644 index 0000000000..fa23153ba7 --- /dev/null +++ b/test/fixtures/poast_unlisted.json @@ -0,0 +1,65 @@ +{ + "@context" : [ + "https://www.w3.org/ns/activitystreams", + "https://poa.st/schemas/litepub-0.1.jsonld", + { + "@language" : "und" + } + ], + "actor" : "https://poa.st/users/TrevorGoodchild", + "attachment" : [], + "attributedTo" : "https://poa.st/users/TrevorGoodchild", + "cc" : [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "context" : "https://poa.st/contexts/c6d125f1-4e7f-43bd-aa31-33de4d90d049", + "conversation" : "https://poa.st/contexts/c6d125f1-4e7f-43bd-aa31-33de4d90d049", + "directMessage" : false, + "id" : "https://poa.st/activities/bbd3347a-4a89-4cdb-bf86-4f9eed9506e3", + "object" : { + "actor" : "https://poa.st/users/TrevorGoodchild", + "attachment" : [], + "attributedTo" : "https://poa.st/users/TrevorGoodchild", + "cc" : [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "content" : "@HoroTheWhiteWolf >please let this be his zero fucks given final statement before he joins the 52%+ tranny club", + "context" : "https://poa.st/contexts/c6d125f1-4e7f-43bd-aa31-33de4d90d049", + "conversation" : "https://poa.st/contexts/c6d125f1-4e7f-43bd-aa31-33de4d90d049", + "id" : "https://poa.st/objects/7eb785d5-a556-4070-9091-f4afb226466c", + "inReplyTo" : "https://poa.st/objects/71995b41-cfb2-48ce-abce-76d570d54edc", + "published" : "2025-05-03T23:54:07.489885Z", + "repliesCount" : 2, + "sensitive" : false, + "source" : { + "content" : ">please let this be his zero fucks given final statement before he joins the 52%+ tranny club", + "mediaType" : "text/plain" + }, + "summary" : "", + "tag" : [ + { + "href" : "https://poa.st/users/HoroTheWhiteWolf", + "name" : "@HoroTheWhiteWolf", + "type" : "Mention" + } + ], + "to" : [ + "https://poa.st/users/HoroTheWhiteWolf", + "https://poa.st/users/TrevorGoodchild/followers" + ], + "type" : "Note" + }, + "published" : "2025-05-03T23:54:07.489837Z", + "tag" : [ + { + "href" : "https://poa.st/users/HoroTheWhiteWolf", + "name" : "@HoroTheWhiteWolf", + "type" : "Mention" + } + ], + "to" : [ + "https://poa.st/users/HoroTheWhiteWolf", + "https://poa.st/users/TrevorGoodchild/followers" + ], + "type" : "Create" +} diff --git a/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs index fd7a3c772d..13982940a7 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs @@ -786,4 +786,35 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do assert object.data["context"] == object.data["inReplyTo"] assert modified.data["context"] == object.data["inReplyTo"] end + + test "it keeps the public address in cc in the activity when it is present" do + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Jason.decode!() + + object = + data["object"] + |> Map.put("cc", ["https://www.w3.org/ns/activitystreams#Public"]) + |> Map.put("to", []) + + data = + data + |> Map.put("object", object) + |> Map.put("cc", ["https://www.w3.org/ns/activitystreams#Public"]) + |> Map.put("to", []) + + {:ok, %Activity{} = modified} = Transmogrifier.handle_incoming(data) + assert modified.data["cc"] == ["https://www.w3.org/ns/activitystreams#Public"] + end + + test "it tries it with the real poast_unlisted.json, ensuring that public is in the cc" do + data = + File.read!("test/fixtures/poast_unlisted.json") + |> Jason.decode!() + + _user = insert(:user, ap_id: data["actor"]) + + {:ok, %Activity{} = modified} = Transmogrifier.handle_incoming(data) + assert modified.data["cc"] == ["https://www.w3.org/ns/activitystreams#Public"] + end end diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs index e0395d7bbc..ef6e004f1d 100644 --- a/test/pleroma/web/activity_pub/transmogrifier_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs @@ -757,6 +757,43 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do refute recipient.follower_address in fixed_object["cc"] refute recipient.follower_address in fixed_object["to"] end + + test "preserves public URL in cc even when not explicitly mentioned", %{user: user} do + public_url = "https://www.w3.org/ns/activitystreams#Public" + + # Case 1: Public URL in cc but no mentions + object = %{ + "actor" => user.ap_id, + "to" => ["https://social.beepboop.ga/users/dirb"], + "cc" => [public_url], + "tag" => [] + } + + fixed_object = Transmogrifier.fix_explicit_addressing(object, user.follower_address) + assert public_url in fixed_object["cc"] + + # Case 2: Public URL in cc, with mentions but public not in to + object = %{ + "actor" => user.ap_id, + "to" => ["https://pleroma.gold/users/user1"], + "cc" => [public_url], + "tag" => [%{"type" => "Mention", "href" => "https://pleroma.gold/users/user1"}] + } + + fixed_object = Transmogrifier.fix_explicit_addressing(object, user.follower_address) + assert public_url in fixed_object["cc"] + + # Case 3: Public URL in to, it should be moved to to + object = %{ + "actor" => user.ap_id, + "to" => [public_url], + "cc" => [], + "tag" => [] + } + + fixed_object = Transmogrifier.fix_explicit_addressing(object, user.follower_address) + assert public_url in fixed_object["to"] + end end describe "fix_summary/1" do From ae2c97fad8121a26146643c7e4a361d8f4d289a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 4 Jun 2025 21:32:30 +0200 Subject: [PATCH 06/25] Use JSON for DeepL API requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/deepl-json.fix | 1 + lib/pleroma/language/translation/deepl.ex | 18 ++++++++---------- 2 files changed, 9 insertions(+), 10 deletions(-) create mode 100644 changelog.d/deepl-json.fix diff --git a/changelog.d/deepl-json.fix b/changelog.d/deepl-json.fix new file mode 100644 index 0000000000..ee6f8664ed --- /dev/null +++ b/changelog.d/deepl-json.fix @@ -0,0 +1 @@ +Use JSON for DeepL API requests diff --git a/lib/pleroma/language/translation/deepl.ex b/lib/pleroma/language/translation/deepl.ex index e027035b4d..aaaac9b0fb 100644 --- a/lib/pleroma/language/translation/deepl.ex +++ b/lib/pleroma/language/translation/deepl.ex @@ -24,17 +24,15 @@ defmodule Pleroma.Language.Translation.Deepl do |> URI.to_string() case Pleroma.HTTP.post( - endpoint <> - "?" <> - URI.encode_query(%{ - text: content, - source_lang: source_language |> String.upcase(), - target_lang: target_language, - tag_handling: "html" - }), - "", + endpoint, + Jason.encode!(%{ + text: [content], + source_lang: source_language |> String.upcase(), + target_lang: target_language, + tag_handling: "html" + }), [ - {"Content-Type", "application/x-www-form-urlencoded"}, + {"Content-Type", "application/json"}, {"Authorization", "DeepL-Auth-Key #{api_key()}"} ] ) do From a817f1800ed335ed5ef2353adce3235bfb0e44c3 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 5 Jun 2025 16:40:52 +0200 Subject: [PATCH 07/25] Remove forgotten Pleroma.OTPVersion usage in mix.exs This was used in OTP releases where the normal OTP_VERSION file is unavailable. If checking against OTP minor versions and patch levels is needed again, revert this commit and commit mentioned below. Context: 1be8deda73add2dde23127be1f4da802dcb25b45 --- changelog.d/remove-forgotten-OTPVersion-usage.skip | 0 mix.exs | 11 +---------- 2 files changed, 1 insertion(+), 10 deletions(-) create mode 100644 changelog.d/remove-forgotten-OTPVersion-usage.skip diff --git a/changelog.d/remove-forgotten-OTPVersion-usage.skip b/changelog.d/remove-forgotten-OTPVersion-usage.skip new file mode 100644 index 0000000000..e69de29bb2 diff --git a/mix.exs b/mix.exs index e34ee0cbc9..971084f942 100644 --- a/mix.exs +++ b/mix.exs @@ -37,22 +37,13 @@ defmodule Pleroma.Mixfile do pleroma: [ include_executables_for: [:unix], applications: [ex_syslogger: :load, syslog: :load, eldap: :transient], - steps: [:assemble, &put_otp_version/1, ©_files/1, ©_nginx_config/1], + steps: [:assemble, ©_files/1, ©_nginx_config/1], config_providers: [{Pleroma.Config.ReleaseRuntimeProvider, []}] ] ] ] end - def put_otp_version(%{path: target_path} = release) do - File.write!( - Path.join([target_path, "OTP_VERSION"]), - Pleroma.OTPVersion.version() - ) - - release - end - def copy_files(%{path: target_path} = release) do File.cp_r!("./rel/files", target_path) release From 8ae4ed0807151f3a1c364c9e7da608cda2387178 Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Thu, 5 Jun 2025 22:12:06 +0300 Subject: [PATCH 08/25] Make the opts in ActivityPub.Builder.block optional --- lib/pleroma/web/activity_pub/builder.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index ecb6df1f01..0463160242 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -328,7 +328,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do end @spec block(User.t(), User.t(), map()) :: {:ok, map(), keyword()} - def block(blocker, blocked, params) do + def block(blocker, blocked, params \\ %{}) do {:ok, %{ "id" => Utils.generate_activity_id(), From a361b84fc97041f69c890f09de9227f51ac905f4 Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Wed, 11 Jun 2025 23:02:42 +0300 Subject: [PATCH 09/25] Relax alsoKnownAs requirements to just being a URI --- changelog.d/relax-also-known-as.change | 1 + lib/pleroma/user.ex | 2 +- .../web/mastodon_api/controllers/search_controller.ex | 2 +- test/pleroma/user_test.exs | 9 +++++++++ 4 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 changelog.d/relax-also-known-as.change diff --git a/changelog.d/relax-also-known-as.change b/changelog.d/relax-also-known-as.change new file mode 100644 index 0000000000..800c3e72a3 --- /dev/null +++ b/changelog.d/relax-also-known-as.change @@ -0,0 +1 @@ +Relax alsoKnownAs requirements to just URI, not necessarily HTTP(S) \ No newline at end of file diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 427f7878d8..84551afd5a 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -150,7 +150,7 @@ defmodule Pleroma.User do field(:allow_following_move, :boolean, default: true) field(:skip_thread_containment, :boolean, default: false) field(:actor_type, :string, default: "Person") - field(:also_known_as, {:array, ObjectValidators.ObjectID}, default: []) + field(:also_known_as, {:array, ObjectValidators.BareUri}, default: []) field(:inbox, :string) field(:shared_inbox, :string) field(:accepts_chat_messages, :boolean, default: nil) diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex index 628aa311b5..d9a1ba41ea 100644 --- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -190,7 +190,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do f.() rescue error -> - Logger.error("#{__MODULE__} search error: #{inspect(error)}") + Logger.error(Exception.format(:error, error, __STACKTRACE__)) fallback end end diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs index 44e2d0d65d..0b4dc9197e 100644 --- a/test/pleroma/user_test.exs +++ b/test/pleroma/user_test.exs @@ -2792,6 +2792,15 @@ defmodule Pleroma.UserTest do assert user_updated.also_known_as |> length() == 1 assert user2.ap_id in user_updated.also_known_as end + + test "should tolerate non-http(s) aliases" do + user = + insert(:user, %{ + also_known_as: ["at://did:plc:xgvzy7ni6ig6ievcbls5jaxe"] + }) + + assert "at://did:plc:xgvzy7ni6ig6ievcbls5jaxe" in user.also_known_as + end end describe "alias_users/1" do From 27ec46814cfb5515b16727d36bb028b5634b6b5d Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 12 Jun 2025 21:27:19 -0700 Subject: [PATCH 10/25] Revert "Public getting stripped from unlisted activity CC: Add possible tests" This reverts commit ded40182b0aa6848b55febe73ec7e41eace1e0f6. --- test/fixtures/poast_unlisted.json | 65 ------------------- .../transmogrifier/note_handling_test.exs | 31 --------- .../web/activity_pub/transmogrifier_test.exs | 37 ----------- 3 files changed, 133 deletions(-) delete mode 100644 test/fixtures/poast_unlisted.json diff --git a/test/fixtures/poast_unlisted.json b/test/fixtures/poast_unlisted.json deleted file mode 100644 index fa23153ba7..0000000000 --- a/test/fixtures/poast_unlisted.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "@context" : [ - "https://www.w3.org/ns/activitystreams", - "https://poa.st/schemas/litepub-0.1.jsonld", - { - "@language" : "und" - } - ], - "actor" : "https://poa.st/users/TrevorGoodchild", - "attachment" : [], - "attributedTo" : "https://poa.st/users/TrevorGoodchild", - "cc" : [ - "https://www.w3.org/ns/activitystreams#Public" - ], - "context" : "https://poa.st/contexts/c6d125f1-4e7f-43bd-aa31-33de4d90d049", - "conversation" : "https://poa.st/contexts/c6d125f1-4e7f-43bd-aa31-33de4d90d049", - "directMessage" : false, - "id" : "https://poa.st/activities/bbd3347a-4a89-4cdb-bf86-4f9eed9506e3", - "object" : { - "actor" : "https://poa.st/users/TrevorGoodchild", - "attachment" : [], - "attributedTo" : "https://poa.st/users/TrevorGoodchild", - "cc" : [ - "https://www.w3.org/ns/activitystreams#Public" - ], - "content" : "@HoroTheWhiteWolf >please let this be his zero fucks given final statement before he joins the 52%+ tranny club", - "context" : "https://poa.st/contexts/c6d125f1-4e7f-43bd-aa31-33de4d90d049", - "conversation" : "https://poa.st/contexts/c6d125f1-4e7f-43bd-aa31-33de4d90d049", - "id" : "https://poa.st/objects/7eb785d5-a556-4070-9091-f4afb226466c", - "inReplyTo" : "https://poa.st/objects/71995b41-cfb2-48ce-abce-76d570d54edc", - "published" : "2025-05-03T23:54:07.489885Z", - "repliesCount" : 2, - "sensitive" : false, - "source" : { - "content" : ">please let this be his zero fucks given final statement before he joins the 52%+ tranny club", - "mediaType" : "text/plain" - }, - "summary" : "", - "tag" : [ - { - "href" : "https://poa.st/users/HoroTheWhiteWolf", - "name" : "@HoroTheWhiteWolf", - "type" : "Mention" - } - ], - "to" : [ - "https://poa.st/users/HoroTheWhiteWolf", - "https://poa.st/users/TrevorGoodchild/followers" - ], - "type" : "Note" - }, - "published" : "2025-05-03T23:54:07.489837Z", - "tag" : [ - { - "href" : "https://poa.st/users/HoroTheWhiteWolf", - "name" : "@HoroTheWhiteWolf", - "type" : "Mention" - } - ], - "to" : [ - "https://poa.st/users/HoroTheWhiteWolf", - "https://poa.st/users/TrevorGoodchild/followers" - ], - "type" : "Create" -} diff --git a/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs index 13982940a7..fd7a3c772d 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs @@ -786,35 +786,4 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do assert object.data["context"] == object.data["inReplyTo"] assert modified.data["context"] == object.data["inReplyTo"] end - - test "it keeps the public address in cc in the activity when it is present" do - data = - File.read!("test/fixtures/mastodon-post-activity.json") - |> Jason.decode!() - - object = - data["object"] - |> Map.put("cc", ["https://www.w3.org/ns/activitystreams#Public"]) - |> Map.put("to", []) - - data = - data - |> Map.put("object", object) - |> Map.put("cc", ["https://www.w3.org/ns/activitystreams#Public"]) - |> Map.put("to", []) - - {:ok, %Activity{} = modified} = Transmogrifier.handle_incoming(data) - assert modified.data["cc"] == ["https://www.w3.org/ns/activitystreams#Public"] - end - - test "it tries it with the real poast_unlisted.json, ensuring that public is in the cc" do - data = - File.read!("test/fixtures/poast_unlisted.json") - |> Jason.decode!() - - _user = insert(:user, ap_id: data["actor"]) - - {:ok, %Activity{} = modified} = Transmogrifier.handle_incoming(data) - assert modified.data["cc"] == ["https://www.w3.org/ns/activitystreams#Public"] - end end diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs index ef6e004f1d..e0395d7bbc 100644 --- a/test/pleroma/web/activity_pub/transmogrifier_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs @@ -757,43 +757,6 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do refute recipient.follower_address in fixed_object["cc"] refute recipient.follower_address in fixed_object["to"] end - - test "preserves public URL in cc even when not explicitly mentioned", %{user: user} do - public_url = "https://www.w3.org/ns/activitystreams#Public" - - # Case 1: Public URL in cc but no mentions - object = %{ - "actor" => user.ap_id, - "to" => ["https://social.beepboop.ga/users/dirb"], - "cc" => [public_url], - "tag" => [] - } - - fixed_object = Transmogrifier.fix_explicit_addressing(object, user.follower_address) - assert public_url in fixed_object["cc"] - - # Case 2: Public URL in cc, with mentions but public not in to - object = %{ - "actor" => user.ap_id, - "to" => ["https://pleroma.gold/users/user1"], - "cc" => [public_url], - "tag" => [%{"type" => "Mention", "href" => "https://pleroma.gold/users/user1"}] - } - - fixed_object = Transmogrifier.fix_explicit_addressing(object, user.follower_address) - assert public_url in fixed_object["cc"] - - # Case 3: Public URL in to, it should be moved to to - object = %{ - "actor" => user.ap_id, - "to" => [public_url], - "cc" => [], - "tag" => [] - } - - fixed_object = Transmogrifier.fix_explicit_addressing(object, user.follower_address) - assert public_url in fixed_object["to"] - end end describe "fix_summary/1" do From 9f79df75082cfc563ce7816a1839800aa22ec350 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 12 Jun 2025 21:28:58 -0700 Subject: [PATCH 11/25] Add test demonstrating public getting stripped from unlisted activity CC --- .../web/activity_pub/publisher_test.exs | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/test/pleroma/web/activity_pub/publisher_test.exs b/test/pleroma/web/activity_pub/publisher_test.exs index 99ed428777..ec3201b964 100644 --- a/test/pleroma/web/activity_pub/publisher_test.exs +++ b/test/pleroma/web/activity_pub/publisher_test.exs @@ -520,4 +520,73 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do assert decoded["cc"] == [] end + + test "retains public address in cc for unlisted activities" do + user = insert(:user) + + activity = + insert(:note_activity, + user: user, + data_attrs: %{ + "cc" => [@as_public], + "to" => [user.follower_address] + } + ) + + assert @as_public in activity.data["cc"] + + # Call prepare_one without an explicit cc parameter (default in production) + prepared = + Publisher.prepare_one(%{ + inbox: "https://remote.instance/users/someone/inbox", + activity_id: activity.id + }) + + # Parse the JSON to verify the cc field in the federated message + {:ok, decoded} = Jason.decode(prepared.json) + + # The public address should be preserved in the cc field + # Currently this will fail because it's being removed + assert @as_public in decoded["cc"] + + # For verification, also test with an explicit cc parameter + # to show the cc field is completely replaced + prepared_with_cc = + Publisher.prepare_one(%{ + inbox: "https://remote.instance/users/someone/inbox", + activity_id: activity.id, + cc: ["https://example.com/specific/user"] + }) + + {:ok, decoded_with_cc} = Jason.decode(prepared_with_cc.json) + + # Verify cc is completely replaced with the provided value + assert decoded_with_cc["cc"] == ["https://example.com/specific/user"] + end + + test "public address in cc parameter is preserved" do + user = insert(:user) + + activity = + insert(:note_activity, + user: user, + data_attrs: %{ + "cc" => [@as_public, "https://example.org/users/other"], + "to" => [user.follower_address] + } + ) + + assert @as_public in activity.data["cc"] + + prepared_with_public_cc = + Publisher.prepare_one(%{ + inbox: "https://remote.instance/users/someone/inbox", + activity_id: activity.id, + cc: [@as_public] + }) + + {:ok, decoded_with_public_cc} = Jason.decode(prepared_with_public_cc.json) + + assert @as_public in decoded_with_public_cc["cc"] + end end From 23be24b92fa4f868b814b2c6927f2a6a69fa882d Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 12 Jun 2025 21:37:50 -0700 Subject: [PATCH 12/25] Fix federation issue where Public visibility information in cc field was lost when sent to remote servers, causing posts to appear with inconsistent visibility across instances --- changelog.d/preserve-public-cc.fix | 1 + lib/pleroma/web/activity_pub/publisher.ex | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 changelog.d/preserve-public-cc.fix diff --git a/changelog.d/preserve-public-cc.fix b/changelog.d/preserve-public-cc.fix new file mode 100644 index 0000000000..1b20ce9adf --- /dev/null +++ b/changelog.d/preserve-public-cc.fix @@ -0,0 +1 @@ +Fix federation issue where Public visibility information in cc field was lost when sent to remote servers, causing posts to appear with inconsistent visibility across instances diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index 0de3a0d434..762c991fdc 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -93,7 +93,20 @@ defmodule Pleroma.Web.ActivityPub.Publisher do {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) - cc = Map.get(params, :cc, []) + param_cc = Map.get(params, :cc, []) + + original_cc = Map.get(data, "cc", []) + + public_address = Pleroma.Constants.as_public() + + # Avoid overriding explicitly set cc values for specific recipients. + # e.g., this ensures unlisted posts are visible to users on other servers. + cc = + if public_address in original_cc and param_cc == [] do + [public_address] + else + param_cc + end json = data From d3adc3e05e09fdcb663ec1a3e20c1bc2d04a6ab5 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 12 Jun 2025 21:59:26 -0700 Subject: [PATCH 13/25] Split this cc test into two individual cases --- .../web/activity_pub/publisher_test.exs | 58 +++++++++++-------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/test/pleroma/web/activity_pub/publisher_test.exs b/test/pleroma/web/activity_pub/publisher_test.exs index ec3201b964..7bc5715957 100644 --- a/test/pleroma/web/activity_pub/publisher_test.exs +++ b/test/pleroma/web/activity_pub/publisher_test.exs @@ -521,9 +521,11 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do assert decoded["cc"] == [] end - test "retains public address in cc for unlisted activities" do + test "unlisted activities retain public address in cc" do user = insert(:user) + # simulate unlistd activity by only having + # public address in cc activity = insert(:note_activity, user: user, @@ -535,58 +537,66 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do assert @as_public in activity.data["cc"] - # Call prepare_one without an explicit cc parameter (default in production) prepared = Publisher.prepare_one(%{ inbox: "https://remote.instance/users/someone/inbox", activity_id: activity.id }) - # Parse the JSON to verify the cc field in the federated message {:ok, decoded} = Jason.decode(prepared.json) - # The public address should be preserved in the cc field - # Currently this will fail because it's being removed assert @as_public in decoded["cc"] - - # For verification, also test with an explicit cc parameter - # to show the cc field is completely replaced - prepared_with_cc = - Publisher.prepare_one(%{ - inbox: "https://remote.instance/users/someone/inbox", - activity_id: activity.id, - cc: ["https://example.com/specific/user"] - }) - - {:ok, decoded_with_cc} = Jason.decode(prepared_with_cc.json) - - # Verify cc is completely replaced with the provided value - assert decoded_with_cc["cc"] == ["https://example.com/specific/user"] end test "public address in cc parameter is preserved" do user = insert(:user) + cc_with_public = [@as_public, "https://example.org/users/other"] + activity = insert(:note_activity, user: user, data_attrs: %{ - "cc" => [@as_public, "https://example.org/users/other"], + "cc" => cc_with_public, "to" => [user.follower_address] } ) assert @as_public in activity.data["cc"] - prepared_with_public_cc = + prepared = Publisher.prepare_one(%{ inbox: "https://remote.instance/users/someone/inbox", activity_id: activity.id, - cc: [@as_public] + cc: cc_with_public }) - {:ok, decoded_with_public_cc} = Jason.decode(prepared_with_public_cc.json) + {:ok, decoded} = Jason.decode(prepared.json) - assert @as_public in decoded_with_public_cc["cc"] + assert cc_with_public == decoded["cc"] + end + + test "cc parameter is preserved" do + user = insert(:user) + + activity = + insert(:note_activity, + user: user, + data_attrs: %{ + "cc" => ["https://example.com/specific/user"], + "to" => [user.follower_address] + } + ) + + prepared = + Publisher.prepare_one(%{ + inbox: "https://remote.instance/users/someone/inbox", + activity_id: activity.id, + cc: ["https://example.com/specific/user"] + }) + + {:ok, decoded} = Jason.decode(prepared.json) + + assert decoded["cc"] == ["https://example.com/specific/user"] end end From fe6d2ecc970008f99f9d948b86e5da07e80c2a29 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 12 Jun 2025 22:33:57 -0700 Subject: [PATCH 14/25] Test for unlisted but Publisher param_cc is not empty --- .../web/activity_pub/publisher_test.exs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/pleroma/web/activity_pub/publisher_test.exs b/test/pleroma/web/activity_pub/publisher_test.exs index 7bc5715957..b7ff0ed5fb 100644 --- a/test/pleroma/web/activity_pub/publisher_test.exs +++ b/test/pleroma/web/activity_pub/publisher_test.exs @@ -546,6 +546,28 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do {:ok, decoded} = Jason.decode(prepared.json) assert @as_public in decoded["cc"] + + # maybe we also have another inbox in cc + # during Publishing + activity = + insert(:note_activity, + user: user, + data_attrs: %{ + "cc" => [@as_public], + "to" => [user.follower_address] + } + ) + + prepared = + Publisher.prepare_one(%{ + inbox: "https://remote.instance/users/someone/inbox", + activity_id: activity.id, + cc: ["https://remote.instance/users/someone_else/inbox"] + }) + + {:ok, decoded} = Jason.decode(prepared.json) + + assert decoded["cc"] == [@as_public, "https://remote.instance/users/someone_else/inbox"] end test "public address in cc parameter is preserved" do From 7c64bfaace454185c4428fec0e7247ba93fff048 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 12 Jun 2025 22:42:40 -0700 Subject: [PATCH 15/25] Include public address in cc if original activity specified it and Publisher param_cc also has values --- lib/pleroma/web/activity_pub/publisher.ex | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index 762c991fdc..f160f1e17e 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -99,11 +99,11 @@ defmodule Pleroma.Web.ActivityPub.Publisher do public_address = Pleroma.Constants.as_public() - # Avoid overriding explicitly set cc values for specific recipients. - # e.g., this ensures unlisted posts are visible to users on other servers. + # Ensure unlisted posts don't lose the public address in the cc + # if the param_cc was set cc = - if public_address in original_cc and param_cc == [] do - [public_address] + if public_address in original_cc and public_address not in param_cc do + [public_address | param_cc] else param_cc end From 33cf49e860c8c3a14c32f0b37b4432e86ee2f433 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 13 Jun 2025 10:17:27 -0700 Subject: [PATCH 16/25] Resurrect MRF.QuietReply This was not working correctly because the Publisher was stripping the public address from the cc when federating unlisted activities --- .../web/activity_pub/mrf/quiet_reply.ex | 62 ++++++++ .../web/activity_pub/mrf/quiet_reply_test.exs | 140 ++++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 lib/pleroma/web/activity_pub/mrf/quiet_reply.ex create mode 100644 test/pleroma/web/activity_pub/mrf/quiet_reply_test.exs diff --git a/lib/pleroma/web/activity_pub/mrf/quiet_reply.ex b/lib/pleroma/web/activity_pub/mrf/quiet_reply.ex new file mode 100644 index 0000000000..66080c47df --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/quiet_reply.ex @@ -0,0 +1,62 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.QuietReply do + @moduledoc """ + QuietReply alters the scope of activities from local users when replying by enforcing them to be "Unlisted" or "Quiet Public". This delivers the activity to all the expected recipients and instances, but it will not be published in the Federated / The Whole Known Network timelines. It will still be published to the Home timelines of the user's followers and visible to anyone who opens the thread. + """ + require Pleroma.Constants + + alias Pleroma.User + + @behaviour Pleroma.Web.ActivityPub.MRF.Policy + + @impl true + def history_awareness, do: :auto + + @impl true + def filter( + %{ + "type" => "Create", + "to" => to, + "cc" => cc, + "object" => %{ + "actor" => actor, + "type" => "Note", + "inReplyTo" => in_reply_to + } + } = activity + ) do + with true <- is_binary(in_reply_to), + false <- match?([], cc), + %User{follower_address: followers_collection, local: true} <- + User.get_by_ap_id(actor) do + updated_to = + to + |> Kernel.++([followers_collection]) + |> Kernel.--([Pleroma.Constants.as_public()]) + + updated_cc = + [Pleroma.Constants.as_public() | cc] + |> Kernel.--([followers_collection]) + + updated_activity = + activity + |> Map.put("to", updated_to) + |> Map.put("cc", updated_cc) + |> put_in(["object", "to"], updated_to) + |> put_in(["object", "cc"], updated_cc) + + {:ok, updated_activity} + else + _ -> {:ok, activity} + end + end + + @impl true + def filter(activity), do: {:ok, activity} + + @impl true + def describe, do: {:ok, %{}} +end diff --git a/test/pleroma/web/activity_pub/mrf/quiet_reply_test.exs b/test/pleroma/web/activity_pub/mrf/quiet_reply_test.exs new file mode 100644 index 0000000000..79e64d650c --- /dev/null +++ b/test/pleroma/web/activity_pub/mrf/quiet_reply_test.exs @@ -0,0 +1,140 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.QuietReplyTest do + use Pleroma.DataCase + import Pleroma.Factory + + require Pleroma.Constants + + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.MRF.QuietReply + alias Pleroma.Web.CommonAPI + + test "replying to public post is forced to be quiet" do + batman = insert(:user, nickname: "batman") + robin = insert(:user, nickname: "robin") + + {:ok, post} = CommonAPI.post(batman, %{status: "To the Batmobile!"}) + + reply = %{ + "type" => "Create", + "actor" => robin.ap_id, + "to" => [ + batman.ap_id, + Pleroma.Constants.as_public() + ], + "cc" => [robin.follower_address], + "object" => %{ + "type" => "Note", + "actor" => robin.ap_id, + "content" => "@batman Wait up, I forgot my spandex!", + "to" => [ + batman.ap_id, + Pleroma.Constants.as_public() + ], + "cc" => [robin.follower_address], + "inReplyTo" => Object.normalize(post).data["id"] + } + } + + expected_to = [batman.ap_id, robin.follower_address] + expected_cc = [Pleroma.Constants.as_public()] + + assert {:ok, filtered} = QuietReply.filter(reply) + + assert expected_to == filtered["to"] + assert expected_cc == filtered["cc"] + assert expected_to == filtered["object"]["to"] + assert expected_cc == filtered["object"]["cc"] + end + + test "replying to unlisted post is unmodified" do + batman = insert(:user, nickname: "batman") + robin = insert(:user, nickname: "robin") + + {:ok, post} = CommonAPI.post(batman, %{status: "To the Batmobile!", visibility: "private"}) + + reply = %{ + "type" => "Create", + "actor" => robin.ap_id, + "to" => [batman.ap_id], + "cc" => [], + "object" => %{ + "type" => "Note", + "actor" => robin.ap_id, + "content" => "@batman Wait up, I forgot my spandex!", + "to" => [batman.ap_id], + "cc" => [], + "inReplyTo" => Object.normalize(post).data["id"] + } + } + + assert {:ok, filtered} = QuietReply.filter(reply) + + assert match?(^filtered, reply) + end + + test "replying direct is unmodified" do + batman = insert(:user, nickname: "batman") + robin = insert(:user, nickname: "robin") + + {:ok, post} = CommonAPI.post(batman, %{status: "To the Batmobile!"}) + + reply = %{ + "type" => "Create", + "actor" => robin.ap_id, + "to" => [batman.ap_id], + "cc" => [], + "object" => %{ + "type" => "Note", + "actor" => robin.ap_id, + "content" => "@batman Wait up, I forgot my spandex!", + "to" => [batman.ap_id], + "cc" => [], + "inReplyTo" => Object.normalize(post).data["id"] + } + } + + assert {:ok, filtered} = QuietReply.filter(reply) + + assert match?(^filtered, reply) + end + + test "replying followers-only is unmodified" do + batman = insert(:user, nickname: "batman") + robin = insert(:user, nickname: "robin") + + {:ok, post} = CommonAPI.post(batman, %{status: "To the Batmobile!"}) + + reply = %{ + "type" => "Create", + "actor" => robin.ap_id, + "to" => [batman.ap_id, robin.follower_address], + "cc" => [], + "object" => %{ + "type" => "Note", + "actor" => robin.ap_id, + "content" => "@batman Wait up, I forgot my spandex!", + "to" => [batman.ap_id, robin.follower_address], + "cc" => [], + "inReplyTo" => Object.normalize(post).data["id"] + } + } + + assert {:ok, filtered} = QuietReply.filter(reply) + + assert match?(^filtered, reply) + end + + test "non-reply posts are unmodified" do + batman = insert(:user, nickname: "batman") + + {:ok, post} = CommonAPI.post(batman, %{status: "To the Batmobile!"}) + + assert {:ok, filtered} = QuietReply.filter(post) + + assert match?(^filtered, post) + end +end From 00d536d9e2c6c76d88724e5c75cc82a857519c65 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Thu, 30 Jan 2025 15:50:50 +0100 Subject: [PATCH 17/25] backports: Copy mkdir_p TOCTOU fix from elixir PR 14242 See: https://github.com/elixir-lang/elixir/pull/14242 --- changelog.d/toctou-mkdir.fix | 1 + lib/pleroma/backports.ex | 72 ++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 changelog.d/toctou-mkdir.fix create mode 100644 lib/pleroma/backports.ex diff --git a/changelog.d/toctou-mkdir.fix b/changelog.d/toctou-mkdir.fix new file mode 100644 index 0000000000..b070db1a05 --- /dev/null +++ b/changelog.d/toctou-mkdir.fix @@ -0,0 +1 @@ +Backport [Elixir PR 14242](https://github.com/elixir-lang/elixir/pull/14242) fixing racy mkdir and lack of error handling of parent directory creation \ No newline at end of file diff --git a/lib/pleroma/backports.ex b/lib/pleroma/backports.ex new file mode 100644 index 0000000000..68cb7b990e --- /dev/null +++ b/lib/pleroma/backports.ex @@ -0,0 +1,72 @@ +# Copyright 2012 Plataformatec +# Copyright 2021 The Elixir Team +# SPDX-License-Identifier: Apache-2.0 + +defmodule Pleroma.Backports do + import File, only: [dir?: 1] + + # + # To be removed when we require Elixir 1.19 + @doc """ + Tries to create the directory `path`. + + Missing parent directories are created. Returns `:ok` if successful, or + `{:error, reason}` if an error occurs. + + Typical error reasons are: + + * `:eacces` - missing search or write permissions for the parent + directories of `path` + * `:enospc` - there is no space left on the device + * `:enotdir` - a component of `path` is not a directory + + """ + @spec mkdir_p(Path.t()) :: :ok | {:error, File.posix() | :badarg} + def mkdir_p(path) do + do_mkdir_p(IO.chardata_to_string(path)) + end + + defp do_mkdir_p("/") do + :ok + end + + defp do_mkdir_p(path) do + parent = Path.dirname(path) + + if parent == path do + :ok + else + case do_mkdir_p(parent) do + :ok -> + case :file.make_dir(path) do + {:error, :eexist} -> + if dir?(path), do: :ok, else: {:error, :enotdir} + + other -> + other + end + + e -> + e + end + end + end + + @doc """ + Same as `mkdir_p/1`, but raises a `File.Error` exception in case of failure. + Otherwise `:ok`. + """ + @spec mkdir_p!(Path.t()) :: :ok + def mkdir_p!(path) do + case mkdir_p(path) do + :ok -> + :ok + + {:error, reason} -> + raise File.Error, + reason: reason, + action: "make directory (with -p)", + path: IO.chardata_to_string(path) + end + end +end From a69e417020bcbbb998b1e8c039b1cfde23f60a02 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Thu, 30 Jan 2025 16:05:49 +0100 Subject: [PATCH 18/25] File.mkdir_p -> Pleroma.Backports.mkdir_p --- lib/mix/tasks/pleroma/instance.ex | 2 +- lib/mix/tasks/pleroma/robots_txt.ex | 2 +- lib/pleroma/emoji/pack.ex | 6 +++--- lib/pleroma/frontend.ex | 4 ++-- lib/pleroma/uploaders/local.ex | 2 +- lib/pleroma/user/backup.ex | 2 +- lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex | 2 +- lib/pleroma/web/instance_document.ex | 2 +- test/mix/tasks/pleroma/frontend_test.exs | 4 ++-- test/mix/tasks/pleroma/instance_test.exs | 2 +- test/mix/tasks/pleroma/uploads_test.exs | 2 +- test/pleroma/emoji/pack_test.exs | 2 +- test/pleroma/frontend_test.exs | 4 ++-- test/pleroma/object_test.exs | 2 +- test/pleroma/safe_zip_test.exs | 10 +++++----- .../admin_api/controllers/frontend_controller_test.exs | 2 +- .../controllers/instance_document_controller_test.exs | 2 +- test/pleroma/web/plugs/frontend_static_plug_test.exs | 8 ++++---- test/pleroma/web/plugs/instance_static_test.exs | 4 ++-- 19 files changed, 32 insertions(+), 32 deletions(-) diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex index 0dc30549c4..143af5cdd3 100644 --- a/lib/mix/tasks/pleroma/instance.ex +++ b/lib/mix/tasks/pleroma/instance.ex @@ -271,7 +271,7 @@ defmodule Mix.Tasks.Pleroma.Instance do [config_dir, psql_dir, static_dir, uploads_dir] |> Enum.reject(&File.exists?/1) |> Enum.each(fn dir -> - File.mkdir_p!(dir) + Pleroma.Backports.mkdir_p!(dir) File.chmod!(dir, 0o700) end) diff --git a/lib/mix/tasks/pleroma/robots_txt.ex b/lib/mix/tasks/pleroma/robots_txt.ex index 5124c7c40a..e741f3cf07 100644 --- a/lib/mix/tasks/pleroma/robots_txt.ex +++ b/lib/mix/tasks/pleroma/robots_txt.ex @@ -22,7 +22,7 @@ defmodule Mix.Tasks.Pleroma.RobotsTxt do static_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static/") if !File.exists?(static_dir) do - File.mkdir_p!(static_dir) + Pleroma.Backports.mkdir_p!(static_dir) end robots_txt_path = Path.join(static_dir, "robots.txt") diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex index c58748d3ce..99fa1994f8 100644 --- a/lib/pleroma/emoji/pack.ex +++ b/lib/pleroma/emoji/pack.ex @@ -488,7 +488,7 @@ defmodule Pleroma.Emoji.Pack do with true <- String.contains?(file_path, "/"), path <- Path.dirname(file_path), false <- File.exists?(path) do - File.mkdir_p!(path) + Pleroma.Backports.mkdir_p!(path) end end @@ -536,7 +536,7 @@ defmodule Pleroma.Emoji.Pack do emoji_path = emoji_path() # Create the directory first if it does not exist. This is probably the first request made # with the API so it should be sufficient - with {:create_dir, :ok} <- {:create_dir, File.mkdir_p(emoji_path)}, + with {:create_dir, :ok} <- {:create_dir, Pleroma.Backports.mkdir_p(emoji_path)}, {:ls, {:ok, results}} <- {:ls, File.ls(emoji_path)} do {:ok, Enum.sort(results)} else @@ -561,7 +561,7 @@ defmodule Pleroma.Emoji.Pack do end defp unzip(archive, pack_info, remote_pack, local_pack) do - with :ok <- File.mkdir_p!(local_pack.path) do + with :ok <- Pleroma.Backports.mkdir_p!(local_pack.path) do files = Enum.map(remote_pack["files"], fn {_, path} -> path end) # Fallback cannot contain a pack.json file files = if pack_info[:fallback], do: files, else: ["pack.json" | files] diff --git a/lib/pleroma/frontend.ex b/lib/pleroma/frontend.ex index fe7f525eab..e651d7d9db 100644 --- a/lib/pleroma/frontend.ex +++ b/lib/pleroma/frontend.ex @@ -66,7 +66,7 @@ defmodule Pleroma.Frontend do def unzip(zip, dest) do File.rm_rf!(dest) - File.mkdir_p!(dest) + Pleroma.Backports.mkdir_p!(dest) case Pleroma.SafeZip.unzip_data(zip, dest) do {:ok, _} -> :ok @@ -90,7 +90,7 @@ defmodule Pleroma.Frontend do defp install_frontend(frontend_info, source, dest) do from = frontend_info["build_dir"] || "dist" File.rm_rf!(dest) - File.mkdir_p!(dest) + Pleroma.Backports.mkdir_p!(dest) File.cp_r!(Path.join([source, from]), dest) :ok end diff --git a/lib/pleroma/uploaders/local.ex b/lib/pleroma/uploaders/local.ex index e4a309cea6..7aab05b367 100644 --- a/lib/pleroma/uploaders/local.ex +++ b/lib/pleroma/uploaders/local.ex @@ -19,7 +19,7 @@ defmodule Pleroma.Uploaders.Local do [file | folders] -> path = Path.join([upload_path()] ++ Enum.reverse(folders)) - File.mkdir_p!(path) + Pleroma.Backports.mkdir_p!(path) {path, file} end diff --git a/lib/pleroma/user/backup.ex b/lib/pleroma/user/backup.ex index 244b08adbd..3f67cdf0ce 100644 --- a/lib/pleroma/user/backup.ex +++ b/lib/pleroma/user/backup.ex @@ -193,7 +193,7 @@ defmodule Pleroma.User.Backup do backup = Repo.preload(backup, :user) tempfile = Path.join([backup.tempdir, backup.file_name]) - with {_, :ok} <- {:mkdir, File.mkdir_p(backup.tempdir)}, + with {_, :ok} <- {:mkdir, Pleroma.Backports.mkdir_p(backup.tempdir)}, {_, :ok} <- {:actor, actor(backup.tempdir, backup.user)}, {_, :ok} <- {:statuses, statuses(backup.tempdir, backup.user)}, {_, :ok} <- {:likes, likes(backup.tempdir, backup.user)}, diff --git a/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex b/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex index 49d17d8b95..54f0e6bc18 100644 --- a/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex @@ -87,7 +87,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do Path.join(Config.get([:instance, :static_dir]), "emoji/stolen") ) - File.mkdir_p(emoji_dir_path) + Pleroma.Backports.mkdir_p(emoji_dir_path) new_emojis = foreign_emojis diff --git a/lib/pleroma/web/instance_document.ex b/lib/pleroma/web/instance_document.ex index 9da3c5008e..143a0b0b83 100644 --- a/lib/pleroma/web/instance_document.ex +++ b/lib/pleroma/web/instance_document.ex @@ -46,7 +46,7 @@ defmodule Pleroma.Web.InstanceDocument do defp put_file(origin_path, destination_path) do with destination <- instance_static_dir(destination_path), - {_, :ok} <- {:mkdir_p, File.mkdir_p(Path.dirname(destination))}, + {_, :ok} <- {:mkdir_p, Pleroma.Backports.mkdir_p(Path.dirname(destination))}, {_, {:ok, _}} <- {:copy, File.copy(origin_path, destination)} do :ok else diff --git a/test/mix/tasks/pleroma/frontend_test.exs b/test/mix/tasks/pleroma/frontend_test.exs index 6d09f8e369..59ebcec92b 100644 --- a/test/mix/tasks/pleroma/frontend_test.exs +++ b/test/mix/tasks/pleroma/frontend_test.exs @@ -11,7 +11,7 @@ defmodule Mix.Tasks.Pleroma.FrontendTest do @dir "test/frontend_static_test" setup do - File.mkdir_p!(@dir) + Pleroma.Backports.mkdir_p!(@dir) clear_config([:instance, :static_dir], @dir) on_exit(fn -> @@ -50,7 +50,7 @@ defmodule Mix.Tasks.Pleroma.FrontendTest do folder = Path.join([@dir, "frontends", "pleroma", "fantasy"]) previously_existing = Path.join([folder, "temp"]) - File.mkdir_p!(folder) + Pleroma.Backports.mkdir_p!(folder) File.write!(previously_existing, "yey") assert File.exists?(previously_existing) diff --git a/test/mix/tasks/pleroma/instance_test.exs b/test/mix/tasks/pleroma/instance_test.exs index b1c10e03c9..5ecb6e445b 100644 --- a/test/mix/tasks/pleroma/instance_test.exs +++ b/test/mix/tasks/pleroma/instance_test.exs @@ -7,7 +7,7 @@ defmodule Mix.Tasks.Pleroma.InstanceTest do use Pleroma.DataCase setup do - File.mkdir_p!(tmp_path()) + Pleroma.Backports.mkdir_p!(tmp_path()) on_exit(fn -> File.rm_rf(tmp_path()) diff --git a/test/mix/tasks/pleroma/uploads_test.exs b/test/mix/tasks/pleroma/uploads_test.exs index f3d5aa64fd..0aa24807ed 100644 --- a/test/mix/tasks/pleroma/uploads_test.exs +++ b/test/mix/tasks/pleroma/uploads_test.exs @@ -62,7 +62,7 @@ defmodule Mix.Tasks.Pleroma.UploadsTest do upload_dir = Config.get([Pleroma.Uploaders.Local, :uploads]) if not File.exists?(upload_dir) || File.ls!(upload_dir) == [] do - File.mkdir_p(upload_dir) + Pleroma.Backports.mkdir_p(upload_dir) Path.join([upload_dir, "file.txt"]) |> File.touch() diff --git a/test/pleroma/emoji/pack_test.exs b/test/pleroma/emoji/pack_test.exs index 6ab3e657e5..b458401a73 100644 --- a/test/pleroma/emoji/pack_test.exs +++ b/test/pleroma/emoji/pack_test.exs @@ -58,7 +58,7 @@ defmodule Pleroma.Emoji.PackTest do test "skips existing emojis when adding from zip file", %{pack: pack} do # First, let's create a test pack with a "bear" emoji test_pack_path = Path.join(@emoji_path, "test_bear_pack") - File.mkdir_p(test_pack_path) + Pleroma.Backports.mkdir_p(test_pack_path) # Create a pack.json file File.write!(Path.join(test_pack_path, "pack.json"), """ diff --git a/test/pleroma/frontend_test.exs b/test/pleroma/frontend_test.exs index c89c56c8c1..22e0ffb9ab 100644 --- a/test/pleroma/frontend_test.exs +++ b/test/pleroma/frontend_test.exs @@ -9,7 +9,7 @@ defmodule Pleroma.FrontendTest do @dir "test/frontend_static_test" setup do - File.mkdir_p!(@dir) + Pleroma.Backports.mkdir_p!(@dir) clear_config([:instance, :static_dir], @dir) on_exit(fn -> @@ -46,7 +46,7 @@ defmodule Pleroma.FrontendTest do folder = Path.join([@dir, "frontends", "pleroma", "fantasy"]) previously_existing = Path.join([folder, "temp"]) - File.mkdir_p!(folder) + Pleroma.Backports.mkdir_p!(folder) File.write!(previously_existing, "yey") assert File.exists?(previously_existing) diff --git a/test/pleroma/object_test.exs b/test/pleroma/object_test.exs index ed5c2b6c80..13e941e4db 100644 --- a/test/pleroma/object_test.exs +++ b/test/pleroma/object_test.exs @@ -156,7 +156,7 @@ defmodule Pleroma.ObjectTest do uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads]) - File.mkdir_p!(uploads_dir) + Pleroma.Backports.mkdir_p!(uploads_dir) file = %Plug.Upload{ content_type: "image/jpeg", diff --git a/test/pleroma/safe_zip_test.exs b/test/pleroma/safe_zip_test.exs index 3312d4e633..f07b25675b 100644 --- a/test/pleroma/safe_zip_test.exs +++ b/test/pleroma/safe_zip_test.exs @@ -9,12 +9,12 @@ defmodule Pleroma.SafeZipTest do setup do # Ensure tmp directory exists - File.mkdir_p!(@tmp_dir) + Pleroma.Backports.mkdir_p!(@tmp_dir) on_exit(fn -> # Clean up any files created during tests File.rm_rf!(@tmp_dir) - File.mkdir_p!(@tmp_dir) + Pleroma.Backports.mkdir_p!(@tmp_dir) end) :ok @@ -89,7 +89,7 @@ defmodule Pleroma.SafeZipTest do # For this test, we'll manually check if the file exists in the archive # by extracting it and verifying it exists extract_dir = Path.join(@tmp_dir, "extract_check") - File.mkdir_p!(extract_dir) + Pleroma.Backports.mkdir_p!(extract_dir) {:ok, files} = SafeZip.unzip_file(zip_path, extract_dir) # Verify the root file was extracted @@ -145,7 +145,7 @@ defmodule Pleroma.SafeZipTest do test "can create zip with directories" do # Create a directory structure dir_path = Path.join(@tmp_dir, "test_dir") - File.mkdir_p!(dir_path) + Pleroma.Backports.mkdir_p!(dir_path) file_in_dir_path = Path.join(dir_path, "file_in_dir.txt") File.write!(file_in_dir_path, "file in directory") @@ -428,7 +428,7 @@ defmodule Pleroma.SafeZipTest do # Create a directory and a file in it dir_path = Path.join(@tmp_dir, "file_in_dir") - File.mkdir_p!(dir_path) + Pleroma.Backports.mkdir_p!(dir_path) file_in_dir_path = Path.join(dir_path, "test_file.txt") File.write!(file_in_dir_path, "file in directory content") diff --git a/test/pleroma/web/admin_api/controllers/frontend_controller_test.exs b/test/pleroma/web/admin_api/controllers/frontend_controller_test.exs index 0d1a4999eb..a6b8dba465 100644 --- a/test/pleroma/web/admin_api/controllers/frontend_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/frontend_controller_test.exs @@ -13,7 +13,7 @@ defmodule Pleroma.Web.AdminAPI.FrontendControllerTest do setup do clear_config([:instance, :static_dir], @dir) - File.mkdir_p!(Pleroma.Frontend.dir()) + Pleroma.Backports.mkdir_p!(Pleroma.Frontend.dir()) on_exit(fn -> File.rm_rf(@dir) diff --git a/test/pleroma/web/admin_api/controllers/instance_document_controller_test.exs b/test/pleroma/web/admin_api/controllers/instance_document_controller_test.exs index 9511dcceaa..344c908fe0 100644 --- a/test/pleroma/web/admin_api/controllers/instance_document_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/instance_document_controller_test.exs @@ -10,7 +10,7 @@ defmodule Pleroma.Web.AdminAPI.InstanceDocumentControllerTest do @default_instance_panel ~s(

Welcome to Pleroma!

) setup do - File.mkdir_p!(@dir) + Pleroma.Backports.mkdir_p!(@dir) on_exit(fn -> File.rm_rf(@dir) end) end diff --git a/test/pleroma/web/plugs/frontend_static_plug_test.exs b/test/pleroma/web/plugs/frontend_static_plug_test.exs index 6f4d24d9e6..a7af3e74e1 100644 --- a/test/pleroma/web/plugs/frontend_static_plug_test.exs +++ b/test/pleroma/web/plugs/frontend_static_plug_test.exs @@ -13,7 +13,7 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do @dir "test/tmp/instance_static" setup do - File.mkdir_p!(@dir) + Pleroma.Backports.mkdir_p!(@dir) on_exit(fn -> File.rm_rf(@dir) end) end @@ -38,7 +38,7 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do clear_config([:frontends, :primary], %{"name" => name, "ref" => ref}) path = "#{@dir}/frontends/#{name}/#{ref}" - File.mkdir_p!(path) + Pleroma.Backports.mkdir_p!(path) File.write!("#{path}/index.html", "from frontend plug") index = get(conn, "/") @@ -52,7 +52,7 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do clear_config([:frontends, :admin], %{"name" => name, "ref" => ref}) path = "#{@dir}/frontends/#{name}/#{ref}" - File.mkdir_p!(path) + Pleroma.Backports.mkdir_p!(path) File.write!("#{path}/index.html", "from frontend plug") index = get(conn, "/pleroma/admin/") @@ -67,7 +67,7 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do clear_config([:frontends, :primary], %{"name" => name, "ref" => ref}) path = "#{@dir}/frontends/#{name}/#{ref}" - File.mkdir_p!("#{path}/proxy/rr/ss") + Pleroma.Backports.mkdir_p!("#{path}/proxy/rr/ss") File.write!("#{path}/proxy/rr/ss/Ek7w8WPVcAApOvN.jpg:large", "FB image") ConfigMock diff --git a/test/pleroma/web/plugs/instance_static_test.exs b/test/pleroma/web/plugs/instance_static_test.exs index 33b74dcf0c..b5a5a33347 100644 --- a/test/pleroma/web/plugs/instance_static_test.exs +++ b/test/pleroma/web/plugs/instance_static_test.exs @@ -8,7 +8,7 @@ defmodule Pleroma.Web.Plugs.InstanceStaticTest do @dir "test/tmp/instance_static" setup do - File.mkdir_p!(@dir) + Pleroma.Backports.mkdir_p!(@dir) on_exit(fn -> File.rm_rf(@dir) end) end @@ -34,7 +34,7 @@ defmodule Pleroma.Web.Plugs.InstanceStaticTest do refute html_response(bundled_index, 200) == "from frontend plug" path = "#{@dir}/frontends/#{name}/#{ref}" - File.mkdir_p!(path) + Pleroma.Backports.mkdir_p!(path) File.write!("#{path}/index.html", "from frontend plug") index = get(conn, "/") From 7ecfb953316d718235042e60a58b464ccdd764f0 Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Fri, 13 Jun 2025 22:47:32 +0300 Subject: [PATCH 19/25] Handle the Dislike activity by transforming into a thumbs-down emote --- changelog.d/dislike-activity.add | 1 + lib/pleroma/constants.ex | 2 + .../web/activity_pub/transmogrifier.ex | 18 +++++ test/fixtures/friendica-dislike-undo.json | 76 +++++++++++++++++++ test/fixtures/friendica-dislike.json | 56 ++++++++++++++ .../transmogrifier/like_handling_test.exs | 36 +++++++++ 6 files changed, 189 insertions(+) create mode 100644 changelog.d/dislike-activity.add create mode 100644 test/fixtures/friendica-dislike-undo.json create mode 100644 test/fixtures/friendica-dislike.json diff --git a/changelog.d/dislike-activity.add b/changelog.d/dislike-activity.add new file mode 100644 index 0000000000..1fcbda78b7 --- /dev/null +++ b/changelog.d/dislike-activity.add @@ -0,0 +1 @@ +Support Dislike activity, as sent by Mitra and Friendica, by changing it into a thumbs-down EmojiReact \ No newline at end of file diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex index 3762c00350..92ca11494a 100644 --- a/lib/pleroma/constants.ex +++ b/lib/pleroma/constants.ex @@ -100,6 +100,7 @@ defmodule Pleroma.Constants do "Add", "Remove", "Like", + "Dislike", "Announce", "Undo", "Flag", @@ -115,6 +116,7 @@ defmodule Pleroma.Constants do "Flag", "Follow", "Like", + "Dislike", "EmojiReact", "Announce" ] diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 6517f5effa..8819e15965 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -664,6 +664,24 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end end + # Rewrite dislikes into the thumbs down emoji + defp handle_incoming_normalized(%{"type" => "Dislike"} = data, options) do + data + |> Map.put("type", "EmojiReact") + |> Map.put("content", "👎") + |> handle_incoming_normalized(options) + end + + defp handle_incoming_normalized( + %{"type" => "Undo", "object" => %{"type" => "Dislike"}} = data, + options + ) do + data + |> put_in(["object", "type"], "EmojiReact") + |> put_in(["object", "content"], "👎") + |> handle_incoming_normalized(options) + end + defp handle_incoming_normalized(_, _), do: :error @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil diff --git a/test/fixtures/friendica-dislike-undo.json b/test/fixtures/friendica-dislike-undo.json new file mode 100644 index 0000000000..b258e00be1 --- /dev/null +++ b/test/fixtures/friendica-dislike-undo.json @@ -0,0 +1,76 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "Hashtag": "as:Hashtag", + "PropertyValue": "schema:PropertyValue", + "conversation": "ostatus:conversation", + "dfrn": "http://purl.org/macgirvin/dfrn/1.0/", + "diaspora": "https://diasporafoundation.org/ns/", + "directMessage": "litepub:directMessage", + "discoverable": "toot:discoverable", + "featured": { + "@id": "toot:featured", + "@type": "@id" + }, + "litepub": "http://litepub.social/ns#", + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "ostatus": "http://ostatus.org#", + "quoteUrl": "as:quoteUrl", + "schema": "http://schema.org#", + "sensitive": "as:sensitive", + "toot": "http://joinmastodon.org/ns#", + "value": "schema:value", + "vcard": "http://www.w3.org/2006/vcard/ns#" + } + ], + "actor": "https://my-place.social/profile/vaartis", + "cc": [ + "https://my-place.social/followers/vaartis" + ], + "id": "https://my-place.social/objects/e599373b-1368-4b20-cd24-837166957182/Undo", + "instrument": { + "id": "https://my-place.social/friendica", + "name": "Friendica 'Interrupted Fern' 2024.12-1576", + "type": "Application", + "url": "https://my-place.social" + }, + "object": { + "actor": "https://my-place.social/profile/vaartis", + "cc": [ + "https://my-place.social/followers/vaartis" + ], + "diaspora:guid": "e599373b-1968-4b20-cd24-80d340160302", + "diaspora:like": "{\"author\":\"vaartis@my-place.social\",\"guid\":\"e599373b-1968-4b20-cd24-80d340160302\",\"parent_guid\":\"cd36feba-c31f3ed3fd5c064a-17c31593\",\"parent_type\":\"Post\",\"positive\":\"false\",\"author_signature\":\"xR2zLJNfc9Nhx1n8LLMWM1kde12my4cqamIsrH\\/UntKzuDwO4DuHBL0fkFhgC\\/dylxm4HqsHD45MQbtwQCVGq6WhC96TrbMuYEK61HIO23dTr3m+qJVtfdH4wyhUNHgiiYPhZpkLDfnR1JiRWmFTlmZC8q8JEkOB5IQsrWia2eOR6IsqDcdKO\\/Urgns9\\/BdQi8KnchBKSEFc1iUtcOEruvhunKGyW5zI\\/Rltfdz3xGH8tlw+YlMXeWXPnqgOJ9GzNA0lwG4U421L6yylYagW7oxIznnBLB4bO46vYZbgXZV1hiI9ZyveHOinLMY1QkmTj5CNvnx3\\/VJwLovd0v+0Nr2vu\\/3ftbpBXc6L1bsNjlRqtsfwJlcgl+tH1DC4W8tKf+Y3tdtzVw0CHXCuacxHLyq5wZd\\/5YfYR9SJQ\\/jInU4PHA5+hIE3PGqNUp5QfFE0umq56H7MQKsIPgM5mMV4fPAA8OpltuMVDvQYUxalrnvoTf00k90x1wCTK71\\/jQGh7r7PmGvSdfPr+65eVTjITD8\\/lTGIb8850v1fl3\\/i2R8Dv17jQIRyX5o9MXPSO6jHo4Swma5RzPA\\/0bRj6qRTyPkM1L9qEIr+2H2I7KKhT2ZE5GhAU7yI9A3VLBWzpTrUPMGbfpd1OjVTEqXAdMjpLDYI3Mh5zQ58p8xCzt+W+t0=\"}", + "id": "https://my-place.social/objects/e599373b-1368-4b20-cd24-837166957182", + "instrument": { + "id": "https://my-place.social/friendica", + "name": "Friendica 'Interrupted Fern' 2024.12-1576", + "type": "Application", + "url": "https://my-place.social" + }, + "object": "https://pl.kotobank.ch/objects/301bce65-8a1b-4c49-a65c-fe2ce861a213", + "published": "2025-06-12T18:47:41Z", + "to": [ + "https://pl.kotobank.ch/users/vaartis", + "https://mitra.social/users/silverpill", + "https://www.w3.org/ns/activitystreams#Public" + ], + "type": "Dislike" + }, + "published": "2025-06-12T18:41:25Z", + "signature": { + "created": "2025-06-12T18:44:16Z", + "creator": "https://my-place.social/profile/vaartis#main-key", + "nonce": "2d67847d4bd4b1b83a30d61eac6cdc7ad6b980df06a8b9b97217e1d8f7b6cf20", + "signatureValue": "LnoRMZuQGDvTICkShGBq28ynaj2lF1bViJFGS6n4gKn3IbxPWATHxao43gxWRc+HCTrHNg7quzgaW4+PYM7UVUz3jO+bjNKsN845nijOVdyFrPOXbuaij3KQh2OoHhFJWoV/ZQQTFF0kRK1qT4BwG+P8NqOOKAMv+Cw7ruQH+f2w7uDgcNIbCD1gLcwb6cw7WVe5qu8yMkKqp2kBdqW3RCsI85RmmFgwehDgH5nrX7ER1qbeLWrqy7echwD9/fO3rqAu13xDNyiGZHDT7JB3RUt0AyMm0XCfjbwSQ0n+MkYXgE4asvFz81+iiPCLt+6gePWAFc5odF1FxdySBpSuUOs4p92NzP9OhQ0c0qrqrzYI7aYklY7oMfxjkva+X+0bm3up+2IRJdnZa/pXlmwdcqTpyMr1sgzaexMUNBp3dq7zA51eEaakLDX3i2onXJowfmze3+6XgPAFHYamR+pRNtuEoY4uyYEK3fj5GgwJ4RtFJMYVoEs/Q8h3OgYRcK1FE9UlDjSqbQ7QIRn2Ib4wjgmkeM0vrHIwh/1CtqA/M/6WuYFzCaJBc8O9ykpK9ZMbw64ToQXKf2SqhZsDoyTWRWTO1PXOk1XCAAElUh8/WCyeghvgqLXn0LHov4lmBsHA5iMUcLqBKD3GJIHd+ExrOFxMZs4mBLLGyz0p5joJ3NY=", + "type": "RsaSignature2017" + }, + "to": [ + "https://pl.kotobank.ch/users/vaartis", + "https://mitra.social/users/silverpill", + "https://www.w3.org/ns/activitystreams#Public" + ], + "type": "Undo" +} diff --git a/test/fixtures/friendica-dislike.json b/test/fixtures/friendica-dislike.json new file mode 100644 index 0000000000..c75939073b --- /dev/null +++ b/test/fixtures/friendica-dislike.json @@ -0,0 +1,56 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "Hashtag": "as:Hashtag", + "PropertyValue": "schema:PropertyValue", + "conversation": "ostatus:conversation", + "dfrn": "http://purl.org/macgirvin/dfrn/1.0/", + "diaspora": "https://diasporafoundation.org/ns/", + "directMessage": "litepub:directMessage", + "discoverable": "toot:discoverable", + "featured": { + "@id": "toot:featured", + "@type": "@id" + }, + "litepub": "http://litepub.social/ns#", + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "ostatus": "http://ostatus.org#", + "quoteUrl": "as:quoteUrl", + "schema": "http://schema.org#", + "sensitive": "as:sensitive", + "toot": "http://joinmastodon.org/ns#", + "value": "schema:value", + "vcard": "http://www.w3.org/2006/vcard/ns#" + } + ], + "actor": "https://my-place.social/profile/vaartis", + "cc": [ + "https://my-place.social/followers/vaartis" + ], + "diaspora:guid": "e599373b-1968-4b20-cd24-80d340160302", + "diaspora:like": "{\"author\":\"vaartis@my-place.social\",\"guid\":\"e599373b-1968-4b20-cd24-80d340160302\",\"parent_guid\":\"cd36feba-c31f3ed3fd5c064a-17c31593\",\"parent_type\":\"Post\",\"positive\":\"false\",\"author_signature\":\"xR2zLJNfc9Nhx1n8LLMWM1kde12my4cqamIsrH\\/UntKzuDwO4DuHBL0fkFhgC\\/dylxm4HqsHD45MQbtwQCVGq6WhC96TrbMuYEK61HIO23dTr3m+qJVtfdH4wyhUNHgiiYPhZpkLDfnR1JiRWmFTlmZC8q8JEkOB5IQsrWia2eOR6IsqDcdKO\\/Urgns9\\/BdQi8KnchBKSEFc1iUtcOEruvhunKGyW5zI\\/Rltfdz3xGH8tlw+YlMXeWXPnqgOJ9GzNA0lwG4U421L6yylYagW7oxIznnBLB4bO46vYZbgXZV1hiI9ZyveHOinLMY1QkmTj5CNvnx3\\/VJwLovd0v+0Nr2vu\\/3ftbpBXc6L1bsNjlRqtsfwJlcgl+tH1DC4W8tKf+Y3tdtzVw0CHXCuacxHLyq5wZd\\/5YfYR9SJQ\\/jInU4PHA5+hIE3PGqNUp5QfFE0umq56H7MQKsIPgM5mMV4fPAA8OpltuMVDvQYUxalrnvoTf00k90x1wCTK71\\/jQGh7r7PmGvSdfPr+65eVTjITD8\\/lTGIb8850v1fl3\\/i2R8Dv17jQIRyX5o9MXPSO6jHo4Swma5RzPA\\/0bRj6qRTyPkM1L9qEIr+2H2I7KKhT2ZE5GhAU7yI9A3VLBWzpTrUPMGbfpd1OjVTEqXAdMjpLDYI3Mh5zQ58p8xCzt+W+t0=\"}", + "id": "https://my-place.social/objects/e599373b-1368-4b20-cd24-837166957182", + "instrument": { + "id": "https://my-place.social/friendica", + "name": "Friendica 'Interrupted Fern' 2024.12-1576", + "type": "Application", + "url": "https://my-place.social" + }, + "object": "https://pl.kotobank.ch/objects/301bce65-8a1b-4c49-a65c-fe2ce861a213", + "published": "2025-06-12T18:47:41Z", + "signature": { + "created": "2025-06-12T18:47:42Z", + "creator": "https://my-place.social/profile/vaartis#main-key", + "nonce": "84e496f80b09d7a299c5cc89e8cadd13abf621b3a0a321684fa74278b68a6dd8", + "signatureValue": "qe2WxY+j7daIYLRadCctgal6A1s9XgoiMfM/8KjJm15w0sSizYYqruyQ5gS44e+cj5GHc9v5gP2ieod5v7eHAPzlcDI4bfkcyHVapAXTqU67ZebW+v6Q+21IMDgqrkYCv5TbV7LTxltW59dlqovpHE4TEe/M7xLKWJ3vVchRUcWqH9kDmak0nacoqYVAb5E9jYnQhUWPTCfPV82qQpeWQPOZ4iIvPw6rDkSSY5jL6bCogBZblHGpUjXfe/FPlacaCWiTQdoga3yOBXB1RYPw9nh5FI5Xkv/oi+52WmJrECinlD6AL8/BpiYvKz236zy7p/TR4BXlCx9RR/msjOnSabkQ4kmYFrRr80UDCGF+CdkdzLl8K9rSE3PbF1+nEqD7X0GOWn/DdtixqXJw6IR4bh32YW2SlcrSRBvI1p82Mv68BeqRaYqL6FAhKFwLhX5JpXngZ3k0g7rWWxc498voPWnFZDyCTRNxO9VIIUavDDEQ0BdFk6WDb8zx9tsAg8JoK57eVDcFly7tfVQffYiHpve06d8ag1DtzipqguRsURmuqpGNMq28XBTxwtrP2LnXXHKxoYN/YQ9cDnCKclbx7/uKmOVMLkLZlM0wAVoZpm5z2fG4voKqFiGZ1PoiFY2sN4URMArJtygV3PlTX4ASAQrak0ksvEo9msrBUD0Su9c=", + "type": "RsaSignature2017" + }, + "to": [ + "https://pl.kotobank.ch/users/vaartis", + "https://mitra.social/users/silverpill", + "https://www.w3.org/ns/activitystreams#Public" + ], + "type": "Dislike" +} diff --git a/test/pleroma/web/activity_pub/transmogrifier/like_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/like_handling_test.exs index fc04c13917..27f8522ce6 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/like_handling_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/like_handling_test.exs @@ -143,4 +143,40 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.LikeHandlingTest do assert {:ok, activity} = Transmogrifier.handle_incoming(data) assert activity.data["type"] == "Like" end + + test "it changes incoming dislikes into emoji reactions" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) + + data = + File.read!("test/fixtures/friendica-dislike.json") + |> Jason.decode!() + |> Map.put("object", activity.data["object"]) + + _actor = insert(:user, ap_id: data["actor"], local: false) + + {:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data) + + refute Enum.empty?(activity.recipients) + + assert data["actor"] == "https://my-place.social/profile/vaartis" + assert data["type"] == "EmojiReact" + assert data["content"] == "👎" + assert data["id"] == "https://my-place.social/objects/e599373b-1368-4b20-cd24-837166957182" + assert data["object"] == activity.data["object"] + + data = + File.read!("test/fixtures/friendica-dislike-undo.json") + |> Jason.decode!() + |> put_in(["object", "object"], activity.data["object"]) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["actor"] == "https://my-place.social/profile/vaartis" + assert data["type"] == "Undo" + + assert data["object"] == + "https://my-place.social/objects/e599373b-1368-4b20-cd24-837166957182" + end end From 0151d99202749b5ccfe01beda3704e40b0f52548 Mon Sep 17 00:00:00 2001 From: Ekaterina Vaartis Date: Wed, 18 Jun 2025 17:36:08 +0300 Subject: [PATCH 20/25] Use manually created variables for CI instead of CI_JOB_TOKEN For protected branches, it seems now just CI_JOB_TOKEN is not enough. https://gitlab.com/gitlab-org/gitlab-foss/-/issues/36898#note_38415655 According to this, the CI_JOB_TOKEN is based on whoever created the job and creating a pipeline on a protected branch requires special permissions. Somehow this still did not work for other people who merged, even though they had access to the docs repo. --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 29ee24a054..bfd9bf4147 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -208,7 +208,7 @@ docs-deploy: before_script: - apk add curl script: - - curl --fail-with-body -X POST -F"token=$CI_JOB_TOKEN" -F'ref=master' -F"variables[BRANCH]=$CI_COMMIT_REF_NAME" https://git.pleroma.social/api/v4/projects/673/trigger/pipeline + - curl --fail-with-body -X POST -F"token=$DOCS_PIPELINE_TRIGGER" -F'ref=master' -F"variables[BRANCH]=$CI_COMMIT_REF_NAME" https://git.pleroma.social/api/v4/projects/673/trigger/pipeline review_app: image: alpine:3.9 stage: deploy @@ -249,7 +249,7 @@ spec-deploy: before_script: - apk add curl script: - - curl --fail-with-body -X POST -F"token=$CI_JOB_TOKEN" -F'ref=master' -F"variables[BRANCH]=$CI_COMMIT_REF_NAME" -F"variables[JOB_REF]=$CI_JOB_ID" https://git.pleroma.social/api/v4/projects/1130/trigger/pipeline + - curl --fail-with-body -X POST -F"token=$API_DOCS_PIPELINE_TRIGGER" -F'ref=master' -F"variables[BRANCH]=$CI_COMMIT_REF_NAME" -F"variables[JOB_REF]=$CI_JOB_ID" https://git.pleroma.social/api/v4/projects/1130/trigger/pipeline stop_review_app: From 37d4ed883c1b042df12facaba82d44929008b918 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 19 Jun 2025 14:50:45 -0700 Subject: [PATCH 21/25] Change MRF logic to match when there is an inReplyTo and the public address is in the "to" field Update the method to alter the to/cc fields for consistency and modify the tests to work without requiring a specific order items in the list --- .../web/activity_pub/mrf/quiet_reply.ex | 5 +- .../web/activity_pub/mrf/quiet_reply_test.exs | 13 +- .../o_auth/oauth_authorization_flow_test.exs | 339 ++++++++++++++++++ 3 files changed, 347 insertions(+), 10 deletions(-) create mode 100644 test/pleroma/web/o_auth/oauth_authorization_flow_test.exs diff --git a/lib/pleroma/web/activity_pub/mrf/quiet_reply.ex b/lib/pleroma/web/activity_pub/mrf/quiet_reply.ex index 66080c47df..b3eb0b3909 100644 --- a/lib/pleroma/web/activity_pub/mrf/quiet_reply.ex +++ b/lib/pleroma/web/activity_pub/mrf/quiet_reply.ex @@ -29,12 +29,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.QuietReply do } = activity ) do with true <- is_binary(in_reply_to), - false <- match?([], cc), + true <- Pleroma.Constants.as_public() in to, %User{follower_address: followers_collection, local: true} <- User.get_by_ap_id(actor) do updated_to = - to - |> Kernel.++([followers_collection]) + [followers_collection | to] |> Kernel.--([Pleroma.Constants.as_public()]) updated_cc = diff --git a/test/pleroma/web/activity_pub/mrf/quiet_reply_test.exs b/test/pleroma/web/activity_pub/mrf/quiet_reply_test.exs index 79e64d650c..f66383bf5e 100644 --- a/test/pleroma/web/activity_pub/mrf/quiet_reply_test.exs +++ b/test/pleroma/web/activity_pub/mrf/quiet_reply_test.exs @@ -39,15 +39,14 @@ defmodule Pleroma.Web.ActivityPub.MRF.QuietReplyTest do } } - expected_to = [batman.ap_id, robin.follower_address] - expected_cc = [Pleroma.Constants.as_public()] - assert {:ok, filtered} = QuietReply.filter(reply) - assert expected_to == filtered["to"] - assert expected_cc == filtered["cc"] - assert expected_to == filtered["object"]["to"] - assert expected_cc == filtered["object"]["cc"] + assert batman.ap_id in filtered["to"] + assert batman.ap_id in filtered["object"]["to"] + assert robin.follower_address in filtered["to"] + assert robin.follower_address in filtered["object"]["to"] + assert Pleroma.Constants.as_public() in filtered["cc"] + assert Pleroma.Constants.as_public() in filtered["object"]["cc"] end test "replying to unlisted post is unmodified" do diff --git a/test/pleroma/web/o_auth/oauth_authorization_flow_test.exs b/test/pleroma/web/o_auth/oauth_authorization_flow_test.exs new file mode 100644 index 0000000000..fdd8cbdb44 --- /dev/null +++ b/test/pleroma/web/o_auth/oauth_authorization_flow_test.exs @@ -0,0 +1,339 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.OAuthAuthorizationFlowTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + + alias Pleroma.Helpers.AuthHelper + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.OAuth.App + alias Pleroma.Web.OAuth.Authorization + alias Pleroma.Web.OAuth.OAuthController + alias Pleroma.Web.OAuth.Token + + @session_opts [ + store: :cookie, + key: "_test", + signing_salt: "cooldude" + ] + + setup do + clear_config([:instance, :account_activation_required], false) + clear_config([:instance, :account_approval_required], false) + end + + describe "OAuth authorization flow with external integration" do + test "complete OAuth flow: create user, create app, authorize, get token, use token" do + # Step 1: Create a user + user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("test")) + + # Step 2: Create a new OAuth client with the required scopes + app = + insert(:oauth_app, + scopes: ["read", "write", "follow", "push"], + redirect_uris: "urn:ietf:wg:oauth:2.0:oob" + ) + + # Step 3: Set up a logged in session + conn = + build_conn() + |> Plug.Session.call(Plug.Session.init(@session_opts)) + |> fetch_session() + |> AuthHelper.put_session_token(insert(:oauth_token, user: user).token) + + # Step 4: Access the /oauth/authorize endpoint with the specified parameters + authorize_params = %{ + "client_id" => app.client_id, + "response_type" => "code", + "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", + "scope" => "read write follow push", + "force_login" => "False", + "state" => "None", + "lang" => "None" + } + + # First, get the authorization page + conn = get(conn, "/oauth/authorize", authorize_params) + assert html_response(conn, 200) + + # Step 5: Submit the authorization (simulate user approving the app) + authorization_data = %{ + "authorization" => %{ + "client_id" => app.client_id, + "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", + "scope" => "read write follow push", + "state" => "None" + } + } + + conn = post(conn, "/oauth/authorize", authorization_data) + + # Should get the OOB authorization page with the code + assert html_response(conn, 200) + + # Extract the authorization code from the response + response = html_response(conn, 200) + assert response =~ "Successfully authorized" + assert response =~ "Token code is" + + # Parse the authorization code from the response + code_match = Regex.run(~r/Token code is
([a-zA-Z0-9_-]+)/, response) + assert code_match + [_, authorization_code] = code_match + + # Step 6: Exchange the authorization code for an access token + token_conn = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "authorization_code", + "code" => authorization_code, + "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + + token_response = json_response(token_conn, 200) + assert %{"access_token" => access_token, "token_type" => "Bearer"} = token_response + assert token_response["scope"] == "read write follow push" + + # Verify the token was created in the database + token_record = Repo.get_by(Token, token: access_token) + assert token_record + assert token_record.scopes == ["read", "write", "follow", "push"] + assert token_record.user_id == user.id + assert token_record.app_id == app.id + + # Step 7: Use the token to access a protected endpoint + protected_conn = + build_conn() + |> put_req_header("authorization", "Bearer #{access_token}") + |> get("/api/v1/accounts/verify_credentials") + + # Should get a 200 response with user information + user_info = json_response(protected_conn, 200) + assert user_info["id"] == to_string(user.id) + assert user_info["username"] == user.nickname + assert user_info["acct"] == user.nickname + + # Step 8: Test that the token has the correct scopes by accessing different endpoints + # Test read:accounts scope (should work) + conn_with_token = + build_conn() + |> put_req_header("authorization", "Bearer #{access_token}") + + # This should work because we have "read" scope + conn_with_token + |> get("/api/v1/accounts/#{user.id}") + |> json_response(200) + + # Test write:accounts scope (should work) - with proper content-type + conn_with_token + |> put_req_header("content-type", "application/json") + |> patch("/api/v1/accounts/update_credentials", %{"display_name" => "Test Name"}) + |> json_response(200) + + # Test that the token is properly associated with the user + assert token_record.user_id == user.id + assert token_record.app_id == app.id + end + + test "OAuth flow with force_login=false and existing session" do + # Create a user and app + user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("test")) + + app = + insert(:oauth_app, + scopes: ["read", "write", "follow", "push"], + redirect_uris: "urn:ietf:wg:oauth:2.0:oob" + ) + + # Create an existing token for the same user and app + existing_token = insert(:oauth_token, user: user, app: app, scopes: ["read", "write"]) + + # Set up a logged in session with the existing token + conn = + build_conn() + |> Plug.Session.call(Plug.Session.init(@session_opts)) + |> fetch_session() + |> AuthHelper.put_session_token(existing_token.token) + + # Access the authorize endpoint with force_login=false + authorize_params = %{ + "client_id" => app.client_id, + "response_type" => "code", + "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", + "scope" => "read write follow push", + "force_login" => "False", + "state" => "test_state" + } + + # Should redirect to the OOB page with the existing token + conn = get(conn, "/oauth/authorize", authorize_params) + assert html_response(conn, 200) + assert html_response(conn, 200) =~ "Authorization exists" + end + + test "OAuth flow with different scopes than existing token" do + # Create a user and app + user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("test")) + + app = + insert(:oauth_app, + scopes: ["read", "write", "follow", "push"], + redirect_uris: "urn:ietf:wg:oauth:2.0:oob" + ) + + # Create an existing token with different scopes + existing_token = insert(:oauth_token, user: user, app: app, scopes: ["read"]) + + # Set up a logged in session + conn = + build_conn() + |> Plug.Session.call(Plug.Session.init(@session_opts)) + |> fetch_session() + |> AuthHelper.put_session_token(existing_token.token) + + # Access the authorize endpoint requesting more scopes + authorize_params = %{ + "client_id" => app.client_id, + "response_type" => "code", + "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", + "scope" => "read write follow push", + "force_login" => "False", + "state" => "test_state" + } + + # Should show the authorization page because scopes are different + conn = get(conn, "/oauth/authorize", authorize_params) + assert html_response(conn, 200) + assert html_response(conn, 200) =~ "Authorization exists" + end + + test "OAuth flow with invalid client_id" do + user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("test")) + + conn = + build_conn() + |> Plug.Session.call(Plug.Session.init(@session_opts)) + |> fetch_session() + |> AuthHelper.put_session_token(insert(:oauth_token, user: user).token) + + # Try to authorize with invalid client_id + authorize_params = %{ + "client_id" => "invalid_client_id", + "response_type" => "code", + "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", + "scope" => "read write follow push", + "force_login" => "False" + } + + conn = get(conn, "/oauth/authorize", authorize_params) + # Should still render the page but with error or missing app info + assert html_response(conn, 200) + end + + test "OAuth flow with unlisted redirect_uri" do + user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("test")) + + app = + insert(:oauth_app, + scopes: ["read", "write", "follow", "push"], + # Different from requested + redirect_uris: "https://example.com/callback" + ) + + conn = + build_conn() + |> Plug.Session.call(Plug.Session.init(@session_opts)) + |> fetch_session() + |> AuthHelper.put_session_token(insert(:oauth_token, user: user).token) + + # Try to authorize with unlisted redirect_uri + authorize_params = %{ + "client_id" => app.client_id, + "response_type" => "code", + "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", + "scope" => "read write follow push", + "force_login" => "False" + } + + conn = get(conn, "/oauth/authorize", authorize_params) + # Should still render the page but with error about unlisted redirect_uri + assert html_response(conn, 200) + end + + test "OAuth flow with expired authorization code" do + user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("test")) + + app = + insert(:oauth_app, + scopes: ["read", "write", "follow", "push"], + redirect_uris: "urn:ietf:wg:oauth:2.0:oob" + ) + + # Create an expired authorization + expired_auth = + insert(:oauth_authorization, + user: user, + app: app, + # 1 hour ago + valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), -3600), + scopes: ["read", "write", "follow", "push"] + ) + + # Try to exchange expired code for token + conn = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "authorization_code", + "code" => expired_auth.token, + "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + + # Should get an error + response = json_response(conn, 400) + assert %{"error" => _} = response + end + + test "OAuth flow with used authorization code" do + user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("test")) + + app = + insert(:oauth_app, + scopes: ["read", "write", "follow", "push"], + redirect_uris: "urn:ietf:wg:oauth:2.0:oob" + ) + + # Create an authorization and mark it as used + auth = + insert(:oauth_authorization, + user: user, + app: app, + scopes: ["read", "write", "follow", "push"] + ) + + {:ok, _} = Authorization.use_token(auth) + + # Try to exchange used code for token + conn = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "authorization_code", + "code" => auth.token, + "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + + # Should get an error + response = json_response(conn, 400) + assert %{"error" => _} = response + end + end +end From 9d6f201e5eb37c74490fc47b9b9c98575c6803e6 Mon Sep 17 00:00:00 2001 From: Pleroma User <66706-pleromian@users.noreply.git.pleroma.social> Date: Fri, 20 Jun 2025 21:22:27 +0000 Subject: [PATCH 22/25] Add tos setting --- changelog.d/tos-setting.add | 1 + config/config.exs | 1 + config/description.exs | 7 +++++++ 3 files changed, 9 insertions(+) create mode 100644 changelog.d/tos-setting.add diff --git a/changelog.d/tos-setting.add b/changelog.d/tos-setting.add new file mode 100644 index 0000000000..db9b0d5f28 --- /dev/null +++ b/changelog.d/tos-setting.add @@ -0,0 +1 @@ +Allow Terms of Service panel behaviour to be configurable diff --git a/config/config.exs b/config/config.exs index a231c5ba04..31d7258ee7 100644 --- a/config/config.exs +++ b/config/config.exs @@ -307,6 +307,7 @@ config :pleroma, :frontend_configurations, collapseMessageWithSubject: false, disableChat: false, greentext: false, + embeddedToS: true, hideFilteredStatuses: false, hideMutedPosts: false, hidePostStats: false, diff --git a/config/description.exs b/config/description.exs index 2f7dc30a09..e20fa4b28a 100644 --- a/config/description.exs +++ b/config/description.exs @@ -1261,6 +1261,7 @@ config :pleroma, :config_description, [ background: "/static/aurora_borealis.jpg", collapseMessageWithSubject: false, greentext: false, + embeddedToS: true, hideFilteredStatuses: false, hideMutedPosts: false, hidePostStats: false, @@ -1312,6 +1313,12 @@ config :pleroma, :config_description, [ type: :boolean, description: "Enables green text on lines prefixed with the > character" }, + %{ + key: :embeddedToS, + label: "Embedded ToS panel", + type: :boolean, + description: "Hide Terms of Service panel decorations on About and Registration pages" + }, %{ key: :hideFilteredStatuses, label: "Hide Filtered Statuses", From 56aab905e898429b7e2744c3ed2afc96f2a9e97c Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 27 Jun 2025 10:56:04 -0700 Subject: [PATCH 23/25] Queue individual jobs for each user that needs to be deleted when deleting an instance. --- changelog.d/delete-instance.change | 1 + lib/pleroma/instances/instance.ex | 13 ------- lib/pleroma/workers/delete_worker.ex | 11 ++++-- test/pleroma/workers/delete_worker_test.exs | 39 +++++++++++++++++++++ 4 files changed, 49 insertions(+), 15 deletions(-) create mode 100644 changelog.d/delete-instance.change create mode 100644 test/pleroma/workers/delete_worker_test.exs diff --git a/changelog.d/delete-instance.change b/changelog.d/delete-instance.change new file mode 100644 index 0000000000..9d84dac54f --- /dev/null +++ b/changelog.d/delete-instance.change @@ -0,0 +1 @@ +Deleting an instance queues individual jobs for each user that needs to be deleted from the server. diff --git a/lib/pleroma/instances/instance.ex b/lib/pleroma/instances/instance.ex index 33f1229d02..7bf38deeea 100644 --- a/lib/pleroma/instances/instance.ex +++ b/lib/pleroma/instances/instance.ex @@ -9,7 +9,6 @@ defmodule Pleroma.Instances.Instance do alias Pleroma.Instances.Instance alias Pleroma.Maps alias Pleroma.Repo - alias Pleroma.User alias Pleroma.Workers.DeleteWorker use Ecto.Schema @@ -300,16 +299,4 @@ defmodule Pleroma.Instances.Instance do DeleteWorker.new(%{"op" => "delete_instance", "host" => host}) |> Oban.insert() end - - def perform(:delete_instance, host) when is_binary(host) do - User.Query.build(%{nickname: "@#{host}"}) - |> Repo.chunk_stream(100, :batches) - |> Stream.each(fn users -> - users - |> Enum.each(fn user -> - User.perform(:delete, user) - end) - end) - |> Stream.run() - end end diff --git a/lib/pleroma/workers/delete_worker.ex b/lib/pleroma/workers/delete_worker.ex index 6a1c7bb383..4f52edd28b 100644 --- a/lib/pleroma/workers/delete_worker.ex +++ b/lib/pleroma/workers/delete_worker.ex @@ -3,7 +3,6 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Workers.DeleteWorker do - alias Pleroma.Instances.Instance alias Pleroma.User use Oban.Worker, queue: :slow @@ -15,7 +14,15 @@ defmodule Pleroma.Workers.DeleteWorker do end def perform(%Job{args: %{"op" => "delete_instance", "host" => host}}) do - Instance.perform(:delete_instance, host) + Pleroma.Repo.transaction(fn -> + User.Query.build(%{nickname: "@#{host}"}) + |> Pleroma.Repo.all() + |> Enum.each(fn user -> + %{"op" => "delete_user", "user_id" => user.id} + |> __MODULE__.new() + |> Oban.insert() + end) + end) end @impl true diff --git a/test/pleroma/workers/delete_worker_test.exs b/test/pleroma/workers/delete_worker_test.exs new file mode 100644 index 0000000000..b914aaee28 --- /dev/null +++ b/test/pleroma/workers/delete_worker_test.exs @@ -0,0 +1,39 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.DeleteWorkerTest do + use Pleroma.DataCase, async: true + use Oban.Testing, repo: Pleroma.Repo + + import Pleroma.Factory + + alias Pleroma.Instances.Instance + alias Pleroma.Tests.ObanHelpers + alias Pleroma.Workers.DeleteWorker + + describe "instance deletion" do + test "creates individual Oban jobs for each user when deleting an instance" do + user1 = insert(:user, nickname: "alice@example.com", name: "Alice") + user2 = insert(:user, nickname: "bob@example.com", name: "Bob") + + {:ok, job} = Instance.delete_users_and_activities("example.com") + + assert_enqueued( + worker: DeleteWorker, + args: %{"op" => "delete_instance", "host" => "example.com"} + ) + + {:ok, :ok} = ObanHelpers.perform(job) + + delete_user_jobs = all_enqueued(worker: DeleteWorker, args: %{"op" => "delete_user"}) + + assert length(delete_user_jobs) == 2 + + user_ids = [user1.id, user2.id] + job_user_ids = Enum.map(delete_user_jobs, fn job -> job.args["user_id"] end) + + assert Enum.sort(user_ids) == Enum.sort(job_user_ids) + end + end +end From 81155a229216b2d58c63f5a0f418d1354ced6990 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 19 Jun 2025 14:53:37 -0700 Subject: [PATCH 24/25] changelog for MRF.QuietReply --- changelog.d/mrf-quietreply.add | 1 + .../o_auth/oauth_authorization_flow_test.exs | 339 ------------------ 2 files changed, 1 insertion(+), 339 deletions(-) create mode 100644 changelog.d/mrf-quietreply.add delete mode 100644 test/pleroma/web/o_auth/oauth_authorization_flow_test.exs diff --git a/changelog.d/mrf-quietreply.add b/changelog.d/mrf-quietreply.add new file mode 100644 index 0000000000..4ed20bce60 --- /dev/null +++ b/changelog.d/mrf-quietreply.add @@ -0,0 +1 @@ +Added MRF.QuietReply which prevents replies to public posts from being published to the timelines diff --git a/test/pleroma/web/o_auth/oauth_authorization_flow_test.exs b/test/pleroma/web/o_auth/oauth_authorization_flow_test.exs deleted file mode 100644 index fdd8cbdb44..0000000000 --- a/test/pleroma/web/o_auth/oauth_authorization_flow_test.exs +++ /dev/null @@ -1,339 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2022 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.OAuthAuthorizationFlowTest do - use Pleroma.Web.ConnCase - - import Pleroma.Factory - - alias Pleroma.Helpers.AuthHelper - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.OAuth.App - alias Pleroma.Web.OAuth.Authorization - alias Pleroma.Web.OAuth.OAuthController - alias Pleroma.Web.OAuth.Token - - @session_opts [ - store: :cookie, - key: "_test", - signing_salt: "cooldude" - ] - - setup do - clear_config([:instance, :account_activation_required], false) - clear_config([:instance, :account_approval_required], false) - end - - describe "OAuth authorization flow with external integration" do - test "complete OAuth flow: create user, create app, authorize, get token, use token" do - # Step 1: Create a user - user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("test")) - - # Step 2: Create a new OAuth client with the required scopes - app = - insert(:oauth_app, - scopes: ["read", "write", "follow", "push"], - redirect_uris: "urn:ietf:wg:oauth:2.0:oob" - ) - - # Step 3: Set up a logged in session - conn = - build_conn() - |> Plug.Session.call(Plug.Session.init(@session_opts)) - |> fetch_session() - |> AuthHelper.put_session_token(insert(:oauth_token, user: user).token) - - # Step 4: Access the /oauth/authorize endpoint with the specified parameters - authorize_params = %{ - "client_id" => app.client_id, - "response_type" => "code", - "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", - "scope" => "read write follow push", - "force_login" => "False", - "state" => "None", - "lang" => "None" - } - - # First, get the authorization page - conn = get(conn, "/oauth/authorize", authorize_params) - assert html_response(conn, 200) - - # Step 5: Submit the authorization (simulate user approving the app) - authorization_data = %{ - "authorization" => %{ - "client_id" => app.client_id, - "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", - "scope" => "read write follow push", - "state" => "None" - } - } - - conn = post(conn, "/oauth/authorize", authorization_data) - - # Should get the OOB authorization page with the code - assert html_response(conn, 200) - - # Extract the authorization code from the response - response = html_response(conn, 200) - assert response =~ "Successfully authorized" - assert response =~ "Token code is" - - # Parse the authorization code from the response - code_match = Regex.run(~r/Token code is
([a-zA-Z0-9_-]+)/, response) - assert code_match - [_, authorization_code] = code_match - - # Step 6: Exchange the authorization code for an access token - token_conn = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "authorization_code", - "code" => authorization_code, - "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - - token_response = json_response(token_conn, 200) - assert %{"access_token" => access_token, "token_type" => "Bearer"} = token_response - assert token_response["scope"] == "read write follow push" - - # Verify the token was created in the database - token_record = Repo.get_by(Token, token: access_token) - assert token_record - assert token_record.scopes == ["read", "write", "follow", "push"] - assert token_record.user_id == user.id - assert token_record.app_id == app.id - - # Step 7: Use the token to access a protected endpoint - protected_conn = - build_conn() - |> put_req_header("authorization", "Bearer #{access_token}") - |> get("/api/v1/accounts/verify_credentials") - - # Should get a 200 response with user information - user_info = json_response(protected_conn, 200) - assert user_info["id"] == to_string(user.id) - assert user_info["username"] == user.nickname - assert user_info["acct"] == user.nickname - - # Step 8: Test that the token has the correct scopes by accessing different endpoints - # Test read:accounts scope (should work) - conn_with_token = - build_conn() - |> put_req_header("authorization", "Bearer #{access_token}") - - # This should work because we have "read" scope - conn_with_token - |> get("/api/v1/accounts/#{user.id}") - |> json_response(200) - - # Test write:accounts scope (should work) - with proper content-type - conn_with_token - |> put_req_header("content-type", "application/json") - |> patch("/api/v1/accounts/update_credentials", %{"display_name" => "Test Name"}) - |> json_response(200) - - # Test that the token is properly associated with the user - assert token_record.user_id == user.id - assert token_record.app_id == app.id - end - - test "OAuth flow with force_login=false and existing session" do - # Create a user and app - user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("test")) - - app = - insert(:oauth_app, - scopes: ["read", "write", "follow", "push"], - redirect_uris: "urn:ietf:wg:oauth:2.0:oob" - ) - - # Create an existing token for the same user and app - existing_token = insert(:oauth_token, user: user, app: app, scopes: ["read", "write"]) - - # Set up a logged in session with the existing token - conn = - build_conn() - |> Plug.Session.call(Plug.Session.init(@session_opts)) - |> fetch_session() - |> AuthHelper.put_session_token(existing_token.token) - - # Access the authorize endpoint with force_login=false - authorize_params = %{ - "client_id" => app.client_id, - "response_type" => "code", - "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", - "scope" => "read write follow push", - "force_login" => "False", - "state" => "test_state" - } - - # Should redirect to the OOB page with the existing token - conn = get(conn, "/oauth/authorize", authorize_params) - assert html_response(conn, 200) - assert html_response(conn, 200) =~ "Authorization exists" - end - - test "OAuth flow with different scopes than existing token" do - # Create a user and app - user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("test")) - - app = - insert(:oauth_app, - scopes: ["read", "write", "follow", "push"], - redirect_uris: "urn:ietf:wg:oauth:2.0:oob" - ) - - # Create an existing token with different scopes - existing_token = insert(:oauth_token, user: user, app: app, scopes: ["read"]) - - # Set up a logged in session - conn = - build_conn() - |> Plug.Session.call(Plug.Session.init(@session_opts)) - |> fetch_session() - |> AuthHelper.put_session_token(existing_token.token) - - # Access the authorize endpoint requesting more scopes - authorize_params = %{ - "client_id" => app.client_id, - "response_type" => "code", - "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", - "scope" => "read write follow push", - "force_login" => "False", - "state" => "test_state" - } - - # Should show the authorization page because scopes are different - conn = get(conn, "/oauth/authorize", authorize_params) - assert html_response(conn, 200) - assert html_response(conn, 200) =~ "Authorization exists" - end - - test "OAuth flow with invalid client_id" do - user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("test")) - - conn = - build_conn() - |> Plug.Session.call(Plug.Session.init(@session_opts)) - |> fetch_session() - |> AuthHelper.put_session_token(insert(:oauth_token, user: user).token) - - # Try to authorize with invalid client_id - authorize_params = %{ - "client_id" => "invalid_client_id", - "response_type" => "code", - "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", - "scope" => "read write follow push", - "force_login" => "False" - } - - conn = get(conn, "/oauth/authorize", authorize_params) - # Should still render the page but with error or missing app info - assert html_response(conn, 200) - end - - test "OAuth flow with unlisted redirect_uri" do - user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("test")) - - app = - insert(:oauth_app, - scopes: ["read", "write", "follow", "push"], - # Different from requested - redirect_uris: "https://example.com/callback" - ) - - conn = - build_conn() - |> Plug.Session.call(Plug.Session.init(@session_opts)) - |> fetch_session() - |> AuthHelper.put_session_token(insert(:oauth_token, user: user).token) - - # Try to authorize with unlisted redirect_uri - authorize_params = %{ - "client_id" => app.client_id, - "response_type" => "code", - "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", - "scope" => "read write follow push", - "force_login" => "False" - } - - conn = get(conn, "/oauth/authorize", authorize_params) - # Should still render the page but with error about unlisted redirect_uri - assert html_response(conn, 200) - end - - test "OAuth flow with expired authorization code" do - user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("test")) - - app = - insert(:oauth_app, - scopes: ["read", "write", "follow", "push"], - redirect_uris: "urn:ietf:wg:oauth:2.0:oob" - ) - - # Create an expired authorization - expired_auth = - insert(:oauth_authorization, - user: user, - app: app, - # 1 hour ago - valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), -3600), - scopes: ["read", "write", "follow", "push"] - ) - - # Try to exchange expired code for token - conn = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "authorization_code", - "code" => expired_auth.token, - "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - - # Should get an error - response = json_response(conn, 400) - assert %{"error" => _} = response - end - - test "OAuth flow with used authorization code" do - user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("test")) - - app = - insert(:oauth_app, - scopes: ["read", "write", "follow", "push"], - redirect_uris: "urn:ietf:wg:oauth:2.0:oob" - ) - - # Create an authorization and mark it as used - auth = - insert(:oauth_authorization, - user: user, - app: app, - scopes: ["read", "write", "follow", "push"] - ) - - {:ok, _} = Authorization.use_token(auth) - - # Try to exchange used code for token - conn = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "authorization_code", - "code" => auth.token, - "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - - # Should get an error - response = json_response(conn, 400) - assert %{"error" => _} = response - end - end -end From ca616e9e73aae9a3f27a44db928bdfe82add21d1 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 27 Jun 2025 12:10:25 -0700 Subject: [PATCH 25/25] Fix Instance and Admin API controller tests for deleting instances Ensure the job was queued, remove the other test validation. We already prove elsewhere that Pleroma.User.delete/1 works, so repeating that here is a waste. --- test/pleroma/instances/instance_test.exs | 35 +++++-------------- .../controllers/instance_controller_test.exs | 14 ++++---- 2 files changed, 14 insertions(+), 35 deletions(-) diff --git a/test/pleroma/instances/instance_test.exs b/test/pleroma/instances/instance_test.exs index 6a718be21c..4b03655cb3 100644 --- a/test/pleroma/instances/instance_test.exs +++ b/test/pleroma/instances/instance_test.exs @@ -6,9 +6,8 @@ defmodule Pleroma.Instances.InstanceTest do alias Pleroma.Instances alias Pleroma.Instances.Instance alias Pleroma.Repo - alias Pleroma.Tests.ObanHelpers - alias Pleroma.Web.CommonAPI + use Oban.Testing, repo: Pleroma.Repo use Pleroma.DataCase import ExUnit.CaptureLog @@ -213,32 +212,14 @@ defmodule Pleroma.Instances.InstanceTest do end end - test "delete_users_and_activities/1 deletes remote instance users and activities" do - [mario, luigi, _peach, wario] = - users = [ - insert(:user, nickname: "mario@mushroom.kingdom", name: "Mario"), - insert(:user, nickname: "luigi@mushroom.kingdom", name: "Luigi"), - insert(:user, nickname: "peach@mushroom.kingdom", name: "Peach"), - insert(:user, nickname: "wario@greedville.biz", name: "Wario") - ] + test "delete_users_and_activities/1 schedules a job to delete the instance and users" do + insert(:user, nickname: "mario@mushroom.kingdom", name: "Mario") - {:ok, post1} = CommonAPI.post(mario, %{status: "letsa go!"}) - {:ok, post2} = CommonAPI.post(luigi, %{status: "itsa me... luigi"}) - {:ok, post3} = CommonAPI.post(wario, %{status: "WHA-HA-HA!"}) + {:ok, _job} = Instance.delete_users_and_activities("mushroom.kingdom") - {:ok, job} = Instance.delete_users_and_activities("mushroom.kingdom") - :ok = ObanHelpers.perform(job) - - [mario, luigi, peach, wario] = Repo.reload(users) - - refute mario.is_active - refute luigi.is_active - refute peach.is_active - refute peach.name == "Peach" - - assert wario.is_active - assert wario.name == "Wario" - - assert [nil, nil, %{}] = Repo.reload([post1, post2, post3]) + assert_enqueued( + worker: Pleroma.Workers.DeleteWorker, + args: %{"op" => "delete_instance", "host" => "mushroom.kingdom"} + ) end end diff --git a/test/pleroma/web/admin_api/controllers/instance_controller_test.exs b/test/pleroma/web/admin_api/controllers/instance_controller_test.exs index 6cca623f35..5adcd069d3 100644 --- a/test/pleroma/web/admin_api/controllers/instance_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/instance_controller_test.exs @@ -8,8 +8,6 @@ defmodule Pleroma.Web.AdminAPI.InstanceControllerTest do import Pleroma.Factory - alias Pleroma.Repo - alias Pleroma.Tests.ObanHelpers alias Pleroma.Web.CommonAPI setup_all do @@ -69,19 +67,19 @@ defmodule Pleroma.Web.AdminAPI.InstanceControllerTest do test "DELETE /instances/:instance", %{conn: conn} do clear_config([:instance, :admin_privileges], [:instances_delete]) - user = insert(:user, nickname: "lain@lain.com") - post = insert(:note_activity, user: user) + insert(:user, nickname: "lain@lain.com") response = conn |> delete("/api/pleroma/admin/instances/lain.com") |> json_response(200) - [:ok] = ObanHelpers.perform_all() - assert response == "lain.com" - refute Repo.reload(user).is_active - refute Repo.reload(post) + + assert_enqueued( + worker: Pleroma.Workers.DeleteWorker, + args: %{"op" => "delete_instance", "host" => "lain.com"} + ) clear_config([:instance, :admin_privileges], [])