From 654a41b2c92acf7fe462c5452fc36d408e32b494 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 31 Oct 2026 15:37:41 +0400 Subject: [PATCH 1/4] fix(activitypub): accept emoji reactions with a single tag object Ported from Akkoma 462225880. Co-authored-by: Oneric --- changelog.d/emoji-react-single-tag.fix | 1 + .../emoji_react_validator.ex | 6 ++++- .../emoji_react_handling_test.exs | 25 +++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 changelog.d/emoji-react-single-tag.fix diff --git a/changelog.d/emoji-react-single-tag.fix b/changelog.d/emoji-react-single-tag.fix new file mode 100644 index 000000000..565775830 --- /dev/null +++ b/changelog.d/emoji-react-single-tag.fix @@ -0,0 +1 @@ +Accept incoming emoji reactions whose tag is a single object instead of an array diff --git a/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex b/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex index 65ba047e6..b7197c3f3 100644 --- a/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex @@ -57,7 +57,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do |> CommonFixes.fix_actor() |> CommonFixes.fix_activity_addressing() - data = Map.put_new(data, "tag", []) + data = fix_tag(data) case Object.normalize(data["object"]) do %Object{} = object -> @@ -70,6 +70,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do end end + # JSON-LD compaction turns a single-element array into a bare object + defp fix_tag(%{"tag" => %{} = tag} = data), do: Map.put(data, "tag", [tag]) + defp fix_tag(data), do: Map.put_new(data, "tag", []) + defp fix_emoji_qualification(%{"content" => emoji} = data) do new_emoji = Pleroma.Emoji.fully_qualify_emoji(emoji) diff --git a/test/pleroma/web/activity_pub/transmogrifier/emoji_react_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/emoji_react_handling_test.exs index f2e1cefa3..2e1841559 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/emoji_react_handling_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/emoji_react_handling_test.exs @@ -86,6 +86,31 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.EmojiReactHandlingTest do ) end + test "it works for incoming custom emoji reactions with a single tag object" do + user = insert(:user) + other_user = insert(:user, local: false) + {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) + + data = + File.read!("test/fixtures/custom-emoji-reaction.json") + |> Jason.decode!() + |> Map.put("object", activity.data["object"]) + |> Map.put("actor", other_user.ap_id) + + [tag] = data["tag"] + data = Map.put(data, "tag", tag) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["content"] == ":hanapog:" + assert [%{"name" => "hanapog"}] = data["tag"] + + object = Object.get_by_ap_id(data["object"]) + url = tag["icon"]["url"] + + assert match?([["hanapog", _, ^url]], object.data["reactions"]) + end + test "it works for incoming unqualified emoji reactions" do user = insert(:user) other_user = insert(:user, local: false) From e0546f188da23c9babb4589c2fc7286d4699cc0f Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 31 Oct 2026 15:37:41 +0400 Subject: [PATCH 2/4] fix(activitypub): do not require the personal inbox owner to be addressed Bridgy Fed delivers Follow and Accept to the personal inbox addressed only to Public. Any server can already address a local user directly, so the check gave no protection. Ported from Akkoma 2b885288f. Co-authored-by: Oneric --- changelog.d/personal-inbox-addressing.fix | 1 + .../activity_pub/activity_pub_controller.ex | 1 - lib/pleroma/web/activity_pub/utils.ex | 16 ----- .../activity_pub_controller_test.exs | 24 +++++++ test/pleroma/web/activity_pub/utils_test.exs | 68 ------------------- 5 files changed, 25 insertions(+), 85 deletions(-) create mode 100644 changelog.d/personal-inbox-addressing.fix diff --git a/changelog.d/personal-inbox-addressing.fix b/changelog.d/personal-inbox-addressing.fix new file mode 100644 index 000000000..9a2ba48a2 --- /dev/null +++ b/changelog.d/personal-inbox-addressing.fix @@ -0,0 +1 @@ +Accept activities delivered to a personal inbox that do not address the inbox owner (fixes Bridgy Fed follows) diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index dd5f6028f..a45f7bf27 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -341,7 +341,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do {:sender_exists, User.get_or_fetch_by_ap_id(params["actor"])}, {:recipient_active, true} <- {:recipient_active, recipient.is_active}, {:sender_active, true} <- {:sender_active, actor.is_active}, - true <- Utils.recipient_in_message(recipient, actor, params), params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do Federator.incoming_ap_doc(params) json(conn, "ok") diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 0af4ceaf5..5c25f4c69 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -78,22 +78,6 @@ defmodule Pleroma.Web.ActivityPub.Utils do [params["to"], params["cc"], params["bto"], params["bcc"]] |> Enum.any?(&label_in_collection?(label, &1)) - @spec unaddressed_message?(map()) :: boolean() - def unaddressed_message?(params), - do: - [params["to"], params["cc"], params["bto"], params["bcc"]] - |> Enum.all?(fn - nil -> true - [] -> true - _ -> false - end) - - @spec recipient_in_message(User.t(), User.t(), map()) :: boolean() - def recipient_in_message(%User{ap_id: ap_id} = recipient, %User{} = actor, params), - do: - label_in_message?(ap_id, params) || unaddressed_message?(params) || - User.following?(recipient, actor) - defp extract_list(target) when is_binary(target), do: [target] defp extract_list(lst) when is_list(lst), do: lst defp extract_list(_), do: [] diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index 606d47c8d..e1376b460 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -1872,6 +1872,30 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert Activity.get_by_ap_id(data["id"]) end + test "it accepts activities that do not address the inbox owner", %{conn: conn} do + user = insert(:user) + actor = insert(:user, local: false) + + data = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Follow", + "id" => "#{actor.ap_id}/follows/1", + "actor" => actor.ap_id, + "object" => user.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Public"] + } + + conn = + conn + |> assign_valid_signature_for_actor(data["actor"]) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/inbox", data) + + assert "ok" == json_response(conn, 200) + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + assert Activity.get_by_ap_id(data["id"]) + end + test "it returns an error when receiving an activity sent to a deactivated user", %{ conn: conn, data: data diff --git a/test/pleroma/web/activity_pub/utils_test.exs b/test/pleroma/web/activity_pub/utils_test.exs index 93234a015..88c8a283b 100644 --- a/test/pleroma/web/activity_pub/utils_test.exs +++ b/test/pleroma/web/activity_pub/utils_test.exs @@ -406,74 +406,6 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do end end - describe "recipient_in_message/3" do - test "returns true when recipient in `to`" do - recipient = insert(:user) - actor = insert(:user) - assert Utils.recipient_in_message(recipient, actor, %{"to" => recipient.ap_id}) - - assert Utils.recipient_in_message( - recipient, - actor, - %{"to" => [recipient.ap_id], "cc" => ""} - ) - end - - test "returns true when recipient in `cc`" do - recipient = insert(:user) - actor = insert(:user) - assert Utils.recipient_in_message(recipient, actor, %{"cc" => recipient.ap_id}) - - assert Utils.recipient_in_message( - recipient, - actor, - %{"cc" => [recipient.ap_id], "to" => ""} - ) - end - - test "returns true when recipient in `bto`" do - recipient = insert(:user) - actor = insert(:user) - assert Utils.recipient_in_message(recipient, actor, %{"bto" => recipient.ap_id}) - - assert Utils.recipient_in_message( - recipient, - actor, - %{"bcc" => "", "bto" => [recipient.ap_id]} - ) - end - - test "returns true when recipient in `bcc`" do - recipient = insert(:user) - actor = insert(:user) - assert Utils.recipient_in_message(recipient, actor, %{"bcc" => recipient.ap_id}) - - assert Utils.recipient_in_message( - recipient, - actor, - %{"bto" => "", "bcc" => [recipient.ap_id]} - ) - end - - test "returns true when message without addresses fields" do - recipient = insert(:user) - actor = insert(:user) - assert Utils.recipient_in_message(recipient, actor, %{"bccc" => recipient.ap_id}) - - assert Utils.recipient_in_message( - recipient, - actor, - %{"btod" => "", "bccc" => [recipient.ap_id]} - ) - end - - test "returns false" do - recipient = insert(:user) - actor = insert(:user) - refute Utils.recipient_in_message(recipient, actor, %{"to" => "ap_id"}) - end - end - describe "lazy_put_activity_defaults/2" do test "returns map with id and published data" do note_activity = insert(:note_activity) From 13663cba71d4e643b6c080caabc8e590c19001e0 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 31 Oct 2026 15:37:42 +0400 Subject: [PATCH 3/4] fix(mastodon_api): use the Mastodon date format for marker updated_at Ported from Akkoma 720785b4c. Co-authored-by: Oneric --- changelog.d/marker-updated-at-format.fix | 1 + lib/pleroma/web/mastodon_api/views/marker_view.ex | 4 +++- .../controllers/marker_controller_test.exs | 5 +++-- .../web/mastodon_api/views/marker_view_test.exs | 14 ++++++++++++-- 4 files changed, 19 insertions(+), 5 deletions(-) create mode 100644 changelog.d/marker-updated-at-format.fix diff --git a/changelog.d/marker-updated-at-format.fix b/changelog.d/marker-updated-at-format.fix new file mode 100644 index 000000000..2ff681525 --- /dev/null +++ b/changelog.d/marker-updated-at-format.fix @@ -0,0 +1 @@ +Mastodon API: Use the Mastodon date format for marker updated_at diff --git a/lib/pleroma/web/mastodon_api/views/marker_view.ex b/lib/pleroma/web/mastodon_api/views/marker_view.ex index 944769b1a..fd067f713 100644 --- a/lib/pleroma/web/mastodon_api/views/marker_view.ex +++ b/lib/pleroma/web/mastodon_api/views/marker_view.ex @@ -5,13 +5,15 @@ defmodule Pleroma.Web.MastodonAPI.MarkerView do use Pleroma.Web, :view + alias Pleroma.Web.CommonAPI.Utils + def render("markers.json", %{markers: markers}) do Map.new(markers, fn m -> {m.timeline, %{ last_read_id: m.last_read_id, version: m.lock_version, - updated_at: NaiveDateTime.to_iso8601(m.updated_at), + updated_at: Utils.to_masto_date(m.updated_at), pleroma: %{ unread_count: m.unread_count } diff --git a/test/pleroma/web/mastodon_api/controllers/marker_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/marker_controller_test.exs index 4050528ff..ff85031f0 100644 --- a/test/pleroma/web/mastodon_api/controllers/marker_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/marker_controller_test.exs @@ -8,6 +8,7 @@ defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do alias Pleroma.Notification alias Pleroma.Repo alias Pleroma.Web.CommonAPI + alias Pleroma.Web.CommonAPI.Utils import Pleroma.Factory @@ -33,7 +34,7 @@ defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do assert response == %{ "notifications" => %{ "last_read_id" => "69420", - "updated_at" => NaiveDateTime.to_iso8601(marker.updated_at), + "updated_at" => Utils.to_masto_date(marker.updated_at), "version" => 0, "pleroma" => %{"unread_count" => 7} } @@ -107,7 +108,7 @@ defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do assert response == %{ "notifications" => %{ "last_read_id" => "69888", - "updated_at" => NaiveDateTime.to_iso8601(marker.updated_at), + "updated_at" => Utils.to_masto_date(marker.updated_at), "version" => 0, "pleroma" => %{"unread_count" => 0} } diff --git a/test/pleroma/web/mastodon_api/views/marker_view_test.exs b/test/pleroma/web/mastodon_api/views/marker_view_test.exs index 43f8c4302..4f4eeca82 100644 --- a/test/pleroma/web/mastodon_api/views/marker_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/marker_view_test.exs @@ -4,6 +4,7 @@ defmodule Pleroma.Web.MastodonAPI.MarkerViewTest do use Pleroma.DataCase, async: true + alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.MastodonAPI.MarkerView import Pleroma.Factory @@ -14,16 +15,25 @@ defmodule Pleroma.Web.MastodonAPI.MarkerViewTest do assert MarkerView.render("markers.json", %{markers: [marker1, marker2]}) == %{ "home" => %{ last_read_id: "42", - updated_at: NaiveDateTime.to_iso8601(marker2.updated_at), + updated_at: Utils.to_masto_date(marker2.updated_at), version: 0, pleroma: %{unread_count: 0} }, "notifications" => %{ last_read_id: "17", - updated_at: NaiveDateTime.to_iso8601(marker1.updated_at), + updated_at: Utils.to_masto_date(marker1.updated_at), version: 0, pleroma: %{unread_count: 5} } } end + + test "renders updated_at in the Mastodon date format" do + marker = insert(:marker, timeline: "home") + + %{"home" => %{updated_at: updated_at}} = + MarkerView.render("markers.json", %{markers: [marker]}) + + assert updated_at =~ ~r/\.\d{3}Z$/ + end end From 542f8b2aef34a1a3cade2d7e3e52ef0c80e3889c Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 31 Oct 2026 15:37:42 +0400 Subject: [PATCH 4/4] fix(media_proxy): do not proxy data: URLs Ported from Akkoma 7632765b4. Co-authored-by: nopjmp --- changelog.d/media-proxy-data-urls.fix | 1 + lib/pleroma/web/media_proxy.ex | 6 ++++-- test/pleroma/web/media_proxy_test.exs | 12 ++++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 changelog.d/media-proxy-data-urls.fix diff --git a/changelog.d/media-proxy-data-urls.fix b/changelog.d/media-proxy-data-urls.fix new file mode 100644 index 000000000..a83b8ef29 --- /dev/null +++ b/changelog.d/media-proxy-data-urls.fix @@ -0,0 +1 @@ +MediaProxy: Do not proxy data: URLs diff --git a/lib/pleroma/web/media_proxy.ex b/lib/pleroma/web/media_proxy.ex index 82a070767..a318ab10f 100644 --- a/lib/pleroma/web/media_proxy.ex +++ b/lib/pleroma/web/media_proxy.ex @@ -45,7 +45,7 @@ defmodule Pleroma.Web.MediaProxy do def url("/" <> _ = url), do: url def url(url) do - if enabled?() and url_proxiable?(url) do + if enabled?() and not data_url?(url) and url_proxiable?(url) do encode_url(url) else url @@ -61,7 +61,7 @@ defmodule Pleroma.Web.MediaProxy do def preview_url("//" <> _ = url, pparams), do: preview_url("https:" <> url, pparams) def preview_url(url, preview_params) do - if preview_enabled?() do + if preview_enabled?() and not data_url?(url) do encode_preview_url(url, preview_params) else url(url) @@ -74,6 +74,8 @@ defmodule Pleroma.Web.MediaProxy do # non-local non-whitelisted URLs through it and be sure that body size constraint is preserved. def preview_enabled?, do: enabled?() and !!Config.get([:media_preview_proxy, :enabled]) + defp data_url?(url), do: url |> String.slice(0, 5) |> String.downcase() == "data:" + def local?(url), do: String.starts_with?(url, Endpoint.url()) def whitelisted?(url) do diff --git a/test/pleroma/web/media_proxy_test.exs b/test/pleroma/web/media_proxy_test.exs index ab707a061..433a0fe95 100644 --- a/test/pleroma/web/media_proxy_test.exs +++ b/test/pleroma/web/media_proxy_test.exs @@ -45,6 +45,18 @@ defmodule Pleroma.Web.MediaProxyTest do assert MediaProxy.url(local_root) == local_root end + test "ignores data url" do + assert MediaProxy.url("data:image/png;base64,AAAA") == "data:image/png;base64,AAAA" + assert MediaProxy.url("DATA:image/png;base64,AAAA") == "DATA:image/png;base64,AAAA" + end + + test "preview_url ignores data url" do + clear_config([:media_preview_proxy, :enabled], true) + + assert MediaProxy.preview_url("data:image/png;base64,AAAA") == + "data:image/png;base64,AAAA" + end + test "encodes and decodes URL" do url = "https://pleroma.soykaf.com/static/logo.png" encoded = MediaProxy.url(url)