From 2876fed876045ded1d86812a550bd0feccd72226 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Sun, 2 Aug 2026 22:13:31 +0200 Subject: [PATCH 1/7] Embed notificaiton details for grouped move/reaction/chat/report notification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/grouped-notifs-embed.skip | 0 .../operations/notification_operation.ex | 21 +++++- .../mastodon_api/views/notification_view.ex | 65 ++++++++++++++++--- .../views/notification_view_test.exs | 35 ++++++++++ 4 files changed, 112 insertions(+), 9 deletions(-) create mode 100644 changelog.d/grouped-notifs-embed.skip diff --git a/changelog.d/grouped-notifs-embed.skip b/changelog.d/grouped-notifs-embed.skip new file mode 100644 index 000000000..e69de29bb diff --git a/lib/pleroma/web/api_spec/operations/notification_operation.ex b/lib/pleroma/web/api_spec/operations/notification_operation.ex index 6c8f7d5e9..740f7645d 100644 --- a/lib/pleroma/web/api_spec/operations/notification_operation.ex +++ b/lib/pleroma/web/api_spec/operations/notification_operation.ex @@ -8,6 +8,7 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do alias Pleroma.Web.ApiSpec.Schemas.Account alias Pleroma.Web.ApiSpec.Schemas.ApiError alias Pleroma.Web.ApiSpec.Schemas.BooleanLike + alias Pleroma.Web.ApiSpec.Schemas.ChatMessage alias Pleroma.Web.ApiSpec.Schemas.Status alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope @@ -304,7 +305,25 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do page_max_id: %Schema{type: :string, nullable: true}, latest_page_notification_at: %Schema{type: :string, format: :"date-time", nullable: true}, sample_account_ids: %Schema{type: :array, items: %Schema{type: :string}}, - status_id: %Schema{type: :string, nullable: true} + status_id: %Schema{type: :string, nullable: true}, + target_id: %Schema{ + type: :string, + description: "ID of the target account for a move notification" + }, + emoji: %Schema{ + type: :string, + description: "Emoji used for an emoji reaction notification" + }, + emoji_url: %Schema{ + type: :string, + nullable: true, + description: "URL of the custom emoji used for an emoji reaction notification" + }, + chat_message: ChatMessage, + report: %Schema{ + type: :object, + description: "Report embedded in a report notification" + } }, required: [ :group_key, diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex index 33d4b2924..eadfac4a0 100644 --- a/lib/pleroma/web/mastodon_api/views/notification_view.ex +++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex @@ -101,13 +101,13 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do |> Enum.filter(& &1) |> Enum.uniq_by(& &1[:id]) - actors = + accounts = notification_groups |> List.flatten() - |> notification_actors() + |> notification_accounts() %{ - accounts: AccountView.render("index.json", %{users: actors, for: reading_user}), + accounts: AccountView.render("index.json", %{users: accounts, for: reading_user}), statuses: statuses, notification_groups: Enum.map( @@ -191,7 +191,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do defp render_group( [%Notification{} = notification | _] = notifications, - _reading_user, + reading_user, grouped_types, notification_group_counts, notification_group_bounds, @@ -237,13 +237,49 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do response end - if status_activity do - Map.put(response, :status_id, to_string(status_activity.id)) - else - response + response = + if status_activity do + Map.put(response, :status_id, to_string(status_activity.id)) + else + response + end + + put_group_details(response, notification, reading_user) + end + + defp put_group_details(response, %Notification{type: "move", activity: activity}, _reading_user) do + case User.get_cached_by_ap_id(activity.data["target"]) do + %User{id: id} -> Map.put(response, :target_id, to_string(id)) + _ -> response end end + defp put_group_details( + response, + %Notification{type: "pleroma:emoji_reaction", activity: activity}, + _reading_user + ) do + put_emoji(response, activity) + end + + defp put_group_details( + response, + %Notification{type: "pleroma:chat_mention", activity: activity}, + reading_user + ) do + put_chat_message(response, activity, reading_user, %{}) + end + + defp put_group_details( + response, + %Notification{type: "pleroma:report", activity: activity}, + _reading_user + ) do + put_report(response, activity) + end + + defp put_group_details(response, _notification, _reading_user), do: response + defp status_activity_for_group([%Notification{} = notification | _], grouped_types) do status_activity_for(notification, grouped_types) end @@ -287,6 +323,19 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do |> Enum.uniq_by(& &1.id) end + defp notification_accounts(notifications) do + move_targets = + notifications + |> Enum.filter(&(&1.type == "move")) + |> Enum.map(&User.get_cached_by_ap_id(&1.activity.data["target"])) + |> Enum.filter(& &1) + + notifications + |> notification_actors() + |> Kernel.++(move_targets) + |> Enum.uniq_by(& &1.id) + end + defp put_emoji(response, activity) do response |> Map.put(:emoji, activity.data["content"]) diff --git a/test/pleroma/web/mastodon_api/views/notification_view_test.exs b/test/pleroma/web/mastodon_api/views/notification_view_test.exs index 2d0801589..e6fb31e28 100644 --- a/test/pleroma/web/mastodon_api/views/notification_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/notification_view_test.exs @@ -65,6 +65,15 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do } test_notifications_rendering([notification], recipient, [expected]) + + grouped = + NotificationView.render("grouped_index.json", %{ + notifications: [notification], + for: recipient + }) + + assert [group] = grouped.notification_groups + assert group.chat_message == expected.chat_message end test "Mention notification" do @@ -182,6 +191,16 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do } test_notifications_rendering([notification], follower, [expected]) + + grouped = + NotificationView.render("grouped_index.json", %{ + notifications: [notification], + for: follower + }) + + assert [group] = grouped.notification_groups + assert group.target_id == new_user.id + assert Enum.any?(grouped.accounts, &(&1.id == new_user.id)) end test "EmojiReact notification" do @@ -210,6 +229,13 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do } test_notifications_rendering([notification], user, [expected]) + + grouped = + NotificationView.render("grouped_index.json", %{notifications: [notification], for: user}) + + assert [group] = grouped.notification_groups + assert group.emoji == expected.emoji + assert group.emoji_url == expected.emoji_url end test "EmojiReact custom emoji notification" do @@ -295,6 +321,15 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do } test_notifications_rendering([notification], moderator_user, [expected]) + + grouped = + NotificationView.render("grouped_index.json", %{ + notifications: [notification], + for: moderator_user + }) + + assert [group] = grouped.notification_groups + assert group.report == expected.report end test "Edit notification" do From f672ab337c53fcb7ef50baaf5d03c40c24e2aed9 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 13 Aug 2026 18:11:18 +0200 Subject: [PATCH 2/7] Revert "Merge pull request 'MediaProxy: detect images when there's no file extension' (#7933) from mediaproxy-content-type into develop" A partial revert, keeping the updated @inline_content_types list and cosmetic changes. This reverts commit 4fe567064a9ea8ff4cc6c56e0f89caad71275a99, reversing changes made to 9b8aa2e2ee225dc69a528e7063be3620a79f9e33. --- changelog.d/mediaproxy-content-type.fix | 1 - lib/pleroma/reverse_proxy.ex | 180 +++--------------- .../web/mastodon_api/views/status_view.ex | 1 - .../web/media_proxy/media_proxy_controller.ex | 1 - test/pleroma/reverse_proxy_test.exs | 112 ----------- .../mastodon_api/views/status_view_test.exs | 17 -- .../media_proxy_controller_test.exs | 5 +- 7 files changed, 23 insertions(+), 294 deletions(-) delete mode 100644 changelog.d/mediaproxy-content-type.fix diff --git a/changelog.d/mediaproxy-content-type.fix b/changelog.d/mediaproxy-content-type.fix deleted file mode 100644 index f36dd6283..000000000 --- a/changelog.d/mediaproxy-content-type.fix +++ /dev/null @@ -1 +0,0 @@ -Detect image content types for extensionless MediaProxy responses. diff --git a/lib/pleroma/reverse_proxy.ex b/lib/pleroma/reverse_proxy.ex index 12e48eadd..ee20303a3 100644 --- a/lib/pleroma/reverse_proxy.ex +++ b/lib/pleroma/reverse_proxy.ex @@ -17,7 +17,6 @@ defmodule Pleroma.ReverseProxy do @max_read_duration :timer.seconds(30) @max_body_length :infinity @failed_request_ttl :timer.seconds(60) - @sniff_bytes 8 * 1024 @methods ~w(GET HEAD) @allowed_mime_types Pleroma.Config.get([Pleroma.Upload, :allowed_mime_types], []) @@ -70,12 +69,7 @@ defmodule Pleroma.ReverseProxy do * `inline_content_types`: * `true` will not alter `content-disposition` (up to the upstream), * `false` will add `content-disposition: attachment` to any request, - * a list of whitelisted content types for which `content-disposition: inline` - is always set (overriding any upstream header) so the media can be embedded - in pages; the filename is derived from the content type - - * `sniff_content_type` (default `false`): detects an image MIME type from the first - response chunk when the upstream type is missing or `application/octet-stream`. + * a list of whitelisted content types * `req_headers`, `resp_headers` additional headers. @@ -112,7 +106,6 @@ defmodule Pleroma.ReverseProxy do | {:req_headers, [{String.t(), String.t()}]} | {:resp_headers, [{String.t(), String.t()}]} | {:inline_content_types, boolean() | list(String.t())} - | {:sniff_content_type, boolean()} | {:redirect_on_failure, boolean()} @spec call(Plug.Conn.t(), String.t(), list(option())) :: Plug.Conn.t() @@ -228,30 +221,11 @@ defmodule Pleroma.ReverseProxy do defp response(conn, client, url, status, headers, opts) do Logger.debug("#{__MODULE__} #{status} #{url} #{inspect(headers)}") - case maybe_prefetch_for_content_type(headers, client, opts) do - {_headers, client, {:error, {:upstream, error}}} -> - Logger.warning( - "#{__MODULE__} request to #{url} failed before streaming: #{inspect(error)}" - ) - - client().close(client) - track_failed_url(url, error, opts) - - conn - |> error_or_redirect(url, 500, "Request failed", opts) - |> halt() - - {headers, client, prefetched} -> - stream_response(conn, client, url, status, headers, opts, prefetched) - end - end - - defp stream_response(conn, client, url, status, headers, opts, prefetched) do result = conn |> put_resp_headers(build_resp_headers(headers, opts)) |> send_chunked(status) - |> chunk_reply(client, opts, prefetched) + |> chunk_reply(client, opts) case result do {:ok, conn} -> @@ -272,29 +246,18 @@ defmodule Pleroma.ReverseProxy do end end - defp chunk_reply(conn, client, opts, :none) do + defp chunk_reply(conn, client, opts) do chunk_reply(conn, client, opts, 0, 0) end - defp chunk_reply(conn, _client, _opts, :done), do: {:ok, conn} - defp chunk_reply(conn, _client, _opts, {:error, error}), do: {:error, error, conn} - - defp chunk_reply(conn, client, opts, {:ok, data, duration}) do - with :ok <- - body_size_constraint( - byte_size(data), - Keyword.get(opts, :max_body_length, @max_body_length) - ), - {:ok, conn} <- write_chunk(conn, data) do - chunk_reply(conn, client, opts, byte_size(data), duration) - else - {:error, {:downstream, error}} -> {:error, {:downstream, error}, conn} - {:error, error} -> {:error, {:upstream, error}, conn} - end - end - defp chunk_reply(conn, client, opts, sent_so_far, duration) do - with {:ok, data, client, duration} <- read_chunk(client, duration, opts), + with {:ok, duration} <- + check_read_duration( + duration, + Keyword.get(opts, :max_read_duration, @max_read_duration) + ), + {:ok, data, client} <- client().stream_body(client), + {:ok, duration} <- increase_read_duration(duration), sent_so_far = sent_so_far + byte_size(data), :ok <- body_size_constraint( @@ -310,24 +273,6 @@ defmodule Pleroma.ReverseProxy do end end - defp read_chunk(client, duration, opts) do - result = - with {:ok, timer} <- - check_read_duration( - duration, - Keyword.get(opts, :max_read_duration, @max_read_duration) - ), - {:ok, data, client} <- client().stream_body(client), - {:ok, duration} <- increase_read_duration(timer) do - {:ok, data, client, duration} - end - - case result do - {:error, error} -> {:error, {:upstream, error}} - result -> result - end - end - defp write_chunk(conn, data) do case chunk(conn, data) do {:error, error} -> {:error, {:downstream, error}} @@ -335,54 +280,6 @@ defmodule Pleroma.ReverseProxy do end end - defp maybe_prefetch_for_content_type(headers, client, opts) do - if Keyword.get(opts, :sniff_content_type, false) and generic_content_type?(headers) do - case read_chunk(client, 0, opts) do - {:ok, data, client, duration} -> - {maybe_put_image_content_type(headers, data), client, {:ok, data, duration}} - - :done -> - {headers, client, :done} - - {:error, error} -> - {headers, client, {:error, error}} - end - else - {headers, client, :none} - end - end - - defp generic_content_type?(headers) do - headers - |> get_content_type() - |> String.trim() - |> String.downcase() - |> then(&(&1 in ["", "application/octet-stream"])) - end - - defp maybe_put_image_content_type(headers, data) do - with false <- data == "", - {:ok, %{mime_type: "image/" <> _ = content_type}} <- - Majic.perform({:bytes, binary_part(data, 0, min(byte_size(data), @sniff_bytes))}, - pool: Pleroma.MajicPool - ) do - [ - {"content-type", content_type} - | Enum.reject(headers, fn {key, _} -> key == "content-type" end) - ] - else - _ -> headers - end - rescue - error -> - Logger.debug("#{__MODULE__}: content-type sniffing failed: #{Exception.message(error)}") - headers - catch - kind, reason -> - Logger.debug("#{__MODULE__}: content-type sniffing failed: #{kind}: #{inspect(reason)}") - headers - end - defp head_response(conn, url, code, headers, opts) do Logger.debug("#{__MODULE__} #{code} #{url} #{inspect(headers)}") @@ -430,33 +327,16 @@ defmodule Pleroma.ReverseProxy do |> build_req_range_or_encoding_header(opts) |> build_req_user_agent_header(opts) |> merge_headers(Keyword.get(opts, :req_headers, [])) - |> maybe_force_identity_encoding(opts) - end - - defp merge_headers(headers, extra_headers) do - Enum.reduce(extra_headers, headers, fn {key, value}, headers -> - List.keystore(headers, String.downcase(key), 0, {String.downcase(key), value}) - end) - end - - defp maybe_force_identity_encoding(headers, opts) do - if Keyword.get(opts, :sniff_content_type, false) do - List.keystore(headers, "accept-encoding", 0, {"accept-encoding", "identity"}) - else - headers - end end # Disable content-encoding if any @range_headers are requested (see #1823). defp build_req_range_or_encoding_header(headers, _opts) do range? = Enum.any?(headers, fn {header, _} -> Enum.member?(@range_headers, header) end) - cond do - range? && List.keymember?(headers, "accept-encoding", 0) -> - List.keydelete(headers, "accept-encoding", 0) - - true -> - headers + if range? && List.keymember?(headers, "accept-encoding", 0) do + List.keydelete(headers, "accept-encoding", 0) + else + headers end end @@ -479,6 +359,12 @@ defmodule Pleroma.ReverseProxy do |> ensure_no_transform() end + defp merge_headers(headers, extra_headers) do + Enum.reduce(extra_headers, headers, fn {key, value}, headers -> + List.keystore(headers, String.downcase(key), 0, {String.downcase(key), value}) + end) + end + defp ensure_no_transform(headers) do {_, cache_control} = List.keyfind(headers, "cache-control", 0, {"cache-control", @default_cache_control_header}) @@ -568,20 +454,7 @@ defmodule Pleroma.ReverseProxy do replace_header(headers, "content-disposition", disposition) else - if opt == true do - headers - else - name = inline_filename(content_type) - - disposition = - if name do - "inline; filename=\"#{name}\"" - else - "inline" - end - - replace_header(headers, "content-disposition", disposition) - end + headers end end @@ -589,13 +462,6 @@ defmodule Pleroma.ReverseProxy do [{key, value} | Enum.reject(headers, fn {header, _} -> header == key end)] end - defp inline_filename(content_type) do - case MIME.extensions(content_type) do - [ext | _] when ext != "" -> "inline.#{ext}" - _ -> nil - end - end - defp header_length_constraint(headers, limit) do lengths = for {"content-length", value} <- headers, @@ -640,9 +506,7 @@ defmodule Pleroma.ReverseProxy do end end - defp check_read_duration(_, _), do: {:ok, :no_duration_limit} - - defp increase_read_duration(:no_duration_limit), do: {:ok, :no_duration_limit} + defp check_read_duration(_, _), do: {:ok, :no_duration_limit, :no_duration_limit} defp increase_read_duration({previous_duration, started}) when is_integer(previous_duration) and is_integer(started) do diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 356e8fce5..4625be47c 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -631,7 +631,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do String.contains?(media_type, "image") -> "image" String.contains?(media_type, "video") -> "video" String.contains?(media_type, "audio") -> "audio" - attachment["type"] == "Image" -> "image" true -> "unknown" end diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index 93f692008..a0aafc32e 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -208,7 +208,6 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do defp media_proxy_opts do Config.get([:media_proxy, :proxy_opts], []) - |> Keyword.put_new(:sniff_content_type, true) end defp sandbox(conn, _params) do diff --git a/test/pleroma/reverse_proxy_test.exs b/test/pleroma/reverse_proxy_test.exs index bf0a809c4..156b7d7ed 100644 --- a/test/pleroma/reverse_proxy_test.exs +++ b/test/pleroma/reverse_proxy_test.exs @@ -579,56 +579,6 @@ defmodule Pleroma.ReverseProxyTest do assert Conn.get_resp_header(conn, "content-type") == ["application/octet-stream"] end - test "sniffs and preserves an image body with a generic content type", %{conn: conn} do - body = File.read!("test/fixtures/image.jpg") - - ClientMock - |> expect(:request, fn :get, "/extensionless", headers, _, _ -> - assert {"accept-encoding", "identity"} in headers - {:ok, 200, [{"content-type", "application/octet-stream"}], %{body: body}} - end) - |> expect(:stream_body, fn %{body: ^body} = client -> - {:ok, body, Map.delete(client, :body)} - end) - |> expect(:stream_body, fn %{} -> :done end) - - conn = - ReverseProxy.call(conn, "/extensionless", - sniff_content_type: true, - max_read_duration: :infinity, - req_headers: [{"accept-encoding", "gzip"}] - ) - - assert conn.resp_body == body - assert Conn.get_resp_header(conn, "content-type") == ["image/jpeg"] - - assert Conn.get_resp_header(conn, "content-disposition") == [ - "inline; filename=\"inline.jpg\"" - ] - end - - test "redirects a content sniffing failure before committing the response", %{conn: conn} do - url = "https://example.com/extensionless" - - ClientMock - |> expect(:request, fn :get, ^url, _, _, _ -> - {:ok, 200, [{"content-type", "application/octet-stream"}], %{url: url}} - end) - |> expect(:stream_body, fn _ -> {:error, :closed} end) - |> expect(:close, fn _ -> :ok end) - - conn = - ReverseProxy.call(conn, url, - sniff_content_type: true, - redirect_on_failure: true - ) - - assert conn.status == 302 - assert Conn.get_resp_header(conn, "location") == [url] - assert conn.state == :sent - assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true} - end - test "preserves a generic content type for a non-image body", %{conn: conn} do body = "not an image" @@ -823,24 +773,6 @@ defmodule Pleroma.ReverseProxyTest do conn = ReverseProxy.call(conn, "/disposition") assert {"content-type", "image/gif"} in conn.resp_headers - assert {"content-disposition", "inline; filename=\"inline.gif\""} in conn.resp_headers - end - - test "forces inline for inline content types overriding upstream attachment", %{ - conn: conn - } do - disposition_headers_mock([ - {"content-type", "image/png"}, - {"content-disposition", "attachment; filename=\"filename.png\""}, - {"content-disposition", "attachment; filename=\"duplicate.png\""}, - {"content-length", "0"} - ]) - - conn = ReverseProxy.call(conn, "/disposition") - - [disposition] = Conn.get_resp_header(conn, "content-disposition") - assert String.starts_with?(disposition, "inline") - refute String.starts_with?(disposition, "attachment") end test "with content-disposition header", %{conn: conn} do @@ -853,50 +785,6 @@ defmodule Pleroma.ReverseProxyTest do assert {"content-disposition", "attachment; filename=\"filename.jpg\""} in conn.resp_headers end - - test "with inline_content_types: true leaves upstream headers untouched", %{ - conn: conn - } do - # opt == true: the proxy must not synthesise or rewrite content-disposition. - disposition_headers_mock([ - {"content-type", "image/png"}, - {"content-disposition", "attachment; filename=\"upstream.png\""}, - {"content-length", "0"} - ]) - - conn = ReverseProxy.call(conn, "/disposition", inline_content_types: true) - - assert {"content-disposition", "attachment; filename=\"upstream.png\""} in conn.resp_headers - end - - test "forces bare inline for a whitelisted type with no MIME extension", %{ - conn: conn - } do - # image/x-foo-bar has no entry in MIME's database, so inline_filename/1 - # returns nil and the disposition should be the bare token "inline". - disposition_headers_mock([ - {"content-type", "image/x-foo-bar"}, - {"content-length", "0"} - ]) - - conn = ReverseProxy.call(conn, "/disposition", inline_content_types: ["image/x-foo-bar"]) - - [disposition] = Conn.get_resp_header(conn, "content-disposition") - assert disposition == "inline" - end - - test "serves modern browser image types inline", %{conn: conn} do - disposition_headers_mock([ - {"content-type", "image/webp"}, - {"content-length", "0"} - ]) - - conn = ReverseProxy.call(conn, "/disposition") - - assert Conn.get_resp_header(conn, "content-disposition") == [ - "inline; filename=\"inline.webp\"" - ] - end end describe "content-type sanitisation" do diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs index 73adb196a..f7b815ed7 100644 --- a/test/pleroma/web/mastodon_api/views/status_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs @@ -668,23 +668,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do assert expected == StatusView.render("attachment.json", %{attachment: object}) assert_schema(expected, "Attachment", api_spec) end - - test "uses the ActivityStreams image type when the MIME type is generic" do - object = %{ - "type" => "Image", - "url" => [ - %{ - "mediaType" => "application/octet-stream", - "href" => "https://example.com/extensionless" - } - ] - } - - attachment = StatusView.render("attachment.json", %{attachment: object}) - - assert attachment.type == "image" - assert attachment.pleroma.mime_type == "application/octet-stream" - end end test "put the url advertised in the Activity in to the url attribute" do diff --git a/test/pleroma/web/media_proxy/media_proxy_controller_test.exs b/test/pleroma/web/media_proxy/media_proxy_controller_test.exs index 800159c21..f7e52483c 100644 --- a/test/pleroma/web/media_proxy/media_proxy_controller_test.exs +++ b/test/pleroma/web/media_proxy/media_proxy_controller_test.exs @@ -71,10 +71,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do test "it performs ReverseProxy.call with valid signature", %{conn: conn, url: url} do with_mock Pleroma.ReverseProxy, - call: fn _conn, _url, opts -> - assert opts[:sniff_content_type] - %Conn{status: :success} - end do + call: fn _conn, _url, _opts -> %Conn{status: :success} end do assert %Conn{status: :success} = get(conn, url) end end From 2dda6cb52ad4814fa13dd565bd29dcf59187643a Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 13 Aug 2026 23:26:44 +0200 Subject: [PATCH 3/7] Docs: Update OTP bundle links left behind in the Forgejo migration --- docs/installation/migrating_from_source_otp_en.md | 2 +- docs/installation/otp_en.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation/migrating_from_source_otp_en.md b/docs/installation/migrating_from_source_otp_en.md index 798862566..548f10f47 100644 --- a/docs/installation/migrating_from_source_otp_en.md +++ b/docs/installation/migrating_from_source_otp_en.md @@ -87,7 +87,7 @@ export FLAVOUR="amd64-musl" # Clone the release build into a temporary directory and unpack it # Replace `stable` with `unstable` if you want to run the unstable branch sudo -Hu pleroma " -curl 'https://git.pleroma.social/api/v4/projects/2/jobs/artifacts/stable/download?job=$FLAVOUR' -o /tmp/pleroma.zip +curl 'https://git.pleroma.social/api/packages/pleroma/generic/pleroma-otp-stable-$FLAVOUR/latest/pleroma.zip' -o /tmp/pleroma.zip unzip /tmp/pleroma.zip -d /tmp/ " diff --git a/docs/installation/otp_en.md b/docs/installation/otp_en.md index 5fc1db21e..7410afe79 100644 --- a/docs/installation/otp_en.md +++ b/docs/installation/otp_en.md @@ -140,7 +140,7 @@ export FLAVOUR="amd64-musl" # Clone the release build into a temporary directory and unpack it sudo -Hu pleroma " -curl 'https://git.pleroma.social/api/v4/projects/2/jobs/artifacts/stable/download?job=$FLAVOUR' -o /tmp/pleroma.zip +curl 'https://git.pleroma.social/api/packages/pleroma/generic/pleroma-otp-stable-$FLAVOUR/latest/pleroma.zip' -o /tmp/pleroma.zip unzip /tmp/pleroma.zip -d /tmp/ " From 5ecc93644423266645410fa756239afc41486b90 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 13 Aug 2026 23:37:34 +0200 Subject: [PATCH 4/7] Docs: split wall of install commands, fix downloading OTP bundle Bundle could not be downloaded, because sudo does not cope with the previous syntax that double quoted the whole command, and the download link was single quoted. --- changelog.d/docs-otp-install-links.fix | 1 + .../migrating_from_source_otp_en.md | 52 +++++++---- docs/installation/otp_en.md | 86 +++++++++++++------ 3 files changed, 93 insertions(+), 46 deletions(-) create mode 100644 changelog.d/docs-otp-install-links.fix diff --git a/changelog.d/docs-otp-install-links.fix b/changelog.d/docs-otp-install-links.fix new file mode 100644 index 000000000..28d504bc0 --- /dev/null +++ b/changelog.d/docs-otp-install-links.fix @@ -0,0 +1 @@ +Docs: Fixed OTP install links in documentation, broke up wall of install commands into smaller chunks, fixed sudo commands execution diff --git a/docs/installation/migrating_from_source_otp_en.md b/docs/installation/migrating_from_source_otp_en.md index 548f10f47..450d33804 100644 --- a/docs/installation/migrating_from_source_otp_en.md +++ b/docs/installation/migrating_from_source_otp_en.md @@ -76,35 +76,51 @@ $EDITOR /etc/pleroma/config.exs ``` ## Installing the release Before proceeding, get the flavour from [Detecting flavour](otp_en.md#detecting-flavour) section in OTP installation guide. + + +Delete all files in pleroma user's directory: ```sh -# Delete all files in pleroma user's directory rm -r ~pleroma/* +``` -# Set the flavour environment variable to the string you got in Detecting flavour section. -# For example if the flavour is `amd64-musl` the command will be +Set the flavour environment variable to the string you got in Detecting flavour section. +For example if the flavour is `amd64-musl` the command will be: + +```sh export FLAVOUR="amd64-musl" +``` -# Clone the release build into a temporary directory and unpack it -# Replace `stable` with `unstable` if you want to run the unstable branch -sudo -Hu pleroma " -curl 'https://git.pleroma.social/api/packages/pleroma/generic/pleroma-otp-stable-$FLAVOUR/latest/pleroma.zip' -o /tmp/pleroma.zip -unzip /tmp/pleroma.zip -d /tmp/ -" +Clone the release build into a temporary directory and unpack it. +Replace `stable` with `develop`, if you want to run the develop branch. -# Move the release to the home directory and delete temporary files -sudo -Hu pleroma " -mv /tmp/release/* ~pleroma/ -rmdir /tmp/release -rm /tmp/pleroma.zip -" +```sh +sudo -Hu pleroma curl "https://git.pleroma.social/api/packages/pleroma/generic/pleroma-otp-stable-$FLAVOUR/latest/pleroma.zip" -o /tmp/pleroma.zip +sudo -Hu pleroma unzip /tmp/pleroma.zip -d /tmp/ +``` -# Start the instance to verify that everything is working as expected +Move the release to the home directory and delete temporary files: + +```sh +sudo -Hu pleroma mv /tmp/release/* ~pleroma/ +sudo -Hu pleroma rmdir /tmp/release +sudo -Hu pleroma rm /tmp/pleroma.zip +``` + +Start the instance to verify that everything is working as expected: + +```sh sudo -Hu pleroma "./bin/pleroma daemon" +``` -# Wait for about 20 seconds and query the instance endpoint, if it shows your uri, name and email correctly, you are configured correctly +Wait for about 20 seconds and query the instance endpoint, if it shows your uri, name and email correctly, you are configured correctly: + +```sh sleep 20 && curl http://localhost:4000/api/v1/instance +``` -# Stop the instance +Stop the instance: + +```sh sudo -Hu pleroma "./bin/pleroma stop" ``` diff --git a/docs/installation/otp_en.md b/docs/installation/otp_en.md index 7410afe79..0f296a9f6 100644 --- a/docs/installation/otp_en.md +++ b/docs/installation/otp_en.md @@ -130,60 +130,90 @@ Restart PostgreSQL to apply configuration changes: ``` ### Installing Pleroma +Create a Pleroma user: + ```sh -# Create a Pleroma user adduser --system --shell /bin/false --home /opt/pleroma pleroma +``` -# Set the flavour environment variable to the string you got in Detecting flavour section. -# For example if the flavour is `amd64-musl` the command will be +Set the flavour environment variable to the string you got in Detecting flavour section. For example if the flavour is `amd64-musl` the command will be: + +```sh export FLAVOUR="amd64-musl" +``` -# Clone the release build into a temporary directory and unpack it -sudo -Hu pleroma " -curl 'https://git.pleroma.social/api/packages/pleroma/generic/pleroma-otp-stable-$FLAVOUR/latest/pleroma.zip' -o /tmp/pleroma.zip -unzip /tmp/pleroma.zip -d /tmp/ -" +Clone the release build into a temporary directory and unpack it: -# Move the release to the home directory and delete temporary files -sudo -Hu pleroma " -mv /tmp/release/* /opt/pleroma -rmdir /tmp/release -rm /tmp/pleroma.zip -" -# Create uploads directory and set proper permissions (skip if planning to use a remote uploader) -# Note: It does not have to be `/var/lib/pleroma/uploads`, the config generator will ask about the upload directory later +```sh +sudo -Hu pleroma curl "https://git.pleroma.social/api/packages/pleroma/generic/pleroma-otp-stable-$FLAVOUR/latest/pleroma.zip" -o /tmp/pleroma.zip +sudo -Hu pleroma unzip /tmp/pleroma.zip -d /tmp/ +``` +Move the release to the home directory and delete temporary files: + +```sh +sudo -Hu pleroma mv /tmp/release/* /opt/pleroma +sudo -Hu pleroma rmdir /tmp/release +sudo -Hu pleroma rm /tmp/pleroma.zip +``` + +Create uploads directory and set proper permissions (skip if planning to use a remote uploader). +Note: It does not have to be `/var/lib/pleroma/uploads`, the config generator will ask about the upload directory later. + +```sh mkdir -p /var/lib/pleroma/uploads chown -R pleroma /var/lib/pleroma +``` -# Create custom public files directory (custom emojis, frontend bundle overrides, robots.txt, etc.) -# Note: It does not have to be `/var/lib/pleroma/static`, the config generator will ask about the custom public files directory later +Create custom public files directory (custom emojis, frontend bundle overrides, robots.txt, etc.) +Note: It does not have to be `/var/lib/pleroma/static`, the config generator will ask about the custom public files directory later. + +```sh mkdir -p /var/lib/pleroma/static chown -R pleroma /var/lib/pleroma +``` -# Create a config directory +Create a config directory: + +```sh mkdir -p /etc/pleroma chown -R pleroma /etc/pleroma +``` -# Run the config generator +Run the config generator: + +```sh sudo -Hu pleroma "./bin/pleroma_ctl instance gen --output /etc/pleroma/config.exs --output-psql /tmp/setup_db.psql" +``` -# Create the postgres database +Create the postgres database and schema: + +```sh sudo -u postgres -s $SHELL -lc "psql -f /tmp/setup_db.psql" - -# Create the database schema sudo -Hu pleroma "./bin/pleroma_ctl migrate" +``` -# If you have installed RUM indexes uncommend and run -# sudo -Hu pleroma "./bin/pleroma_ctl migrate --migrations-path priv/repo/optional_migrations/rum_indexing/" +(Optional) If you have installed RUM indexes: -# Start the instance to verify that everything is working as expected +```sh +sudo -Hu pleroma "./bin/pleroma_ctl migrate --migrations-path priv/repo/optional_migrations/rum_indexing/" +``` + +Start the instance to verify that everything is working as expected: + +```sh sudo -Hu pleroma "./bin/pleroma daemon" +``` -# Wait for about 20 seconds and query the instance endpoint, if it shows your uri, name and email correctly, you are configured correctly +Wait for about 20 seconds and query the instance endpoint, if it shows your uri, name and email correctly, you are configured correctly: + +```sh sleep 20 && curl http://localhost:4000/api/v1/instance +``` -# Stop the instance +Stop the instance: + +```sh sudo -Hu pleroma "./bin/pleroma stop" ``` From 6a5ca6213a7965094769b7e51658c958984972ce Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 18 Aug 2026 01:01:26 +0200 Subject: [PATCH 5/7] Oban: Enable Reindexer plugin to reduce oban_jobs index DB bloat --- changelog.d/oban-reindexer.add | 1 + config/config.exs | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changelog.d/oban-reindexer.add diff --git a/changelog.d/oban-reindexer.add b/changelog.d/oban-reindexer.add new file mode 100644 index 000000000..5b56d9482 --- /dev/null +++ b/changelog.d/oban-reindexer.add @@ -0,0 +1 @@ +Oban: Enabled the Reindexer plugin to reduce oban_jobs index DB bloat diff --git a/config/config.exs b/config/config.exs index d4a70450f..e2c110689 100644 --- a/config/config.exs +++ b/config/config.exs @@ -607,7 +607,11 @@ config :pleroma, Oban, search_indexing: [limit: 10, paused: true], slow: 5 ], - plugins: [Oban.Plugins.Lazarus, {Oban.Plugins.Pruner, max_age: 900}], + plugins: [ + Oban.Plugins.Lazarus, + {Oban.Plugins.Pruner, max_age: 900}, + {Oban.Plugins.Reindexer, schedule: "@weekly"} + ], crontab: [ {"0 0 * * 0", Pleroma.Workers.Cron.DigestEmailsWorker}, {"0 0 * * *", Pleroma.Workers.Cron.NewUsersDigestWorker}, From 16890c051ac1a64e6a7ddb376d4f11d33a1f26de Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 18 Aug 2026 19:25:26 +0200 Subject: [PATCH 6/7] Syslog: Fix intermittent failure to compile due to old pc version Upstream hasn't made a release with the fix, pin fixed commit for the time being. --- changelog.d/syslog-pc.fix | 1 + mix.exs | 7 +++++++ mix.lock | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 changelog.d/syslog-pc.fix diff --git a/changelog.d/syslog-pc.fix b/changelog.d/syslog-pc.fix new file mode 100644 index 000000000..3f08cec47 --- /dev/null +++ b/changelog.d/syslog-pc.fix @@ -0,0 +1 @@ +Fix intermittent failure to compile syslog package diff --git a/mix.exs b/mix.exs index 4f82b1f53..23af9d7df 100644 --- a/mix.exs +++ b/mix.exs @@ -172,6 +172,13 @@ defmodule Pleroma.Mixfile do {:gen_smtp, "~> 0.15"}, {:mua, "~> 0.2.4"}, {:mail, "~> 0.3.1"}, + # Fixes intermittent failure to compile due to an old pc version, + # upstream hasn't made a release with the fix yet. + # https://github.com/Vagabond/erlang-syslog/pull/44 + {:syslog, + git: "https://github.com/Vagabond/erlang-syslog", + ref: "614a1fde7a8654bee2815b599bbdaae1bb0323b5", + override: true}, {:ex_syslogger, "~> 1.5"}, {:floki, "~> 0.38"}, {:timex, "~> 3.7"}, diff --git a/mix.lock b/mix.lock index a4db3f68d..bca6f777b 100644 --- a/mix.lock +++ b/mix.lock @@ -139,7 +139,7 @@ "statistex": {:hex, :statistex, "1.1.0", "7fec1eb2f580a0d2c1a05ed27396a084ab064a40cfc84246dbfb0c72a5c761e5", [:mix], [], "hexpm", "f5950ea26ad43246ba2cce54324ac394a4e7408fdcf98b8e230f503a0cba9cf5"}, "sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"}, "swoosh": {:hex, :swoosh, "1.16.12", "cbb24ad512f2f7f24c7a469661c188a00a8c2cd64e0ab54acd1520f132092dfd", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0e262df1ae510d59eeaaa3db42189a2aa1b3746f73771eb2616fc3f7ee63cc20"}, - "syslog": {:hex, :syslog, "1.1.0", "6419a232bea84f07b56dc575225007ffe34d9fdc91abe6f1b2f254fd71d8efc2", [:rebar3], [], "hexpm", "4c6a41373c7e20587be33ef841d3de6f3beba08519809329ecc4d27b15b659e1"}, + "syslog": {:git, "https://github.com/Vagabond/erlang-syslog", "614a1fde7a8654bee2815b599bbdaae1bb0323b5", [ref: "614a1fde7a8654bee2815b599bbdaae1bb0323b5"]}, "table_rex": {:hex, :table_rex, "4.1.0", "fbaa8b1ce154c9772012bf445bfb86b587430fb96f3b12022d3f35ee4a68c918", [:mix], [], "hexpm", "95932701df195d43bc2d1c6531178fc8338aa8f38c80f098504d529c43bc2601"}, "telemetry": {:hex, :telemetry, "1.0.0", "0f453a102cdf13d506b7c0ab158324c337c41f1cc7548f0bc0e130bbf0ae9452", [:rebar3], [], "hexpm", "73bc09fa59b4a0284efb4624335583c528e07ec9ae76aca96ea0673850aec57a"}, "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"}, From 59042e2793f67b4ca8fc89a3ad67c8409244d45e Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 18 Aug 2026 21:41:48 +0200 Subject: [PATCH 7/7] Syslog: set runtime: false to fix OTP releases The syslog package is not directly part of the runtime. --- changelog.d/syslog-fix-otp.skip | 0 mix.exs | 3 ++- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/syslog-fix-otp.skip diff --git a/changelog.d/syslog-fix-otp.skip b/changelog.d/syslog-fix-otp.skip new file mode 100644 index 000000000..e69de29bb diff --git a/mix.exs b/mix.exs index 23af9d7df..140aafb98 100644 --- a/mix.exs +++ b/mix.exs @@ -178,7 +178,8 @@ defmodule Pleroma.Mixfile do {:syslog, git: "https://github.com/Vagabond/erlang-syslog", ref: "614a1fde7a8654bee2815b599bbdaae1bb0323b5", - override: true}, + override: true, + runtime: false}, {:ex_syslogger, "~> 1.5"}, {:floki, "~> 0.38"}, {:timex, "~> 3.7"},