diff --git a/CHANGELOG.md b/CHANGELOG.md index a481ee66b..fee4d95e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,11 +11,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - handling of GET /api/v1/preferences - Akkoma API is now documented - ability to auto-approve follow requests from users you are already following +- The SimplePolicy MRF can now strip user backgrounds from selected remote hosts ## Changed - OTP builds are now built on erlang OTP26 - The base Phoenix framework is now updated to 1.7 - An `outbox` field has been added to actor profiles to comply with AP spec +- User profile backgrounds do now federate with other Akkoma instances and Sharkey ## Fixed - Documentation issue in which a non-existing nginx file was referenced diff --git a/docs/docs/configuration/cheatsheet.md b/docs/docs/configuration/cheatsheet.md index 2f53f0c78..0933b29c5 100644 --- a/docs/docs/configuration/cheatsheet.md +++ b/docs/docs/configuration/cheatsheet.md @@ -144,6 +144,7 @@ To add configuration to your config file, you can copy it from the base config. * `report_removal`: List of instances to reject reports from and the reason for doing so. * `avatar_removal`: List of instances to strip avatars from and the reason for doing so. * `banner_removal`: List of instances to strip banners from and the reason for doing so. +* `background_removal`: List of instances to strip user backgrounds from and the reason for doing so. * `reject_deletes`: List of instances to reject deletions from and the reason for doing so. #### :mrf_subchain diff --git a/docs/docs/configuration/mrf.md b/docs/docs/configuration/mrf.md index 170b26792..0a17b3112 100644 --- a/docs/docs/configuration/mrf.md +++ b/docs/docs/configuration/mrf.md @@ -35,6 +35,7 @@ Once `SimplePolicy` is enabled, you can configure various groups in the `:mrf_si * `media_removal`: Servers in this group will have media stripped from incoming messages. * `avatar_removal`: Avatars from these servers will be stripped from incoming messages. * `banner_removal`: Banner images from these servers will be stripped from incoming messages. +* `background_removal`: User background images from these servers will be stripped from incoming messages. * `report_removal`: Servers in this group will have their reports (flags) rejected. * `federated_timeline_removal`: Servers in this group will have their messages unlisted from the public timelines by flipping the `to` and `cc` fields. * `reject_deletes`: Deletion requests will be rejected from these servers. diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 35f416e6c..8449af620 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -382,6 +382,10 @@ def banner_url(user, options \\ []) do do_optional_url(user.banner, "#{Endpoint.url()}/images/banner.png", options) end + def background_url(user) do + do_optional_url(user.background, nil, no_default: true) + end + defp do_optional_url(field, default, options) do case field do %{"url" => [%{"href" => href} | _]} when is_binary(href) -> @@ -466,6 +470,7 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do :avatar, :ap_enabled, :banner, + :background, :is_locked, :last_refreshed_at, :uri, diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index e4c626d36..4a8ce2d3d 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1603,6 +1603,7 @@ defp object_to_user_data(data, additional) do uri: get_actor_url(data["url"]), ap_enabled: true, banner: normalize_image(data["image"]), + background: normalize_image(data["backgroundUrl"]), fields: fields, emoji: emojis, is_locked: is_locked, diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex index c2e17ca9e..0b8b846ec 100644 --- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex @@ -178,6 +178,23 @@ defp check_banner_removal(%{host: actor_host} = _actor_info, %{"image" => _image defp check_banner_removal(_actor_info, object), do: {:ok, object} + defp check_background_removal( + %{host: actor_host} = _actor_info, + %{"backgroundUrl" => _bg} = object + ) do + background_removal = + instance_list(:background_removal) + |> MRF.subdomains_regex() + + if MRF.subdomain_match?(background_removal, actor_host) do + {:ok, Map.delete(object, "backgroundUrl")} + else + {:ok, object} + end + end + + defp check_background_removal(_actor_info, object), do: {:ok, object} + defp extract_context_uri(%{"conversation" => "tag:" <> rest}) do rest |> String.split(",", parts: 2, trim: true) @@ -283,7 +300,8 @@ def filter(%{"id" => actor, "type" => obj_type} = object) with {:ok, _} <- check_accept(actor_info), {:ok, _} <- check_reject(actor_info), {:ok, object} <- check_avatar_removal(actor_info, object), - {:ok, object} <- check_banner_removal(actor_info, object) do + {:ok, object} <- check_banner_removal(actor_info, object), + {:ok, object} <- check_background_removal(actor_info, object) do {:ok, object} else {:reject, nil} -> {:reject, "[SimplePolicy]"} @@ -447,6 +465,11 @@ def config_description do key: :banner_removal, description: "List of instances to strip banners from and the reason for doing so" }, + %{ + key: :background_removal, + description: + "List of instances to strip user backgrounds from and the reason for doing so" + }, %{ key: :reject_deletes, description: "List of instances to reject deletions from and the reason for doing so" diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index d6d62a9a4..fe70022f1 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -112,6 +112,8 @@ def render("user.json", %{user: user}) do } |> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user)) |> Map.merge(maybe_make_image(&User.banner_url/2, "image", user)) + # Yes, the key is named ...Url eventhough it is a whole 'Image' object + |> Map.merge(maybe_insert_image("backgroundUrl", User.background_url(user))) |> Map.merge(Utils.make_json_ld_header()) end @@ -287,7 +289,12 @@ def collection(collection, iri, page, show_items \\ true, total \\ nil) do end defp maybe_make_image(func, key, user) do - if image = func.(user, no_default: true) do + image = func.(user, no_default: true) + maybe_insert_image(key, image) + end + + defp maybe_insert_image(key, image) do + if image do %{ key => %{ "type" => "Image", diff --git a/test/pleroma/web/activity_pub/mrf/simple_policy_test.exs b/test/pleroma/web/activity_pub/mrf/simple_policy_test.exs index c6600f001..1ae42036d 100644 --- a/test/pleroma/web/activity_pub/mrf/simple_policy_test.exs +++ b/test/pleroma/web/activity_pub/mrf/simple_policy_test.exs @@ -19,6 +19,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do accept: [], avatar_removal: [], banner_removal: [], + background_removal: [], reject_deletes: [] ) @@ -618,6 +619,42 @@ test "match with wildcard domain" do end end + describe "when :background_removal" do + test "is empty" do + clear_config([:mrf_simple, :background_removal], []) + + remote_user = build_remote_user() + + assert SimplePolicy.filter(remote_user) == {:ok, remote_user} + end + + test "is not empty but it doesn't have a matching host" do + clear_config([:mrf_simple, :background_removal], [{"non.matching.remote", ""}]) + + remote_user = build_remote_user() + + assert SimplePolicy.filter(remote_user) == {:ok, remote_user} + end + + test "has a matching host" do + clear_config([:mrf_simple, :background_removal], [{"remote.instance", ""}]) + + remote_user = build_remote_user() + {:ok, filtered} = SimplePolicy.filter(remote_user) + + refute filtered["backgroundUrl"] + end + + test "match with wildcard domain" do + clear_config([:mrf_simple, :background_removal], [{"*.remote.instance", ""}]) + + remote_user = build_remote_user() + {:ok, filtered} = SimplePolicy.filter(remote_user) + + refute filtered["backgroundUrl"] + end + end + describe "when :reject_deletes is empty" do setup do: clear_config([:mrf_simple, :reject_deletes], []) @@ -701,6 +738,10 @@ defp build_remote_user do "url" => "http://example.com/image.jpg", "type" => "Image" }, + "backgroundUrl" => %{ + "url" => "http://example.com/background.jpg", + "type" => "Image" + }, "type" => "Person" } end diff --git a/test/pleroma/web/activity_pub/side_effects_test.exs b/test/pleroma/web/activity_pub/side_effects_test.exs index 80714b1db..28a591d3c 100644 --- a/test/pleroma/web/activity_pub/side_effects_test.exs +++ b/test/pleroma/web/activity_pub/side_effects_test.exs @@ -155,7 +155,13 @@ test "it blocks but does not unfollow if the relevant setting is set", %{ user = insert(:user, local: false) {:ok, update_data, []} = - Builder.update(user, %{"id" => user.ap_id, "type" => "Person", "name" => "new name!"}) + Builder.update(user, %{ + "id" => user.ap_id, + "type" => "Person", + "name" => "new name!", + "icon" => %{"type" => "Image", "url" => "https://example.org/icon.png"}, + "backgroundUrl" => %{"type" => "Image", "url" => "https://example.org/bg.jxl"} + }) {:ok, update, _meta} = ActivityPub.persist(update_data, local: true) @@ -165,7 +171,10 @@ test "it blocks but does not unfollow if the relevant setting is set", %{ test "it updates the user", %{user: user, update: update} do {:ok, _, _} = SideEffects.handle(update) user = User.get_by_id(user.id) + assert user.name == "new name!" + assert [%{"href" => "https://example.org/icon.png"}] = user.avatar["url"] + assert [%{"href" => "https://example.org/bg.jxl"}] = user.background["url"] end test "it uses a given changeset to update", %{user: user, update: update} do diff --git a/test/pleroma/web/activity_pub/views/user_view_test.exs b/test/pleroma/web/activity_pub/views/user_view_test.exs index ef1bd4fde..abe91cdea 100644 --- a/test/pleroma/web/activity_pub/views/user_view_test.exs +++ b/test/pleroma/web/activity_pub/views/user_view_test.exs @@ -58,16 +58,19 @@ test "Does not add an avatar image if the user hasn't set one" do result = UserView.render("user.json", %{user: user}) refute result["icon"] refute result["image"] + refute result["backgroundUrl"] user = insert(:user, avatar: %{"url" => [%{"href" => "https://someurl"}]}, - banner: %{"url" => [%{"href" => "https://somebanner"}]} + banner: %{"url" => [%{"href" => "https://somebanner"}]}, + background: %{"url" => [%{"href" => "https://somebackground"}]} ) result = UserView.render("user.json", %{user: user}) assert result["icon"]["url"] == "https://someurl" assert result["image"]["url"] == "https://somebanner" + assert result["backgroundUrl"]["url"] == "https://somebackground" end test "renders an invisible user with the invisible property set to true" do