From 627c944fecebe3d48a6bc35ed62273fc058814ff Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 26 Jun 2024 09:20:46 -0400 Subject: [PATCH 001/127] Search Indexing: filter indexable activities before inserting Oban jobs --- lib/pleroma/search.ex | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/search.ex b/lib/pleroma/search.ex index fd0218cb8..b60ca3d05 100644 --- a/lib/pleroma/search.ex +++ b/lib/pleroma/search.ex @@ -1,12 +1,26 @@ defmodule Pleroma.Search do + alias Pleroma.Activity + alias Pleroma.Object alias Pleroma.Workers.SearchIndexingWorker - def add_to_index(%Pleroma.Activity{id: activity_id}) do - SearchIndexingWorker.enqueue("add_to_index", %{"activity" => activity_id}) + @spec add_to_index(Activity.t()) :: :ok | :error + def add_to_index(%Pleroma.Activity{id: activity_id} = activity) do + with true <- indexable?(activity), + {:ok, %Oban.Job{}} <- + SearchIndexingWorker.enqueue("add_to_index", %{"activity" => activity_id}) do + :ok + else + false -> :ok + _ -> :error + end end + @spec remove_from_index(Object.t()) :: :ok | :error def remove_from_index(%Pleroma.Object{id: object_id}) do - SearchIndexingWorker.enqueue("remove_from_index", %{"object" => object_id}) + case SearchIndexingWorker.enqueue("remove_from_index", %{"object" => object_id}) do + {:ok, %Oban.Job{}} -> :ok + _ -> :error + end end def search(query, options) do @@ -18,4 +32,7 @@ defmodule Pleroma.Search do search_module = Pleroma.Config.get([Pleroma.Search, :module]) search_module.healthcheck_endpoints end + + defp indexable?(%Activity{object: %Object{}, data: %{"type" => "Create"}}), do: true + defp indexable?(_), do: false end From a5c88eb39b8d01b7d831dd1ba95fc1c00f0eba52 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 26 Jun 2024 09:24:48 -0400 Subject: [PATCH 002/127] Remove redundant checks from backends' add_to_index/1 --- lib/pleroma/search/meilisearch.ex | 34 +++++++++++++---------------- lib/pleroma/search/qdrant_search.ex | 26 ++++++++++------------ 2 files changed, 26 insertions(+), 34 deletions(-) diff --git a/lib/pleroma/search/meilisearch.ex b/lib/pleroma/search/meilisearch.ex index 9bba5b30f..fb8fdea1b 100644 --- a/lib/pleroma/search/meilisearch.ex +++ b/lib/pleroma/search/meilisearch.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Search.Meilisearch do alias Pleroma.Activity alias Pleroma.Config.Getting, as: Config + alias Pleroma.Object import Pleroma.Search.DatabaseSearch import Ecto.Query @@ -155,28 +156,23 @@ defmodule Pleroma.Search.Meilisearch do end @impl true - def add_to_index(activity) do - maybe_search_data = object_to_search_data(activity.object) + def add_to_index(%Activity{object: %Object{} = object} = activity) do + search_data = object_to_search_data(object) - if activity.data["type"] == "Create" and maybe_search_data do - result = - meili_put( - "/indexes/objects/documents", - [maybe_search_data] - ) + result = + meili_put( + "/indexes/objects/documents", + [search_data] + ) - with {:ok, %{"status" => "enqueued"}} <- result do - # Added successfully - :ok - else - _ -> - # There was an error, report it - Logger.error("Failed to add activity #{activity.id} to index: #{inspect(result)}") - {:error, result} - end - else - # The post isn't something we can search, that's ok + with {:ok, %{"status" => "enqueued"}} <- result do + # Added successfully :ok + else + _ -> + # There was an error, report it + Logger.error("Failed to add activity #{activity.id} to index: #{inspect(result)}") + {:error, result} end end diff --git a/lib/pleroma/search/qdrant_search.ex b/lib/pleroma/search/qdrant_search.ex index b659bb682..f00475799 100644 --- a/lib/pleroma/search/qdrant_search.ex +++ b/lib/pleroma/search/qdrant_search.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Search.QdrantSearch do alias Pleroma.Activity alias Pleroma.Config.Getting, as: Config + alias Pleroma.Object alias __MODULE__.OpenAIClient alias __MODULE__.QdrantClient @@ -82,23 +83,18 @@ defmodule Pleroma.Search.QdrantSearch do end @impl true - def add_to_index(activity) do - # This will only index public or unlisted notes - maybe_search_data = object_to_search_data(activity.object) + def add_to_index(%Activity{object: %Object{} = object} = activity) do + search_data = object_to_search_data(object) - if activity.data["type"] == "Create" and maybe_search_data do - with {:ok, embedding} <- get_embedding(maybe_search_data.content), - {:ok, %{status: 200}} <- - QdrantClient.put( - "/collections/posts/points", - build_index_payload(activity, embedding) - ) do - :ok - else - e -> {:error, e} - end - else + with {:ok, embedding} <- get_embedding(search_data.content), + {:ok, %{status: 200}} <- + QdrantClient.put( + "/collections/posts/points", + build_index_payload(activity, embedding) + ) do :ok + else + e -> {:error, e} end end From 77436451adf736285f3aa3435cd79d0429eb797d Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 26 Jun 2024 09:26:02 -0400 Subject: [PATCH 003/127] Indexable changelog --- changelog.d/search-indexing.change | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/search-indexing.change diff --git a/changelog.d/search-indexing.change b/changelog.d/search-indexing.change new file mode 100644 index 000000000..766934f3f --- /dev/null +++ b/changelog.d/search-indexing.change @@ -0,0 +1 @@ +Filter indexable activities before inserting indexing jobs into the queue. From 7c09150cdbdf8c270021bb7b1ccfcc1632354e23 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 26 Jun 2024 10:02:48 -0400 Subject: [PATCH 004/127] Validate the activity is public before indexing Additional tests added --- lib/pleroma/search.ex | 9 ++-- test/pleroma/search/meilisearch_test.exs | 23 -------- test/pleroma/search_test.exs | 69 ++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 26 deletions(-) create mode 100644 test/pleroma/search_test.exs diff --git a/lib/pleroma/search.ex b/lib/pleroma/search.ex index b60ca3d05..2c7a60c48 100644 --- a/lib/pleroma/search.ex +++ b/lib/pleroma/search.ex @@ -1,16 +1,19 @@ defmodule Pleroma.Search do alias Pleroma.Activity alias Pleroma.Object + alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Workers.SearchIndexingWorker @spec add_to_index(Activity.t()) :: :ok | :error - def add_to_index(%Pleroma.Activity{id: activity_id} = activity) do - with true <- indexable?(activity), + def add_to_index(%Pleroma.Activity{id: activity_id, object: %Object{} = object} = activity) do + with {_, true} <- {:indexable, indexable?(activity)}, + {_, "public"} <- {:visibility, Visibility.get_visibility(object)}, {:ok, %Oban.Job{}} <- SearchIndexingWorker.enqueue("add_to_index", %{"activity" => activity_id}) do :ok else - false -> :ok + {:indexable, false} -> :ok + {:visibility, _} -> :ok _ -> :error end end diff --git a/test/pleroma/search/meilisearch_test.exs b/test/pleroma/search/meilisearch_test.exs index eea454323..ff32491c5 100644 --- a/test/pleroma/search/meilisearch_test.exs +++ b/test/pleroma/search/meilisearch_test.exs @@ -74,29 +74,6 @@ defmodule Pleroma.Search.MeilisearchTest do assert_received("posted_to_meilisearch") end - test "doesn't index posts that are not public" do - user = insert(:user) - - Enum.each(["private", "direct"], fn visibility -> - {:ok, activity} = - CommonAPI.post(user, %{ - status: "guys i just don't wanna leave the swamp", - visibility: visibility - }) - - args = %{"op" => "add_to_index", "activity" => activity.id} - - Config - |> expect(:get, fn - [Pleroma.Search, :module], nil -> - Meilisearch - end) - - assert_enqueued(worker: SearchIndexingWorker, args: args) - assert :ok = perform_job(SearchIndexingWorker, args) - end) - end - test "deletes posts from index when deleted locally" do user = insert(:user) diff --git a/test/pleroma/search_test.exs b/test/pleroma/search_test.exs new file mode 100644 index 000000000..cdd2a72bd --- /dev/null +++ b/test/pleroma/search_test.exs @@ -0,0 +1,69 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.SearchTest do + use Pleroma.DataCase, async: true + use Oban.Testing, repo: Pleroma.Repo + + import Pleroma.Factory + + alias Pleroma.Web.CommonAPI + alias Pleroma.Workers.SearchIndexingWorker + + test "indexes posts that are public" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Well this is a story all about how my life got flipped turned upside down", + visibility: "public" + }) + + args = %{"op" => "add_to_index", "activity" => activity.id} + + assert_enqueued(worker: SearchIndexingWorker, args: args) + end + + test "doesn't index posts that are not public" do + user = insert(:user) + + Enum.each(["private", "direct"], fn visibility -> + {:ok, activity} = + CommonAPI.post(user, %{ + status: "guys i just don't wanna leave the swamp", + visibility: visibility + }) + + args = %{"op" => "add_to_index", "activity" => activity.id} + + refute_enqueued(worker: SearchIndexingWorker, args: args) + end) + end + + test "Indexes appropriate activity types" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "I'm my own hype man", + visibility: "public" + }) + + args = %{"op" => "add_to_index", "activity" => activity.id} + + assert_enqueued(worker: SearchIndexingWorker, args: args) + + {:ok, fav_activity} = CommonAPI.favorite(user, activity.id) + + args = %{"op" => "add_to_index", "activity" => fav_activity.id} + + refute_enqueued(worker: SearchIndexingWorker, args: args) + + {:ok, repeat_activity} = CommonAPI.repeat(activity.id, user) + + args = %{"op" => "add_to_index", "activity" => repeat_activity.id} + + refute_enqueued(worker: SearchIndexingWorker, args: args) + end +end From 592955a895fff4003e62155b08209490625d156d Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 26 Jun 2024 11:04:52 -0400 Subject: [PATCH 005/127] Improve add_to_index/1 when there is no Object --- lib/pleroma/search.ex | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/search.ex b/lib/pleroma/search.ex index 2c7a60c48..7eb88672b 100644 --- a/lib/pleroma/search.ex +++ b/lib/pleroma/search.ex @@ -5,7 +5,7 @@ defmodule Pleroma.Search do alias Pleroma.Workers.SearchIndexingWorker @spec add_to_index(Activity.t()) :: :ok | :error - def add_to_index(%Pleroma.Activity{id: activity_id, object: %Object{} = object} = activity) do + def add_to_index(%Activity{id: activity_id, object: %Object{} = object} = activity) do with {_, true} <- {:indexable, indexable?(activity)}, {_, "public"} <- {:visibility, Visibility.get_visibility(object)}, {:ok, %Oban.Job{}} <- @@ -18,6 +18,13 @@ defmodule Pleroma.Search do end end + def add_to_index(%Activity{id: activity_id}) do + case Activity.get_by_id_with_object(activity_id) do + %Activity{} = preloaded -> add_to_index(preloaded) + _ -> :ok + end + end + @spec remove_from_index(Object.t()) :: :ok | :error def remove_from_index(%Pleroma.Object{id: object_id}) do case SearchIndexingWorker.enqueue("remove_from_index", %{"object" => object_id}) do @@ -36,6 +43,6 @@ defmodule Pleroma.Search do search_module.healthcheck_endpoints end - defp indexable?(%Activity{object: %Object{}, data: %{"type" => "Create"}}), do: true + defp indexable?(%Activity{data: %{"type" => "Create"}}), do: true defp indexable?(_), do: false end From 9ede9b92d3604486fdf7189ef6d55c356ffb190a Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Tue, 6 Jan 2026 15:32:01 +0400 Subject: [PATCH 006/127] RateLimiterTest: Add failing test for invalid values. --- test/pleroma/web/plugs/rate_limiter_test.exs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/pleroma/web/plugs/rate_limiter_test.exs b/test/pleroma/web/plugs/rate_limiter_test.exs index 19cee8aee..10c93fa73 100644 --- a/test/pleroma/web/plugs/rate_limiter_test.exs +++ b/test/pleroma/web/plugs/rate_limiter_test.exs @@ -268,6 +268,23 @@ defmodule Pleroma.Web.Plugs.RateLimiterTest do refute {:err, :not_found} == RateLimiter.inspect_bucket(conn, limiter_name, opts) end + test "doesn't crash if rate limit scale is invalid (e.g. broken DB config)" do + limiter_name = :test_invalid_rate_limit_config + + clear_config([:rate_limit, limiter_name], [{"", 0}, {"", ""}]) + clear_config([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) + + opts = RateLimiter.init(name: limiter_name) + + conn = %{build_conn(:get, "/") | remote_ip: {127, 0, 0, 1}} + + conn_limited = RateLimiter.call(conn, opts) + + refute conn_limited.status == Conn.Status.code(:too_many_requests) + refute conn_limited.resp_body + refute conn_limited.halted + end + def expire_ttl(%{remote_ip: remote_ip} = _conn, bucket_name_root) do bucket_name = "anon:#{bucket_name_root}" |> String.to_atom() key_name = "ip::#{remote_ip |> Tuple.to_list() |> Enum.join(".")}" From 958a4581d647110f00a108e9b77ccfd17fe2a78a Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Tue, 6 Jan 2026 15:54:06 +0400 Subject: [PATCH 007/127] RateLimiter: Ensure that the rate limiter doesn't crash on bad values --- lib/pleroma/web/plugs/rate_limiter.ex | 96 ++++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/plugs/rate_limiter.ex b/lib/pleroma/web/plugs/rate_limiter.ex index aa79dbf6b..22c145514 100644 --- a/lib/pleroma/web/plugs/rate_limiter.ex +++ b/lib/pleroma/web/plugs/rate_limiter.ex @@ -67,6 +67,7 @@ defmodule Pleroma.Web.Plugs.RateLimiter do import Plug.Conn alias Pleroma.Config + alias Pleroma.Config.Holder alias Pleroma.User alias Pleroma.Web.Plugs.RateLimiter.LimiterSupervisor @@ -143,7 +144,7 @@ defmodule Pleroma.Web.Plugs.RateLimiter do def action_settings(plug_opts) do with limiter_name when is_atom(limiter_name) <- plug_opts[:name], - limits when not is_nil(limits) <- Config.get([:rate_limit, limiter_name]) do + {:ok, limits} <- fetch_and_normalize_limits(limiter_name) do bucket_name_root = Keyword.get(plug_opts, :bucket_name, limiter_name) %{ @@ -151,9 +152,102 @@ defmodule Pleroma.Web.Plugs.RateLimiter do limits: limits, opts: plug_opts } + else + :disabled -> nil end end + defp fetch_and_normalize_limits(limiter_name) do + limits = Config.get([:rate_limit, limiter_name]) + + case normalize_limits(limits) do + {:ok, limits} -> + {:ok, limits} + + :disabled -> + :disabled + + :error -> + default_limits = + Holder.default_config(:pleroma, :rate_limit) + |> get_default_limits(limiter_name) + + case normalize_limits(default_limits) do + {:ok, normalized_limits} -> + warn_invalid_limits_once(limiter_name, limits) + {:ok, normalized_limits} + + _ -> + warn_invalid_limits_once(limiter_name, limits) + :disabled + end + end + end + + defp get_default_limits(%{} = rate_limit, limiter_name), do: Map.get(rate_limit, limiter_name) + + defp get_default_limits(rate_limit, limiter_name) when is_list(rate_limit) do + if Keyword.keyword?(rate_limit) do + Keyword.get(rate_limit, limiter_name) + else + nil + end + end + + defp get_default_limits(_, _), do: nil + + @invalid_limits_warned_key {__MODULE__, :invalid_limits_warned} + + defp warn_invalid_limits_once(limiter_name, limits) do + warned = :persistent_term.get(@invalid_limits_warned_key, MapSet.new()) + + if MapSet.member?(warned, limiter_name) do + :ok + else + :persistent_term.put(@invalid_limits_warned_key, MapSet.put(warned, limiter_name)) + + Logger.warning( + "Invalid rate limiter config for #{inspect(limiter_name)}: #{inspect(limits)}. Falling back to defaults or disabling this limiter." + ) + end + end + + defp normalize_limits(nil), do: :disabled + + defp normalize_limits({scale, limit}) do + with {:ok, scale} <- normalize_integer(scale), + {:ok, limit} <- normalize_integer(limit), + true <- scale >= 1 and limit >= 1 do + {:ok, {scale, limit}} + else + _ -> :error + end + end + + defp normalize_limits([{_, _} = first, {_, _} = second]) do + with {:ok, first} <- normalize_limits(first), + {:ok, second} <- normalize_limits(second) do + {:ok, [first, second]} + else + _ -> :error + end + end + + defp normalize_limits(_), do: :error + + defp normalize_integer(value) when is_integer(value), do: {:ok, value} + + defp normalize_integer(value) when is_binary(value) do + value = String.trim(value) + + case Integer.parse(value) do + {number, ""} -> {:ok, number} + _ -> :error + end + end + + defp normalize_integer(_), do: :error + defp check_rate(action_settings) do bucket_name = make_bucket_name(action_settings) key_name = make_key_name(action_settings) From 47f4bde0ea0475cd530dd3127ccf863131a0c335 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Tue, 6 Jan 2026 16:06:51 +0400 Subject: [PATCH 008/127] ConfigDBTest: Add failing test for invalid rate limiter values. --- test/pleroma/config_db_test.exs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/pleroma/config_db_test.exs b/test/pleroma/config_db_test.exs index d68e4e6fa..bb20ac4cd 100644 --- a/test/pleroma/config_db_test.exs +++ b/test/pleroma/config_db_test.exs @@ -174,6 +174,22 @@ defmodule Pleroma.ConfigDBTest do assert updated1.value == [groups: [c: 3, d: 4], key: [a: 1, b: 2]] assert updated2.value == [mascots: [c: 3, d: 4], key: [a: 1, b: 2]] end + + test "rejects invalid :rate_limit values (e.g. empty-string scale from AdminFE)" do + assert {:error, _changeset} = + ConfigDB.update_or_create(%{ + group: ":pleroma", + key: ":rate_limit", + value: [ + %{ + "tuple" => [ + ":statuses_actions", + [%{"tuple" => ["", 0]}, %{"tuple" => ["", ""]}] + ] + } + ] + }) + end end describe "delete/1" do From bd619162708487adfc034cf2859f1190eb3717ef Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Tue, 6 Jan 2026 16:34:17 +0400 Subject: [PATCH 009/127] ConfigDB, RateLimiter, RateLimit: Use new type to parse and cast rate limits. --- lib/pleroma/config_db.ex | 52 ++++++++++++++++ lib/pleroma/ecto_type/config/rate_limit.ex | 71 ++++++++++++++++++++++ lib/pleroma/web/plugs/rate_limiter.ex | 36 ++--------- 3 files changed, 128 insertions(+), 31 deletions(-) create mode 100644 lib/pleroma/ecto_type/config/rate_limit.ex diff --git a/lib/pleroma/config_db.ex b/lib/pleroma/config_db.ex index e9990fa35..2c3df773b 100644 --- a/lib/pleroma/config_db.ex +++ b/lib/pleroma/config_db.ex @@ -10,6 +10,7 @@ defmodule Pleroma.ConfigDB do import Pleroma.Web.Gettext alias __MODULE__ + alias Pleroma.EctoType.Config.RateLimit alias Pleroma.Repo @type t :: %__MODULE__{} @@ -60,8 +61,59 @@ defmodule Pleroma.ConfigDB do |> cast(params, [:key, :group, :value]) |> validate_required([:key, :group, :value]) |> unique_constraint(:key, name: :config_group_key_index) + |> validate_rate_limit() end + defp validate_rate_limit(changeset) do + group = get_field(changeset, :group) + key = get_field(changeset, :key) + + if group == :pleroma and key == :rate_limit do + value = get_field(changeset, :value) + + case normalize_rate_limit(value) do + {:ok, normalized_value} -> + put_change(changeset, :value, normalized_value) + + {:error, {limiter_name, reason}} -> + add_error( + changeset, + :value, + "invalid :rate_limit value for #{inspect(limiter_name)}: #{reason}" + ) + end + else + changeset + end + end + + defp normalize_rate_limit(nil), do: {:ok, nil} + + defp normalize_rate_limit(%{} = value), do: normalize_rate_limit(Map.to_list(value)) + + defp normalize_rate_limit(value) when is_list(value) do + if Keyword.keyword?(value) do + value + |> Enum.reduce_while({:ok, []}, fn {limiter_name, limiter_value}, {:ok, acc} -> + case RateLimit.cast_with_error(limiter_value) do + {:ok, normalized_limiter_value} -> + {:cont, {:ok, [{limiter_name, normalized_limiter_value} | acc]}} + + {:error, reason} -> + {:halt, {:error, {limiter_name, reason}}} + end + end) + |> case do + {:ok, acc} -> {:ok, Enum.reverse(acc)} + {:error, _} = error -> error + end + else + {:error, {:rate_limit, "must be a keyword list"}} + end + end + + defp normalize_rate_limit(_), do: {:error, {:rate_limit, "must be a keyword list"}} + defp create(params) do %ConfigDB{} |> changeset(params) diff --git a/lib/pleroma/ecto_type/config/rate_limit.ex b/lib/pleroma/ecto_type/config/rate_limit.ex new file mode 100644 index 000000000..0518ffd7e --- /dev/null +++ b/lib/pleroma/ecto_type/config/rate_limit.ex @@ -0,0 +1,71 @@ +# Pleroma: A lightweight social networking server +# Copyright © Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.EctoType.Config.RateLimit do + @moduledoc false + + use Ecto.Type + + @type t :: + nil + | {non_neg_integer(), non_neg_integer()} + | [{non_neg_integer(), non_neg_integer()}] + + @impl true + def type, do: :term + + @impl true + def cast(value) do + case cast_with_error(value) do + {:ok, normalized} -> {:ok, normalized} + {:error, _reason} -> :error + end + end + + @impl true + def load(value), do: cast(value) + + @impl true + def dump(value), do: cast(value) + + @spec cast_with_error(term()) :: {:ok, t()} | {:error, String.t()} + def cast_with_error(nil), do: {:ok, nil} + + def cast_with_error({scale, limit}) do + with {:ok, scale} <- parse_integer(scale, "scale"), + {:ok, limit} <- parse_integer(limit, "limit"), + true <- scale >= 1 and limit >= 1 do + {:ok, {scale, limit}} + else + false -> {:error, "scale and limit must be >= 1"} + {:error, reason} -> {:error, reason} + end + end + + def cast_with_error([{_, _} = unauth, {_, _} = auth]) do + with {:ok, unauth} <- cast_with_error(unauth), + {:ok, auth} <- cast_with_error(auth) do + {:ok, [unauth, auth]} + else + {:error, reason} -> {:error, reason} + end + end + + def cast_with_error(_), + do: + {:error, "must be a {scale, limit} tuple, a [{scale, limit}, {scale, limit}] list, or nil"} + + defp parse_integer(value, _label) when is_integer(value), do: {:ok, value} + + defp parse_integer(value, label) when is_binary(value) do + value = String.trim(value) + + case Integer.parse(value) do + {number, ""} -> {:ok, number} + _ -> {:error, "#{label} must be an integer"} + end + end + + defp parse_integer(_value, label), do: {:error, "#{label} must be an integer"} +end diff --git a/lib/pleroma/web/plugs/rate_limiter.ex b/lib/pleroma/web/plugs/rate_limiter.ex index 22c145514..0ebb73bbc 100644 --- a/lib/pleroma/web/plugs/rate_limiter.ex +++ b/lib/pleroma/web/plugs/rate_limiter.ex @@ -68,6 +68,7 @@ defmodule Pleroma.Web.Plugs.RateLimiter do alias Pleroma.Config alias Pleroma.Config.Holder + alias Pleroma.EctoType.Config.RateLimit alias Pleroma.User alias Pleroma.Web.Plugs.RateLimiter.LimiterSupervisor @@ -214,40 +215,13 @@ defmodule Pleroma.Web.Plugs.RateLimiter do defp normalize_limits(nil), do: :disabled - defp normalize_limits({scale, limit}) do - with {:ok, scale} <- normalize_integer(scale), - {:ok, limit} <- normalize_integer(limit), - true <- scale >= 1 and limit >= 1 do - {:ok, {scale, limit}} - else - _ -> :error + defp normalize_limits(limits) do + case RateLimit.cast(limits) do + {:ok, normalized_limits} -> {:ok, normalized_limits} + :error -> :error end end - defp normalize_limits([{_, _} = first, {_, _} = second]) do - with {:ok, first} <- normalize_limits(first), - {:ok, second} <- normalize_limits(second) do - {:ok, [first, second]} - else - _ -> :error - end - end - - defp normalize_limits(_), do: :error - - defp normalize_integer(value) when is_integer(value), do: {:ok, value} - - defp normalize_integer(value) when is_binary(value) do - value = String.trim(value) - - case Integer.parse(value) do - {number, ""} -> {:ok, number} - _ -> :error - end - end - - defp normalize_integer(_), do: :error - defp check_rate(action_settings) do bucket_name = make_bucket_name(action_settings) key_name = make_key_name(action_settings) From b9c281a0c3087d67de2292a0b27ce33c159b2246 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Tue, 6 Jan 2026 16:42:29 +0400 Subject: [PATCH 010/127] Add changelog --- changelog.d/rate-limiter-hardening.fix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/rate-limiter-hardening.fix diff --git a/changelog.d/rate-limiter-hardening.fix b/changelog.d/rate-limiter-hardening.fix new file mode 100644 index 000000000..a3af8fcc4 --- /dev/null +++ b/changelog.d/rate-limiter-hardening.fix @@ -0,0 +1 @@ +Stop the rate limiter from crashing when run with wrong settings. From 1af8997462c52e52c72be436aee990621b692dd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 16 Jan 2026 21:32:35 +0100 Subject: [PATCH 011/127] do not ever allow setting database_config_whitelist to database MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- lib/pleroma/web/admin_api/controllers/config_controller.ex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/pleroma/web/admin_api/controllers/config_controller.ex b/lib/pleroma/web/admin_api/controllers/config_controller.ex index 2c9c27294..f3adeb35a 100644 --- a/lib/pleroma/web/admin_api/controllers/config_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/config_controller.ex @@ -174,6 +174,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do end end + defp whitelisted_config?(:pleroma, :database_config_whitelist), do: false + defp whitelisted_config?(group, key) do if whitelisted_configs = Config.get(:database_config_whitelist) do Enum.any?(whitelisted_configs, fn From 57a3b1f6d0069934cc564c192dd46e834be8497e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 16 Jan 2026 21:33:14 +0100 Subject: [PATCH 012/127] Add sane defaults for :database_config_whitelist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- config/config.exs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/config/config.exs b/config/config.exs index 683805fe3..5bf2c5c2e 100644 --- a/config/config.exs +++ b/config/config.exs @@ -960,6 +960,15 @@ config :pleroma, Pleroma.Search.QdrantSearch, vectors: %{size: 384, distance: "Cosine"} } +config :pleroma, :database_config_whitelist, [ + {:pleroma}, + {:cors_plug}, + {:ex_aws, :s3}, + {:mime}, + {:prometheus, Pleroma.Web.Endpoint.MetricsExporter}, + {:web_push_encryption, :vapid_details} +] + # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{Mix.env()}.exs" From f0669997d30d79fc2d93e7bbadbb8f6fd418cc03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 16 Jan 2026 21:34:06 +0100 Subject: [PATCH 013/127] Add test for default whitelist config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../web/admin_api/controllers/config_controller_test.exs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/pleroma/web/admin_api/controllers/config_controller_test.exs b/test/pleroma/web/admin_api/controllers/config_controller_test.exs index e12115ea1..ab216e49f 100644 --- a/test/pleroma/web/admin_api/controllers/config_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/config_controller_test.exs @@ -1472,5 +1472,13 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do web_endpoint = Enum.find(children, fn c -> c["key"] == "Pleroma.Upload" end) assert web_endpoint["children"] end + + test "all keys from description are whitelisted", %{conn: conn} do + conn = get(conn, "/api/pleroma/admin/config/descriptions") + + assert response = json_response_and_validate_schema(conn, 200) + + assert length(response) == length(Pleroma.Docs.JSON.compiled_descriptions()) + end end end From b66b93a94ada205c710f1e93efe7a984485ae43d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 16 Jan 2026 21:34:45 +0100 Subject: [PATCH 014/127] Add task for filtering non-whitelisted configs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/database-config-whitelist.add | 1 + docs/administration/CLI_tasks/config.md | 16 ++++++- lib/mix/tasks/pleroma/config.ex | 56 +++++++++++++++++++++++ test/mix/tasks/pleroma/config_test.exs | 18 ++++++++ 4 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 changelog.d/database-config-whitelist.add diff --git a/changelog.d/database-config-whitelist.add b/changelog.d/database-config-whitelist.add new file mode 100644 index 000000000..a78960c98 --- /dev/null +++ b/changelog.d/database-config-whitelist.add @@ -0,0 +1 @@ +Add reasonable defaults for :database_config_whitelist \ No newline at end of file diff --git a/docs/administration/CLI_tasks/config.md b/docs/administration/CLI_tasks/config.md index 13d671a7e..35e02145e 100644 --- a/docs/administration/CLI_tasks/config.md +++ b/docs/administration/CLI_tasks/config.md @@ -169,4 +169,18 @@ This forcibly removes any enabled MRF that does not exist and will fix the abili === "From Source" ```sh mix pleroma.config fix_mrf_policies - ``` \ No newline at end of file + ``` + +## Remove non-whitelisted configs from the database + +This removes any configuration value that is not explicitly whitelisted by `:pleroma, :database_config_whitelist`. Might be useful after updating the whitelist. + +=== "OTP" + ```sh + ./bin/pleroma_ctl config filter_whitelisted + ``` + +=== "From Source" + ```sh + mix pleroma.config filter_whitelisted + ``` diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex index 834b4fe14..fc6982ea8 100644 --- a/lib/mix/tasks/pleroma/config.ex +++ b/lib/mix/tasks/pleroma/config.ex @@ -234,6 +234,57 @@ defmodule Mix.Tasks.Pleroma.Config do end) end + # Removes non-whitelisted configuration sections + def run(["filter_whitelisted" | rest]) do + {options, [], []} = + OptionParser.parse( + rest, + strict: [force: :boolean], + aliases: [f: :force] + ) + + force = Keyword.get(options, :force, false) + + start_pleroma() + + whitelisted_configs = Pleroma.Config.get(:database_config_whitelist) + + whitelisted_groups = + whitelisted_configs + |> Enum.filter(fn + {_group} -> true + _ -> false + end) + |> Enum.map(fn {group} -> group end) + + whitelisted_keys = + whitelisted_configs + |> Enum.filter(fn + {_group, _key} -> true + _ -> false + end) + + filtered = + from(c in ConfigDB) + |> Repo.all() + |> Enum.filter(¬_whitelisted?(&1, whitelisted_groups, whitelisted_keys)) + + if not Enum.empty?(filtered) do + shell_info("The following settings will be removed from ConfigDB:\n") + Enum.each(filtered, &dump(&1)) + + if force or shell_prompt("Are you sure you want to continue?", "n") in ~w(Yn Y y) do + filtered_ids = Enum.map(filtered, fn %{id: id} -> id end) + + Repo.delete_all(from(c in ConfigDB, where: c.id in ^filtered_ids)) + else + shell_error("No changes made.") + end + else + shell_error("No unwanted settings in ConfigDB. No changes made.") + end + end + @spec migrate_to_db(Path.t() | nil) :: any() def migrate_to_db(file_path \\ nil) do with :ok <- Pleroma.Config.DeprecationWarnings.warn() do @@ -434,4 +485,9 @@ defmodule Mix.Tasks.Pleroma.Config do Ecto.Adapters.SQL.query!(Repo, "TRUNCATE config;") Ecto.Adapters.SQL.query!(Repo, "ALTER SEQUENCE config_id_seq RESTART;") end + + defp not_whitelisted?(%{group: group, key: key}, whitelisted_groups, whitelisted_keys) do + not Enum.member?(whitelisted_groups, group) and + not Enum.member?(whitelisted_keys, {group, key}) + end end diff --git a/test/mix/tasks/pleroma/config_test.exs b/test/mix/tasks/pleroma/config_test.exs index 942cfa83d..3b1037f0a 100644 --- a/test/mix/tasks/pleroma/config_test.exs +++ b/test/mix/tasks/pleroma/config_test.exs @@ -329,5 +329,23 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do assert config_records() == [] end + + test "filters non-whitelisted settings" do + clear_config(:database_config_whitelist, [ + {:pleroma}, + {:web_push_encryption, :vapid_details} + ]) + + insert_config_record(:web_push_encryption, :non_whitelisted_key, a: 1) + insert_config_record(:web_push_encryption, :vapid_details, b: 1) + + MixTask.run(["filter_whitelisted", "--force"]) + + assert [ + %ConfigDB{group: :pleroma, key: :instance}, + %ConfigDB{group: :pleroma, key: Pleroma.Captcha}, + %ConfigDB{group: :web_push_encryption, key: :vapid_details} + ] = config_records() + end end end From 92fd157cd82f487899e451635d1b4a94560062b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 16 Jan 2026 21:35:53 +0100 Subject: [PATCH 015/127] Update cheatsheet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- docs/configuration/cheatsheet.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 54dd4a5f0..b156c74cf 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -1132,8 +1132,9 @@ Boolean, enables/disables in-database configuration. Read [Transferring the conf List of valid configuration sections which are allowed to be configured from the database. Settings stored in the database before the whitelist is configured are -still applied, so it is suggested to only use the whitelist on instances that -have not migrated the config to the database. +still applied. Consider running the `mix pleroma.config filter_whitelisted` task +after updating the whitelist. Read [Remove non-whitelisted configs from the database](../administration//CLI_tasks/config.md#remove-non-whitelisted-configs-from-the-database) +for more information. Example: ```elixir From 49985b1614467120f1585f9590f5fc7c5c363eed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 16 Jan 2026 21:37:02 +0100 Subject: [PATCH 016/127] Update tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../controllers/config_controller_test.exs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/test/pleroma/web/admin_api/controllers/config_controller_test.exs b/test/pleroma/web/admin_api/controllers/config_controller_test.exs index ab216e49f..7cb4ec938 100644 --- a/test/pleroma/web/admin_api/controllers/config_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/config_controller_test.exs @@ -194,6 +194,16 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do setup do: clear_config(:configurable_from_database, true) + setup do: + clear_config(:database_config_whitelist, [ + {:pleroma}, + {:http}, + {:idna}, + {:oban}, + {:tesla}, + {:ueberauth} + ]) + test "create new config setting in db", %{conn: conn} do ueberauth = Application.get_env(:ueberauth, Ueberauth) on_exit(fn -> Application.put_env(:ueberauth, Ueberauth, ueberauth) end) @@ -807,7 +817,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do %{ "tuple" => [ "/websocket", - "Phoenix.Endpoint.CowboyWebSocket", + ":sth", %{ "tuple" => [ "Phoenix.Transports.WebSocket", @@ -871,7 +881,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do %{ "tuple" => [ "/websocket", - "Phoenix.Endpoint.CowboyWebSocket", + ":sth", %{ "tuple" => [ "Phoenix.Transports.WebSocket", From 77a1d79f92398476a081c3adcca8d227035e6f21 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 17 Jan 2026 12:31:35 +0400 Subject: [PATCH 017/127] ConfigTest: Don't crash when whitelist is unset / disabled --- test/mix/tasks/pleroma/config_test.exs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/mix/tasks/pleroma/config_test.exs b/test/mix/tasks/pleroma/config_test.exs index 3b1037f0a..ef1adc235 100644 --- a/test/mix/tasks/pleroma/config_test.exs +++ b/test/mix/tasks/pleroma/config_test.exs @@ -347,5 +347,21 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do %ConfigDB{group: :web_push_encryption, key: :vapid_details} ] = config_records() end + + test "filter_whitelisted doesn't crash when whitelist is unset" do + clear_config(:database_config_whitelist, nil) + + existing = config_records() + MixTask.run(["filter_whitelisted", "--force"]) + assert config_records() == existing + end + + test "filter_whitelisted doesn't crash when whitelist is disabled" do + clear_config(:database_config_whitelist, false) + + existing = config_records() + MixTask.run(["filter_whitelisted", "--force"]) + assert config_records() == existing + end end end From 0b871ff1f298d84c7b3c12444ee923bcfb1ac02a Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 17 Jan 2026 12:32:10 +0400 Subject: [PATCH 018/127] ConfigController: Don't allow updating the whitelist --- .../controllers/config_controller_test.exs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/pleroma/web/admin_api/controllers/config_controller_test.exs b/test/pleroma/web/admin_api/controllers/config_controller_test.exs index 7cb4ec938..e62d95fad 100644 --- a/test/pleroma/web/admin_api/controllers/config_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/config_controller_test.exs @@ -1220,6 +1220,31 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do assert Application.get_env(:not_real, :anything) == "value6" end + test "doesn't allow updating the database_config_whitelist itself", %{conn: conn} do + original_whitelist = Pleroma.Config.get(:database_config_whitelist) + + refute ConfigDB.get_by_group_and_key(:pleroma, :database_config_whitelist) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + group: ":pleroma", + key: ":database_config_whitelist", + value: [%{"tuple" => [":pleroma", ":key1"]}] + } + ] + }) + + %{"configs" => configs} = json_response_and_validate_schema(conn, 200) + + assert configs == [] + assert Pleroma.Config.get(:database_config_whitelist) == original_whitelist + refute ConfigDB.get_by_group_and_key(:pleroma, :database_config_whitelist) + end + test "args for Pleroma.Upload.Filter.Mogrify with custom tuples", %{conn: conn} do assert conn |> put_req_header("content-type", "application/json") From 49f9ab3034b9809c53369cf0ade33dac8409faa4 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 17 Jan 2026 13:02:18 +0400 Subject: [PATCH 019/127] Cheatsheet: Fix double slash --- docs/configuration/cheatsheet.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index b156c74cf..9efa1c8b3 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -1133,7 +1133,7 @@ Boolean, enables/disables in-database configuration. Read [Transferring the conf List of valid configuration sections which are allowed to be configured from the database. Settings stored in the database before the whitelist is configured are still applied. Consider running the `mix pleroma.config filter_whitelisted` task -after updating the whitelist. Read [Remove non-whitelisted configs from the database](../administration//CLI_tasks/config.md#remove-non-whitelisted-configs-from-the-database) +after updating the whitelist. Read [Remove non-whitelisted configs from the database](../administration/CLI_tasks/config.md#remove-non-whitelisted-configs-from-the-database) for more information. Example: From 117b0bd79ed21338a46a558061cc54e86313f5b5 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 17 Jan 2026 13:03:02 +0400 Subject: [PATCH 020/127] Config: Don't crash on falsy whitelist config --- lib/mix/tasks/pleroma/config.ex | 68 +++++++++++++++++---------------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex index fc6982ea8..c5f2e9b0a 100644 --- a/lib/mix/tasks/pleroma/config.ex +++ b/lib/mix/tasks/pleroma/config.ex @@ -249,39 +249,43 @@ defmodule Mix.Tasks.Pleroma.Config do whitelisted_configs = Pleroma.Config.get(:database_config_whitelist) - whitelisted_groups = - whitelisted_configs - |> Enum.filter(fn - {_group} -> true - _ -> false - end) - |> Enum.map(fn {group} -> group end) - - whitelisted_keys = - whitelisted_configs - |> Enum.filter(fn - {_group, _key} -> true - _ -> false - end) - - filtered = - from(c in ConfigDB) - |> Repo.all() - |> Enum.filter(¬_whitelisted?(&1, whitelisted_groups, whitelisted_keys)) - - if not Enum.empty?(filtered) do - shell_info("The following settings will be removed from ConfigDB:\n") - Enum.each(filtered, &dump(&1)) - - if force or shell_prompt("Are you sure you want to continue?", "n") in ~w(Yn Y y) do - filtered_ids = Enum.map(filtered, fn %{id: id} -> id end) - - Repo.delete_all(from(c in ConfigDB, where: c.id in ^filtered_ids)) - else - shell_error("No changes made.") - end - else + if whitelisted_configs in [nil, false] do shell_error("No unwanted settings in ConfigDB. No changes made.") + else + whitelisted_groups = + whitelisted_configs + |> Enum.filter(fn + {_group} -> true + _ -> false + end) + |> Enum.map(fn {group} -> group end) + + whitelisted_keys = + whitelisted_configs + |> Enum.filter(fn + {_group, _key} -> true + _ -> false + end) + + filtered = + from(c in ConfigDB) + |> Repo.all() + |> Enum.filter(¬_whitelisted?(&1, whitelisted_groups, whitelisted_keys)) + + if not Enum.empty?(filtered) do + shell_info("The following settings will be removed from ConfigDB:\n") + Enum.each(filtered, &dump(&1)) + + if force or shell_prompt("Are you sure you want to continue?", "n") in ~w(Yn Y y) do + filtered_ids = Enum.map(filtered, fn %{id: id} -> id end) + + Repo.delete_all(from(c in ConfigDB, where: c.id in ^filtered_ids)) + else + shell_error("No changes made.") + end + else + shell_error("No unwanted settings in ConfigDB. No changes made.") + end end end From a4fb651fac4416db7d5102672f10c9a9c3ce14fc Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 17 Jan 2026 13:30:07 +0400 Subject: [PATCH 021/127] ConfigController: Don't allow whitelist modification. --- lib/pleroma/web/admin_api/controllers/config_controller.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/admin_api/controllers/config_controller.ex b/lib/pleroma/web/admin_api/controllers/config_controller.ex index f3adeb35a..3eba03525 100644 --- a/lib/pleroma/web/admin_api/controllers/config_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/config_controller.ex @@ -174,7 +174,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do end end - defp whitelisted_config?(:pleroma, :database_config_whitelist), do: false + defp whitelisted_config?(":pleroma", ":database_config_whitelist"), do: false defp whitelisted_config?(group, key) do if whitelisted_configs = Config.get(:database_config_whitelist) do From 80ede85f75f24c968bfa63fb52a7616c32fc788b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 19 Sep 2025 16:43:55 +0200 Subject: [PATCH 022/127] Allow assigning users to reports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/assign-users.add | 1 + docs/development/API/admin_api.md | 33 +++++++ lib/pleroma/constants.ex | 3 +- lib/pleroma/moderation_log.ex | 38 +++++++- lib/pleroma/web/activity_pub/activity_pub.ex | 9 ++ lib/pleroma/web/activity_pub/utils.ex | 28 ++++++ .../controllers/report_controller.ex | 55 ++++++++++- lib/pleroma/web/admin_api/report.ex | 13 ++- .../web/admin_api/views/report_view.ex | 16 +++- .../operations/admin/report_operation.ex | 55 ++++++++++- lib/pleroma/web/common_api.ex | 16 ++++ lib/pleroma/web/router.ex | 1 + ...00_add_activity_assigned_account_index.exs | 11 +++ test/pleroma/web/activity_pub/utils_test.exs | 13 +++ .../controllers/report_controller_test.exs | 92 +++++++++++++++++++ .../web/admin_api/views/report_view_test.exs | 2 + test/pleroma/web/common_api_test.exs | 23 +++++ 17 files changed, 402 insertions(+), 7 deletions(-) create mode 100644 changelog.d/assign-users.add create mode 100644 priv/repo/migrations/20220225164000_add_activity_assigned_account_index.exs diff --git a/changelog.d/assign-users.add b/changelog.d/assign-users.add new file mode 100644 index 000000000..f50ad94c6 --- /dev/null +++ b/changelog.d/assign-users.add @@ -0,0 +1 @@ +Allow assigning users to reports \ No newline at end of file diff --git a/docs/development/API/admin_api.md b/docs/development/API/admin_api.md index 64c06ca2b..3719ceeb9 100644 --- a/docs/development/API/admin_api.md +++ b/docs/development/API/admin_api.md @@ -665,6 +665,7 @@ Status: 404 - *optional* `limit`: **integer** the number of records to retrieve - *optional* `page`: **integer** page number - *optional* `page_size`: **integer** number of log entries per page (default is `50`) + - *optional* `assigned_account`: **string** assigned account ID - Response: - On failure: 403 Forbidden error `{"error": "error_msg"}` when requested by anonymous or non-admin - On success: JSON, returns a list of reports, where: @@ -749,6 +750,7 @@ Status: 404 "url": "https://pleroma.example.org/users/lain", "username": "lain" }, + "assigned_account": null, "content": "Please delete it", "created_at": "2019-04-29T19:48:15.000Z", "id": "9iJGOv1j8hxuw19bcm", @@ -868,6 +870,37 @@ Status: 404 ] ``` +- Response: + - On failure: + - 400 Bad Request, JSON: + + ```json + [ + { + `id`, // report id + `error` // error message + } + ] + ``` + + - On success: `204`, empty response + +## `POST /api/v1/pleroma/admin/reports/assign_account` + +### Assign account to one or multiple reports + +- Params: + +```json + `reports`: [ + { + `id`, // required, report id + `nickname` // account nickname, use null to unassign account + }, + ... + ] +``` + - Response: - On failure: - 400 Bad Request, JSON: diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex index c0411edbf..f78b84099 100644 --- a/lib/pleroma/constants.ex +++ b/lib/pleroma/constants.ex @@ -22,7 +22,8 @@ defmodule Pleroma.Constants do "generator", "rules", "language", - "voters" + "voters", + "assigned_account" ] ) diff --git a/lib/pleroma/moderation_log.ex b/lib/pleroma/moderation_log.ex index 52a71bc2d..90219312c 100644 --- a/lib/pleroma/moderation_log.ex +++ b/lib/pleroma/moderation_log.ex @@ -132,11 +132,18 @@ defmodule Pleroma.ModerationLog do end def insert_log(%{actor: %User{}, action: action, subject: %Activity{} = subject} = attrs) - when action in ["report_note_delete", "report_update", "report_note"] do + when action in [ + "report_note_delete", + "report_update", + "report_note", + "report_unassigned", + "report_assigned" + ] do data = attrs |> prepare_log_data |> Pleroma.Maps.put_if_present("text", attrs[:text]) + |> Pleroma.Maps.put_if_present("assigned_account", attrs[:assigned_account]) |> Map.merge(%{"subject" => report_to_map(subject)}) insert_log_entry_with_message(%ModerationLog{data: data}) @@ -441,6 +448,35 @@ defmodule Pleroma.ModerationLog do " with '#{state}' state" end + def get_log_entry_message( + %ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "report_assigned", + "subject" => %{"id" => subject_id, "type" => "report"}, + "assigned_account" => assigned_account + } + } = log + ) do + "@#{actor_nickname} assigned report ##{subject_id}" <> + subject_actor_nickname(log, " (on user ", ")") <> + " to user #{assigned_account}" + end + + def get_log_entry_message( + %ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "report_unassigned", + "subject" => %{"id" => subject_id, "type" => "report"} + } + } = log + ) do + "@#{actor_nickname} unassigned report ##{subject_id}" <> + subject_actor_nickname(log, " (on user ", ")") <> + " from a user" + end + def get_log_entry_message( %ModerationLog{ data: %{ diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index e58e3dd57..44e9a22e5 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1003,6 +1003,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp restrict_state(query, _), do: query + defp restrict_assigned_account(query, %{assigned_account: assigned_account}) do + from(activity in query, + where: fragment("?->>'assigned_account' = ?", activity.data, ^assigned_account) + ) + end + + defp restrict_assigned_account(query, _), do: query + defp restrict_favorited_by(query, %{favorited_by: ap_id}) do from( [_activity, object] in query, @@ -1471,6 +1479,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do |> restrict_actor(opts) |> restrict_type(opts) |> restrict_state(opts) + |> restrict_assigned_account(opts) |> restrict_favorited_by(opts) |> restrict_blocked(restrict_blocked_opts) |> restrict_blockers_visibility(opts) diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index c5a6901d4..43c0f456d 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -863,6 +863,34 @@ defmodule Pleroma.Web.ActivityPub.Utils do def update_report_state(_, _), do: {:error, "Unsupported state"} + def assign_report_to_account(%Activity{} = activity, nil = _account) do + new_data = Map.delete(activity.data, "assigned_account") + + activity + |> Changeset.change(data: new_data) + |> Repo.update() + end + + def assign_report_to_account(%Activity{} = activity, account) do + new_data = Map.put(activity.data, "assigned_account", account) + + activity + |> Changeset.change(data: new_data) + |> Repo.update() + end + + def assign_report_to_account(activity_ids, account) do + activities_num = length(activity_ids) + + from(a in Activity, where: a.id in ^activity_ids) + |> update(set: [data: fragment("jsonb_set(data, '{assigned_account}', ?)", ^account)]) + |> Repo.update_all([]) + |> case do + {^activities_num, _} -> :ok + _ -> {:error, activity_ids} + end + end + def strip_report_status_data(%Activity{} = activity) do with {:ok, new_data} <- strip_report_status_data(activity.data) do {:ok, %{activity | data: new_data}} diff --git a/lib/pleroma/web/admin_api/controllers/report_controller.ex b/lib/pleroma/web/admin_api/controllers/report_controller.ex index 89d8cc820..dbac03ef4 100644 --- a/lib/pleroma/web/admin_api/controllers/report_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/report_controller.ex @@ -10,6 +10,7 @@ defmodule Pleroma.Web.AdminAPI.ReportController do alias Pleroma.Activity alias Pleroma.ModerationLog alias Pleroma.ReportNote + alias Pleroma.User alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.AdminAPI alias Pleroma.Web.AdminAPI.Report @@ -24,7 +25,7 @@ defmodule Pleroma.Web.AdminAPI.ReportController do plug( OAuthScopesPlug, %{scopes: ["admin:write:reports"]} - when action in [:update, :notes_create, :notes_delete] + when action in [:update, :assign_account, :notes_create, :notes_delete] ) action_fallback(AdminAPI.FallbackController) @@ -79,6 +80,22 @@ defmodule Pleroma.Web.AdminAPI.ReportController do end end + def assign_account( + %{ + assigns: %{user: admin}, + private: %{open_api_spex: %{body_params: %{reports: reports}}} + } = conn, + _ + ) do + result = Enum.map(reports, &do_assign_account(&1, admin)) + + if Enum.any?(result, &Map.has_key?(&1, :error)) do + json_response(conn, :bad_request, result) + else + json_response(conn, :no_content, "") + end + end + def notes_create( %{ assigns: %{user: user}, @@ -131,4 +148,40 @@ defmodule Pleroma.Web.AdminAPI.ReportController do _ -> json_response(conn, :bad_request, "") end end + + defp do_assign_account(%{assigned_account: nil, id: id}, admin) do + with {:ok, activity} <- CommonAPI.assign_report_to_account(id, nil), + report <- Activity.get_by_id_with_user_actor(activity.id) do + ModerationLog.insert_log(%{ + action: "report_unassigned", + actor: admin, + subject: activity, + subject_actor: report.user_actor + }) + + activity + else + {:error, message} -> + %{id: id, error: message} + end + end + + defp do_assign_account(%{assigned_account: assigned_account, id: id}, admin) do + with %User{id: account} = user <- User.get_cached_by_nickname(assigned_account), + {:ok, activity} <- CommonAPI.assign_report_to_account(id, account), + report <- Activity.get_by_id_with_user_actor(activity.id) do + ModerationLog.insert_log(%{ + action: "report_assigned", + actor: admin, + subject: activity, + subject_actor: report.user_actor, + assigned_account: user.nickname + }) + + activity + else + {:error, message} -> + %{id: id, error: message} + end + end end diff --git a/lib/pleroma/web/admin_api/report.ex b/lib/pleroma/web/admin_api/report.ex index fa89e3405..753b92d88 100644 --- a/lib/pleroma/web/admin_api/report.ex +++ b/lib/pleroma/web/admin_api/report.ex @@ -13,6 +13,11 @@ defmodule Pleroma.Web.AdminAPI.Report do user = User.get_cached_by_ap_id(actor) account = User.get_cached_by_ap_id(account_ap_id) + assigned_account = + if Map.has_key?(report.data, "assigned_account") do + User.get_cached_by_id(report.data["assigned_account"]) + end + statuses = status_ap_ids |> Enum.reject(&is_nil(&1)) @@ -26,7 +31,13 @@ defmodule Pleroma.Web.AdminAPI.Report do Activity.get_by_ap_id_with_object(act) end) - %{report: report, user: user, account: account, statuses: statuses} + %{ + report: report, + user: user, + account: account, + statuses: statuses, + assigned_account: assigned_account + } end defp make_fake_activity(act, user) do diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex index b4b0be267..da6166050 100644 --- a/lib/pleroma/web/admin_api/views/report_view.ex +++ b/lib/pleroma/web/admin_api/views/report_view.ex @@ -26,7 +26,13 @@ defmodule Pleroma.Web.AdminAPI.ReportView do } end - def render("show.json", %{report: report, user: user, account: account, statuses: statuses}) do + def render("show.json", %{ + report: report, + user: user, + account: account, + statuses: statuses, + assigned_account: assigned_account + }) do created_at = Utils.to_masto_date(report.data["published"]) content = @@ -36,6 +42,11 @@ defmodule Pleroma.Web.AdminAPI.ReportView do nil end + assigned_account = + if assigned_account do + merge_account_views(assigned_account) + end + %{ id: report.id, account: merge_account_views(account), @@ -49,7 +60,8 @@ defmodule Pleroma.Web.AdminAPI.ReportView do }), state: report.data["state"], notes: render(__MODULE__, "index_notes.json", %{notes: report.report_notes}), - rules: rules(Map.get(report.data, "rules", nil)) + rules: rules(Map.get(report.data, "rules", nil)), + assigned_account: assigned_account } end diff --git a/lib/pleroma/web/api_spec/operations/admin/report_operation.ex b/lib/pleroma/web/api_spec/operations/admin/report_operation.ex index 25a604beb..58669a1fc 100644 --- a/lib/pleroma/web/api_spec/operations/admin/report_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/report_operation.ex @@ -53,6 +53,12 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do :query, %Schema{type: :integer, default: 50}, "Number number of log entries per page" + ), + Operation.parameter( + :assigned_account, + :query, + %Schema{type: :string}, + "Filter by assigned account ID" ) | admin_api_params() ], @@ -103,6 +109,22 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do } end + def assign_account_operation do + %Operation{ + tags: ["Report management"], + summary: "Assign account to specified reports", + operationId: "AdminAPI.ReportController.assign_account", + security: [%{"oAuth" => ["admin:write:reports"]}], + parameters: admin_api_params(), + requestBody: request_body("Parameters", assign_account_request(), required: true), + responses: %{ + 204 => no_content_response(), + 400 => Operation.response("Bad Request", "application/json", update_400_response()), + 403 => Operation.response("Forbidden", "application/json", ApiError) + } + } + end + def notes_create_operation do %Operation{ tags: ["Report management"], @@ -186,7 +208,10 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do hint: %Schema{type: :string, nullable: true} } } - } + }, + assigned_account: + account_admin() + |> Map.put(:nullable, true) } } end @@ -242,6 +267,34 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do } end + defp assign_account_request do + %Schema{ + type: :object, + required: [:reports], + properties: %{ + reports: %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + id: %Schema{allOf: [FlakeID], description: "Required, report ID"}, + assigned_account: %Schema{ + type: :string, + description: "User nickname", + nullable: true + } + } + }, + example: %{ + "reports" => [ + %{"id" => "123", "assigned_account" => "pleroma"} + ] + } + } + } + } + end + defp update_400_response do %Schema{ type: :array, diff --git a/lib/pleroma/web/common_api.ex b/lib/pleroma/web/common_api.ex index 8e96ef5b6..04181ad8f 100644 --- a/lib/pleroma/web/common_api.ex +++ b/lib/pleroma/web/common_api.ex @@ -709,6 +709,22 @@ defmodule Pleroma.Web.CommonAPI do end end + def assign_report_to_account(activity_ids, user) when is_list(activity_ids) do + case Utils.assign_report_to_account(activity_ids, user) do + :ok -> {:ok, activity_ids} + _ -> {:error, dgettext("errors", "Could not assign account")} + end + end + + def assign_report_to_account(activity_id, user) do + with %Activity{} = activity <- Activity.get_by_id(activity_id) do + Utils.assign_report_to_account(activity, user) + else + nil -> {:error, :not_found} + _ -> {:error, dgettext("errors", "Could not assign account")} + end + end + @spec update_activity_scope(String.t(), map()) :: {:ok, any()} | {:error, any()} def update_activity_scope(activity_id, opts \\ %{}) do with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id), diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 008f48575..20ac1c67b 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -395,6 +395,7 @@ defmodule Pleroma.Web.Router do get("/reports", ReportController, :index) get("/reports/:id", ReportController, :show) patch("/reports", ReportController, :update) + post("/reports/assign_account", ReportController, :assign_account) post("/reports/:id/notes", ReportController, :notes_create) delete("/reports/:report_id/notes/:id", ReportController, :notes_delete) end diff --git a/priv/repo/migrations/20220225164000_add_activity_assigned_account_index.exs b/priv/repo/migrations/20220225164000_add_activity_assigned_account_index.exs new file mode 100644 index 000000000..b54daf43d --- /dev/null +++ b/priv/repo/migrations/20220225164000_add_activity_assigned_account_index.exs @@ -0,0 +1,11 @@ +defmodule Pleroma.Repo.Migrations.AddActivityAssignedAccountIndex do + use Ecto.Migration + + def change do + create_if_not_exists( + index(:activities, ["(data->>'assigned_account')"], + name: :activities_assigned_account_index + ) + ) + end +end diff --git a/test/pleroma/web/activity_pub/utils_test.exs b/test/pleroma/web/activity_pub/utils_test.exs index f162f3684..3b77f0867 100644 --- a/test/pleroma/web/activity_pub/utils_test.exs +++ b/test/pleroma/web/activity_pub/utils_test.exs @@ -671,6 +671,19 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do end end + describe "assign_report_to_account/2" do + test "assigns report to an account" do + reporter = insert(:user) + target_account = insert(:user) + %{id: assigned_id} = insert(:user) + + {:ok, report} = CommonAPI.report(reporter, %{account_id: target_account.id}) + {:ok, report} = Utils.assign_report_to_account(report, assigned_id) + + assert %{data: %{"assigned_account" => ^assigned_id}} = report + end + end + describe "maybe_anonymize_reporter/1" do setup do reporter = insert(:user) diff --git a/test/pleroma/web/admin_api/controllers/report_controller_test.exs b/test/pleroma/web/admin_api/controllers/report_controller_test.exs index b626ddf55..9fbb608c4 100644 --- a/test/pleroma/web/admin_api/controllers/report_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/report_controller_test.exs @@ -388,6 +388,38 @@ defmodule Pleroma.Web.AdminAPI.ReportControllerTest do |> json_response_and_validate_schema(:ok) end + test "returns reports with specified assigned user", %{conn: conn, admin: admin} do + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, _report} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "I feel offended", + status_ids: [activity.id] + }) + + {:ok, %{id: second_report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "I don't like this user" + }) + + CommonAPI.assign_report_to_account(second_report_id, admin.id) + + response = + conn + |> get(report_path(conn, :index, %{assigned_account: admin.id})) + |> json_response_and_validate_schema(:ok) + + assert [open_report] = response["reports"] + + assert length(response["reports"]) == 1 + assert open_report["id"] == second_report_id + + assert response["total"] == 1 + end + test "renders content correctly", %{conn: conn} do [reporter, target_user] = insert_pair(:user) note = insert(:note, user: target_user, data: %{"content" => "mew 1"}) @@ -467,6 +499,66 @@ defmodule Pleroma.Web.AdminAPI.ReportControllerTest do end end + describe "POST /api/pleroma/admin/reports/assign_account" do + test "assigns account to report", %{conn: conn, admin: admin} do + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, %{id: report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + status_ids: [activity.id] + }) + + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/reports/assign_account", %{ + "reports" => [ + %{"assigned_account" => admin.nickname, "id" => report_id} + ] + }) + |> json_response_and_validate_schema(:no_content) + + activity = Activity.get_by_id_with_user_actor(report_id) + assert activity.data["assigned_account"] == admin.id + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} assigned report ##{report_id} (on user @#{activity.user_actor.nickname}) to user #{admin.nickname}" + end + + test "unassigns account from report", %{conn: conn, admin: admin} do + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, %{id: report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + status_ids: [activity.id] + }) + + CommonAPI.assign_report_to_account(report_id, admin.id) + + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/reports/assign_account", %{ + "reports" => [ + %{"assigned_account" => nil, "id" => report_id} + ] + }) + |> json_response_and_validate_schema(:no_content) + + activity = Activity.get_by_id_with_user_actor(report_id) + assert activity.data["assigned_account"] == nil + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} unassigned report ##{report_id} (on user @#{activity.user_actor.nickname}) from a user" + end + end + describe "POST /api/pleroma/admin/reports/:id/notes" do setup %{conn: conn, admin: admin} do clear_config([:instance, :admin_privileges], [:reports_manage_reports]) diff --git a/test/pleroma/web/admin_api/views/report_view_test.exs b/test/pleroma/web/admin_api/views/report_view_test.exs index 1b16aca6a..6e155ef58 100644 --- a/test/pleroma/web/admin_api/views/report_view_test.exs +++ b/test/pleroma/web/admin_api/views/report_view_test.exs @@ -36,6 +36,7 @@ defmodule Pleroma.Web.AdminAPI.ReportViewTest do }), AdminAPI.AccountView.render("show.json", %{user: other_user}) ), + assigned_account: nil, statuses: [], notes: [], state: "open", @@ -75,6 +76,7 @@ defmodule Pleroma.Web.AdminAPI.ReportViewTest do }), AdminAPI.AccountView.render("show.json", %{user: other_user}) ), + assigned_account: nil, statuses: [StatusView.render("show.json", %{activity: activity})], state: "open", notes: [], diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index 52829b734..4eb057712 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -1458,6 +1458,29 @@ defmodule Pleroma.Web.CommonAPITest do } } = flag_activity end + + test "assigns report to an account" do + [reporter, target_user] = insert_pair(:user) + %{id: assigned} = insert(:user) + + {:ok, %Activity{id: report_id}} = CommonAPI.report(reporter, %{account_id: target_user.id}) + + {:ok, activity} = CommonAPI.assign_report_to_account(report_id, assigned) + + assert %{data: %{"assigned_account" => ^assigned}} = activity + end + + test "unassigns report from account" do + [reporter, target_user] = insert_pair(:user) + %{id: assigned} = insert(:user) + + {:ok, %Activity{id: report_id}} = CommonAPI.report(reporter, %{account_id: target_user.id}) + + CommonAPI.assign_report_to_account(report_id, assigned) + {:ok, activity} = CommonAPI.assign_report_to_account(report_id, nil) + + refute Map.has_key?(activity.data, "assigned_account") + end end describe "reblog muting" do From 6fac6ff7f1aa61a80295097ef2760b98c00313ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 28 Jan 2026 13:49:34 +0100 Subject: [PATCH 023/127] MastoAPI AccountView: Add mute/block expiry to the relationship object MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- lib/pleroma/user_relationship.ex | 2 +- .../api_spec/schemas/account_relationship.ex | 4 +- .../web/mastodon_api/views/account_view.ex | 76 +++++++++++++------ 3 files changed, 56 insertions(+), 26 deletions(-) diff --git a/lib/pleroma/user_relationship.ex b/lib/pleroma/user_relationship.ex index 07b6e46f7..cf5025670 100644 --- a/lib/pleroma/user_relationship.ex +++ b/lib/pleroma/user_relationship.ex @@ -45,7 +45,7 @@ defmodule Pleroma.UserRelationship do do: exists?(unquote(relationship_type), source, target) # `def get_block_expire_date/2`, `def get_mute_expire_date/2`, - # `def get_reblog_mute_expire_date/2`, `def get_notification_mute_exists?/2`, + # `def get_reblog_mute_expire_date/2`, `def get_notification_mute_expire_date/2`, # `def get_inverse_subscription_expire_date/2`, `def get_inverse_endorsement_expire_date/2` def unquote(:"get_#{relationship_type}_expire_date")(source, target), do: get_expire_date(unquote(relationship_type), source, target) diff --git a/lib/pleroma/web/api_spec/schemas/account_relationship.ex b/lib/pleroma/web/api_spec/schemas/account_relationship.ex index 68219a099..247a94bb9 100644 --- a/lib/pleroma/web/api_spec/schemas/account_relationship.ex +++ b/lib/pleroma/web/api_spec/schemas/account_relationship.ex @@ -26,7 +26,9 @@ defmodule Pleroma.Web.ApiSpec.Schemas.AccountRelationship do requested: %Schema{type: :boolean}, showing_reblogs: %Schema{type: :boolean}, subscribing: %Schema{type: :boolean}, - notifying: %Schema{type: :boolean} + notifying: %Schema{type: :boolean}, + mute_expires_at: %Schema{type: :string, format: "date-time", nullable: true}, + block_expires_at: %Schema{type: :string, format: "date-time", nullable: true} }, example: %{ "blocked_by" => false, diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index a7d994593..bfcc170d6 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -96,6 +96,24 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do followed_by = FollowingRelationship.following?(target, reading_user) following = FollowingRelationship.following?(reading_user, target) + blocking = + UserRelationship.exists?( + user_relationships, + :block, + reading_user, + target, + &User.blocks_user?(&1, &2) + ) + + muting = + UserRelationship.exists?( + user_relationships, + :mute, + reading_user, + target, + &User.mutes?(&1, &2) + ) + requested = cond do following -> false @@ -116,14 +134,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do id: to_string(target.id), following: following, followed_by: followed_by, - blocking: - UserRelationship.exists?( - user_relationships, - :block, - reading_user, - target, - &User.blocks_user?(&1, &2) - ), + blocking: blocking, blocked_by: UserRelationship.exists?( user_relationships, @@ -132,14 +143,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do reading_user, &User.blocks_user?(&1, &2) ), - muting: - UserRelationship.exists?( - user_relationships, - :mute, - reading_user, - target, - &User.mutes?(&1, &2) - ), + muting: muting, muting_notifications: UserRelationship.exists?( user_relationships, @@ -174,6 +178,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do &User.endorses?(&1, &2) ) } + |> maybe_put_mute_expires_at(target, reading_user, %{mutes: muting}) + |> maybe_put_block_expires_at(target, reading_user, %{blocks: blocking}) end def render("relationships.json", %{user: user, targets: targets} = opts) do @@ -343,8 +349,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do |> maybe_put_unread_conversation_count(user, opts[:for]) |> maybe_put_unread_notification_count(user, opts[:for]) |> maybe_put_email_address(user, opts[:for]) - |> maybe_put_mute_expires_at(user, opts[:for], opts) - |> maybe_put_block_expires_at(user, opts[:for], opts) + |> maybe_put_mute_expires_at(user, opts[:for], opts, relationship) + |> maybe_put_block_expires_at(user, opts[:for], opts, relationship) |> maybe_show_birthday(user, opts[:for]) end @@ -472,25 +478,47 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do defp maybe_put_email_address(data, _, _), do: data - defp maybe_put_mute_expires_at(data, %User{} = user, target, %{mutes: true}) do + defp maybe_put_mute_expires_at(data, target, user, opts, relationship \\ nil) + + defp maybe_put_mute_expires_at(data, _target, _user, %{mutes: true}, %{ + mute_expires_at: mute_expires_at + }) do + Map.put(data, :mute_expires_at, mute_expires_at) + end + + defp maybe_put_mute_expires_at(data, %User{} = target, user, %{mutes: true}, _relationship) do Map.put( data, :mute_expires_at, - UserRelationship.get_mute_expire_date(target, user) + UserRelationship.get_mute_expire_date(user, target) ) end - defp maybe_put_mute_expires_at(data, _, _, _), do: data + defp maybe_put_mute_expires_at(data, _, _, _, _), do: data - defp maybe_put_block_expires_at(data, %User{} = user, target, %{blocks: true}) do + defp maybe_put_block_expires_at(data, target, user, opts, relationship \\ nil) + + defp maybe_put_block_expires_at(data, _target, _user, %{blocks: true}, %{ + block_expires_at: block_expires_at + }) do + Map.put(data, :block_expires_at, block_expires_at) + end + + defp maybe_put_block_expires_at( + data, + %User{} = target, + %User{} = user, + %{blocks: true}, + _relationship + ) do Map.put( data, :block_expires_at, - UserRelationship.get_block_expire_date(target, user) + UserRelationship.get_block_expire_date(user, target) ) end - defp maybe_put_block_expires_at(data, _, _, _), do: data + defp maybe_put_block_expires_at(data, _, _, _, _), do: data defp maybe_show_birthday(data, %User{id: user_id} = user, %User{id: user_id}) do data From e7a4d5ea66af84aca8f972e65308c231abb4d2a7 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 22 Jan 2026 23:01:11 +0100 Subject: [PATCH 024/127] MastoAPI AccountView: Add mute/block expiry to the relationship key --- .../mastodon_api/views/account_view_test.exs | 78 ++++++++++++++++++- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/test/pleroma/web/mastodon_api/views/account_view_test.exs b/test/pleroma/web/mastodon_api/views/account_view_test.exs index bd151cc61..c230cf653 100644 --- a/test/pleroma/web/mastodon_api/views/account_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/account_view_test.exs @@ -439,8 +439,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do following: false, followed_by: false, blocking: false, + block_expires_at: nil, blocked_by: false, muting: false, + mute_expires_at: nil, muting_notifications: false, subscribing: false, notifying: false, @@ -536,6 +538,53 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do test_relationship_rendering(user, other_user, expected) end + test "represent a relationship for the blocking and blocked user with expiry" do + user = insert(:user) + other_user = insert(:user) + date = DateTime.utc_now() |> DateTime.add(24 * 60 * 60) |> DateTime.truncate(:second) + + {:ok, user, other_user} = User.follow(user, other_user) + {:ok, _subscription} = User.subscribe(user, other_user) + {:ok, _user_relationship} = User.block(user, other_user, %{duration: 24 * 60 * 60}) + {:ok, _user_relationship} = User.block(other_user, user) + + expected = + Map.merge( + @blank_response, + %{ + following: false, + blocking: true, + block_expires_at: date, + blocked_by: true, + id: to_string(other_user.id) + } + ) + + test_relationship_rendering(user, other_user, expected) + end + + test "represent a relationship for the muting user with expiry" do + user = insert(:user) + other_user = insert(:user) + date = DateTime.utc_now() |> DateTime.add(24 * 60 * 60) |> DateTime.truncate(:second) + + {:ok, _user_relationship} = + User.mute(user, other_user, %{notifications: true, duration: 24 * 60 * 60}) + + expected = + Map.merge( + @blank_response, + %{ + muting: true, + mute_expires_at: date, + muting_notifications: true, + id: to_string(other_user.id) + } + ) + + test_relationship_rendering(user, other_user, expected) + end + test "represent a relationship for the user blocking a domain" do user = insert(:user) other_user = insert(:user, ap_id: "https://bad.site/users/other_user") @@ -856,12 +905,37 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do User.mute(user, other_user, %{notifications: true, duration: 24 * 60 * 60}) %{ - mute_expires_at: mute_expires_at - } = AccountView.render("show.json", %{user: other_user, for: user, mutes: true}) + pleroma: %{ + relationship: %{ + mute_expires_at: mute_expires_at + } + } + } = AccountView.render("show.json", %{user: other_user, for: user, embed_relationships: true}) assert DateTime.diff( mute_expires_at, DateTime.utc_now() |> DateTime.add(24 * 60 * 60) ) in -3..3 end + + test "renders block expiration date" do + user = insert(:user) + other_user = insert(:user) + + {:ok, _user_relationships} = + User.block(user, other_user, %{duration: 24 * 60 * 60}) + + %{ + pleroma: %{ + relationship: %{ + block_expires_at: block_expires_at + } + } + } = AccountView.render("show.json", %{user: other_user, for: user, embed_relationships: true}) + + assert DateTime.diff( + block_expires_at, + DateTime.utc_now() |> DateTime.add(24 * 60 * 60) + ) in -3..3 + end end From c1e33bfadbc82cc7f019927b4b77a07883501cb7 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 24 Jan 2026 21:35:27 +0100 Subject: [PATCH 025/127] MastoAPI AccountView AccountController: Add more block/mute expiry tests --- .../controllers/account_controller_test.exs | 103 ++++++++++++++++-- .../mastodon_api/views/account_view_test.exs | 6 +- 2 files changed, 97 insertions(+), 12 deletions(-) diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs index 02da781dd..ea98b53a8 100644 --- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs @@ -1901,7 +1901,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do {:ok, _user_relationships} = User.mute(user, other_user1) {:ok, _user_relationships} = User.mute(user, other_user2) - {:ok, _user_relationships} = User.mute(user, other_user3) + {:ok, _user_relationships} = User.mute(user, other_user3, %{duration: 24 * 60 * 60}) + + date = + DateTime.utc_now() + |> DateTime.add(24 * 60 * 60) + |> DateTime.truncate(:second) + |> DateTime.to_iso8601() result = conn @@ -1937,6 +1943,17 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do |> json_response_and_validate_schema(200) assert [%{"id" => ^id3}] = result + + result = + conn + |> get("/api/v1/mutes") + |> json_response_and_validate_schema(200) + + assert [ + %{"id" => ^id3, "mute_expires_at" => ^date}, + %{"id" => ^id2, "mute_expires_at" => nil}, + %{"id" => ^id1, "mute_expires_at" => nil} + ] = result end test "list of mutes with with_relationships parameter" do @@ -1951,20 +1968,44 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do {:ok, _} = User.mute(user, other_user1) {:ok, _} = User.mute(user, other_user2) - {:ok, _} = User.mute(user, other_user3) + {:ok, _} = User.mute(user, other_user3, %{duration: 24 * 60 * 60}) + + date = + DateTime.utc_now() + |> DateTime.add(24 * 60 * 60) + |> DateTime.truncate(:second) + |> DateTime.to_iso8601() assert [ %{ "id" => ^id3, - "pleroma" => %{"relationship" => %{"muting" => true, "followed_by" => true}} + "pleroma" => %{ + "relationship" => %{ + "muting" => true, + "mute_expires_at" => ^date, + "followed_by" => true + } + } }, %{ "id" => ^id2, - "pleroma" => %{"relationship" => %{"muting" => true, "followed_by" => true}} + "pleroma" => %{ + "relationship" => %{ + "muting" => true, + "mute_expires_at" => nil, + "followed_by" => true + } + } }, %{ "id" => ^id1, - "pleroma" => %{"relationship" => %{"muting" => true, "followed_by" => true}} + "pleroma" => %{ + "relationship" => %{ + "muting" => true, + "mute_expires_at" => nil, + "followed_by" => true + } + } } ] = conn @@ -1980,7 +2021,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do {:ok, _user_relationship} = User.block(user, other_user1) {:ok, _user_relationship} = User.block(user, other_user3) - {:ok, _user_relationship} = User.block(user, other_user2) + {:ok, _user_relationship} = User.block(user, other_user2, %{duration: 24 * 60 * 60}) + + date = + DateTime.utc_now() + |> DateTime.add(24 * 60 * 60) + |> DateTime.truncate(:second) + |> DateTime.to_iso8601() result = conn @@ -2045,6 +2092,18 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do |> json_response_and_validate_schema(200) assert [%{"id" => ^id1}] = result + + result = + conn + |> assign(:user, user) + |> get("api/v1/blocks") + |> json_response_and_validate_schema(200) + + assert [ + %{"id" => ^id3, "block_expires_at" => nil}, + %{"id" => ^id2, "block_expires_at" => ^date}, + %{"id" => ^id1, "block_expires_at" => nil} + ] = result end test "list of blocks with with_relationships parameter" do @@ -2059,20 +2118,44 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do {:ok, _} = User.block(user, other_user1) {:ok, _} = User.block(user, other_user2) - {:ok, _} = User.block(user, other_user3) + {:ok, _} = User.block(user, other_user3, %{duration: 24 * 60 * 60}) + + date = + DateTime.utc_now() + |> DateTime.add(24 * 60 * 60) + |> DateTime.truncate(:second) + |> DateTime.to_iso8601() assert [ %{ "id" => ^id3, - "pleroma" => %{"relationship" => %{"blocking" => true, "followed_by" => false}} + "pleroma" => %{ + "relationship" => %{ + "blocking" => true, + "block_expires_at" => ^date, + "followed_by" => false + } + } }, %{ "id" => ^id2, - "pleroma" => %{"relationship" => %{"blocking" => true, "followed_by" => false}} + "pleroma" => %{ + "relationship" => %{ + "blocking" => true, + "block_expires_at" => nil, + "followed_by" => false + } + } }, %{ "id" => ^id1, - "pleroma" => %{"relationship" => %{"blocking" => true, "followed_by" => false}} + "pleroma" => %{ + "relationship" => %{ + "blocking" => true, + "block_expires_at" => nil, + "followed_by" => false + } + } } ] = conn diff --git a/test/pleroma/web/mastodon_api/views/account_view_test.exs b/test/pleroma/web/mastodon_api/views/account_view_test.exs index c230cf653..6984442cc 100644 --- a/test/pleroma/web/mastodon_api/views/account_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/account_view_test.exs @@ -909,7 +909,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do relationship: %{ mute_expires_at: mute_expires_at } - } + }, + mute_expires_at: mute_expires_at } = AccountView.render("show.json", %{user: other_user, for: user, embed_relationships: true}) assert DateTime.diff( @@ -930,7 +931,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do relationship: %{ block_expires_at: block_expires_at } - } + }, + block_expires_at: block_expires_at } = AccountView.render("show.json", %{user: other_user, for: user, embed_relationships: true}) assert DateTime.diff( From bc0c7fb3105d1c9fb0f54390f37f35b613062976 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 28 Jan 2026 13:58:33 +0100 Subject: [PATCH 026/127] Fix tests, relationship should always define `_expires_at` 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/account_view.ex | 2 ++ test/pleroma/web/mastodon_api/views/account_view_test.exs | 6 ++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index bfcc170d6..29c63eb60 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -143,6 +143,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do reading_user, &User.blocks_user?(&1, &2) ), + block_expires_at: nil, muting: muting, muting_notifications: UserRelationship.exists?( @@ -152,6 +153,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do target, &User.muted_notifications?(&1, &2) ), + mute_expires_at: nil, subscribing: subscribing, notifying: subscribing, requested: requested, diff --git a/test/pleroma/web/mastodon_api/views/account_view_test.exs b/test/pleroma/web/mastodon_api/views/account_view_test.exs index 6984442cc..c230cf653 100644 --- a/test/pleroma/web/mastodon_api/views/account_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/account_view_test.exs @@ -909,8 +909,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do relationship: %{ mute_expires_at: mute_expires_at } - }, - mute_expires_at: mute_expires_at + } } = AccountView.render("show.json", %{user: other_user, for: user, embed_relationships: true}) assert DateTime.diff( @@ -931,8 +930,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do relationship: %{ block_expires_at: block_expires_at } - }, - block_expires_at: block_expires_at + } } = AccountView.render("show.json", %{user: other_user, for: user, embed_relationships: true}) assert DateTime.diff( From 5001fb3a78737273fef7c36a9507ba0e35fd47f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 28 Jan 2026 14:02:55 +0100 Subject: [PATCH 027/127] 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/relationship-expires-at.change | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/relationship-expires-at.change diff --git a/changelog.d/relationship-expires-at.change b/changelog.d/relationship-expires-at.change new file mode 100644 index 000000000..286dba197 --- /dev/null +++ b/changelog.d/relationship-expires-at.change @@ -0,0 +1 @@ +Add mute/block expiry to the relationship object From bd30d461b05cc8b29f5a0818596ea640e2484487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 28 Jan 2026 22:02:22 +0100 Subject: [PATCH 028/127] Add /api/v2/instance profile fields limits info used by Mastodon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/instance-profile-fields.add | 1 + .../web/api_spec/operations/instance_operation.ex | 12 ++++++++++++ lib/pleroma/web/mastodon_api/views/instance_view.ex | 9 +++++++++ 3 files changed, 22 insertions(+) create mode 100644 changelog.d/instance-profile-fields.add diff --git a/changelog.d/instance-profile-fields.add b/changelog.d/instance-profile-fields.add new file mode 100644 index 000000000..712bd68d9 --- /dev/null +++ b/changelog.d/instance-profile-fields.add @@ -0,0 +1 @@ +Add /api/v2/instance profile fields limits info used by Mastodon diff --git a/lib/pleroma/web/api_spec/operations/instance_operation.ex b/lib/pleroma/web/api_spec/operations/instance_operation.ex index d8b2901d3..6be4ea996 100644 --- a/lib/pleroma/web/api_spec/operations/instance_operation.ex +++ b/lib/pleroma/web/api_spec/operations/instance_operation.ex @@ -342,6 +342,18 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do max_pinned_statuses: %Schema{ type: :integer, description: "The maximum number of pinned statuses for each account." + }, + max_profile_fields: %Schema{ + type: :integer, + description: "The maximum number of custom profile fields allowed to be set." + }, + profile_field_name_limit: %Schema{ + type: :integer, + description: "The maximum size of a profile field name, in characters." + }, + profile_field_value_limit: %Schema{ + type: :integer, + description: "The maximum size of a profile field value, in characters." } } }, diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index 0dc7a5fea..d64ce4fb5 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -303,6 +303,15 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do defp configuration2 do configuration() |> put_in([:accounts, :max_pinned_statuses], Config.get([:instance, :max_pinned_statuses], 0)) + |> put_in([:accounts, :max_profile_fields], Config.get([:instance, :max_account_fields])) + |> put_in( + [:accounts, :profile_field_name_limit], + Config.get([:instance, :account_field_name_length]) + ) + |> put_in( + [:accounts, :profile_field_value_limit], + Config.get([:instance, :account_field_value_length]) + ) |> put_in([:statuses, :characters_reserved_per_url], 0) |> Map.merge(%{ urls: %{ From feda4d0718b06732bee9011bc95485159f5f61f5 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 9 Feb 2026 09:01:16 +0400 Subject: [PATCH 029/127] CI: Add basic woodpecker file --- .woodpecker.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .woodpecker.yml diff --git a/.woodpecker.yml b/.woodpecker.yml new file mode 100644 index 000000000..9cc4f7261 --- /dev/null +++ b/.woodpecker.yml @@ -0,0 +1,29 @@ +when: + - event: + - push + - pull_request + +steps: + test: + image: elixir:1.15-alpine + environment: + MIX_ENV: test + DB_HOST: postgres + DB_PORT: 5432 + commands: + - apk add --no-cache build-base cmake exiftool ffmpeg file-dev git openssl + - adduser -D -h /home/testuser testuser + - mkdir -p /home/testuser/.mix /home/testuser/.hex + - chown -R testuser:testuser . /home/testuser + - su testuser -c "HOME=/home/testuser mix local.hex --force" + - su testuser -c "HOME=/home/testuser mix local.rebar --force" + - su testuser -c "HOME=/home/testuser mix deps.get" + - su testuser -c "HOME=/home/testuser mix test" + +services: + postgres: + image: postgres:13-alpine + environment: + POSTGRES_DB: pleroma_test + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres From 4693dc837b8d8da7276b7d1816eea684b7b6bdfa Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 9 Feb 2026 10:13:29 +0400 Subject: [PATCH 030/127] CI: Only run on PR --- .woodpecker.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.woodpecker.yml b/.woodpecker.yml index 9cc4f7261..4ceb1cab5 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -1,6 +1,5 @@ when: - event: - - push - pull_request steps: From 2e80c786bb27dc2fceb3269b92db05b04aad9ec1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Tue, 10 Feb 2026 14:37:30 +0100 Subject: [PATCH 031/127] Update comment for prepare_object, rename prepare_outgoing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/update-comment.ignore | 1 + lib/pleroma/user/backup.ex | 2 +- lib/pleroma/web/activity_pub/publisher.ex | 6 +-- .../web/activity_pub/transmogrifier.ex | 20 ++++---- .../web/activity_pub/transmogrifier/api.ex | 2 +- .../web/activity_pub/views/object_view.ex | 2 +- .../web/activity_pub/views/user_view.ex | 2 +- .../activity_pub/mrf/hashtag_policy_test.exs | 4 +- .../article_note_page_validator_test.exs | 2 +- .../update_handling_test.exs | 2 +- .../web/activity_pub/publisher_test.exs | 4 +- .../transmogrifier/answer_handling_test.exs | 2 +- .../transmogrifier/note_handling_test.exs | 4 +- .../web/activity_pub/transmogrifier_test.exs | 48 +++++++++---------- 14 files changed, 51 insertions(+), 50 deletions(-) create mode 100644 changelog.d/update-comment.ignore diff --git a/changelog.d/update-comment.ignore b/changelog.d/update-comment.ignore new file mode 100644 index 000000000..733e813b3 --- /dev/null +++ b/changelog.d/update-comment.ignore @@ -0,0 +1 @@ +Update comment for prepare_object, rename prepare_outgoing diff --git a/lib/pleroma/user/backup.ex b/lib/pleroma/user/backup.ex index 3f67cdf0c..35535528b 100644 --- a/lib/pleroma/user/backup.ex +++ b/lib/pleroma/user/backup.ex @@ -342,7 +342,7 @@ defmodule Pleroma.User.Backup do dir, "outbox", fn a -> - with {:ok, activity} <- Transmogrifier.prepare_outgoing(a.data) do + with {:ok, activity} <- Transmogrifier.prepare_activity(a.data) do {:ok, Map.delete(activity, "@context")} end end diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index b6c814aed..e3c4e01a4 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -79,7 +79,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do Determine if an activity can be represented by running it through Transmogrifier. """ def representable?(%Activity{} = activity) do - with {:ok, _data} <- @transmogrifier_impl.prepare_outgoing(activity.data) do + with {:ok, _data} <- @transmogrifier_impl.prepare_activity(activity.data) do true else _e -> @@ -102,14 +102,14 @@ defmodule Pleroma.Web.ActivityPub.Publisher do Logger.debug("Federating #{ap_id} to #{inbox}") uri = %{path: path} = URI.parse(inbox) - {:ok, data} = @transmogrifier_impl.prepare_outgoing(activity.data) + {:ok, data} = @transmogrifier_impl.prepare_activity(activity.data) {actor, data} = with {_, false} <- {:actor_changed?, data["actor"] != activity.data["actor"]} do {actor, data} else {:actor_changed?, true} -> - # If prepare_outgoing changes the actor, re-get it from the db + # If prepare_activity changes the actor, re-get it from the db new_actor = User.get_cached_by_ap_id(data["actor"]) {new_actor, data} end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index ba6f4030d..6af5ee89d 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -783,7 +783,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def set_replies(obj_data), do: obj_data - # Prepares the object of an outgoing create activity. + # Prepares and sanitizes the object for federation. def prepare_object(object) do object |> add_hashtags @@ -824,7 +824,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do # internal -> Mastodon # """ - def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data) + def prepare_activity(%{"type" => activity_type, "object" => object_id} = data) when activity_type in ["Create", "Listen"] do object = object_id @@ -840,7 +840,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do {:ok, data} end - def prepare_outgoing(%{"type" => "Update", "object" => %{"type" => objtype} = object} = data) + def prepare_activity(%{"type" => "Update", "object" => %{"type" => objtype} = object} = data) when objtype in Pleroma.Constants.updatable_object_types() do data = data @@ -851,7 +851,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do {:ok, data} end - def prepare_outgoing(%{"type" => "Update", "object" => %{"type" => objtype} = object} = data) + def prepare_activity(%{"type" => "Update", "object" => %{"type" => objtype} = object} = data) when objtype in Pleroma.Constants.actor_types() do object = object @@ -868,11 +868,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do {:ok, data} end - def prepare_outgoing(%{"type" => "Update", "object" => %{}} = data) do + def prepare_activity(%{"type" => "Update", "object" => %{}} = data) do raise "Requested to serve an Update for non-updateable object type: #{inspect(data)}" end - def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do + def prepare_activity(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do object = object_id |> Object.normalize(fetch: false) @@ -895,7 +895,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs, # because of course it does. - def prepare_outgoing(%{"type" => "Accept"} = data) do + def prepare_activity(%{"type" => "Accept"} = data) do with follow_activity <- Activity.normalize(data["object"]) do object = %{ "actor" => follow_activity.actor, @@ -913,7 +913,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end end - def prepare_outgoing(%{"type" => "Reject"} = data) do + def prepare_activity(%{"type" => "Reject"} = data) do with follow_activity <- Activity.normalize(data["object"]) do object = %{ "actor" => follow_activity.actor, @@ -931,7 +931,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end end - def prepare_outgoing(%{"type" => "Flag"} = data) do + def prepare_activity(%{"type" => "Flag"} = data) do with {:ok, stripped_activity} <- Utils.strip_report_status_data(data), stripped_activity <- Utils.maybe_anonymize_reporter(stripped_activity), stripped_activity <- Map.merge(stripped_activity, Utils.make_json_ld_header()) do @@ -939,7 +939,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end end - def prepare_outgoing(%{"type" => _type} = data) do + def prepare_activity(%{"type" => _type} = data) do data = data |> strip_internal_fields diff --git a/lib/pleroma/web/activity_pub/transmogrifier/api.ex b/lib/pleroma/web/activity_pub/transmogrifier/api.ex index b9f65d17c..f2e416575 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier/api.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier/api.ex @@ -7,5 +7,5 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.API do Behaviour for the subset of Transmogrifier used by Publisher. """ - @callback prepare_outgoing(map()) :: {:ok, map()} | {:error, term()} + @callback prepare_activity(map()) :: {:ok, map()} | {:error, term()} end diff --git a/lib/pleroma/web/activity_pub/views/object_view.ex b/lib/pleroma/web/activity_pub/views/object_view.ex index a672ccc8b..9334b797a 100644 --- a/lib/pleroma/web/activity_pub/views/object_view.ex +++ b/lib/pleroma/web/activity_pub/views/object_view.ex @@ -19,7 +19,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do end def render("object.json", %{object: %Activity{} = activity}) do - {:ok, ap_data} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, ap_data} = Transmogrifier.prepare_activity(activity.data) ap_data end diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 4362db324..c7fb2a1d7 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -283,7 +283,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do }) do collection = Enum.map(activities, fn activity -> - {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, data} = Transmogrifier.prepare_activity(activity.data) data end) diff --git a/test/pleroma/web/activity_pub/mrf/hashtag_policy_test.exs b/test/pleroma/web/activity_pub/mrf/hashtag_policy_test.exs index 32991c966..e231f88a8 100644 --- a/test/pleroma/web/activity_pub/mrf/hashtag_policy_test.exs +++ b/test/pleroma/web/activity_pub/mrf/hashtag_policy_test.exs @@ -15,7 +15,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicyTest do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{status: "#nsfw hey"}) - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, modified} = Transmogrifier.prepare_activity(activity.data) assert modified["object"]["sensitive"] end @@ -94,7 +94,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicyTest do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{status: "#cofe hey"}) - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, modified} = Transmogrifier.prepare_activity(activity.data) refute modified["object"]["sensitive"] end diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs index 3c7ff0eeb..c32811c5b 100644 --- a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs @@ -67,7 +67,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest {:ok, edit} = Pleroma.Web.CommonAPI.update(activity, user, %{status: "edited :blank:"}) {:ok, %{"object" => external_rep}} = - Pleroma.Web.ActivityPub.Transmogrifier.prepare_outgoing(edit.data) + Pleroma.Web.ActivityPub.Transmogrifier.prepare_activity(edit.data) %{external_rep: external_rep} end diff --git a/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs b/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs index c88339d14..347c5b578 100644 --- a/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs @@ -133,7 +133,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do user = insert(:user) {:ok, activity} = Pleroma.Web.CommonAPI.post(user, %{status: "mew mew :dinosaur:"}) {:ok, edit} = Pleroma.Web.CommonAPI.update(activity, user, %{status: "edited :blank:"}) - {:ok, external_rep} = Pleroma.Web.ActivityPub.Transmogrifier.prepare_outgoing(edit.data) + {:ok, external_rep} = Pleroma.Web.ActivityPub.Transmogrifier.prepare_activity(edit.data) %{external_rep: external_rep} end diff --git a/test/pleroma/web/activity_pub/publisher_test.exs b/test/pleroma/web/activity_pub/publisher_test.exs index 1f9e0bfe5..7f07ee3a7 100644 --- a/test/pleroma/web/activity_pub/publisher_test.exs +++ b/test/pleroma/web/activity_pub/publisher_test.exs @@ -259,7 +259,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do ) end - test "Publishes with the new actor if prepare_outgoing changes the actor." do + test "Publishes with the new actor if prepare_activity changes the actor." do mock(fn %{method: :post, url: "https://domain.com/users/nick1/inbox", body: body} -> {:ok, %Tesla.Env{status: 200, body: body}} @@ -281,7 +281,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do ) Pleroma.Web.ActivityPub.TransmogrifierMock - |> Mox.expect(:prepare_outgoing, fn data -> + |> Mox.expect(:prepare_activity, fn data -> {:ok, Map.put(data, "actor", replaced_actor.ap_id)} end) diff --git a/test/pleroma/web/activity_pub/transmogrifier/answer_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/answer_handling_test.exs index 39a1598a8..913d143a5 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/answer_handling_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/answer_handling_test.exs @@ -72,7 +72,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.AnswerHandlingTest do |> Kernel.put_in(["object", "to"], user.ap_id) {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) - {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, data} = Transmogrifier.prepare_activity(activity.data) assert data["object"]["type"] == "Note" end 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 403c98a2d..31b9a699d 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs @@ -508,7 +508,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do {:ok, activity} = Transmogrifier.handle_incoming(message) - {:ok, _} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, _} = Transmogrifier.prepare_activity(activity.data) end test "successfully reserializes a message with AS2 objects in IR" do @@ -537,7 +537,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do {:ok, activity} = Transmogrifier.handle_incoming(message) - {:ok, _} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, _} = Transmogrifier.prepare_activity(activity.data) end end diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs index b54196a3c..f0a64e781 100644 --- a/test/pleroma/web/activity_pub/transmogrifier_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs @@ -433,7 +433,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do {:ok, announce_activity} = CommonAPI.repeat(activity.id, user) - {:ok, modified} = Transmogrifier.prepare_outgoing(announce_activity.data) + {:ok, modified} = Transmogrifier.prepare_activity(announce_activity.data) assert modified["object"]["content"] == "hey" assert modified["object"]["actor"] == modified["object"]["attributedTo"] @@ -448,7 +448,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do with_mock Pleroma.Notification, get_notified_from_activity: fn _, _ -> [] end do - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, modified} = Transmogrifier.prepare_activity(activity.data) object = modified["object"] @@ -474,7 +474,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, modified} = Transmogrifier.prepare_activity(activity.data) assert modified["@context"] == Utils.make_json_ld_header()["@context"] @@ -485,7 +485,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, modified} = Transmogrifier.prepare_activity(activity.data) assert modified["object"]["actor"] == modified["object"]["attributedTo"] end @@ -501,7 +501,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do "name" => "#2hu" } - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, modified} = Transmogrifier.prepare_activity(activity.data) assert modified["object"]["tag"] == [expected_tag] end @@ -524,7 +524,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do url: "https://pleroma.social" } == activity.object.data["generator"] - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, modified} = Transmogrifier.prepare_activity(activity.data) assert length(modified["object"]["tag"]) == 2 @@ -541,7 +541,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do test "it strips internal fields of article" do activity = insert(:article_activity) - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, modified} = Transmogrifier.prepare_activity(activity.data) assert length(modified["object"]["tag"]) == 2 @@ -558,13 +558,13 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do {:ok, activity} = CommonAPI.post(user, %{status: "2hu :moominmamma:"}) - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, modified} = Transmogrifier.prepare_activity(activity.data) assert modified["directMessage"] == false {:ok, activity} = CommonAPI.post(user, %{status: "@#{other_user.nickname} :moominmamma:"}) - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, modified} = Transmogrifier.prepare_activity(activity.data) assert modified["directMessage"] == false @@ -574,7 +574,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do visibility: "direct" }) - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, modified} = Transmogrifier.prepare_activity(activity.data) assert modified["directMessage"] == true end @@ -585,7 +585,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, modified} = Transmogrifier.prepare_activity(activity.data) assert is_nil(modified["bcc"]) end @@ -594,7 +594,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do listen_activity = insert(:listen) # This has an inlined object as in ObjectView - {:ok, modified} = Transmogrifier.prepare_outgoing(listen_activity.data) + {:ok, modified} = Transmogrifier.prepare_activity(listen_activity.data) assert modified["type"] == "Listen" @@ -610,7 +610,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do object_type = activity.object.data["type"] # This does not have an inlined object - {:ok, modified2} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, modified2} = Transmogrifier.prepare_activity(activity.data) assert match?( %{ @@ -640,7 +640,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do {:ok, activity} = CommonAPI.post(user, %{status: "everybody do the dinosaur :dinosaur:"}) - {:ok, prepared} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, prepared} = Transmogrifier.prepare_activity(activity.data) assert length(prepared["object"]["tag"]) == 1 @@ -655,7 +655,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do {:ok, activity} = CommonAPI.post(user, %{status: "everybody do the dinosaur :dinosaur:"}) {:ok, update} = CommonAPI.update(activity, user, %{status: "mew mew :blank:"}) - {:ok, prepared} = Transmogrifier.prepare_outgoing(update.data) + {:ok, prepared} = Transmogrifier.prepare_activity(update.data) assert %{ "content" => "mew mew :blank:", @@ -689,7 +689,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do user_update_changeset: changeset ) - assert {:ok, prepared} = Transmogrifier.prepare_outgoing(activity.data) + assert {:ok, prepared} = Transmogrifier.prepare_activity(activity.data) assert prepared["type"] == "Update" assert prepared["@context"] assert prepared["object"]["type"] == user.actor_type @@ -704,7 +704,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do {:ok, %Activity{} = block_activity} = CommonAPI.block(blocked, blocker) {:ok, %Activity{} = undo_activity} = CommonAPI.unblock(blocked, blocker) - {:ok, data} = Transmogrifier.prepare_outgoing(undo_activity.data) + {:ok, data} = Transmogrifier.prepare_activity(undo_activity.data) block_ap_id = block_activity.data["id"] assert is_binary(block_ap_id) @@ -738,7 +738,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do assert is_binary(note_ap_id) {:ok, react_activity} = CommonAPI.react_with_emoji(note_activity.id, user, "🐈") - {:ok, data} = Transmogrifier.prepare_outgoing(react_activity.data) + {:ok, data} = Transmogrifier.prepare_activity(react_activity.data) assert match?( %{ @@ -764,7 +764,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do note_activity = insert(:note_activity) {:ok, react_activity} = CommonAPI.react_with_emoji(note_activity.id, user, ":dinosaur:") - {:ok, data} = Transmogrifier.prepare_outgoing(react_activity.data) + {:ok, data} = Transmogrifier.prepare_activity(react_activity.data) assert length(data["tag"]) == 1 @@ -781,7 +781,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do {:ok, quoted_post} = CommonAPI.post(user, %{status: "hey"}) {:ok, quote_post} = CommonAPI.post(user, %{status: "hey", quoted_status_id: quoted_post.id}) - {:ok, modified} = Transmogrifier.prepare_outgoing(quote_post.data) + {:ok, modified} = Transmogrifier.prepare_activity(quote_post.data) %{data: %{"id" => quote_id}} = Object.normalize(quoted_post) @@ -793,7 +793,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{status: "Cześć", language: "pl"}) - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.object.data) + {:ok, modified} = Transmogrifier.prepare_activity(activity.object.data) assert [_, _, %{"@language" => "pl"}] = modified["@context"] end @@ -802,7 +802,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{status: "Cześć", language: "pl"}) - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, modified} = Transmogrifier.prepare_activity(activity.data) assert [_, _, %{"@language" => "pl"}] = modified["@context"] end @@ -825,7 +825,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do content: content }) - {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, data} = Transmogrifier.prepare_activity(activity.data) expected_data = activity.data @@ -859,7 +859,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do clear_config([:activitypub, :anonymize_reporter], true) clear_config([:activitypub, :anonymize_reporter_local_nickname], placeholder.nickname) - {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) + {:ok, data} = Transmogrifier.prepare_activity(activity.data) expected_data = activity.data From b798f7d6e9f57f80851eb5e1eacf49711e4f5f60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Tue, 10 Feb 2026 14:51:37 +0100 Subject: [PATCH 032/127] Add issue and pull request templates for Forgejo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .forgejo/issue_template/bug.yaml | 25 +++++++++++++++++++++++++ .forgejo/pull_request_template.md | 10 ++++++++++ 2 files changed, 35 insertions(+) create mode 100644 .forgejo/issue_template/bug.yaml create mode 100644 .forgejo/pull_request_template.md diff --git a/.forgejo/issue_template/bug.yaml b/.forgejo/issue_template/bug.yaml new file mode 100644 index 000000000..1b92658f7 --- /dev/null +++ b/.forgejo/issue_template/bug.yaml @@ -0,0 +1,25 @@ +name: 'Bug report' +about: 'Report a bug in Pleroma' +body: +- type: markdown + attributes: + value: | + ### Precheck + + * For support use https://git.pleroma.social/pleroma/pleroma-support or [community channels](https://git.pleroma.social/pleroma/pleroma#community-channels). + * Please do a quick search to ensure no similar bug has been reported before. If the bug has not been addressed after 2 weeks, it's fine to bump it. + * Try to ensure that the bug is actually related to the Pleroma backend. For example, if a bug happens in Pleroma-FE but not in Mastodon-FE or mobile clients, it's likely that the bug should be filed in [Pleroma-FE](https://git.pleroma.social/pleroma/pleroma-fe/issues/new) repository. +- type: textarea + id: environment + attributes: + label: Environment + value: | + * Installation type (OTP or From Source): + * Pleroma version (could be found in the "Version" tab of settings in Pleroma-FE): + * Elixir version (`elixir -v` for from source installations, N/A for OTP): + * Operating system: + * PostgreSQL version (`psql -V`): +- type: textarea + id: bug-description + attributes: + label: Bug description \ No newline at end of file diff --git a/.forgejo/pull_request_template.md b/.forgejo/pull_request_template.md new file mode 100644 index 000000000..799da5355 --- /dev/null +++ b/.forgejo/pull_request_template.md @@ -0,0 +1,10 @@ +### Checklist +- [ ] Adding a changelog: In the `changelog.d` directory, create a file named `.`. + + `` can be anything, but we recommend using a more or less unique identifier to avoid collisions, such as the branch name. + + `` can be `add`, `change`, `remove`, `fix`, `security` or `skip`. `skip` is only used if there is no user-visible change in the PR (for example, only editing comments in the code). Otherwise, choose a type that corresponds to your change. + + In the file, write the changelog entry. For example, if a PR adds group functionality, we can create a file named `group.add` and write `Add group functionality` in it. + + If one changelog entry is not enough, you may add more. But that might mean you can split it into two PRs. Only use more than one changelog entry if you really need to (for example, when one change in the code fix two different bugs, or when refactoring). \ No newline at end of file From 1c685ea41a771d74a1d4e1769a28911d738591b0 Mon Sep 17 00:00:00 2001 From: feld Date: Thu, 12 Feb 2026 00:34:08 +0000 Subject: [PATCH 033/127] Update README.md fix logo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8a5eb238f..982a7249a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ - + ## About From f80c5744b14f14211ab972573ab1cf3bebdb018e Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 12 Feb 2026 18:45:36 +0100 Subject: [PATCH 034/127] Normalize Hubzilla alsoKnownAs from string to array --- changelog.d/hubzilla-alsoknownas.fix | 1 + lib/pleroma/web/activity_pub/activity_pub.ex | 6 +++- .../hubzilla-actor-alsoknownas-string.json | 1 + .../web/activity_pub/activity_pub_test.exs | 29 +++++++++++++++++++ 4 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 changelog.d/hubzilla-alsoknownas.fix create mode 100644 test/fixtures/users_mock/hubzilla-actor-alsoknownas-string.json diff --git a/changelog.d/hubzilla-alsoknownas.fix b/changelog.d/hubzilla-alsoknownas.fix new file mode 100644 index 000000000..2a2969807 --- /dev/null +++ b/changelog.d/hubzilla-alsoknownas.fix @@ -0,0 +1 @@ +Fix fetching Hubzilla Actors with alsoKnownAs as string diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 44e9a22e5..071d634db 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1618,6 +1618,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp normalize_image(urls) when is_list(urls), do: urls |> List.first() |> normalize_image() defp normalize_image(_), do: nil + defp normalize_also_known_as(urls) when is_list(urls), do: urls + defp normalize_also_known_as(url) when is_binary(url), do: [url] + defp normalize_also_known_as(nil), do: [] + defp maybe_put_description(map, %{"name" => description}) when is_binary(description) do Map.put(map, "name", description) end @@ -1693,7 +1697,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do featured_address: featured_address, bio: data["summary"] || "", actor_type: actor_type, - also_known_as: Map.get(data, "alsoKnownAs", []), + also_known_as: normalize_also_known_as(data["alsoKnownAs"]), public_key: public_key, inbox: data["inbox"], shared_inbox: shared_inbox, diff --git a/test/fixtures/users_mock/hubzilla-actor-alsoknownas-string.json b/test/fixtures/users_mock/hubzilla-actor-alsoknownas-string.json new file mode 100644 index 000000000..086db73b1 --- /dev/null +++ b/test/fixtures/users_mock/hubzilla-actor-alsoknownas-string.json @@ -0,0 +1 @@ +{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1","https://purl.archive.org/socialweb/webfinger",{"zot":"https://hub.netzgemeinde.eu/apschema#","contextHistory":"https://w3id.org/fep/171b/contextHistory","schema":"http://schema.org#","ostatus":"http://ostatus.org#","diaspora":"https://diasporafoundation.org/ns/","litepub":"http://litepub.social/ns#","toot":"http://joinmastodon.org/ns#","commentPolicy":"zot:commentPolicy","Bookmark":"zot:Bookmark","Category":"zot:Category","Emoji":"toot:Emoji","directMessage":"litepub:directMessage","PropertyValue":"schema:PropertyValue","value":"schema:value","uuid":"schema:identifier","conversation":"ostatus:conversation","manuallyApprovesFollowers":"as:manuallyApprovesFollowers","Hashtag":"as:Hashtag","quoteUrl":"as:quoteUrl","quoteUri":"http://fedibird.com/ns#quoteUri"}],"type":"Person","manuallyApprovesFollowers":true,"id":"https://hub.netzgemeinde.eu/channel/jupiter_rowland","preferredUsername":"jupiter_rowland","name":"Jupiter Rowland","updated":"0001-01-01T00:00:00Z","icon":{"type":"Image","mediaType":"image/png","updated":"2024-10-15T21:10:02Z","url":"https://hub.netzgemeinde.eu/photo/profile/l/1035?rev=1729019402","height":300,"width":300},"url":"https://hub.netzgemeinde.eu/channel/jupiter_rowland","publicKey":{"id":"https://hub.netzgemeinde.eu/channel/jupiter_rowland","owner":"https://hub.netzgemeinde.eu/channel/jupiter_rowland","signatureAlgorithm":"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAmDB9nkcdhjzcfSPQG5q3\nxRVAYaWa+pKC38NhhRMbmd7+P8+8be3HUuX97bwhLdSA3+IRMz9JmX8bqqtKPK8A\nKxWFdUxoZKefwAAVpT+R89hHvi6Ib56Tp5lNlUSTIg3QXfm2gyRc3ehYbI9i6+Wr\nuFgCzdYT9bIeLqhZdFabPP4NORnyPgBQtktXcQUoDOKSaaKuitxFP1dhM9Dco3uX\nLcpQOLdY6yct5J+1Y6+/GUZgtO2pjMtkoEo6Ro0+Wlo7xfvdPnk5moljDVzPFoQE\nhUao5amorPhm1/iEpQXI0eEUW8IXdObFS8gyQJLmS30AjMkaWfwM9HFGmUmn8CSw\nKGBaKDN2C93fvmLOLpaoIRqgVTHBxfv3bN/CtvTRP83/eWvmhnlYo0fmE49tj2ZH\neCHCvRPJ+XM44WntbrUmwJ61+6nO/Io7qe7zmLm+0ew1VD9xTWAd96isW8HEmhcu\nO2iP2GALb0PqE7mgQmV1x/WAYB40+29C03UINHAnZY+nvBW6xd0wAOFiYXqjJF/4\nxEOWPvwcwOtltX+dTerFBC2KzLQQVk/CK2JdKtD2ssiYrvdC/IqPQzVMz3EAtVoP\n8sq/lzffCD3T+zhhnLhgxu7p9JNjRq83jMOOB3DUSd5izO+u6TBn5lasfoa7Eh+X\nQezGvMXhRQR4mnzrSLwZZ1ECAwEAAQ==\n-----END PUBLIC KEY-----\n"},"tag":[{"type":"PropertyValue","name":"Protocol","value":"zot6"},{"type":"PropertyValue","name":"Protocol","value":"activitypub"}],"outbox":"https://hub.netzgemeinde.eu/outbox/jupiter_rowland","webfinger":"jupiter_rowland@hub.netzgemeinde.eu","inbox":"https://hub.netzgemeinde.eu/inbox/jupiter_rowland","followers":"https://hub.netzgemeinde.eu/followers/jupiter_rowland","following":"https://hub.netzgemeinde.eu/following/jupiter_rowland","endpoints":{"sharedInbox":"https://hub.netzgemeinde.eu/inbox"},"discoverable":true,"assertionMethod":[{"id":"https://hub.netzgemeinde.eu/channel/jupiter_rowland#z6Mkw5vE2YnuCwke9VY6Xn1jnaXqLgDCKoJsRE2PjDmAUw9w","type":"Multikey","controller":"https://hub.netzgemeinde.eu/channel/jupiter_rowland","publicKeyMultibase":"z6Mkw5vE2YnuCwke9VY6Xn1jnaXqLgDCKoJsRE2PjDmAUw9w"}],"copiedTo":"https://hub.hubzilla.de/channel/jupiter_rowland","alsoKnownAs":"https://hub.hubzilla.de/channel/jupiter_rowland","image":{"type":"Image","mediaType":"image/jpeg","url":"https://hub.netzgemeinde.eu/photo/5bd87e54-e328-47d3-a8ed-7a33547ad882-7"},"summary":"An avatar roaming the decentralised and federated 3-D virtual worlds based on OpenSimulator, a free and open-source server-side re-implementation of Second Life. Mostly talking about OpenSim, sometimes about other virtual worlds, occasionally about the Fediverse beyond Mastodon. No, the Fediverse is not only Mastodon.

If you're looking for real-life people posting about real-life topics, go look somewhere else. This channel is never about real life.

Even if you see me on Mastodon, I'm not on Mastodon myself. I'm on Hubzilla which is neither a Mastodon instance nor a Mastodon fork. In fact, it's older and much more powerful than Mastodon. And it has always been connected to Mastodon.

I regularly write posts with way more than 500 characters. If that disturbs you, block me now, but don't complain. I'm not on Mastodon, I don't have a character limit here.

I rather give too many content warnings than too few. But I have absolutely no means of blanking out pictures for Mastodon users.

I always describe my images, no matter how long it takes. My posts with image descriptions tend to be my longest. Don't go looking for my image descriptions in the alt-text; they're always in the post text which is always hidden behind a content warning due to being over 500 characters long.

If you follow me, and I "follow" you back, I don't actually follow you and receive your posts. Unless you've got something to say that's interesting to me within the scope of this channel, or I know you from OpenSim, I'll most likely deny you the permission to send me your posts. I only "follow" you back because Hubzilla requires me to do that to allow you to follow me. But I do let you send me your comments and direct messages. If you boost a lot of uninteresting stuff, I'll block you boosts.

My "birthday" isn't my actual birthday but my rezday. My first avatar has been around since that day.

If you happen to know German, maybe my "homepage" is something for you, a blog which, much like this channel, is about OpenSim and generally virtual worlds.

#OpenSim #OpenSimulator #VirtualWorlds #Metaverse #SocialVR  #fedi22","proof":{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","created":"2026-02-12T17:18:56Z","verificationMethod":"https://hub.netzgemeinde.eu/channel/jupiter_rowland#z6Mkw5vE2YnuCwke9VY6Xn1jnaXqLgDCKoJsRE2PjDmAUw9w","proofPurpose":"assertionMethod","proofValue":"z65hrf1X7c2kZgiyAvBnZ1pd16LGRaLYN4zt6kLdPQtYoYpxuVYgL7zZGFEh3h6gcsq8f6FL5XDjXVNivwg9eJL3u"},"signature":{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1"],"type":"RsaSignature2017","nonce":"87c52f614ebea65c8ea41d99487e8f8c8883bd5bc65b2538438df3870c532823","creator":"https://hub.netzgemeinde.eu/channel/jupiter_rowland","created":"2026-02-12T17:18:56Z","signatureValue":"FtiD/BaezB1SFiYj18hfAuyLgYQ0c+Mjc9CXiB+ikd8re/uXV5jr+kMgpYzY+nUB3vpU03XnWVi+lHkJ55vzcTjfbUi7puspF7e88yjIFJcHhs+fc9/+bzBbL2PSQ026bTgH3N1Hj2I7qQ3NcmvKmDF/OovWDks/HPGFiiS9uUezZHRF5be7KfUgEJjESk+5zKGkr7yG4M+f23xaScGxT+/uGK69/RT7QbgSiF2A1IiD2gtwVqxWe9eP//HeoXPCT+O+MdKLWtSZwwzfboO/iwNehMtSAAq+POBW7emHTcM1FEZD6abEQAQwpTq28qAnjuYNCU4TzBdKg1q4akhqnTWEyqpscS9xVUUv/iRiyXvWh9WtcGSCvcMb6avUsxGmdk0nO8hv2zigR/s/ZZJhjXUrID+/bwP2o9Uie6ibdwmt1bCHR8U6z7NCQXEJXw9qZuZYpiTFeuNSftICVRuE/NIYpSOPyqERlRnjU6AX6qEQXHQsOnvhB8BUKcRXpWv0xuzWN3Sr2MZyul8A98nvla9Uwqh9BYn70nRcpRGfiY3m41N4kn5WbPw5PezOB8RSFZjji963oum8tu0/NjzKQ/5UOh3aPO+h+AJsnjau2KOeRHLeChalOOLrt3KFalkyGxLzxt5o/OcRvyM37nnih14lqeTNo60PmR/l/QpiwBs="}} \ No newline at end of file diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs index 73f53db56..895e01be1 100644 --- a/test/pleroma/web/activity_pub/activity_pub_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_test.exs @@ -499,6 +499,35 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do "https://queef.in/storage/banner.gif" end + test "works with alsoKnownAs as string" do + user_id = "https://hub.netzgemeinde.eu/channel/jupiter_rowland" + + user_data = + "test/fixtures/users_mock/hubzilla-actor-alsoknownas-string.json" + |> File.read!() + + user_data_decoded = + user_data + |> Jason.decode!() + + Tesla.Mock.mock(fn + %{ + method: :get, + url: ^user_id + } -> + %Tesla.Env{ + status: 200, + body: user_data, + headers: [{"content-type", "application/activity+json"}] + } + end) + + {:ok, user} = ActivityPub.make_user_from_ap_id(user_id) + + assert is_list(user.also_known_as) + assert user.also_known_as == [user_data_decoded["alsoKnownAs"]] + end + test "it fetches the appropriate tag-restricted posts" do user = insert(:user) From eed4f4bba84bb9b5cedafe707bfd0a5a6a91b134 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 17 Feb 2026 00:35:15 +0100 Subject: [PATCH 035/127] Gopher: Fix crash on (re)boot when ConfigDB is enabled Ranch listener wasn't being properly stopped when the Gopher GenServer received a shutdown message due Restarter rebooting Pleroma to apply ConfigDB configuration (originating from Config.TransferTask.load_and_update_env/2) when ConfigDB is enabled. Handle by trapping exits in the GenServer, which causes the terminate/2 function to be called and the Ranch listener to be stopped from there. 23:22:29.871 [error] GenServer Restarter.Pleroma terminating ** (MatchError) no match of right hand side value: {:error, {{:shutdown, {:failed_to_start_child, Pleroma.Gopher.Server, {{:badmatch, {:error, {:already_started, #PID<0.4801.0>}}}, [ {Pleroma.Gopher.Server, :init, 1, [file: ~c"lib/pleroma/gopher/server.ex", line: 25]}, {:gen_server, :init_it, 2, [file: ~c"gen_server.erl", line: 2276]}, {:gen_server, :init_it, 6, [file: ~c"gen_server.erl", line: 2236]}, {:proc_lib, :init_p_do_apply, 3, [file: ~c"proc_lib.erl", line: 333]} ]}}}, {Pleroma.Application, :start, [:normal, []]}}} (restarter 0.1.0) lib/pleroma.ex:104: Restarter.Pleroma.do_restart/1 (restarter 0.1.0) lib/pleroma.ex:96: Restarter.Pleroma.handle_cast/2 (stdlib 7.2) gen_server.erl:2460: :gen_server.try_handle_cast/3 (stdlib 7.2) gen_server.erl:2418: :gen_server.handle_msg/3 (stdlib 7.2) proc_lib.erl:333: :proc_lib.init_p_do_apply/3 Last message: {:"$gen_cast", {:after_boot, :dev}} State: %{rebooted: false, need_reboot: false, after_boot: false} --- changelog.d/gopher-genserver-crash-on-boot.fix | 1 + lib/pleroma/gopher/server.ex | 11 +++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 changelog.d/gopher-genserver-crash-on-boot.fix diff --git a/changelog.d/gopher-genserver-crash-on-boot.fix b/changelog.d/gopher-genserver-crash-on-boot.fix new file mode 100644 index 000000000..3b51662be --- /dev/null +++ b/changelog.d/gopher-genserver-crash-on-boot.fix @@ -0,0 +1 @@ +Gopher: Fix Ranch listener not being stopped properly on Pleroma restart when database configuration is enabled diff --git a/lib/pleroma/gopher/server.ex b/lib/pleroma/gopher/server.ex index add3ba925..7b6985efe 100644 --- a/lib/pleroma/gopher/server.ex +++ b/lib/pleroma/gopher/server.ex @@ -21,10 +21,13 @@ defmodule Pleroma.Gopher.Server do def init([ip, port]) do Logger.info("Starting gopher server on #{port}") + Process.flag(:trap_exit, true) + + listener = :gopher {:ok, _pid} = :ranch.start_listener( - :gopher, + listener, :ranch_tcp, %{ num_acceptors: 100, @@ -35,7 +38,11 @@ defmodule Pleroma.Gopher.Server do [] ) - {:ok, %{ip: ip, port: port}} + {:ok, %{ip: ip, port: port, listener: listener}} + end + + def terminate(_reason, state) do + :ranch.stop_listener(state.listener) end end From 3d9ac413af0626bc60176b205eb2f23a8c721ac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Tue, 17 Feb 2026 14:00:21 +0100 Subject: [PATCH 036/127] Move avatar_description and header_description fields to the account object MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/avatar-description-mastodon-api.change | 1 + docs/development/API/differences_in_mastoapi_responses.md | 2 -- lib/pleroma/web/api_spec/schemas/account.ex | 6 ++++-- lib/pleroma/web/mastodon_api/views/account_view.ex | 2 ++ test/pleroma/web/mastodon_api/views/account_view_test.exs | 4 ++++ 5 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 changelog.d/avatar-description-mastodon-api.change diff --git a/changelog.d/avatar-description-mastodon-api.change b/changelog.d/avatar-description-mastodon-api.change new file mode 100644 index 000000000..6a454c01e --- /dev/null +++ b/changelog.d/avatar-description-mastodon-api.change @@ -0,0 +1 @@ +Move avatar_description and header_description fields to the account object diff --git a/docs/development/API/differences_in_mastoapi_responses.md b/docs/development/API/differences_in_mastoapi_responses.md index 052b2716b..358d05bf4 100644 --- a/docs/development/API/differences_in_mastoapi_responses.md +++ b/docs/development/API/differences_in_mastoapi_responses.md @@ -127,8 +127,6 @@ Has these additional fields under the `pleroma` object: - `notification_settings`: object, can be absent. See `/api/v1/pleroma/notification_settings` for the parameters/keys returned. - `accepts_chat_messages`: boolean, but can be null if we don't have that information about a user - `favicon`: nullable URL string, Favicon image of the user's instance -- `avatar_description`: string, image description for user avatar, defaults to empty string -- `header_description`: string, image description for user banner, defaults to empty string ### Source diff --git a/lib/pleroma/web/api_spec/schemas/account.ex b/lib/pleroma/web/api_spec/schemas/account.ex index 7d0b83afe..efdced316 100644 --- a/lib/pleroma/web/api_spec/schemas/account.ex +++ b/lib/pleroma/web/api_spec/schemas/account.ex @@ -21,6 +21,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do acct: %Schema{type: :string}, avatar_static: %Schema{type: :string, format: :uri}, avatar: %Schema{type: :string, format: :uri}, + avatar_description: %Schema{type: :string}, bot: %Schema{type: :boolean}, created_at: %Schema{type: :string, format: "date-time"}, display_name: %Schema{type: :string}, @@ -31,6 +32,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do following_count: %Schema{type: :integer}, header_static: %Schema{type: :string, format: :uri}, header: %Schema{type: :string, format: :uri}, + header_description: %Schema{type: :string}, id: FlakeID, locked: %Schema{type: :boolean}, note: %Schema{type: :string, format: :html}, @@ -111,8 +113,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do nullable: true, description: "Favicon image of the user's instance" }, - avatar_description: %Schema{type: :string}, - header_description: %Schema{type: :string} + avatar_description: %Schema{type: :string, deprecated: true}, + header_description: %Schema{type: :string, deprecated: true} } }, source: %Schema{ diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 29c63eb60..5386c5a6c 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -300,8 +300,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do note: user.bio, url: user.uri || user.ap_id, avatar: avatar, + avatar_description: avatar_description, avatar_static: avatar_static, header: header, + header_description: header_description, header_static: header_static, emojis: emojis, fields: user.fields, diff --git a/test/pleroma/web/mastodon_api/views/account_view_test.exs b/test/pleroma/web/mastodon_api/views/account_view_test.exs index c230cf653..f34a801c1 100644 --- a/test/pleroma/web/mastodon_api/views/account_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/account_view_test.exs @@ -54,8 +54,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do note: "valid html. a
b
c
d
f '&<>"", url: user.ap_id, avatar: "http://localhost:4001/images/avi.png", + avatar_description: "", avatar_static: "http://localhost:4001/images/avi.png", header: "http://localhost:4001/images/banner.png", + header_description: "", header_static: "http://localhost:4001/images/banner.png", emojis: [ %{ @@ -326,8 +328,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do note: user.bio, url: user.ap_id, avatar: "http://localhost:4001/images/avi.png", + avatar_description: "", avatar_static: "http://localhost:4001/images/avi.png", header: "http://localhost:4001/images/banner.png", + header_description: "", header_static: "http://localhost:4001/images/banner.png", emojis: [], fields: [], From 699a7e57e82e71b9e706ded28185ee490a903c6d Mon Sep 17 00:00:00 2001 From: Phantasm Date: Mon, 16 Feb 2026 17:04:00 +0100 Subject: [PATCH 037/127] Fix LiveDashboard redirect not working when user added a path segment /phoenix/live_dashboard -> /pleroma/live_dashboard would work /phoenix/live_dashboard/ecto_stats -> /pleroma/live_dashboard/ecto_stats would not work and instead reply with FE. --- changelog.d/live-dashboard-redirect.fix | 1 + lib/pleroma/web/fallback/redirect_controller.ex | 13 +++++++++++-- lib/pleroma/web/router.ex | 2 +- test/pleroma/web/fallback_test.exs | 6 ++++++ 4 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 changelog.d/live-dashboard-redirect.fix diff --git a/changelog.d/live-dashboard-redirect.fix b/changelog.d/live-dashboard-redirect.fix new file mode 100644 index 000000000..10588d89e --- /dev/null +++ b/changelog.d/live-dashboard-redirect.fix @@ -0,0 +1 @@ +Fix /phoenix/live_dashboard redirect not working when user added a path segment diff --git a/lib/pleroma/web/fallback/redirect_controller.ex b/lib/pleroma/web/fallback/redirect_controller.ex index d75a95fb3..c7f80ad77 100644 --- a/lib/pleroma/web/fallback/redirect_controller.ex +++ b/lib/pleroma/web/fallback/redirect_controller.ex @@ -29,9 +29,18 @@ defmodule Pleroma.Web.Fallback.RedirectController do ) end - def live_dashboard(conn, _params) do + def live_dashboard(conn, %{"path" => path}) do + query_params = conn.query_string + + redirect_path = + if query_params == "" do + "/pleroma/live_dashboard/#{path}" + else + "/pleroma/live_dashboard/#{path}?#{query_params}" + end + conn - |> redirect(to: "/pleroma/live_dashboard") + |> redirect(to: redirect_path) end def redirector(conn, _params, code \\ 200) do diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 20ac1c67b..3ef60c2d9 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -1088,7 +1088,7 @@ defmodule Pleroma.Web.Router do get("/:maybe_nickname_or_id", RedirectController, :redirector_with_meta) match(:*, "/api/pleroma/*path", LegacyPleromaApiRerouterPlug, []) get("/api/*path", RedirectController, :api_not_implemented) - get("/phoenix/live_dashboard", RedirectController, :live_dashboard) + get("/phoenix/live_dashboard/*path", RedirectController, :live_dashboard) get("/*path", RedirectController, :redirector_with_preload) options("/*path", RedirectController, :empty) diff --git a/test/pleroma/web/fallback_test.exs b/test/pleroma/web/fallback_test.exs index 6d0ba3d2a..bc3052959 100644 --- a/test/pleroma/web/fallback_test.exs +++ b/test/pleroma/web/fallback_test.exs @@ -79,6 +79,12 @@ defmodule Pleroma.Web.FallbackTest do test "GET /phoenix/live_dashboard -> /pleroma/live_dashboard", %{conn: conn} do assert redirected_to(get(conn, "/phoenix/live_dashboard")) =~ "/pleroma/live_dashboard" + assert redirected_to(get(conn, "/phoenix/live_dashboard/")) =~ "/pleroma/live_dashboard/" + end + + test "GET /phoenix/live_dashboard/* -> /pleroma/live_dashboard/*", %{conn: conn} do + assert redirected_to(get(conn, "/phoenix/live_dashboard/ecto_stats?nav=diagnose")) =~ + "/pleroma/live_dashboard/ecto_stats?nav=diagnose" end test "OPTIONS /*path", %{conn: conn} do From 0b950f62533579960970a80e11d875036444cd18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 18 Feb 2026 13:37:10 +0100 Subject: [PATCH 038/127] comment out stuff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .forgejo/pull_request_template.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.forgejo/pull_request_template.md b/.forgejo/pull_request_template.md index 799da5355..5bb204a14 100644 --- a/.forgejo/pull_request_template.md +++ b/.forgejo/pull_request_template.md @@ -1,10 +1,13 @@ ### Checklist + - [ ] Adding a changelog: In the `changelog.d` directory, create a file named `.`. + From 23a4d68c97f710f753b42180ea9cbda067b192a3 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 28 Jan 2026 22:06:10 +0100 Subject: [PATCH 039/127] ReverseProxy: Follow redirects recursively until redirect_limit Test post: https://possum.city/notes/ahqdvbhu3wug0at2 --- .../reverseproxy-recursive-redirect.fix | 1 + lib/pleroma/reverse_proxy/client/hackney.ex | 40 +++++++++------ ...ackney_follow_redirect_regression_test.exs | 49 +++++++++++++++++++ 3 files changed, 76 insertions(+), 14 deletions(-) create mode 100644 changelog.d/reverseproxy-recursive-redirect.fix diff --git a/changelog.d/reverseproxy-recursive-redirect.fix b/changelog.d/reverseproxy-recursive-redirect.fix new file mode 100644 index 000000000..744109fd6 --- /dev/null +++ b/changelog.d/reverseproxy-recursive-redirect.fix @@ -0,0 +1 @@ +ReverseProxy: Recursively follow redirects until redirect_limit is reached diff --git a/lib/pleroma/reverse_proxy/client/hackney.ex b/lib/pleroma/reverse_proxy/client/hackney.ex index 7e1fca80d..c39406106 100644 --- a/lib/pleroma/reverse_proxy/client/hackney.ex +++ b/lib/pleroma/reverse_proxy/client/hackney.ex @@ -4,6 +4,7 @@ defmodule Pleroma.ReverseProxy.Client.Hackney do @behaviour Pleroma.ReverseProxy.Client + @redirect_limit 5 # In-app redirect handler to avoid Hackney redirect bugs: # - https://github.com/benoitc/hackney/issues/527 (relative/protocol-less redirects can crash Hackney) @@ -31,26 +32,15 @@ defmodule Pleroma.ReverseProxy.Client.Hackney do if opts[:follow_redirect] != false do {_state, req_opts} = Access.get_and_update(opts, :follow_redirect, fn a -> {a, false} end) + env = %{method: method, headers: headers, body: body, req_opts: req_opts} res = :hackney.request(method, url, headers, body, req_opts) case res do {:ok, code, resp_headers, _client} when code in @redirect_statuses -> - :hackney.request( - method, - absolute_redirect_url(url, resp_headers), - headers, - body, - req_opts - ) + redirect(url, resp_headers, env, @redirect_limit) {:ok, code, resp_headers} when code in @redirect_statuses -> - :hackney.request( - method, - absolute_redirect_url(url, resp_headers), - headers, - body, - req_opts - ) + redirect(url, resp_headers, env, @redirect_limit) _ -> res @@ -71,4 +61,26 @@ defmodule Pleroma.ReverseProxy.Client.Hackney do @impl true def close(ref), do: :hackney.close(ref) + + defp redirect(url, resp_headers, env, limit) when limit == 0 do + new_url = absolute_redirect_url(url, resp_headers) + + :hackney.request(env.method, new_url, env.headers, env.body, env.req_opts) + end + + defp redirect(url, resp_headers, env, limit) do + new_url = absolute_redirect_url(url, resp_headers) + res = :hackney.request(env.method, new_url, env.headers, env.body, env.req_opts) + + case res do + {:ok, code, new_resp_headers, _client} when code in @redirect_statuses -> + redirect(new_url, new_resp_headers, env, limit - 1) + + {:ok, code, new_resp_headers} when code in @redirect_statuses -> + redirect(new_url, new_resp_headers, env, limit - 1) + + _ -> + res + end + end end diff --git a/test/pleroma/http/hackney_follow_redirect_regression_test.exs b/test/pleroma/http/hackney_follow_redirect_regression_test.exs index 71fda4479..7ac040324 100644 --- a/test/pleroma/http/hackney_follow_redirect_regression_test.exs +++ b/test/pleroma/http/hackney_follow_redirect_regression_test.exs @@ -88,6 +88,49 @@ defmodule Pleroma.HTTP.HackneyFollowRedirectRegressionTest do Pleroma.ReverseProxy.Client.Hackney.close(ref) end + test "hackney reverse proxy follows nested redirects via proxy", %{ + tls_server: tls_server, + proxy: proxy + } do + url = "#{tls_server.base_url}/nested_redirect" + + opts = [ + pool: :media, + proxy: proxy.proxy_url, + insecure: true, + connect_timeout: 1_000, + recv_timeout: 1_000, + follow_redirect: true + ] + + assert {:ok, 200, _headers, ref} = + Pleroma.ReverseProxy.Client.Hackney.request(:get, url, [], "", opts) + + assert collect_body(ref) == "ok" + Pleroma.ReverseProxy.Client.Hackney.close(ref) + end + + test "hackney reverse proxy stop following redirects after limit", %{ + tls_server: tls_server, + proxy: proxy + } do + url = "#{tls_server.base_url}/infinite_redirect" + + opts = [ + pool: :media, + proxy: proxy.proxy_url, + insecure: true, + connect_timeout: 1_000, + recv_timeout: 1_000, + follow_redirect: true + ] + + assert {:ok, 302, _headers, ref} = + Pleroma.ReverseProxy.Client.Hackney.request(:get, url, [], "", opts) + + Pleroma.ReverseProxy.Client.Hackney.close(ref) + end + defp collect_body(ref, acc \\ "") do case Pleroma.ReverseProxy.Client.Hackney.stream_body(ref) do :done -> acc @@ -148,6 +191,12 @@ defmodule Pleroma.HTTP.HackneyFollowRedirectRegressionTest do {:ok, data} <- recv_ssl_headers(ssl_socket), {:ok, path} <- parse_path(data) do case path do + "/infinite_redirect" -> + send_ssl_response(ssl_socket, 302, "Found", [{"Location", "/infinite_redirect"}], "") + + "/nested_redirect" -> + send_ssl_response(ssl_socket, 302, "Found", [{"Location", "/redirect"}], "") + "/redirect" -> send_ssl_response(ssl_socket, 302, "Found", [{"Location", "/final"}], "") From cbc2ea331594305c29fb737adcd7e8203af6ab5e Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 28 Jan 2026 22:48:35 +0100 Subject: [PATCH 040/127] typo --- test/pleroma/http/hackney_follow_redirect_regression_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/pleroma/http/hackney_follow_redirect_regression_test.exs b/test/pleroma/http/hackney_follow_redirect_regression_test.exs index 7ac040324..4fef26af6 100644 --- a/test/pleroma/http/hackney_follow_redirect_regression_test.exs +++ b/test/pleroma/http/hackney_follow_redirect_regression_test.exs @@ -110,7 +110,7 @@ defmodule Pleroma.HTTP.HackneyFollowRedirectRegressionTest do Pleroma.ReverseProxy.Client.Hackney.close(ref) end - test "hackney reverse proxy stop following redirects after limit", %{ + test "hackney reverse proxy stops following redirects after limit is reached", %{ tls_server: tls_server, proxy: proxy } do From 95c8b4732f5a44031221cd73a86578afc4cc1b71 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sun, 22 Feb 2026 11:20:04 +0100 Subject: [PATCH 041/127] ReverseProxy Hackney: Add redirect handling logging --- lib/pleroma/reverse_proxy/client/hackney.ex | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/pleroma/reverse_proxy/client/hackney.ex b/lib/pleroma/reverse_proxy/client/hackney.ex index c39406106..4c81a6225 100644 --- a/lib/pleroma/reverse_proxy/client/hackney.ex +++ b/lib/pleroma/reverse_proxy/client/hackney.ex @@ -6,6 +6,8 @@ defmodule Pleroma.ReverseProxy.Client.Hackney do @behaviour Pleroma.ReverseProxy.Client @redirect_limit 5 + require Logger + # In-app redirect handler to avoid Hackney redirect bugs: # - https://github.com/benoitc/hackney/issues/527 (relative/protocol-less redirects can crash Hackney) # - https://github.com/benoitc/hackney/issues/273 (redirects not followed when using HTTP proxy) @@ -65,11 +67,17 @@ defmodule Pleroma.ReverseProxy.Client.Hackney do defp redirect(url, resp_headers, env, limit) when limit == 0 do new_url = absolute_redirect_url(url, resp_headers) + Logger.debug( + "#{__MODULE__}: Handling redirect #{url} -> #{new_url}; redirect limit was reached - returning response after final redirect" + ) + :hackney.request(env.method, new_url, env.headers, env.body, env.req_opts) end defp redirect(url, resp_headers, env, limit) do new_url = absolute_redirect_url(url, resp_headers) + Logger.debug("#{__MODULE__}: handling redirect #{url} -> #{new_url}; limit = #{limit}") + res = :hackney.request(env.method, new_url, env.headers, env.body, env.req_opts) case res do From e32ab8aef278ebcef7ecbc9ae67f417b6862cff9 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 14 Oct 2025 22:59:15 +0200 Subject: [PATCH 042/127] DB prune: Check if user follows hashtag with no objects before deletion --- changelog.d/prune-hashtag-follow-3376.fix | 1 + lib/mix/tasks/pleroma/database.ex | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 changelog.d/prune-hashtag-follow-3376.fix diff --git a/changelog.d/prune-hashtag-follow-3376.fix b/changelog.d/prune-hashtag-follow-3376.fix new file mode 100644 index 000000000..cdb4e9a79 --- /dev/null +++ b/changelog.d/prune-hashtag-follow-3376.fix @@ -0,0 +1 @@ +DB prune: Check if user follows hashtag with no objects before deletion diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex index e52b5e0a7..396536827 100644 --- a/lib/mix/tasks/pleroma/database.ex +++ b/lib/mix/tasks/pleroma/database.ex @@ -226,7 +226,12 @@ defmodule Mix.Tasks.Pleroma.Database do DELETE FROM hashtags AS ht WHERE NOT EXISTS ( SELECT 1 FROM hashtags_objects hto - WHERE ht.id = hto.hashtag_id) + WHERE ht.id = hto.hashtag_id + ) + AND NOT EXISTS ( + SELECT 1 FROM user_follows_hashtag ufh + WHERE ht.id = ufh.hashtag_id + ) """ |> Repo.query() From ef7be0a1e513841beba15ccce6b6211b7369332a Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 15 Oct 2025 23:14:52 +0200 Subject: [PATCH 043/127] DB prune: Add test for hashtags --- test/mix/tasks/pleroma/database_test.exs | 34 ++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/mix/tasks/pleroma/database_test.exs b/test/mix/tasks/pleroma/database_test.exs index 19df17b60..5b567325c 100644 --- a/test/mix/tasks/pleroma/database_test.exs +++ b/test/mix/tasks/pleroma/database_test.exs @@ -8,6 +8,7 @@ defmodule Mix.Tasks.Pleroma.DatabaseTest do alias Pleroma.Activity alias Pleroma.Bookmark + alias Pleroma.Hashtag alias Pleroma.Object alias Pleroma.Repo alias Pleroma.User @@ -550,6 +551,39 @@ defmodule Mix.Tasks.Pleroma.DatabaseTest do assert length(activities) == 3 end + + test "it prunes hashtags with no objects associated", %{old_insert_date: old_insert_date} do + user = insert(:user) + + {:ok, hashtag_post_activity} = + CommonAPI.post(user, %{status: "morning #cofe", local: true}) + + hashtag_post_object = Object.normalize(hashtag_post_activity) + + {:ok, hashtag_post2_activity} = + CommonAPI.post(user, %{status: "morning #cawfee", local: true}) + + hashtag_post2_object = Object.normalize(hashtag_post2_activity) + + hashtag_post_object + |> Ecto.Changeset.change(%{updated_at: old_insert_date}) + |> Repo.update!() + + hashtag_post2_object + |> Ecto.Changeset.change(%{updated_at: old_insert_date}) + |> Repo.update!() + + # Test whether hashtags with follow relationships are kept + User.follow_hashtag(user, Hashtag.get_by_name("cofe")) + + assert length(Repo.all(Hashtag)) == 2 + assert length(Repo.all(Object)) == 2 + + Mix.Tasks.Pleroma.Database.run(["prune_objects"]) + assert length(Repo.all(Hashtag)) == 1 + assert length(Repo.all(Object)) == 0 + assert Repo.one(Hashtag) |> Map.fetch!(:name) == "cofe" + end end describe "running update_users_following_followers_counts" do From c392b21db18e416f2af7e917cb1a7f3a6cfa130d Mon Sep 17 00:00:00 2001 From: mkljczk Date: Fri, 27 Feb 2026 09:22:14 +0000 Subject: [PATCH 044/127] Update docs on scrobbles --- docs/development/API/pleroma_api.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/development/API/pleroma_api.md b/docs/development/API/pleroma_api.md index 7946ba1f6..b19523bce 100644 --- a/docs/development/API/pleroma_api.md +++ b/docs/development/API/pleroma_api.md @@ -690,6 +690,7 @@ Audio scrobbling in Pleroma is **deprecated**. * `album`: the album of the media playing [optional] * `artist`: the artist of the media playing [optional] * `length`: the length of the media playing [optional] + * `external_link`: a URL referencing the media playing [optional] * Response: the newly created media metadata entity representing the Listen activity # Emoji Reactions From 938ee4cb01c12a4243d3518d8bba7dc817f34071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 27 Feb 2026 12:04:25 +0100 Subject: [PATCH 045/127] mix.exs: use correct override value MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/mix-exs-fix.skip | 0 mix.exs | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 changelog.d/mix-exs-fix.skip diff --git a/changelog.d/mix-exs-fix.skip b/changelog.d/mix-exs-fix.skip new file mode 100644 index 000000000..e69de29bb diff --git a/mix.exs b/mix.exs index a4415fddc..0532bffd7 100644 --- a/mix.exs +++ b/mix.exs @@ -154,7 +154,7 @@ defmodule Pleroma.Mixfile do {:gun, "~> 2.2"}, {:finch, "~> 0.15"}, {:jason, "~> 1.2"}, - {:mogrify, "~> 0.9.0", override: "true"}, + {:mogrify, "~> 0.9.0", override: true}, {:ex_aws, "~> 2.1.6"}, {:ex_aws_s3, "~> 2.0"}, {:sweet_xml, "~> 0.7.2"}, From a9b5a28c26324bcb02ed20c39ace80d33a636244 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 27 Feb 2026 16:24:10 +0100 Subject: [PATCH 046/127] Do not use Enum.map for side-effects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/map-side-effects.skip | 0 lib/mix/tasks/pleroma/openapi_spec.ex | 2 +- lib/pleroma/activity/html.ex | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 changelog.d/map-side-effects.skip diff --git a/changelog.d/map-side-effects.skip b/changelog.d/map-side-effects.skip new file mode 100644 index 000000000..e69de29bb diff --git a/lib/mix/tasks/pleroma/openapi_spec.ex b/lib/mix/tasks/pleroma/openapi_spec.ex index 1ea468476..852e1e9af 100644 --- a/lib/mix/tasks/pleroma/openapi_spec.ex +++ b/lib/mix/tasks/pleroma/openapi_spec.ex @@ -22,7 +22,7 @@ defmodule Mix.Tasks.Pleroma.OpenapiSpec do else {_, errors} -> IO.puts(IO.ANSI.format([:red, :bright, "Spec check failed, errors:"])) - Enum.map(errors, &IO.puts/1) + Enum.each(errors, &IO.puts/1) raise "Spec check failed" end diff --git a/lib/pleroma/activity/html.ex b/lib/pleroma/activity/html.ex index ba284b4d5..c83889c87 100644 --- a/lib/pleroma/activity/html.ex +++ b/lib/pleroma/activity/html.ex @@ -38,7 +38,7 @@ defmodule Pleroma.Activity.HTML do def invalidate_cache_for(activity_id) do keys = get_cache_keys_for(activity_id) - Enum.map(keys, &@cachex.del(:scrubber_cache, &1)) + Enum.each(keys, &@cachex.del(:scrubber_cache, &1)) @cachex.del(:scrubber_management_cache, activity_id) end From 120719f28c9aba33a57d07c5fd28ddcb1d8a3db5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 27 Feb 2026 19:53:25 +0100 Subject: [PATCH 047/127] Don't use the confusing TwitterAPI namespace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/twitter-api.skip | 0 ...operation.ex => pleroma_util_operation.ex} | 74 ++----- .../remote_interaction_operation.ex | 61 ++++++ .../controllers/account_controller.ex | 6 +- .../controllers/auth_controller.ex | 4 +- .../password_controller.ex | 6 +- .../views => o_auth}/password_view.ex | 2 +- .../token_controller.ex} | 4 +- .../views => o_auth}/token_view.ex | 4 +- .../controllers/password_controller.ex | 52 +++++ .../controllers/token_controller.ex | 59 +++++ .../controllers/util_controller.ex | 135 +----------- .../web/pleroma_api/views/util_view.ex | 13 ++ lib/pleroma/web/preload/providers/instance.ex | 2 +- .../twitter_api.ex => registration.ex} | 2 +- .../remote_interaction_controller.ex} | 131 +++++++++++- .../remote_interaction_view.ex} | 4 +- lib/pleroma/web/router.ex | 42 ++-- .../password/invalid_token.html.eex | 0 .../password/reset.html.eex | 0 .../password/reset_failed.html.eex | 0 .../password/reset_success.html.eex | 0 .../remote_interaction}/follow.html.eex | 2 +- .../remote_interaction}/follow_login.html.eex | 2 +- .../remote_interaction}/follow_mfa.html.eex | 2 +- .../remote_interaction}/followed.html.eex | 0 .../status_interact.html.eex | 2 +- .../remote_interaction}/subscribe.html.eex | 2 +- .../web/twitter_api/views/util_view.ex | 31 --- .../password_controller_test.exs | 2 +- .../token_controller_test.exs} | 2 +- .../controllers}/util_controller_test.exs | 149 +------------ ...ter_api_test.exs => registration_test.exs} | 42 ++-- .../remote_interaction_controller_test.exs} | 202 +++++++++++++++--- 34 files changed, 578 insertions(+), 461 deletions(-) create mode 100644 changelog.d/twitter-api.skip rename lib/pleroma/web/api_spec/operations/{twitter_util_operation.ex => pleroma_util_operation.ex} (84%) create mode 100644 lib/pleroma/web/api_spec/operations/remote_interaction_operation.ex rename lib/pleroma/web/{twitter_api/controllers => o_auth}/password_controller.ex (90%) rename lib/pleroma/web/{twitter_api/views => o_auth}/password_view.ex (83%) rename lib/pleroma/web/{twitter_api/controller.ex => o_auth/token_controller.ex} (94%) rename lib/pleroma/web/{twitter_api/views => o_auth}/token_view.ex (81%) create mode 100644 lib/pleroma/web/pleroma_api/controllers/password_controller.ex create mode 100644 lib/pleroma/web/pleroma_api/controllers/token_controller.ex rename lib/pleroma/web/{twitter_api => pleroma_api}/controllers/util_controller.ex (65%) create mode 100644 lib/pleroma/web/pleroma_api/views/util_view.ex rename lib/pleroma/web/{twitter_api/twitter_api.ex => registration.ex} (98%) rename lib/pleroma/web/{twitter_api/controllers/remote_follow_controller.ex => remote_interaction/remote_interaction_controller.ex} (57%) rename lib/pleroma/web/{twitter_api/views/remote_follow_view.ex => remote_interaction/remote_interaction_view.ex} (75%) rename lib/pleroma/web/templates/{twitter_api => o_auth}/password/invalid_token.html.eex (100%) rename lib/pleroma/web/templates/{twitter_api => o_auth}/password/reset.html.eex (100%) rename lib/pleroma/web/templates/{twitter_api => o_auth}/password/reset_failed.html.eex (100%) rename lib/pleroma/web/templates/{twitter_api => o_auth}/password/reset_success.html.eex (100%) rename lib/pleroma/web/templates/{twitter_api/remote_follow => remote_interaction/remote_interaction}/follow.html.eex (83%) rename lib/pleroma/web/templates/{twitter_api/remote_follow => remote_interaction/remote_interaction}/follow_login.html.eex (88%) rename lib/pleroma/web/templates/{twitter_api/remote_follow => remote_interaction/remote_interaction}/follow_mfa.html.eex (86%) rename lib/pleroma/web/templates/{twitter_api/remote_follow => remote_interaction/remote_interaction}/followed.html.eex (100%) rename lib/pleroma/web/templates/{twitter_api/util => remote_interaction/remote_interaction}/status_interact.html.eex (88%) rename lib/pleroma/web/templates/{twitter_api/util => remote_interaction/remote_interaction}/subscribe.html.eex (85%) delete mode 100644 lib/pleroma/web/twitter_api/views/util_view.ex rename test/pleroma/web/{twitter_api => o_auth}/password_controller_test.exs (99%) rename test/pleroma/web/{twitter_api/controller_test.exs => o_auth/token_controller_test.exs} (97%) rename test/pleroma/web/{twitter_api => pleroma_api/controllers}/util_controller_test.exs (85%) rename test/pleroma/web/{twitter_api/twitter_api_test.exs => registration_test.exs} (90%) rename test/pleroma/web/{twitter_api/remote_follow_controller_test.exs => remote_interaction/remote_interaction_controller_test.exs} (68%) diff --git a/changelog.d/twitter-api.skip b/changelog.d/twitter-api.skip new file mode 100644 index 000000000..e69de29bb diff --git a/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_util_operation.ex similarity index 84% rename from lib/pleroma/web/api_spec/operations/twitter_util_operation.ex rename to lib/pleroma/web/api_spec/operations/pleroma_util_operation.ex index 724d873c0..4bb4f112c 100644 --- a/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_util_operation.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2022 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do +defmodule Pleroma.Web.ApiSpec.PleromaUtilOperation do alias OpenApiSpex.Operation alias OpenApiSpex.Schema alias Pleroma.Web.ApiSpec.Schemas.ApiError @@ -19,7 +19,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do %Operation{ tags: ["Custom emojis"], summary: "List all custom emojis", - operationId: "UtilController.emoji", + operationId: "PleromaAPI.UtilController.emoji", parameters: [], responses: %{ 200 => @@ -48,7 +48,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do %Operation{ tags: ["Others"], summary: "Dump frontend configurations", - operationId: "UtilController.frontend_configurations", + operationId: "PleromaAPI.UtilController.frontend_configurations", parameters: [], responses: %{ 200 => @@ -70,7 +70,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do tags: ["Account credentials"], summary: "Change account password", security: [%{"oAuth" => ["write:accounts"]}], - operationId: "UtilController.change_password", + operationId: "PleromaAPI.UtilController.change_password", requestBody: request_body("Parameters", change_password_request(), required: true), responses: %{ 200 => @@ -106,7 +106,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do tags: ["Account credentials"], summary: "Change account email", security: [%{"oAuth" => ["write:accounts"]}], - operationId: "UtilController.change_email", + operationId: "PleromaAPI.UtilController.change_email", requestBody: request_body("Parameters", change_email_request(), required: true), responses: %{ 200 => @@ -141,7 +141,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do tags: ["Settings"], summary: "Update Notification Settings", security: [%{"oAuth" => ["write:accounts"]}], - operationId: "UtilController.update_notification_settings", + operationId: "PleromaAPI.UtilController.update_notification_settings", parameters: [ Operation.parameter( :block_from_strangers, @@ -173,7 +173,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do tags: ["Account credentials"], summary: "Disable Account", security: [%{"oAuth" => ["write:accounts"]}], - operationId: "UtilController.disable_account", + operationId: "PleromaAPI.UtilController.disable_account", parameters: [ Operation.parameter(:password, :query, :string, "Password") ], @@ -193,7 +193,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do tags: ["Account credentials"], summary: "Delete Account", security: [%{"oAuth" => ["write:accounts"]}], - operationId: "UtilController.delete_account", + operationId: "PleromaAPI.UtilController.delete_account", parameters: [ Operation.parameter(:password, :query, :string, "Password") ], @@ -212,7 +212,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do def captcha_operation do %Operation{ summary: "Get a captcha", - operationId: "UtilController.captcha", + operationId: "PleromaAPI.UtilController.captcha", tags: ["Others"], parameters: [], responses: %{ @@ -226,7 +226,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do tags: ["Account credentials"], summary: "Move account", security: [%{"oAuth" => ["write:accounts"]}], - operationId: "UtilController.move_account", + operationId: "PleromaAPI.UtilController.move_account", requestBody: request_body("Parameters", move_account_request(), required: true), responses: %{ 200 => @@ -262,7 +262,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do tags: ["Account credentials"], summary: "List account aliases", security: [%{"oAuth" => ["read:accounts"]}], - operationId: "UtilController.list_aliases", + operationId: "PleromaAPI.UtilController.list_aliases", responses: %{ 200 => Operation.response("Success", "application/json", %Schema{ @@ -286,7 +286,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do tags: ["Account credentials"], summary: "Add an alias to this account", security: [%{"oAuth" => ["write:accounts"]}], - operationId: "UtilController.add_alias", + operationId: "PleromaAPI.UtilController.add_alias", requestBody: request_body("Parameters", add_alias_request(), required: true), responses: %{ 200 => @@ -326,7 +326,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do tags: ["Account credentials"], summary: "Delete an alias from this account", security: [%{"oAuth" => ["write:accounts"]}], - operationId: "UtilController.delete_alias", + operationId: "PleromaAPI.UtilController.delete_alias", requestBody: request_body("Parameters", delete_alias_request(), required: true), responses: %{ 200 => @@ -366,7 +366,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do tags: ["Others"], summary: "Quick status check on the instance", security: [%{"oAuth" => ["write:accounts"]}], - operationId: "UtilController.healthcheck", + operationId: "PleromaAPI.UtilController.healthcheck", parameters: [], responses: %{ 200 => Operation.response("Healthy", "application/json", %Schema{type: :object}), @@ -376,52 +376,6 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do } end - def remote_subscribe_operation do - %Operation{ - tags: ["Remote interaction"], - summary: "Remote Subscribe", - operationId: "UtilController.remote_subscribe", - parameters: [], - responses: %{200 => Operation.response("Web Page", "test/html", %Schema{type: :string})} - } - end - - def remote_interaction_operation do - %Operation{ - tags: ["Remote interaction"], - summary: "Remote interaction", - operationId: "UtilController.remote_interaction", - requestBody: request_body("Parameters", remote_interaction_request(), required: true), - responses: %{ - 200 => - Operation.response("Remote interaction URL", "application/json", %Schema{type: :object}) - } - } - end - - defp remote_interaction_request do - %Schema{ - title: "RemoteInteractionRequest", - description: "POST body for remote interaction", - type: :object, - required: [:ap_id, :profile], - properties: %{ - ap_id: %Schema{type: :string, description: "Profile or status ActivityPub ID"}, - profile: %Schema{type: :string, description: "Remote profile webfinger"} - } - } - end - - def show_subscribe_form_operation do - %Operation{ - tags: ["Remote interaction"], - summary: "Show remote subscribe form", - operationId: "UtilController.show_subscribe_form", - parameters: [], - responses: %{200 => Operation.response("Web Page", "test/html", %Schema{type: :string})} - } - end - defp delete_account_request do %Schema{ title: "AccountDeleteRequest", diff --git a/lib/pleroma/web/api_spec/operations/remote_interaction_operation.ex b/lib/pleroma/web/api_spec/operations/remote_interaction_operation.ex new file mode 100644 index 000000000..a490bd491 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/remote_interaction_operation.ex @@ -0,0 +1,61 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.RemoteInteractionOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def remote_subscribe_operation do + %Operation{ + tags: ["Remote interaction"], + summary: "Remote Subscribe", + operationId: "RemoteInteractionController.remote_subscribe", + parameters: [], + responses: %{200 => Operation.response("Web Page", "text/html", %Schema{type: :string})} + } + end + + def remote_interaction_operation do + %Operation{ + tags: ["Remote interaction"], + summary: "Remote interaction", + operationId: "RemoteInteractionController.remote_interaction", + requestBody: request_body("Parameters", remote_interaction_request(), required: true), + responses: %{ + 200 => + Operation.response("Remote interaction URL", "application/json", %Schema{type: :object}) + } + } + end + + defp remote_interaction_request do + %Schema{ + title: "RemoteInteractionRequest", + description: "POST body for remote interaction", + type: :object, + required: [:ap_id, :profile], + properties: %{ + ap_id: %Schema{type: :string, description: "Profile or status ActivityPub ID"}, + profile: %Schema{type: :string, description: "Remote profile webfinger"} + } + } + end + + def show_subscribe_form_operation do + %Operation{ + tags: ["Remote interaction"], + summary: "Show remote subscribe form", + operationId: "RemoteInteractionController.show_subscribe_form", + parameters: [], + responses: %{200 => Operation.response("Web Page", "text/html", %Schema{type: :string})} + } + end +end diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 6dc731ed4..6d5851029 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -26,7 +26,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do alias Pleroma.Web.OAuth.OAuthController alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.RateLimiter - alias Pleroma.Web.TwitterAPI.TwitterAPI + alias Pleroma.Web.Registration alias Pleroma.Web.Utils.Params plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false) @@ -111,8 +111,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do _params ) do with :ok <- validate_email_param(params), - :ok <- TwitterAPI.validate_captcha(app, params), - {:ok, user} <- TwitterAPI.register_user(params), + :ok <- Registration.validate_captcha(app, params), + {:ok, user} <- Registration.register_user(params), {_, {:ok, token}} <- {:login, OAuthController.login(user, app, app.scopes)} do OAuthController.after_token_exchange(conn, %{user: user, token: token}) diff --git a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex index fbb54a171..653b5fc29 100644 --- a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do import Pleroma.Web.ControllerHelper, only: [json_response: 3] - alias Pleroma.Web.TwitterAPI.TwitterAPI + alias Pleroma.Web.Registration action_fallback(Pleroma.Web.MastodonAPI.FallbackController) @@ -17,7 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do def password_reset(conn, params) do nickname_or_email = params["email"] || params["nickname"] - TwitterAPI.password_reset(nickname_or_email) + Registration.password_reset(nickname_or_email) json_response(conn, :no_content, "") end diff --git a/lib/pleroma/web/twitter_api/controllers/password_controller.ex b/lib/pleroma/web/o_auth/password_controller.ex similarity index 90% rename from lib/pleroma/web/twitter_api/controllers/password_controller.ex rename to lib/pleroma/web/o_auth/password_controller.ex index e5482de9d..b209e7564 100644 --- a/lib/pleroma/web/twitter_api/controllers/password_controller.ex +++ b/lib/pleroma/web/o_auth/password_controller.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2022 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.TwitterAPI.PasswordController do +defmodule Pleroma.Web.OAuth.PasswordController do @moduledoc """ The module contains functions for password reset. """ @@ -16,7 +16,7 @@ defmodule Pleroma.Web.TwitterAPI.PasswordController do alias Pleroma.PasswordResetToken alias Pleroma.Repo alias Pleroma.User - alias Pleroma.Web.TwitterAPI.TwitterAPI + alias Pleroma.Web.Registration plug(Pleroma.Web.Plugs.RateLimiter, [name: :request] when action == :request) @@ -24,7 +24,7 @@ defmodule Pleroma.Web.TwitterAPI.PasswordController do def request(conn, params) do nickname_or_email = params["email"] || params["nickname"] - TwitterAPI.password_reset(nickname_or_email) + Registration.password_reset(nickname_or_email) json_response(conn, :no_content, "") end diff --git a/lib/pleroma/web/twitter_api/views/password_view.ex b/lib/pleroma/web/o_auth/password_view.ex similarity index 83% rename from lib/pleroma/web/twitter_api/views/password_view.ex rename to lib/pleroma/web/o_auth/password_view.ex index 55790941f..0b85c76e8 100644 --- a/lib/pleroma/web/twitter_api/views/password_view.ex +++ b/lib/pleroma/web/o_auth/password_view.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2022 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.TwitterAPI.PasswordView do +defmodule Pleroma.Web.OAuth.PasswordView do use Pleroma.Web, :view import Phoenix.HTML.Form alias Pleroma.Web.Gettext diff --git a/lib/pleroma/web/twitter_api/controller.ex b/lib/pleroma/web/o_auth/token_controller.ex similarity index 94% rename from lib/pleroma/web/twitter_api/controller.ex rename to lib/pleroma/web/o_auth/token_controller.ex index 6db3d6067..37d08eea3 100644 --- a/lib/pleroma/web/twitter_api/controller.ex +++ b/lib/pleroma/web/o_auth/token_controller.ex @@ -2,13 +2,13 @@ # Copyright © 2017-2022 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.TwitterAPI.Controller do +defmodule Pleroma.Web.OAuth.TokenController do use Pleroma.Web, :controller alias Pleroma.User alias Pleroma.Web.OAuth.Token alias Pleroma.Web.Plugs.OAuthScopesPlug - alias Pleroma.Web.TwitterAPI.TokenView + alias Pleroma.Web.OAuth.TokenView require Logger diff --git a/lib/pleroma/web/twitter_api/views/token_view.ex b/lib/pleroma/web/o_auth/token_view.ex similarity index 81% rename from lib/pleroma/web/twitter_api/views/token_view.ex rename to lib/pleroma/web/o_auth/token_view.ex index 36776ce3b..f9894399a 100644 --- a/lib/pleroma/web/twitter_api/views/token_view.ex +++ b/lib/pleroma/web/o_auth/token_view.ex @@ -2,12 +2,12 @@ # Copyright © 2017-2022 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.TwitterAPI.TokenView do +defmodule Pleroma.Web.OAuth.TokenView do use Pleroma.Web, :view def render("index.json", %{tokens: tokens}) do tokens - |> render_many(Pleroma.Web.TwitterAPI.TokenView, "show.json") + |> render_many(Pleroma.Web.OAuth.TokenView, "show.json") |> Enum.filter(&Enum.any?/1) end diff --git a/lib/pleroma/web/pleroma_api/controllers/password_controller.ex b/lib/pleroma/web/pleroma_api/controllers/password_controller.ex new file mode 100644 index 000000000..444477245 --- /dev/null +++ b/lib/pleroma/web/pleroma_api/controllers/password_controller.ex @@ -0,0 +1,52 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.PasswordController do + @moduledoc """ + The module contains functions for password reset. + """ + + use Pleroma.Web, :controller + + require Logger + + import Pleroma.Web.ControllerHelper, only: [json_response: 3] + + alias Pleroma.PasswordResetToken + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.Registration + + plug(Pleroma.Web.Plugs.RateLimiter, [name: :request] when action == :request) + + @doc "POST /auth/password" + def request(conn, params) do + nickname_or_email = params["email"] || params["nickname"] + + Registration.password_reset(nickname_or_email) + + json_response(conn, :no_content, "") + end + + def reset(conn, %{"token" => token}) do + with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}), + false <- PasswordResetToken.expired?(token), + %User{} = user <- User.get_cached_by_id(token.user_id) do + render(conn, "reset.html", %{ + token: token, + user: user + }) + else + _e -> render(conn, "invalid_token.html") + end + end + + def do_reset(conn, %{"data" => data}) do + with {:ok, _} <- PasswordResetToken.reset_password(data["token"], data) do + render(conn, "reset_success.html") + else + _e -> render(conn, "reset_failed.html") + end + end +end diff --git a/lib/pleroma/web/pleroma_api/controllers/token_controller.ex b/lib/pleroma/web/pleroma_api/controllers/token_controller.ex new file mode 100644 index 000000000..581dd569a --- /dev/null +++ b/lib/pleroma/web/pleroma_api/controllers/token_controller.ex @@ -0,0 +1,59 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.TokenController do + use Pleroma.Web, :controller + + alias Pleroma.User + alias Pleroma.Web.OAuth.Token + alias Pleroma.Web.Plugs.OAuthScopesPlug + alias Pleroma.Web.PleromaAPI.TokenView + + require Logger + + plug(:skip_auth when action == :confirm_email) + plug(:skip_plug, OAuthScopesPlug when action in [:oauth_tokens, :revoke_token]) + + action_fallback(:errors) + + def confirm_email(conn, %{"user_id" => uid, "token" => token}) do + with %User{} = user <- User.get_cached_by_id(uid), + true <- user.local and !user.is_confirmed and user.confirmation_token == token, + {:ok, _} <- User.confirm(user) do + redirect(conn, to: "/") + end + end + + def oauth_tokens(%{assigns: %{user: user}} = conn, _params) do + with oauth_tokens <- Token.get_user_tokens(user) do + conn + |> put_view(TokenView) + |> render("index.json", %{tokens: oauth_tokens}) + end + end + + def revoke_token(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do + Token.delete_user_token(user, id) + + json_reply(conn, 201, "") + end + + defp errors(conn, {:param_cast, _}) do + conn + |> put_status(400) + |> json("Invalid parameters") + end + + defp errors(conn, _) do + conn + |> put_status(500) + |> json("Something went wrong") + end + + defp json_reply(conn, status, json) do + conn + |> put_resp_content_type("application/json") + |> send_resp(status, json) + end +end diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/pleroma_api/controllers/util_controller.ex similarity index 65% rename from lib/pleroma/web/twitter_api/controllers/util_controller.ex rename to lib/pleroma/web/pleroma_api/controllers/util_controller.ex index 1c072f98a..9528d5564 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/util_controller.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2022 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.TwitterAPI.UtilController do +defmodule Pleroma.Web.PleromaAPI.UtilController do use Pleroma.Web, :controller require Logger @@ -17,19 +17,8 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do alias Pleroma.Web.Auth.WrapperAuthenticator, as: Authenticator alias Pleroma.Web.CommonAPI alias Pleroma.Web.Plugs.OAuthScopesPlug - alias Pleroma.Web.WebFinger - plug( - Pleroma.Web.ApiSpec.CastAndValidate, - [replace_params: false] - when action != :remote_subscribe and action != :show_subscribe_form - ) - - plug( - Pleroma.Web.Plugs.FederatingPlug - when action == :remote_subscribe - when action == :show_subscribe_form - ) + plug(Pleroma.Web.ApiSpec.CastAndValidate, [replace_params: false]) plug( OAuthScopesPlug, @@ -54,125 +43,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do ] ) - defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.TwitterUtilOperation - - def show_subscribe_form(conn, %{"nickname" => nick}) do - with %User{} = user <- User.get_cached_by_nickname(nick), - avatar = User.avatar_url(user) do - conn - |> render("subscribe.html", %{nickname: nick, avatar: avatar, error: false}) - else - _e -> - render(conn, "subscribe.html", %{ - nickname: nick, - avatar: nil, - error: - Pleroma.Web.Gettext.dpgettext( - "static_pages", - "remote follow error message - user not found", - "Could not find user" - ) - }) - end - end - - def show_subscribe_form(conn, %{"status_id" => id}) do - with %Activity{} = activity <- Activity.get_by_id(id), - {:ok, ap_id} <- get_ap_id(activity), - %User{} = user <- User.get_cached_by_ap_id(activity.actor), - avatar = User.avatar_url(user) do - conn - |> render("status_interact.html", %{ - status_link: ap_id, - status_id: id, - nickname: user.nickname, - avatar: avatar, - error: false - }) - else - _e -> - render(conn, "status_interact.html", %{ - status_id: id, - avatar: nil, - error: - Pleroma.Web.Gettext.dpgettext( - "static_pages", - "status interact error message - status not found", - "Could not find status" - ) - }) - end - end - - def remote_subscribe(conn, %{"nickname" => nick, "profile" => _}) do - show_subscribe_form(conn, %{"nickname" => nick}) - end - - def remote_subscribe(conn, %{"status_id" => id, "profile" => _}) do - show_subscribe_form(conn, %{"status_id" => id}) - end - - def remote_subscribe(conn, %{"user" => %{"nickname" => nick, "profile" => profile}}) do - with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile), - %User{ap_id: ap_id} <- User.get_cached_by_nickname(nick) do - conn - |> Phoenix.Controller.redirect(external: String.replace(template, "{uri}", ap_id)) - else - _e -> - render(conn, "subscribe.html", %{ - nickname: nick, - avatar: nil, - error: - Pleroma.Web.Gettext.dpgettext( - "static_pages", - "remote follow error message - unknown error", - "Something went wrong." - ) - }) - end - end - - def remote_subscribe(conn, %{"status" => %{"status_id" => id, "profile" => profile}}) do - with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile), - %Activity{} = activity <- Activity.get_by_id(id), - {:ok, ap_id} <- get_ap_id(activity) do - conn - |> Phoenix.Controller.redirect(external: String.replace(template, "{uri}", ap_id)) - else - _e -> - render(conn, "status_interact.html", %{ - status_id: id, - avatar: nil, - error: - Pleroma.Web.Gettext.dpgettext( - "static_pages", - "status interact error message - unknown error", - "Something went wrong." - ) - }) - end - end - - def remote_interaction( - %{private: %{open_api_spex: %{body_params: %{ap_id: ap_id, profile: profile}}}} = conn, - _params - ) do - with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile) do - conn - |> json(%{url: String.replace(template, "{uri}", ap_id)}) - else - _e -> json(conn, %{error: "Couldn't find user"}) - end - end - - defp get_ap_id(activity) do - object = Pleroma.Object.normalize(activity, fetch: false) - - case object do - %{data: %{"id" => ap_id}} -> {:ok, ap_id} - _ -> {:no_ap_id, nil} - end - end + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaUtilOperation def frontend_configurations(conn, _params) do render(conn, "frontend_configurations.json") diff --git a/lib/pleroma/web/pleroma_api/views/util_view.ex b/lib/pleroma/web/pleroma_api/views/util_view.ex new file mode 100644 index 000000000..b5e07c006 --- /dev/null +++ b/lib/pleroma/web/pleroma_api/views/util_view.ex @@ -0,0 +1,13 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.UtilView do + use Pleroma.Web, :view + alias Pleroma.Config + + def render("frontend_configurations.json", _) do + Config.get(:frontend_configurations, %{}) + |> Enum.into(%{}) + end +end diff --git a/lib/pleroma/web/preload/providers/instance.ex b/lib/pleroma/web/preload/providers/instance.ex index 6183f7b70..d5417af30 100644 --- a/lib/pleroma/web/preload/providers/instance.ex +++ b/lib/pleroma/web/preload/providers/instance.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Web.Preload.Providers.Instance do alias Pleroma.Web.Nodeinfo.Nodeinfo alias Pleroma.Web.Plugs.InstanceStatic alias Pleroma.Web.Preload.Providers.Provider - alias Pleroma.Web.TwitterAPI.UtilView + alias Pleroma.Web.PleromaAPI.UtilView @behaviour Provider @instance_url "/api/v1/instance" diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/registration.ex similarity index 98% rename from lib/pleroma/web/twitter_api/twitter_api.ex rename to lib/pleroma/web/registration.ex index ef2eb75f4..df71300ce 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/registration.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2022 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.TwitterAPI.TwitterAPI do +defmodule Pleroma.Web.Registration do import Pleroma.Web.Gettext alias Pleroma.Emails.Mailer diff --git a/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex b/lib/pleroma/web/remote_interaction/remote_interaction_controller.ex similarity index 57% rename from lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex rename to lib/pleroma/web/remote_interaction/remote_interaction_controller.ex index 38ebc8c5d..8592fc990 100644 --- a/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex +++ b/lib/pleroma/web/remote_interaction/remote_interaction_controller.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2022 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do +defmodule Pleroma.Web.RemoteInteraction.RemoteInteractionController do use Pleroma.Web, :controller require Logger @@ -14,9 +14,16 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do alias Pleroma.Web.Auth.TOTPAuthenticator alias Pleroma.Web.Auth.WrapperAuthenticator alias Pleroma.Web.CommonAPI + alias Pleroma.Web.WebFinger @status_types ["Article", "Event", "Note", "Video", "Page", "Question"] + plug( + Pleroma.Web.ApiSpec.CastAndValidate, + [replace_params: false] + when action == :remote_interaction + ) + plug(Pleroma.Web.Plugs.FederatingPlug) # Note: follower can submit the form (with password auth) not being signed in (having no token) @@ -26,6 +33,8 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do when action in [:do_follow] ) + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.RemoteInteractionOperation + # GET /ostatus_subscribe # def follow(%{assigns: %{user: user}} = conn, %{"acct" => acct}) do @@ -125,7 +134,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do # def authorize_interaction(conn, %{"uri" => uri}) do conn - |> redirect(to: Routes.remote_follow_path(conn, :follow, %{acct: uri})) + |> redirect(to: Routes.remote_interaction_path(conn, :follow, %{acct: uri})) end defp handle_follow_error(conn, {:mfa_token, followee, _} = _) do @@ -162,4 +171,122 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do Logger.debug("Remote follow failed with error #{inspect(error)}") render(conn, "followed.html", %{error: "Something went wrong."}) end + + def show_subscribe_form(conn, %{"nickname" => nick}) do + with %User{} = user <- User.get_cached_by_nickname(nick), + avatar = User.avatar_url(user) do + conn + |> render("subscribe.html", %{nickname: nick, avatar: avatar, error: false}) + else + _e -> + render(conn, "subscribe.html", %{ + nickname: nick, + avatar: nil, + error: + Pleroma.Web.Gettext.dpgettext( + "static_pages", + "remote follow error message - user not found", + "Could not find user" + ) + }) + end + end + + def show_subscribe_form(conn, %{"status_id" => id}) do + with %Activity{} = activity <- Activity.get_by_id(id), + {:ok, ap_id} <- get_ap_id(activity), + %User{} = user <- User.get_cached_by_ap_id(activity.actor), + avatar = User.avatar_url(user) do + conn + |> render("status_interact.html", %{ + status_link: ap_id, + status_id: id, + nickname: user.nickname, + avatar: avatar, + error: false + }) + else + _e -> + render(conn, "status_interact.html", %{ + status_id: id, + avatar: nil, + error: + Pleroma.Web.Gettext.dpgettext( + "static_pages", + "status interact error message - status not found", + "Could not find status" + ) + }) + end + end + + def remote_subscribe(conn, %{"nickname" => nick, "profile" => _}) do + show_subscribe_form(conn, %{"nickname" => nick}) + end + + def remote_subscribe(conn, %{"status_id" => id, "profile" => _}) do + show_subscribe_form(conn, %{"status_id" => id}) + end + + def remote_subscribe(conn, %{"user" => %{"nickname" => nick, "profile" => profile}}) do + with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile), + %User{ap_id: ap_id} <- User.get_cached_by_nickname(nick) do + conn + |> Phoenix.Controller.redirect(external: String.replace(template, "{uri}", ap_id)) + else + _e -> + render(conn, "subscribe.html", %{ + nickname: nick, + avatar: nil, + error: + Pleroma.Web.Gettext.dpgettext( + "static_pages", + "remote follow error message - unknown error", + "Something went wrong." + ) + }) + end + end + + def remote_subscribe(conn, %{"status" => %{"status_id" => id, "profile" => profile}}) do + with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile), + %Activity{} = activity <- Activity.get_by_id(id), + {:ok, ap_id} <- get_ap_id(activity) do + conn + |> Phoenix.Controller.redirect(external: String.replace(template, "{uri}", ap_id)) + else + _e -> + render(conn, "status_interact.html", %{ + status_id: id, + avatar: nil, + error: + Pleroma.Web.Gettext.dpgettext( + "static_pages", + "status interact error message - unknown error", + "Something went wrong." + ) + }) + end + end + + def remote_interaction( + %{private: %{open_api_spex: %{body_params: %{ap_id: ap_id, profile: profile}}}} = conn, + _params + ) do + with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile) do + conn + |> json(%{url: String.replace(template, "{uri}", ap_id)}) + else + _e -> json(conn, %{error: "Couldn't find user"}) + end + end + + defp get_ap_id(activity) do + object = Pleroma.Object.normalize(activity, fetch: false) + + case object do + %{data: %{"id" => ap_id}} -> {:ok, ap_id} + _ -> {:no_ap_id, nil} + end + end end diff --git a/lib/pleroma/web/twitter_api/views/remote_follow_view.ex b/lib/pleroma/web/remote_interaction/remote_interaction_view.ex similarity index 75% rename from lib/pleroma/web/twitter_api/views/remote_follow_view.ex rename to lib/pleroma/web/remote_interaction/remote_interaction_view.ex index 8902261b0..6e7f40749 100644 --- a/lib/pleroma/web/twitter_api/views/remote_follow_view.ex +++ b/lib/pleroma/web/remote_interaction/remote_interaction_view.ex @@ -2,9 +2,11 @@ # Copyright © 2017-2022 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.TwitterAPI.RemoteFollowView do +defmodule Pleroma.Web.RemoteInteraction.RemoteInteractionView do use Pleroma.Web, :view + import Phoenix.HTML import Phoenix.HTML.Form + import Phoenix.HTML.Link alias Pleroma.Web.Gettext def avatar_url(user) do diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 20ac1c67b..22e82568a 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -226,23 +226,27 @@ defmodule Pleroma.Web.Router do plug(Pleroma.Web.Plugs.StaticFEPlug) end - scope "/api/v1/pleroma", Pleroma.Web.TwitterAPI do + scope "/api/v1/pleroma", Pleroma.Web.OAuth do pipe_through(:pleroma_api) get("/password_reset/:token", PasswordController, :reset, as: :reset_password) post("/password_reset", PasswordController, :do_reset, as: :reset_password) - get("/emoji", UtilController, :emoji) - get("/captcha", UtilController, :captcha) - get("/healthcheck", UtilController, :healthcheck) - post("/remote_interaction", UtilController, :remote_interaction) end scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do - pipe_through(:pleroma_api) + get("/emoji", UtilController, :emoji) + get("/captcha", UtilController, :captcha) + get("/healthcheck", UtilController, :healthcheck) get("/federation_status", InstancesController, :show) end + scope "/api/v1/pleroma", Pleroma.Web.RemoteInteraction do + pipe_through(:pleroma_api) + + post("/remote_interaction", RemoteInteractionController, :remote_interaction) + end + scope "/api/v1/pleroma", Pleroma.Web do pipe_through(:pleroma_api) post("/uploader_callback/:upload_path", UploaderController, :callback) @@ -484,18 +488,18 @@ defmodule Pleroma.Web.Router do end end - scope "/", Pleroma.Web.TwitterAPI do + scope "/", Pleroma.Web.RemoteInteraction do pipe_through(:pleroma_html) - post("/main/ostatus", UtilController, :remote_subscribe) - get("/main/ostatus", UtilController, :show_subscribe_form) - get("/ostatus_subscribe", RemoteFollowController, :follow) - post("/ostatus_subscribe", RemoteFollowController, :do_follow) + post("/main/ostatus", RemoteInteractionController, :remote_subscribe) + get("/main/ostatus", RemoteInteractionController, :show_subscribe_form) + get("/ostatus_subscribe", RemoteInteractionController, :follow) + post("/ostatus_subscribe", RemoteInteractionController, :do_follow) - get("/authorize_interaction", RemoteFollowController, :authorize_interaction) + get("/authorize_interaction", RemoteInteractionController, :authorize_interaction) end - scope "/api/pleroma", Pleroma.Web.TwitterAPI do + scope "/api/pleroma", Pleroma.Web.PleromaAPI do pipe_through(:authenticated_api) post("/change_email", UtilController, :change_email) @@ -853,7 +857,7 @@ defmodule Pleroma.Web.Router do scope "/api", Pleroma.Web do pipe_through(:config) - get("/pleroma/frontend_configurations", TwitterAPI.UtilController, :frontend_configurations) + get("/pleroma/frontend_configurations", PleromaAPI.UtilController, :frontend_configurations) end scope "/api", Pleroma.Web do @@ -861,7 +865,7 @@ defmodule Pleroma.Web.Router do get( "/account/confirm_email/:user_id/:token", - TwitterAPI.Controller, + OAuth.TokenController, :confirm_email, as: :confirm_email ) @@ -873,11 +877,11 @@ defmodule Pleroma.Web.Router do get("/openapi", OpenApiSpex.Plug.RenderSpec, []) end - scope "/api", Pleroma.Web, as: :authenticated_twitter_api do + scope "/api", Pleroma.Web, as: :authenticated_pleroma_api do pipe_through(:authenticated_api) - get("/oauth_tokens", TwitterAPI.Controller, :oauth_tokens) - delete("/oauth_tokens/:id", TwitterAPI.Controller, :revoke_token) + get("/oauth_tokens", OAuth.TokenController, :oauth_tokens) + delete("/oauth_tokens/:id", OAuth.TokenController, :revoke_token) end scope "/", Pleroma.Web do @@ -1026,7 +1030,7 @@ defmodule Pleroma.Web.Router do scope "/", Pleroma.Web do pipe_through(:pleroma_html) - post("/auth/password", TwitterAPI.PasswordController, :request) + post("/auth/password", OAuth.PasswordController, :request) end scope "/proxy/", Pleroma.Web do diff --git a/lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex b/lib/pleroma/web/templates/o_auth/password/invalid_token.html.eex similarity index 100% rename from lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex rename to lib/pleroma/web/templates/o_auth/password/invalid_token.html.eex diff --git a/lib/pleroma/web/templates/twitter_api/password/reset.html.eex b/lib/pleroma/web/templates/o_auth/password/reset.html.eex similarity index 100% rename from lib/pleroma/web/templates/twitter_api/password/reset.html.eex rename to lib/pleroma/web/templates/o_auth/password/reset.html.eex diff --git a/lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex b/lib/pleroma/web/templates/o_auth/password/reset_failed.html.eex similarity index 100% rename from lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex rename to lib/pleroma/web/templates/o_auth/password/reset_failed.html.eex diff --git a/lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex b/lib/pleroma/web/templates/o_auth/password/reset_success.html.eex similarity index 100% rename from lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex rename to lib/pleroma/web/templates/o_auth/password/reset_success.html.eex diff --git a/lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex b/lib/pleroma/web/templates/remote_interaction/remote_interaction/follow.html.eex similarity index 83% rename from lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex rename to lib/pleroma/web/templates/remote_interaction/remote_interaction/follow.html.eex index e2d251fac..00cc7d383 100644 --- a/lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex +++ b/lib/pleroma/web/templates/remote_interaction/remote_interaction/follow.html.eex @@ -4,7 +4,7 @@

<%= Gettext.dpgettext("static_pages", "remote follow header", "Remote follow") %>

<%= @followee.nickname %>

- <%= form_for @conn, Routes.remote_follow_path(@conn, :do_follow), [as: "user"], fn f -> %> + <%= form_for @conn, Routes.remote_interaction_path(@conn, :do_follow), [as: "user"], fn f -> %> <%= hidden_input f, :id, value: @followee.id %> <%= submit Gettext.dpgettext("static_pages", "remote follow authorization button", "Authorize") %> <% end %> diff --git a/lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex b/lib/pleroma/web/templates/remote_interaction/remote_interaction/follow_login.html.eex similarity index 88% rename from lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex rename to lib/pleroma/web/templates/remote_interaction/remote_interaction/follow_login.html.eex index 26340a906..e5fe720e8 100644 --- a/lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex +++ b/lib/pleroma/web/templates/remote_interaction/remote_interaction/follow_login.html.eex @@ -4,7 +4,7 @@

<%= Gettext.dpgettext("static_pages", "remote follow header, need login", "Log in to follow") %>

<%= @followee.nickname %>

-<%= form_for @conn, Routes.remote_follow_path(@conn, :do_follow), [as: "authorization"], fn f -> %> +<%= form_for @conn, Routes.remote_interaction_path(@conn, :do_follow), [as: "authorization"], fn f -> %> <%= text_input f, :name, placeholder: Gettext.dpgettext("static_pages", "placeholder text for username entry", "Username"), required: true, autocomplete: "username" %>
<%= password_input f, :password, placeholder: Gettext.dpgettext("static_pages", "placeholder text for password entry", "Password"), required: true, autocomplete: "password" %> diff --git a/lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex b/lib/pleroma/web/templates/remote_interaction/remote_interaction/follow_mfa.html.eex similarity index 86% rename from lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex rename to lib/pleroma/web/templates/remote_interaction/remote_interaction/follow_mfa.html.eex index 638212c1e..e5f26f104 100644 --- a/lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex +++ b/lib/pleroma/web/templates/remote_interaction/remote_interaction/follow_mfa.html.eex @@ -4,7 +4,7 @@

<%= Gettext.dpgettext("static_pages", "remote follow mfa header", "Two-factor authentication") %>

<%= @followee.nickname %>

-<%= form_for @conn, Routes.remote_follow_path(@conn, :do_follow), [as: "mfa"], fn f -> %> +<%= form_for @conn, Routes.remote_interaction_path(@conn, :do_follow), [as: "mfa"], fn f -> %> <%= text_input f, :code, placeholder: Gettext.dpgettext("static_pages", "placeholder text for auth code entry", "Authentication code"), required: true %>
<%= hidden_input f, :id, value: @followee.id %> diff --git a/lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex b/lib/pleroma/web/templates/remote_interaction/remote_interaction/followed.html.eex similarity index 100% rename from lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex rename to lib/pleroma/web/templates/remote_interaction/remote_interaction/followed.html.eex diff --git a/lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex b/lib/pleroma/web/templates/remote_interaction/remote_interaction/status_interact.html.eex similarity index 88% rename from lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex rename to lib/pleroma/web/templates/remote_interaction/remote_interaction/status_interact.html.eex index d77174967..b3d590186 100644 --- a/lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex +++ b/lib/pleroma/web/templates/remote_interaction/remote_interaction/status_interact.html.eex @@ -2,7 +2,7 @@

<%= Gettext.dpgettext("static_pages", "status interact error", "Error: %{error}", error: @error) %>

<% else %>

<%= raw Gettext.dpgettext("static_pages", "status interact header", "Interacting with %{nickname}'s %{status_link}", nickname: safe_to_string(html_escape(@nickname)), status_link: safe_to_string(link(Gettext.dpgettext("static_pages", "status interact header - status link text", "status"), to: @status_link))) %>

- <%= form_for @conn, Routes.util_path(@conn, :remote_subscribe), [as: "status"], fn f -> %> + <%= form_for @conn, Routes.remote_interaction_path(@conn, :remote_subscribe), [as: "status"], fn f -> %> <%= hidden_input f, :status_id, value: @status_id %> <%= text_input f, :profile, placeholder: Gettext.dpgettext("static_pages", "placeholder text for account id", "Your account ID, e.g. lain@quitter.se") %> <%= submit Gettext.dpgettext("static_pages", "status interact authorization button", "Interact") %> diff --git a/lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex b/lib/pleroma/web/templates/remote_interaction/remote_interaction/subscribe.html.eex similarity index 85% rename from lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex rename to lib/pleroma/web/templates/remote_interaction/remote_interaction/subscribe.html.eex index 848660f26..7e1dd723c 100644 --- a/lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex +++ b/lib/pleroma/web/templates/remote_interaction/remote_interaction/subscribe.html.eex @@ -2,7 +2,7 @@

<%= Gettext.dpgettext("static_pages", "remote follow error", "Error: %{error}", error: @error) %>

<% else %>

<%= Gettext.dpgettext("static_pages", "remote follow header", "Remotely follow %{nickname}", nickname: @nickname) %>

- <%= form_for @conn, Routes.util_path(@conn, :remote_subscribe), [as: "user"], fn f -> %> + <%= form_for @conn, Routes.remote_interaction_path(@conn, :remote_subscribe), [as: "user"], fn f -> %> <%= hidden_input f, :nickname, value: @nickname %> <%= text_input f, :profile, placeholder: Gettext.dpgettext("static_pages", "placeholder text for account id", "Your account ID, e.g. lain@quitter.se") %> <%= submit Gettext.dpgettext("static_pages", "remote follow authorization button for following with a remote account", "Follow") %> diff --git a/lib/pleroma/web/twitter_api/views/util_view.ex b/lib/pleroma/web/twitter_api/views/util_view.ex deleted file mode 100644 index 31b7c0c0c..000000000 --- a/lib/pleroma/web/twitter_api/views/util_view.ex +++ /dev/null @@ -1,31 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2022 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.TwitterAPI.UtilView do - use Pleroma.Web, :view - import Phoenix.HTML - import Phoenix.HTML.Form - import Phoenix.HTML.Link - alias Pleroma.Config - alias Pleroma.Web.Endpoint - alias Pleroma.Web.Gettext - - def status_net_config(instance) do - """ - - - #{Keyword.get(instance, :name)} - #{Endpoint.url()} - #{Keyword.get(instance, :limit)} - #{!Keyword.get(instance, :registrations_open)} - - - """ - end - - def render("frontend_configurations.json", _) do - Config.get(:frontend_configurations, %{}) - |> Enum.into(%{}) - end -end diff --git a/test/pleroma/web/twitter_api/password_controller_test.exs b/test/pleroma/web/o_auth/password_controller_test.exs similarity index 99% rename from test/pleroma/web/twitter_api/password_controller_test.exs rename to test/pleroma/web/o_auth/password_controller_test.exs index 26cca1345..bb61b24fd 100644 --- a/test/pleroma/web/twitter_api/password_controller_test.exs +++ b/test/pleroma/web/o_auth/password_controller_test.exs @@ -2,7 +2,7 @@ # Copyright © 2017-2022 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.TwitterAPI.PasswordControllerTest do +defmodule Pleroma.Web.OAuth.PasswordControllerTest do use Pleroma.Web.ConnCase alias Pleroma.Config diff --git a/test/pleroma/web/twitter_api/controller_test.exs b/test/pleroma/web/o_auth/token_controller_test.exs similarity index 97% rename from test/pleroma/web/twitter_api/controller_test.exs rename to test/pleroma/web/o_auth/token_controller_test.exs index 494be9ec7..5c64cb394 100644 --- a/test/pleroma/web/twitter_api/controller_test.exs +++ b/test/pleroma/web/o_auth/token_controller_test.exs @@ -2,7 +2,7 @@ # Copyright © 2017-2022 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.TwitterAPI.ControllerTest do +defmodule Pleroma.Web.OAuth.TokenControllerTest do use Pleroma.Web.ConnCase, async: true alias Pleroma.Repo diff --git a/test/pleroma/web/twitter_api/util_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/util_controller_test.exs similarity index 85% rename from test/pleroma/web/twitter_api/util_controller_test.exs rename to test/pleroma/web/pleroma_api/controllers/util_controller_test.exs index d06ae71aa..6e817df56 100644 --- a/test/pleroma/web/twitter_api/util_controller_test.exs +++ b/test/pleroma/web/pleroma_api/controllers/util_controller_test.exs @@ -2,7 +2,7 @@ # Copyright © 2017-2022 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do +defmodule Pleroma.Web.PleromaAPI.UtilControllerTest do use Pleroma.Web.ConnCase use Oban.Testing, repo: Pleroma.Repo @@ -182,153 +182,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do end end - describe "POST /main/ostatus - remote_subscribe/2" do - setup do: clear_config([:instance, :federating], true) - - test "renders subscribe form", %{conn: conn} do - user = insert(:user) - - response = - conn - |> post("/main/ostatus", %{"nickname" => user.nickname, "profile" => ""}) - |> response(:ok) - - refute response =~ "Could not find user" - assert response =~ "Remotely follow #{user.nickname}" - end - - test "renders subscribe form with error when user not found", %{conn: conn} do - response = - conn - |> post("/main/ostatus", %{"nickname" => "nickname", "profile" => ""}) - |> response(:ok) - - assert response =~ "Could not find user" - refute response =~ "Remotely follow" - end - - test "it redirect to webfinger url", %{conn: conn} do - user = insert(:user) - user2 = insert(:user, ap_id: "shp@social.heldscal.la") - - conn = - conn - |> post("/main/ostatus", %{ - "user" => %{"nickname" => user.nickname, "profile" => user2.ap_id} - }) - - assert redirected_to(conn) == - "https://social.heldscal.la/main/ostatussub?profile=#{user.ap_id}" - end - - test "it renders form with error when user not found", %{conn: conn} do - user2 = insert(:user, ap_id: "shp@social.heldscal.la") - - response = - conn - |> post("/main/ostatus", %{"user" => %{"nickname" => "jimm", "profile" => user2.ap_id}}) - |> response(:ok) - - assert response =~ "Something went wrong." - end - end - - describe "POST /main/ostatus - remote_subscribe/2 - with statuses" do - setup do: clear_config([:instance, :federating], true) - - test "renders subscribe form", %{conn: conn} do - user = insert(:user) - status = insert(:note_activity, %{user: user}) - status_id = status.id - - assert is_binary(status_id) - - response = - conn - |> post("/main/ostatus", %{"status_id" => status_id, "profile" => ""}) - |> response(:ok) - - refute response =~ "Could not find status" - assert response =~ "Interacting with" - end - - test "renders subscribe form with error when status not found", %{conn: conn} do - response = - conn - |> post("/main/ostatus", %{"status_id" => "somerandomid", "profile" => ""}) - |> response(:ok) - - assert response =~ "Could not find status" - refute response =~ "Interacting with" - end - - test "it redirect to webfinger url", %{conn: conn} do - user = insert(:user) - status = insert(:note_activity, %{user: user}) - status_id = status.id - status_ap_id = status.data["object"] - - assert is_binary(status_id) - assert is_binary(status_ap_id) - - user2 = insert(:user, ap_id: "shp@social.heldscal.la") - - conn = - conn - |> post("/main/ostatus", %{ - "status" => %{"status_id" => status_id, "profile" => user2.ap_id} - }) - - assert redirected_to(conn) == - "https://social.heldscal.la/main/ostatussub?profile=#{status_ap_id}" - end - - test "it renders form with error when status not found", %{conn: conn} do - user2 = insert(:user, ap_id: "shp@social.heldscal.la") - - response = - conn - |> post("/main/ostatus", %{ - "status" => %{"status_id" => "somerandomid", "profile" => user2.ap_id} - }) - |> response(:ok) - - assert response =~ "Something went wrong." - end - end - - describe "GET /main/ostatus - show_subscribe_form/2" do - setup do: clear_config([:instance, :federating], true) - - test "it works with users", %{conn: conn} do - user = insert(:user) - - response = - conn - |> get("/main/ostatus", %{"nickname" => user.nickname}) - |> response(:ok) - - refute response =~ "Could not find user" - assert response =~ "Remotely follow #{user.nickname}" - end - - test "it works with statuses", %{conn: conn} do - user = insert(:user) - status = insert(:note_activity, %{user: user}) - status_id = status.id - - assert is_binary(status_id) - - response = - conn - |> get("/main/ostatus", %{"status_id" => status_id}) - |> response(:ok) - - refute response =~ "Could not find status" - assert response =~ "Interacting with" - end - end - test "it returns new captcha", %{conn: conn} do with_mock Pleroma.Captcha, new: fn -> "test_captcha" end do diff --git a/test/pleroma/web/twitter_api/twitter_api_test.exs b/test/pleroma/web/registration_test.exs similarity index 90% rename from test/pleroma/web/twitter_api/twitter_api_test.exs rename to test/pleroma/web/registration_test.exs index b3cd80146..896e1a600 100644 --- a/test/pleroma/web/twitter_api/twitter_api_test.exs +++ b/test/pleroma/web/registration_test.exs @@ -2,14 +2,14 @@ # Copyright © 2017-2022 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do +defmodule Pleroma.Web.RegistrationTest do use Pleroma.DataCase import Pleroma.Factory alias Pleroma.Repo alias Pleroma.Tests.ObanHelpers alias Pleroma.User alias Pleroma.UserInviteToken - alias Pleroma.Web.TwitterAPI.TwitterAPI + alias Pleroma.Web.Registration setup_all do Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) @@ -25,7 +25,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do :confirm => "bear" } - {:ok, user} = TwitterAPI.register_user(data) + {:ok, user} = Registration.register_user(data) assert user == User.get_cached_by_nickname("lain") end @@ -40,7 +40,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do :confirm => "bear" } - {:ok, user} = TwitterAPI.register_user(data) + {:ok, user} = Registration.register_user(data) assert user == User.get_cached_by_nickname("lain") end @@ -57,7 +57,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do :confirm => "bear" } - {:ok, user} = TwitterAPI.register_user(data) + {:ok, user} = Registration.register_user(data) ObanHelpers.perform_all() refute user.is_confirmed @@ -89,7 +89,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do :reason => "I love anime" } - {:ok, user} = TwitterAPI.register_user(data) + {:ok, user} = Registration.register_user(data) ObanHelpers.perform_all() refute user.is_approved @@ -125,7 +125,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do :confirm => "bear" } - {:ok, user1} = TwitterAPI.register_user(data1) + {:ok, user1} = Registration.register_user(data1) data2 = %{ :username => "lain", @@ -136,7 +136,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do :confirm => "bear" } - {:ok, user2} = TwitterAPI.register_user(data2) + {:ok, user2} = Registration.register_user(data2) expected_text = ~s(@john test) @@ -160,7 +160,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do :token => invite.token } - {:ok, user} = TwitterAPI.register_user(data) + {:ok, user} = Registration.register_user(data) assert user == User.get_cached_by_nickname("vinny") @@ -179,7 +179,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do :token => "DudeLetMeInImAFairy" } - {:error, msg} = TwitterAPI.register_user(data) + {:error, msg} = Registration.register_user(data) assert msg == "Invalid token" refute User.get_cached_by_nickname("GrimReaper") @@ -199,7 +199,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do :token => invite.token } - {:error, msg} = TwitterAPI.register_user(data) + {:error, msg} = Registration.register_user(data) assert msg == "Expired token" refute User.get_cached_by_nickname("GrimReaper") @@ -221,7 +221,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do check_fn = fn invite -> data = Map.put(data, :token, invite.token) - {:ok, user} = TwitterAPI.register_user(data) + {:ok, user} = Registration.register_user(data) assert user == User.get_cached_by_nickname("vinny") end @@ -254,7 +254,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do data = Map.put(data, "token", invite.token) - {:error, msg} = TwitterAPI.register_user(data) + {:error, msg} = Registration.register_user(data) assert msg == "Expired token" refute User.get_cached_by_nickname("vinny") @@ -282,7 +282,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do :token => invite.token } - {:ok, user} = TwitterAPI.register_user(data) + {:ok, user} = Registration.register_user(data) assert user == User.get_cached_by_nickname("vinny") invite = Repo.get_by(UserInviteToken, token: invite.token) @@ -298,7 +298,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do :token => invite.token } - {:error, msg} = TwitterAPI.register_user(data) + {:error, msg} = Registration.register_user(data) assert msg == "Expired token" refute User.get_cached_by_nickname("GrimReaper") @@ -321,7 +321,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do :token => invite.token } - {:ok, user} = TwitterAPI.register_user(data) + {:ok, user} = Registration.register_user(data) assert user == User.get_cached_by_nickname("vinny") invite = Repo.get_by(UserInviteToken, token: invite.token) @@ -343,7 +343,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do :token => invite.token } - {:ok, user} = TwitterAPI.register_user(data) + {:ok, user} = Registration.register_user(data) assert user == User.get_cached_by_nickname("vinny") invite = Repo.get_by(UserInviteToken, token: invite.token) @@ -359,7 +359,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do :token => invite.token } - {:error, msg} = TwitterAPI.register_user(data) + {:error, msg} = Registration.register_user(data) assert msg == "Expired token" refute User.get_cached_by_nickname("GrimReaper") @@ -379,7 +379,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do :token => invite.token } - {:error, msg} = TwitterAPI.register_user(data) + {:error, msg} = Registration.register_user(data) assert msg == "Expired token" refute User.get_cached_by_nickname("GrimReaper") @@ -401,7 +401,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do :token => invite.token } - {:error, msg} = TwitterAPI.register_user(data) + {:error, msg} = Registration.register_user(data) assert msg == "Expired token" refute User.get_cached_by_nickname("GrimReaper") @@ -416,7 +416,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do :bio => "close the world." } - {:error, error} = TwitterAPI.register_user(data) + {:error, error} = Registration.register_user(data) assert is_binary(error) refute User.get_cached_by_nickname("lain") diff --git a/test/pleroma/web/twitter_api/remote_follow_controller_test.exs b/test/pleroma/web/remote_interaction/remote_interaction_controller_test.exs similarity index 68% rename from test/pleroma/web/twitter_api/remote_follow_controller_test.exs rename to test/pleroma/web/remote_interaction/remote_interaction_controller_test.exs index f762b1356..116609013 100644 --- a/test/pleroma/web/twitter_api/remote_follow_controller_test.exs +++ b/test/pleroma/web/remote_interaction/remote_interaction_controller_test.exs @@ -2,7 +2,7 @@ # Copyright © 2017-2022 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do +defmodule Pleroma.Web.RemoteInteraction.RemoteInteractionControllerTest do use Pleroma.Web.ConnCase alias Pleroma.MFA @@ -16,6 +16,11 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do import Mox import Pleroma.Factory + setup do + Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + setup_all do: clear_config([:instance, :federating], true) setup do: clear_config([:user, :deny_follow_blocked]) @@ -49,7 +54,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do assert conn |> get( - remote_follow_path(conn, :follow, %{ + remote_interaction_path(conn, :follow, %{ acct: "https://mastodon.social/users/emelie/statuses/101849165031453009" }) ) @@ -78,7 +83,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do response = conn - |> get(remote_follow_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"})) + |> get(remote_interaction_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"})) |> html_response(200) assert response =~ "Log in to follow" @@ -109,7 +114,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do response = conn |> assign(:user, user) - |> get(remote_follow_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"})) + |> get(remote_interaction_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"})) |> html_response(200) assert response =~ "Remote follow" @@ -130,7 +135,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do conn |> assign(:user, user) |> get( - remote_follow_path(conn, :follow, %{ + remote_interaction_path(conn, :follow, %{ acct: "https://mastodon.social/users/not_found" }) ) @@ -152,7 +157,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do conn |> assign(:user, user) |> assign(:token, read_token) - |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) + |> post(remote_interaction_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) |> response(200) assert response =~ "Error following account" @@ -167,7 +172,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do conn |> assign(:user, user) |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:follows"])) - |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) + |> post(remote_interaction_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) assert redirected_to(conn) == "/users/#{user2.id}" end @@ -179,7 +184,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do response = conn |> assign(:user, user) - |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) + |> post(remote_interaction_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) |> response(200) assert response =~ "Error following account" @@ -195,7 +200,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do response = conn |> assign(:user, user) - |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) + |> post(remote_interaction_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) |> response(200) assert response =~ "Error following account" @@ -207,7 +212,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do response = conn |> assign(:user, user) - |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => "jimm"}}) + |> post(remote_interaction_path(conn, :do_follow), %{"user" => %{"id" => "jimm"}}) |> response(200) assert response =~ "Error following account" @@ -222,7 +227,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do conn |> assign(:user, refresh_record(user)) |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:follows"])) - |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) + |> post(remote_interaction_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) assert redirected_to(conn) == "/users/#{user2.id}" end @@ -244,7 +249,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do response = conn - |> post(remote_follow_path(conn, :do_follow), %{ + |> post(remote_interaction_path(conn, :do_follow), %{ "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id} }) |> response(200) @@ -272,7 +277,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do response = conn - |> post(remote_follow_path(conn, :do_follow), %{ + |> post(remote_interaction_path(conn, :do_follow), %{ "authorization" => %{"name" => user.nickname, "password" => "test1", "id" => user2.id} }) |> response(200) @@ -300,7 +305,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do conn = conn |> post( - remote_follow_path(conn, :do_follow), + remote_interaction_path(conn, :do_follow), %{ "mfa" => %{"code" => otp_token, "token" => token, "id" => user2.id} } @@ -329,7 +334,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do response = conn |> post( - remote_follow_path(conn, :do_follow), + remote_interaction_path(conn, :do_follow), %{ "mfa" => %{"code" => otp_token, "token" => token, "id" => user2.id} } @@ -348,7 +353,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do conn = conn - |> post(remote_follow_path(conn, :do_follow), %{ + |> post(remote_interaction_path(conn, :do_follow), %{ "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id} }) @@ -361,7 +366,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do response = conn - |> post(remote_follow_path(conn, :do_follow), %{ + |> post(remote_interaction_path(conn, :do_follow), %{ "authorization" => %{"name" => user.nickname, "password" => "test", "id" => "jimm"} }) |> response(200) @@ -374,7 +379,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do response = conn - |> post(remote_follow_path(conn, :do_follow), %{ + |> post(remote_interaction_path(conn, :do_follow), %{ "authorization" => %{"name" => "jimm", "password" => "test", "id" => user.id} }) |> response(200) @@ -388,7 +393,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do response = conn - |> post(remote_follow_path(conn, :do_follow), %{ + |> post(remote_interaction_path(conn, :do_follow), %{ "authorization" => %{"name" => user.nickname, "password" => "42", "id" => user2.id} }) |> response(200) @@ -404,7 +409,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do response = conn - |> post(remote_follow_path(conn, :do_follow), %{ + |> post(remote_interaction_path(conn, :do_follow), %{ "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id} }) |> response(200) @@ -423,7 +428,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do avatar: %{"url" => [%{"href" => "https://remote.org/avatar.png"}]} }) - avatar_url = Pleroma.Web.TwitterAPI.RemoteFollowView.avatar_url(user) + avatar_url = Pleroma.Web.PleromaAPI.RemoteFollowView.avatar_url(user) assert avatar_url == "https://remote.org/avatar.png" end @@ -440,7 +445,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do avatar: %{"url" => [%{"href" => "https://remote.org/avatar.png"}]} }) - avatar_url = Pleroma.Web.TwitterAPI.RemoteFollowView.avatar_url(user) + avatar_url = Pleroma.Web.PleromaAPI.RemoteFollowView.avatar_url(user) url = Pleroma.Web.Endpoint.url() assert String.starts_with?(avatar_url, url) @@ -455,7 +460,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do avatar: %{"url" => [%{"href" => "#{Pleroma.Web.Endpoint.url()}/localuser/avatar.png"}]} }) - avatar_url = Pleroma.Web.TwitterAPI.RemoteFollowView.avatar_url(user) + avatar_url = Pleroma.Web.PleromaAPI.RemoteFollowView.avatar_url(user) assert avatar_url == "#{Pleroma.Web.Endpoint.url()}/localuser/avatar.png" end @@ -485,13 +490,160 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do conn = conn |> get( - remote_follow_path(conn, :authorize_interaction, %{ + remote_interaction_path(conn, :authorize_interaction, %{ uri: "https://mastodon.social/users/emelie" }) ) assert redirected_to(conn) == - remote_follow_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"}) + remote_interaction_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"}) + end + end + + describe "POST /main/ostatus - remote_subscribe/2" do + setup do: clear_config([:instance, :federating], true) + + test "renders subscribe form", %{conn: conn} do + user = insert(:user) + + response = + conn + |> post("/main/ostatus", %{"nickname" => user.nickname, "profile" => ""}) + |> response(:ok) + + refute response =~ "Could not find user" + assert response =~ "Remotely follow #{user.nickname}" + end + + test "renders subscribe form with error when user not found", %{conn: conn} do + response = + conn + |> post("/main/ostatus", %{"nickname" => "nickname", "profile" => ""}) + |> response(:ok) + + assert response =~ "Could not find user" + refute response =~ "Remotely follow" + end + + test "it redirect to webfinger url", %{conn: conn} do + user = insert(:user) + user2 = insert(:user, ap_id: "shp@social.heldscal.la") + + conn = + conn + |> post("/main/ostatus", %{ + "user" => %{"nickname" => user.nickname, "profile" => user2.ap_id} + }) + + assert redirected_to(conn) == + "https://social.heldscal.la/main/ostatussub?profile=#{user.ap_id}" + end + + test "it renders form with error when user not found", %{conn: conn} do + user2 = insert(:user, ap_id: "shp@social.heldscal.la") + + response = + conn + |> post("/main/ostatus", %{"user" => %{"nickname" => "jimm", "profile" => user2.ap_id}}) + |> response(:ok) + + assert response =~ "Something went wrong." + end + end + + describe "POST /main/ostatus - remote_subscribe/2 - with statuses" do + setup do: clear_config([:instance, :federating], true) + + test "renders subscribe form", %{conn: conn} do + user = insert(:user) + status = insert(:note_activity, %{user: user}) + status_id = status.id + + assert is_binary(status_id) + + response = + conn + |> post("/main/ostatus", %{"status_id" => status_id, "profile" => ""}) + |> response(:ok) + + refute response =~ "Could not find status" + assert response =~ "Interacting with" + end + + test "renders subscribe form with error when status not found", %{conn: conn} do + response = + conn + |> post("/main/ostatus", %{"status_id" => "somerandomid", "profile" => ""}) + |> response(:ok) + + assert response =~ "Could not find status" + refute response =~ "Interacting with" + end + + test "it redirect to webfinger url", %{conn: conn} do + user = insert(:user) + status = insert(:note_activity, %{user: user}) + status_id = status.id + status_ap_id = status.data["object"] + + assert is_binary(status_id) + assert is_binary(status_ap_id) + + user2 = insert(:user, ap_id: "shp@social.heldscal.la") + + conn = + conn + |> post("/main/ostatus", %{ + "status" => %{"status_id" => status_id, "profile" => user2.ap_id} + }) + + assert redirected_to(conn) == + "https://social.heldscal.la/main/ostatussub?profile=#{status_ap_id}" + end + + test "it renders form with error when status not found", %{conn: conn} do + user2 = insert(:user, ap_id: "shp@social.heldscal.la") + + response = + conn + |> post("/main/ostatus", %{ + "status" => %{"status_id" => "somerandomid", "profile" => user2.ap_id} + }) + |> response(:ok) + + assert response =~ "Something went wrong." + end + end + + describe "GET /main/ostatus - show_subscribe_form/2" do + setup do: clear_config([:instance, :federating], true) + + test "it works with users", %{conn: conn} do + user = insert(:user) + + response = + conn + |> get("/main/ostatus", %{"nickname" => user.nickname}) + |> response(:ok) + + refute response =~ "Could not find user" + assert response =~ "Remotely follow #{user.nickname}" + end + + test "it works with statuses", %{conn: conn} do + user = insert(:user) + status = insert(:note_activity, %{user: user}) + status_id = status.id + + assert is_binary(status_id) + + response = + conn + |> get("/main/ostatus", %{"status_id" => status_id}) + |> response(:ok) + + refute response =~ "Could not find status" + assert response =~ "Interacting with" end end end From 37041aae6020af547a03126e4e916d04de0c27d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 2 Mar 2026 22:50:49 +0100 Subject: [PATCH 048/127] update mix.exs deps versions to match mix.lock so they don't look that scary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/mix-exs-update.skip | 0 mix.exs | 86 ++++++++++++++++----------------- 2 files changed, 43 insertions(+), 43 deletions(-) create mode 100644 changelog.d/mix-exs-update.skip diff --git a/changelog.d/mix-exs-update.skip b/changelog.d/mix-exs-update.skip new file mode 100644 index 000000000..e69de29bb diff --git a/mix.exs b/mix.exs index 0532bffd7..dc9fa31f5 100644 --- a/mix.exs +++ b/mix.exs @@ -114,7 +114,7 @@ defmodule Pleroma.Mixfile do if Version.match?(System.version(), "<1.15.0-rc.0") do [] else - [{:logger_backends, "~> 1.0"}] + [{:logger_backends, "~> 1.0.0"}] end end @@ -125,61 +125,61 @@ defmodule Pleroma.Mixfile do [ {:phoenix, git: "https://github.com/feld/phoenix", branch: "v1.7.14-websocket-headers", override: true}, - {:phoenix_ecto, "~> 4.4"}, - {:ecto_sql, "~> 3.10"}, + {:phoenix_ecto, "~> 4.6"}, + {:ecto_sql, "~> 3.13"}, {:ecto_enum, "~> 1.4"}, - {:postgrex, ">= 0.20.0"}, + {:postgrex, ">= 0.21.1"}, {:phoenix_html, "~> 3.3"}, - {:phoenix_live_view, "~> 1.1.0"}, - {:phoenix_live_dashboard, "~> 0.8.0"}, + {:phoenix_live_view, "~> 1.1.19"}, + {:phoenix_live_dashboard, "~> 0.8.7"}, {:telemetry_metrics, "~> 0.6"}, - {:telemetry_poller, "~> 1.0"}, - {:tzdata, "~> 1.0.3"}, + {:telemetry_poller, "~> 1.3"}, + {:tzdata, "~> 1.0.5"}, {:plug_cowboy, "~> 2.7"}, - {:oban, "~> 2.19.0"}, + {:oban, "~> 2.19.4"}, {:oban_plugins_lazarus, git: "https://git.pleroma.social/pleroma/elixir-libraries/oban_plugins_lazarus.git", ref: "e49fc355baaf0e435208bf5f534d31e26e897711"}, {:oban_web, "~> 2.11"}, - {:gettext, "~> 0.20"}, - {:bcrypt_elixir, "~> 2.2"}, + {:gettext, "~> 0.24"}, + {:bcrypt_elixir, "~> 2.3"}, {:trailing_format_plug, "~> 0.0.7"}, - {:fast_sanitize, "~> 0.2.0"}, + {:fast_sanitize, "~> 0.2.3"}, {:html_entities, "~> 0.5", override: true}, {:calendar, "~> 1.0"}, - {:cachex, "~> 3.2"}, - {:tesla, "~> 1.11"}, + {:cachex, "~> 3.6"}, + {:tesla, "~> 1.15"}, {:castore, "~> 1.0"}, {:cowlib, "~> 2.15"}, {:gun, "~> 2.2"}, - {:finch, "~> 0.15"}, - {:jason, "~> 1.2"}, - {:mogrify, "~> 0.9.0", override: true}, - {:ex_aws, "~> 2.1.6"}, - {:ex_aws_s3, "~> 2.0"}, - {:sweet_xml, "~> 0.7.2"}, + {:finch, "~> 0.20"}, + {:jason, "~> 1.4"}, + {:mogrify, "~> 0.9.3", override: true}, + {:ex_aws, "~> 2.1.9"}, + {:ex_aws_s3, "~> 2.5"}, + {:sweet_xml, "~> 0.7.5"}, {:earmark, "1.4.46"}, {:bbcode_pleroma, "~> 0.2.0"}, {:cors_plug, "~> 2.0"}, {:web_push_encryption, "~> 0.3.1"}, - {:swoosh, "~> 1.16.9"}, - {:phoenix_swoosh, "~> 1.1"}, - {:gen_smtp, "~> 0.13"}, - {:mua, "~> 0.2.0"}, - {:mail, "~> 0.3.0"}, - {:ex_syslogger, "~> 1.4"}, - {:floki, "~> 0.35"}, - {:timex, "~> 3.6"}, - {:ueberauth, "~> 0.4"}, + {:swoosh, "~> 1.16.12"}, + {:phoenix_swoosh, "~> 1.2"}, + {:gen_smtp, "~> 0.15"}, + {:mua, "~> 0.2.4"}, + {:mail, "~> 0.3.1"}, + {:ex_syslogger, "~> 1.5"}, + {:floki, "~> 0.38"}, + {:timex, "~> 3.7"}, + {:ueberauth, "~> 0.10"}, {:linkify, "~> 0.5.3"}, {:http_signatures, "~> 0.1.2"}, {:telemetry, "~> 1.0.0", override: true}, {:poolboy, "~> 1.5"}, {:prom_ex, "~> 1.9"}, {:recon, "~> 2.5"}, - {:joken, "~> 2.0"}, + {:joken, "~> 2.6"}, {:pot, "~> 1.0"}, - {:ex_const, "~> 0.2"}, + {:ex_const, "~> 0.3"}, {:plug_static_index_html, "~> 1.0.0"}, {:flake_id, "~> 0.1.0"}, {:concurrent_limiter, "~> 0.1.1"}, @@ -190,31 +190,31 @@ defmodule Pleroma.Mixfile do git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", ref: "e7b7cc34cc16b383461b966484c297e4ec9aeef6"}, {:restarter, path: "./restarter"}, - {:majic, "~> 1.0"}, - {:open_api_spex, "~> 3.16"}, + {:majic, "~> 1.1"}, + {:open_api_spex, "~> 3.22"}, {:ecto_psql_extras, "~> 0.8"}, {:vix, "~> 0.36"}, - {:elixir_make, "~> 0.7.7", override: true}, + {:elixir_make, "~> 0.7.8", override: true}, {:blurhash, "~> 0.1.0", hex: :rinpatch_blurhash}, {:exile, "~> 0.10.0"}, - {:bandit, "~> 1.5.2"}, - {:websock_adapter, "~> 0.5.6"}, + {:bandit, "~> 1.5.7"}, + {:websock_adapter, "~> 0.5.8"}, {:oban_live_dashboard, "~> 0.1.1"}, {:multipart, "~> 0.4.0", optional: true}, - {:argon2_elixir, "~> 4.0"}, + {:argon2_elixir, "~> 4.1"}, ## dev & test {:phoenix_live_reload, "~> 1.3.3", only: :dev}, - {:poison, "~> 3.0", only: :test}, - {:ex_doc, "~> 0.22", only: :dev, runtime: false}, - {:ex_machina, "~> 2.4", only: :test}, + {:poison, "~> 3.1", only: :test}, + {:ex_doc, "~> 0.38", only: :dev, runtime: false}, + {:ex_machina, "~> 2.8", only: :test}, {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, - {:mock, "~> 0.3.5", only: :test}, + {:mock, "~> 0.3.9", only: :test}, {:covertool, "~> 2.0", only: :test}, {:hackney, "~> 1.25.0", override: true}, - {:mox, "~> 1.0", only: :test}, + {:mox, "~> 1.2", only: :test}, {:websockex, "~> 0.4.3", only: :test}, - {:benchee, "~> 1.0", only: :benchmark}, + {:benchee, "~> 1.4", only: :benchmark}, {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false} ] ++ oauth_deps() ++ logger_deps() end From 2086561fbd0956422744831ac9db2ee26b8f46d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Tue, 17 Feb 2026 20:02:21 +0100 Subject: [PATCH 049/127] Various bookmark folders-related improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/bookmark-folders.ignore | 1 + .../API/differences_in_mastoapi_responses.md | 2 +- lib/pleroma/bookmark.ex | 11 ++------- lib/pleroma/bookmark_folder.ex | 2 +- .../web/api_spec/schemas/bookmark_folder.ex | 10 ++++++-- .../controllers/status_controller.ex | 2 -- .../controllers/bookmark_folder_controller.ex | 2 -- .../pleroma_api/views/bookmark_folder_view.ex | 23 ++++++++----------- 8 files changed, 23 insertions(+), 30 deletions(-) create mode 100644 changelog.d/bookmark-folders.ignore diff --git a/changelog.d/bookmark-folders.ignore b/changelog.d/bookmark-folders.ignore new file mode 100644 index 000000000..8705ac00b --- /dev/null +++ b/changelog.d/bookmark-folders.ignore @@ -0,0 +1 @@ +Various bookmark folders-related improvements diff --git a/docs/development/API/differences_in_mastoapi_responses.md b/docs/development/API/differences_in_mastoapi_responses.md index 052b2716b..0d6fc09c8 100644 --- a/docs/development/API/differences_in_mastoapi_responses.md +++ b/docs/development/API/differences_in_mastoapi_responses.md @@ -74,7 +74,7 @@ Pleroma does not process remote images and therefore cannot include fields such The `GET /api/v1/bookmarks` endpoint accepts optional parameter `folder_id` for bookmark folder ID. -The `POST /api/v1/statuses/:id/bookmark` endpoint accepts optional parameter `folder_id` for bookmark folder ID. +The `POST /api/v1/statuses/:id/bookmark` endpoint accepts optional parameter `folder_id` for bookmark folder ID. Bookmarking an already bookmarked post will update the folder association, or remove it if `folder_id` is omitted or `null`. ## Accounts diff --git a/lib/pleroma/bookmark.ex b/lib/pleroma/bookmark.ex index 1a2a63b82..1c78d495f 100644 --- a/lib/pleroma/bookmark.ex +++ b/lib/pleroma/bookmark.ex @@ -19,7 +19,7 @@ defmodule Pleroma.Bookmark do schema "bookmarks" do belongs_to(:user, User, type: FlakeId.Ecto.CompatType) belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType) - belongs_to(:folder, BookmarkFolder, type: FlakeId.Ecto.CompatType) + belongs_to(:folder, BookmarkFolder, type: FlakeId.Ecto.Type) timestamps() end @@ -38,7 +38,7 @@ defmodule Pleroma.Bookmark do |> validate_required([:user_id, :activity_id]) |> unique_constraint(:activity_id, name: :bookmarks_user_id_activity_id_index) |> Repo.insert( - on_conflict: [set: [folder_id: folder_id]], + on_conflict: [set: [folder_id: folder_id, updated_at: NaiveDateTime.utc_now()]], conflict_target: [:user_id, :activity_id] ) end @@ -76,11 +76,4 @@ defmodule Pleroma.Bookmark do |> Repo.one() |> Repo.delete() end - - def set_folder(bookmark, folder_id) do - bookmark - |> cast(%{folder_id: folder_id}, [:folder_id]) - |> validate_required([:folder_id]) - |> Repo.update() - end end diff --git a/lib/pleroma/bookmark_folder.ex b/lib/pleroma/bookmark_folder.ex index 14d37e197..65856bb29 100644 --- a/lib/pleroma/bookmark_folder.ex +++ b/lib/pleroma/bookmark_folder.ex @@ -14,7 +14,7 @@ defmodule Pleroma.BookmarkFolder do alias Pleroma.User @type t :: %__MODULE__{} - @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true} + @primary_key {:id, FlakeId.Ecto.Type, autogenerate: true} schema "bookmark_folders" do field(:name, :string) diff --git a/lib/pleroma/web/api_spec/schemas/bookmark_folder.ex b/lib/pleroma/web/api_spec/schemas/bookmark_folder.ex index e8b4f43b7..f5ce9e8a1 100644 --- a/lib/pleroma/web/api_spec/schemas/bookmark_folder.ex +++ b/lib/pleroma/web/api_spec/schemas/bookmark_folder.ex @@ -15,12 +15,18 @@ defmodule Pleroma.Web.ApiSpec.Schemas.BookmarkFolder do properties: %{ id: FlakeID, name: %Schema{type: :string, description: "Folder name"}, - emoji: %Schema{type: :string, description: "Folder emoji", nullable: true} + emoji: %Schema{type: :string, description: "Folder emoji", nullable: true}, + emoji_url: %Schema{ + type: :string, + description: "URL of the folder emoji if it's a custom emoji, null otherwise", + nullable: true + } }, example: %{ "id" => "9toJCu5YZW7O7gfvH6", "name" => "Read later", - "emoji" => nil + "emoji" => nil, + "emoji_url" => nil } }) end diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index db2b61b3c..26e38aac8 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -81,10 +81,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action in [:pin, :unpin]) - # Note: scope not present in Mastodon: read:bookmarks plug(OAuthScopesPlug, %{scopes: ["read:bookmarks"]} when action == :bookmarks) - # Note: scope not present in Mastodon: write:bookmarks plug( OAuthScopesPlug, %{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark] diff --git a/lib/pleroma/web/pleroma_api/controllers/bookmark_folder_controller.ex b/lib/pleroma/web/pleroma_api/controllers/bookmark_folder_controller.ex index 6d6e2e940..55f323a16 100644 --- a/lib/pleroma/web/pleroma_api/controllers/bookmark_folder_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/bookmark_folder_controller.ex @@ -10,10 +10,8 @@ defmodule Pleroma.Web.PleromaAPI.BookmarkFolderController do plug(Pleroma.Web.ApiSpec.CastAndValidate) - # Note: scope not present in Mastodon: read:bookmarks plug(OAuthScopesPlug, %{scopes: ["read:bookmarks"]} when action == :index) - # Note: scope not present in Mastodon: write:bookmarks plug( OAuthScopesPlug, %{scopes: ["write:bookmarks"]} when action in [:create, :update, :delete] diff --git a/lib/pleroma/web/pleroma_api/views/bookmark_folder_view.ex b/lib/pleroma/web/pleroma_api/views/bookmark_folder_view.ex index 29028781f..ddffa25d3 100644 --- a/lib/pleroma/web/pleroma_api/views/bookmark_folder_view.ex +++ b/lib/pleroma/web/pleroma_api/views/bookmark_folder_view.ex @@ -9,11 +9,13 @@ defmodule Pleroma.Web.PleromaAPI.BookmarkFolderView do alias Pleroma.Emoji def render("show.json", %{folder: %BookmarkFolder{} = folder}) do + {emoji, emoji_url} = get_emoji(folder.emoji) + %{ id: folder.id |> to_string(), name: folder.name, - emoji: folder.emoji, - emoji_url: get_emoji_url(folder.emoji) + emoji: emoji, + emoji_url: emoji_url } end @@ -21,20 +23,15 @@ defmodule Pleroma.Web.PleromaAPI.BookmarkFolderView do render_many(folders, __MODULE__, "show.json", Map.delete(opts, :folders)) end - defp get_emoji_url(nil) do - nil - end + defp get_emoji(nil), do: {nil, nil} - defp get_emoji_url(emoji) do + defp get_emoji(emoji) do if Emoji.unicode?(emoji) do - nil + {emoji, nil} else - emoji = Emoji.get(emoji) - - if emoji != nil do - Emoji.local_url(emoji.file) - else - nil + case Emoji.get(emoji) do + nil -> {nil, nil} + emoji_data -> {emoji, Emoji.local_url(emoji_data.file)} end end end From 1b182b07dcef7f915454043985bb28e030d8d401 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 2 Mar 2026 23:10:20 +0100 Subject: [PATCH 050/127] is this what i was meant to do? MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../remote_interaction_operation.ex | 68 ++++++++++++++++--- .../controllers/util_controller.ex | 2 +- .../remote_interaction_controller.ex | 2 +- 3 files changed, 60 insertions(+), 12 deletions(-) diff --git a/lib/pleroma/web/api_spec/operations/remote_interaction_operation.ex b/lib/pleroma/web/api_spec/operations/remote_interaction_operation.ex index a490bd491..b5bd9d72f 100644 --- a/lib/pleroma/web/api_spec/operations/remote_interaction_operation.ex +++ b/lib/pleroma/web/api_spec/operations/remote_interaction_operation.ex @@ -13,16 +13,6 @@ defmodule Pleroma.Web.ApiSpec.RemoteInteractionOperation do apply(__MODULE__, operation, []) end - def remote_subscribe_operation do - %Operation{ - tags: ["Remote interaction"], - summary: "Remote Subscribe", - operationId: "RemoteInteractionController.remote_subscribe", - parameters: [], - responses: %{200 => Operation.response("Web Page", "text/html", %Schema{type: :string})} - } - end - def remote_interaction_operation do %Operation{ tags: ["Remote interaction"], @@ -49,6 +39,64 @@ defmodule Pleroma.Web.ApiSpec.RemoteInteractionOperation do } end + def follow_operation do + %Operation{ + tags: ["Remote interaction"], + summary: "Display follow form", + operationId: "RemoteInteractionController.follow", + parameters: [], + responses: %{ + 200 => Operation.response("Web Page", "text/html", %Schema{type: :string}), + 302 => Operation.response("Redirect to the status page", nil, nil) + } + } + end + + def do_follow_operation do + %Operation{ + tags: ["Remote interaction"], + summary: "Perform follow activity", + operationId: "RemoteInteractionController.do_follow", + parameters: [], + responses: %{ + 200 => Operation.response("Web page", "text/html", %Schema{type: :string}), + 302 => Operation.response("Redirect to the account page", nil, nil) + } + } + end + + def authorize_interaction_operation do + %Operation{ + tags: ["Remote interaction"], + summary: "Authorize remote interaction", + operationId: "RemoteInteractionController.authorize_interaction", + parameters: [], + responses: %{ + 302 => Operation.response("Redirect to remote_interaction path", nil, nil) + } + } + end + + def show_subscribe_form_operation do + %Operation{ + tags: ["Remote interaction"], + summary: "Show remote subscribe form", + operationId: "RemoteInteractionController.show_subscribe_form", + parameters: [], + responses: %{200 => Operation.response("Web Page", "text/html", %Schema{type: :string})} + } + end + + def remote_subscribe_operation do + %Operation{ + tags: ["Remote interaction"], + summary: "Remote Subscribe", + operationId: "RemoteInteractionController.remote_subscribe", + parameters: [], + responses: %{200 => Operation.response("Web Page", "text/html", %Schema{type: :string})} + } + end + def show_subscribe_form_operation do %Operation{ tags: ["Remote interaction"], diff --git a/lib/pleroma/web/pleroma_api/controllers/util_controller.ex b/lib/pleroma/web/pleroma_api/controllers/util_controller.ex index 9528d5564..70b0b9cf6 100644 --- a/lib/pleroma/web/pleroma_api/controllers/util_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/util_controller.ex @@ -18,7 +18,7 @@ defmodule Pleroma.Web.PleromaAPI.UtilController do alias Pleroma.Web.CommonAPI alias Pleroma.Web.Plugs.OAuthScopesPlug - plug(Pleroma.Web.ApiSpec.CastAndValidate, [replace_params: false]) + plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false) plug( OAuthScopesPlug, diff --git a/lib/pleroma/web/remote_interaction/remote_interaction_controller.ex b/lib/pleroma/web/remote_interaction/remote_interaction_controller.ex index 8592fc990..90f15cdb6 100644 --- a/lib/pleroma/web/remote_interaction/remote_interaction_controller.ex +++ b/lib/pleroma/web/remote_interaction/remote_interaction_controller.ex @@ -30,7 +30,7 @@ defmodule Pleroma.Web.RemoteInteraction.RemoteInteractionController do plug( Pleroma.Web.Plugs.OAuthScopesPlug, %{fallback: :proceed_unauthenticated, scopes: ["follow", "write:follows"]} - when action in [:do_follow] + when action == :do_follow ) defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.RemoteInteractionOperation From b645643cfb5aa835876001af48e1d237a36f1f9d Mon Sep 17 00:00:00 2001 From: Oneric Date: Tue, 10 Jun 2025 18:33:59 +0000 Subject: [PATCH 051/127] Merge pull request 'Allow fine-grained announce visibilities' (#941) from Oneric/akkoma:announce-visibility into develop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/941 Reviewed-by: floatingghost Signed-off-by: nicole mikołajczyk --- lib/pleroma/web/activity_pub/builder.ex | 26 +++--- lib/pleroma/web/common_api.ex | 14 ++-- lib/pleroma/web/common_api/utils.ex | 84 ++++++++++++------- .../announce_validation_test.exs | 21 +++-- .../web/activity_pub/side_effects_test.exs | 8 +- 5 files changed, 91 insertions(+), 62 deletions(-) diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index 167c769a9..3b208ab77 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -332,21 +332,18 @@ defmodule Pleroma.Web.ActivityPub.Builder do @spec announce(User.t(), Object.t(), keyword()) :: {:ok, map(), keyword()} def announce(actor, object, options \\ []) do - public? = Keyword.get(options, :public, false) + visibility = Keyword.get(options, :visibility, "public") - to = - cond do - actor.ap_id == Relay.ap_id() -> - [actor.follower_address] - - public? and Visibility.local_public?(object) -> - [actor.follower_address, object.data["actor"], Utils.as_local_public()] - - public? -> - [actor.follower_address, object.data["actor"], Pleroma.Constants.as_public()] - - true -> - [actor.follower_address, object.data["actor"]] + {to, cc} = + if actor.ap_id == Relay.ap_id() do + {[actor.follower_address], []} + else + Pleroma.Web.CommonAPI.Utils.get_to_and_cc_for_visibility( + visibility, + actor.follower_address, + nil, + [object.data["actor"]] + ) end {:ok, @@ -355,6 +352,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do "actor" => actor.ap_id, "object" => object.data["id"], "to" => to, + "cc" => cc, "context" => object.data["context"], "type" => "Announce", "published" => Utils.make_date() diff --git a/lib/pleroma/web/common_api.ex b/lib/pleroma/web/common_api.ex index 04181ad8f..cb9d521b3 100644 --- a/lib/pleroma/web/common_api.ex +++ b/lib/pleroma/web/common_api.ex @@ -222,8 +222,8 @@ defmodule Pleroma.Web.CommonAPI do with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id), object = %Object{} <- Object.normalize(activity, fetch: false), {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)}, - public = public_announce?(object, params), - {:ok, announce, _} <- Builder.announce(user, object, public: public), + visibility = announce_visibility(object, params), + {:ok, announce, _} <- Builder.announce(user, object, visibility: visibility), {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do {:ok, activity} else @@ -407,13 +407,11 @@ defmodule Pleroma.Web.CommonAPI do end end - defp public_announce?(_, %{visibility: visibility}) - when visibility in ~w{public unlisted private direct}, - do: visibility in ~w(public unlisted) + def announce_visibility(_, %{visibility: visibility}) + when visibility in ~w{public unlisted private direct local}, + do: visibility - defp public_announce?(object, _) do - Visibility.public?(object) - end + def announce_visibility(object, _), do: Visibility.get_visibility(object) @spec get_visibility(map(), map() | nil, Participation.t() | nil) :: {String.t() | nil, String.t() | nil} diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 91bf9c502..32572a721 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -75,48 +75,70 @@ defmodule Pleroma.Web.CommonAPI.Utils do {Enum.map(participation.recipients, & &1.ap_id), []} end - def get_to_and_cc(%{visibility: visibility} = draft) when visibility in ["public", "local"] do - to = - case visibility do - "public" -> [Pleroma.Constants.as_public() | draft.mentions] - "local" -> [Utils.as_local_public() | draft.mentions] + def get_to_and_cc(%{visibility: visibility} = draft) do + # If the OP is a DM already, add the implicit actor + mentions = + if visibility == "direct" && draft.in_reply_to && Visibility.direct?(draft.in_reply_to) do + Enum.uniq([draft.in_reply_to.data["actor"] | draft.mentions]) + else + draft.mentions end - cc = [draft.user.follower_address] - - if draft.in_reply_to do - {Enum.uniq([draft.in_reply_to.data["actor"] | to]), cc} - else - {to, cc} - end + get_to_and_cc_for_visibility( + visibility, + draft.user.follower_address, + draft.in_reply_to && draft.in_reply_to.data["actor"], + mentions + ) end - def get_to_and_cc(%{visibility: "unlisted"} = draft) do - to = [draft.user.follower_address | draft.mentions] - cc = [Pleroma.Constants.as_public()] + def get_to_and_cc_for_visibility("public", follower_collection, parent_actor, mentions) do + scope_addr = Pleroma.Constants.as_public() - if draft.in_reply_to do - {Enum.uniq([draft.in_reply_to.data["actor"] | to]), cc} - else - {to, cc} - end + to = + if parent_actor, + do: Enum.uniq([parent_actor, scope_addr | mentions]), + else: [scope_addr | mentions] + + {to, [follower_collection]} end - def get_to_and_cc(%{visibility: "private"} = draft) do - {to, cc} = get_to_and_cc(struct(draft, visibility: "direct")) - {[draft.user.follower_address | to], cc} + def get_to_and_cc_for_visibility("local", follower_collection, parent_actor, mentions) do + recipients = + if parent_actor, + do: Enum.uniq([parent_actor | mentions]), + else: mentions + + to = [ + Utils.as_local_public() + | Enum.filter(recipients, fn addr -> + String.starts_with?(addr, Pleroma.Web.Endpoint.url() <> "/") + end) + ] + + {to, [follower_collection]} end - def get_to_and_cc(%{visibility: "direct"} = draft) do - # If the OP is a DM already, add the implicit actor. - if draft.in_reply_to && Visibility.direct?(draft.in_reply_to) do - {Enum.uniq([draft.in_reply_to.data["actor"] | draft.mentions]), []} - else - {draft.mentions, []} - end + def get_to_and_cc_for_visibility("unlisted", follower_collection, parent_actor, mentions) do + to = + if parent_actor, + do: Enum.uniq([parent_actor, follower_collection | mentions]), + else: [follower_collection | mentions] + + {to, [Pleroma.Constants.as_public()]} end - def get_to_and_cc(%{visibility: {:list, _}, mentions: mentions}), do: {mentions, []} + def get_to_and_cc_for_visibility("private", follower_collection, _, mentions) do + {[follower_collection | mentions], []} + end + + def get_to_and_cc_for_visibility("direct", _, _, mentions) do + {mentions, []} + end + + def get_to_and_cc_for_visibility({:list, _}, _, _, mentions) do + {mentions, []} + end def get_addressed_users(_, to) when is_list(to) do User.get_ap_ids_by_nicknames(to) diff --git a/test/pleroma/web/activity_pub/object_validators/announce_validation_test.exs b/test/pleroma/web/activity_pub/object_validators/announce_validation_test.exs index 5b2fcb26d..1ca4f04f6 100644 --- a/test/pleroma/web/activity_pub/object_validators/announce_validation_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/announce_validation_test.exs @@ -86,23 +86,32 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidationTest do object = Object.normalize(post_activity, fetch: false) # Another user can't announce it - {:ok, announce, []} = Builder.announce(announcer, object, public: false) + {:ok, announce, []} = Builder.announce(announcer, object, visibility: "private") {:error, cng} = ObjectValidator.validate(announce, []) assert {:actor, {"can not announce this object", []}} in cng.errors - # The actor of the object can announce it - {:ok, announce, []} = Builder.announce(user, object, public: false) + # The actor of the object can announce it with a restrictive scope + {:ok, announce, []} = Builder.announce(user, object, visibility: "private") + assert {:ok, _, _} = ObjectValidator.validate(announce, []) + {:ok, announce, []} = Builder.announce(user, object, visibility: "direct") assert {:ok, _, _} = ObjectValidator.validate(announce, []) # The actor of the object can not announce it publicly - {:ok, announce, []} = Builder.announce(user, object, public: true) + {:ok, announce, []} = Builder.announce(user, object, visibility: "public") + {:error, cng1} = ObjectValidator.validate(announce, []) - {:error, cng} = ObjectValidator.validate(announce, []) + {:ok, announce, []} = Builder.announce(user, object, visibility: "unlisted") + {:error, cng2} = ObjectValidator.validate(announce, []) - assert {:actor, {"can not announce this object publicly", []}} in cng.errors + {:ok, announce, []} = Builder.announce(user, object, visibility: "local") + {:error, cng3} = ObjectValidator.validate(announce, []) + + for cng <- [cng1, cng2, cng3] do + assert {:actor, {"can not announce this object publicly", []}} in cng.errors + end end end end diff --git a/test/pleroma/web/activity_pub/side_effects_test.exs b/test/pleroma/web/activity_pub/side_effects_test.exs index 4a18cab68..6d20c591c 100644 --- a/test/pleroma/web/activity_pub/side_effects_test.exs +++ b/test/pleroma/web/activity_pub/side_effects_test.exs @@ -784,13 +784,15 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do {:ok, post} = CommonAPI.post(poster, %{status: "hey"}) {:ok, private_post} = CommonAPI.post(poster, %{status: "hey", visibility: "private"}) - {:ok, announce_data, _meta} = Builder.announce(user, post.object, public: true) + {:ok, announce_data, _meta} = Builder.announce(user, post.object, visibility: "public") {:ok, private_announce_data, _meta} = - Builder.announce(user, private_post.object, public: false) + Builder.announce(user, private_post.object, visibility: "private") {:ok, relay_announce_data, _meta} = - Builder.announce(Pleroma.Web.ActivityPub.Relay.get_actor(), post.object, public: true) + Builder.announce(Pleroma.Web.ActivityPub.Relay.get_actor(), post.object, + visibility: "public" + ) {:ok, announce, _meta} = ActivityPub.persist(announce_data, local: true) {:ok, private_announce, _meta} = ActivityPub.persist(private_announce_data, local: true) From 8921dbfffd882d657ffaa7f4c8cfc8e723b0240d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 18 Feb 2026 13:35:20 +0100 Subject: [PATCH 052/127] changelog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/boost-visibilities.add | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/boost-visibilities.add diff --git a/changelog.d/boost-visibilities.add b/changelog.d/boost-visibilities.add new file mode 100644 index 000000000..317d9840d --- /dev/null +++ b/changelog.d/boost-visibilities.add @@ -0,0 +1 @@ +Allow fine-grained announce visibilities From 490cd33bc9e2d58799b9c7702ffdaa2614506c29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 18 Feb 2026 11:06:13 +0100 Subject: [PATCH 053/127] Support lists `exclusive` param MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/exclusive-lists.add | 1 + lib/pleroma/list.ex | 23 ++++++-- .../web/api_spec/operations/list_operation.ex | 26 +++++++-- lib/pleroma/web/api_spec/schemas/list.ex | 6 +- .../controllers/list_controller.ex | 10 ++-- .../controllers/timeline_controller.ex | 6 +- .../web/mastodon_api/views/list_view.ex | 3 +- .../20260218000000_add_exclusive_to_lists.exs | 9 +++ test/pleroma/list_test.exs | 56 ++++++++++++------- .../controllers/list_controller_test.exs | 16 +++--- .../controllers/timeline_controller_test.exs | 25 +++++++++ 11 files changed, 133 insertions(+), 48 deletions(-) create mode 100644 changelog.d/exclusive-lists.add create mode 100644 priv/repo/migrations/20260218000000_add_exclusive_to_lists.exs diff --git a/changelog.d/exclusive-lists.add b/changelog.d/exclusive-lists.add new file mode 100644 index 000000000..bbd722f07 --- /dev/null +++ b/changelog.d/exclusive-lists.add @@ -0,0 +1 @@ +Support lists `exclusive` param diff --git a/lib/pleroma/list.ex b/lib/pleroma/list.ex index b446b91a0..cd23bd0a9 100644 --- a/lib/pleroma/list.ex +++ b/lib/pleroma/list.ex @@ -17,13 +17,14 @@ defmodule Pleroma.List do field(:title, :string) field(:following, {:array, :string}, default: []) field(:ap_id, :string) + field(:exclusive, :boolean, default: false) timestamps() end - def title_changeset(list, attrs \\ %{}) do + def update_changeset(list, attrs \\ %{}) do list - |> cast(attrs, [:title]) + |> cast(attrs, [:title, :exclusive]) |> validate_required([:title]) end @@ -91,14 +92,14 @@ defmodule Pleroma.List do |> Repo.all() end - def rename(%Pleroma.List{} = list, title) do + def update(%Pleroma.List{} = list, params) do list - |> title_changeset(%{title: title}) + |> update_changeset(params) |> Repo.update() end - def create(title, %User{} = creator) do - changeset = title_changeset(%Pleroma.List{user_id: creator.id}, %{title: title}) + def create(params, %User{} = creator) do + changeset = update_changeset(%Pleroma.List{user_id: creator.id}, params) if changeset.valid? do Repo.transaction(fn -> @@ -149,4 +150,14 @@ defmodule Pleroma.List do end def member?(_, _), do: false + + def get_exclusive_list_members(%User{id: user_id}) do + Pleroma.List + |> where([l], l.user_id == ^user_id) + |> where([l], l.exclusive == true) + |> select([l], l.following) + |> Repo.all() + |> List.flatten() + |> Enum.uniq() + end end diff --git a/lib/pleroma/web/api_spec/operations/list_operation.ex b/lib/pleroma/web/api_spec/operations/list_operation.ex index 7d876ae2d..d2e803178 100644 --- a/lib/pleroma/web/api_spec/operations/list_operation.ex +++ b/lib/pleroma/web/api_spec/operations/list_operation.ex @@ -36,7 +36,7 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do summary: "Create a list", description: "Fetch the list with the given ID. Used for verifying the title of a list.", operationId: "ListController.create", - requestBody: create_update_request(), + requestBody: create_request(), security: [%{"oAuth" => ["write:lists"]}], responses: %{ 200 => Operation.response("List", "application/json", List), @@ -68,7 +68,7 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do description: "Change the title of a list", operationId: "ListController.update", parameters: [id_param()], - requestBody: create_update_request(), + requestBody: update_request(), security: [%{"oAuth" => ["write:lists"]}], responses: %{ 200 => Operation.response("List", "application/json", List), @@ -164,14 +164,15 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do ) end - defp create_update_request do + defp create_request do request_body( "Parameters", %Schema{ - description: "POST body for creating or updating a List", + description: "POST body for creating a List", type: :object, properties: %{ - title: %Schema{type: :string, description: "List title"} + title: %Schema{type: :string, description: "List title"}, + exclusive: %Schema{type: :boolean, description: "Whether members of the list should be removed from the “Home” feed"} }, required: [:title] }, @@ -179,6 +180,21 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do ) end + defp update_request do + request_body( + "Parameters", + %Schema{ + description: "PUT body for updating a List", + type: :object, + properties: %{ + title: %Schema{type: :string, description: "List title"}, + exclusive: %Schema{type: :boolean, description: "Whether members of the list should be removed from the “Home” feed"} + } + }, + required: true + ) + end + defp add_remove_accounts_request(required) when is_boolean(required) do request_body( "Parameters", diff --git a/lib/pleroma/web/api_spec/schemas/list.ex b/lib/pleroma/web/api_spec/schemas/list.ex index e57de7917..5df674894 100644 --- a/lib/pleroma/web/api_spec/schemas/list.ex +++ b/lib/pleroma/web/api_spec/schemas/list.ex @@ -13,7 +13,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.List do type: :object, properties: %{ id: %Schema{type: :string, description: "The internal database ID of the list"}, - title: %Schema{type: :string, description: "The user-defined title of the list"} + title: %Schema{type: :string, description: "The user-defined title of the list"}, + exclusive: %Schema{ + type: :boolean, + description: "Whether members of the list should be removed from the “Home” feed" + } }, example: %{ "id" => "12249", diff --git a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex index 3bfc365a5..048012ae6 100644 --- a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex @@ -28,27 +28,27 @@ defmodule Pleroma.Web.MastodonAPI.ListController do # POST /api/v1/lists def create( - %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: %{title: title}}}} = + %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: params}}} = conn, _ ) do - with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do + with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(params, user) do render(conn, "show.json", list: list) end end - # GET /api/v1/lists/:idOB + # GET /api/v1/lists/:id def show(%{assigns: %{list: list}} = conn, _) do render(conn, "show.json", list: list) end # PUT /api/v1/lists/:id def update( - %{assigns: %{list: list}, private: %{open_api_spex: %{body_params: %{title: title}}}} = + %{assigns: %{list: list}, private: %{open_api_spex: %{body_params: params}}} = conn, _ ) do - with {:ok, list} <- Pleroma.List.rename(list, title) do + with {:ok, list} <- Pleroma.List.update(list, params) do render(conn, "show.json", list: list) end end diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index 5ee74a80e..99a5b6957 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -45,6 +45,10 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do |> User.followed_hashtags() |> Enum.map(& &1.id) + excluded_list_members = + user + |> Pleroma.List.get_exclusive_list_members() + params = params |> Map.put(:type, ["Create", "Announce"]) @@ -58,7 +62,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do |> Map.delete(:local) activities = - [user.ap_id | User.following(user)] + [user.ap_id | User.following(user) -- excluded_list_members] |> ActivityPub.fetch_activities(params) |> Enum.reverse() diff --git a/lib/pleroma/web/mastodon_api/views/list_view.ex b/lib/pleroma/web/mastodon_api/views/list_view.ex index a7ae7c5f7..740f458d4 100644 --- a/lib/pleroma/web/mastodon_api/views/list_view.ex +++ b/lib/pleroma/web/mastodon_api/views/list_view.ex @@ -13,7 +13,8 @@ defmodule Pleroma.Web.MastodonAPI.ListView do def render("show.json", %{list: list}) do %{ id: to_string(list.id), - title: list.title + title: list.title, + exclusive: list.exclusive } end end diff --git a/priv/repo/migrations/20260218000000_add_exclusive_to_lists.exs b/priv/repo/migrations/20260218000000_add_exclusive_to_lists.exs new file mode 100644 index 000000000..253a8acb7 --- /dev/null +++ b/priv/repo/migrations/20260218000000_add_exclusive_to_lists.exs @@ -0,0 +1,9 @@ +defmodule Pleroma.Repo.Migrations.AddExclusiveToLists do + use Ecto.Migration + + def change do + alter table(:lists) do + add(:exclusive, :boolean, default: false) + end + end +end diff --git a/test/pleroma/list_test.exs b/test/pleroma/list_test.exs index a68146b0d..d44310689 100644 --- a/test/pleroma/list_test.exs +++ b/test/pleroma/list_test.exs @@ -10,22 +10,23 @@ defmodule Pleroma.ListTest do test "creating a list" do user = insert(:user) - {:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", user) - %Pleroma.List{title: title} = Pleroma.List.get(list.id, user) + {:ok, %Pleroma.List{} = list} = Pleroma.List.create(%{title: "title"}, user) + %Pleroma.List{title: title, exclusive: exclusive} = Pleroma.List.get(list.id, user) assert title == "title" + assert exclusive == false end test "validates title" do user = insert(:user) - assert {:error, changeset} = Pleroma.List.create("", user) + assert {:error, changeset} = Pleroma.List.create(%{title: ""}, user) assert changeset.errors == [title: {"can't be blank", [validation: :required]}] end test "getting a list not belonging to the user" do user = insert(:user) other_user = insert(:user) - {:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", user) + {:ok, %Pleroma.List{} = list} = Pleroma.List.create(%{title: "title"}, user) ret = Pleroma.List.get(list.id, other_user) assert is_nil(ret) end @@ -33,7 +34,7 @@ defmodule Pleroma.ListTest do test "adding an user to a list" do user = insert(:user) other_user = insert(:user) - {:ok, list} = Pleroma.List.create("title", user) + {:ok, list} = Pleroma.List.create(%{title: "title"}, user) {:ok, %{following: following}} = Pleroma.List.follow(list, other_user) assert [other_user.follower_address] == following end @@ -41,7 +42,7 @@ defmodule Pleroma.ListTest do test "removing an user from a list" do user = insert(:user) other_user = insert(:user) - {:ok, list} = Pleroma.List.create("title", user) + {:ok, list} = Pleroma.List.create(%{title: "title"}, user) {:ok, %{following: _following}} = Pleroma.List.follow(list, other_user) {:ok, %{following: following}} = Pleroma.List.unfollow(list, other_user) assert [] == following @@ -49,14 +50,27 @@ defmodule Pleroma.ListTest do test "renaming a list" do user = insert(:user) - {:ok, list} = Pleroma.List.create("title", user) - {:ok, %{title: title}} = Pleroma.List.rename(list, "new") + {:ok, list} = Pleroma.List.create(%{title: "title"}, user) + {:ok, %{title: title}} = Pleroma.List.update(list, %{title: "new"}) assert "new" == title end + test "updating a list exclusivity" do + user = insert(:user) + + {:ok, %{exclusive: exclusive} = list} = + Pleroma.List.create(%{title: "title", exclusive: true}, user) + + assert exclusive == true + {:ok, %{exclusive: exclusive} = list} = Pleroma.List.update(list, %{exclusive: false}) + assert exclusive == false + {:ok, %{exclusive: exclusive}} = Pleroma.List.update(list, %{exclusive: true}) + assert exclusive == true + end + test "deleting a list" do user = insert(:user) - {:ok, list} = Pleroma.List.create("title", user) + {:ok, list} = Pleroma.List.create(%{title: "title"}, user) {:ok, list} = Pleroma.List.delete(list) assert is_nil(Repo.get(Pleroma.List, list.id)) end @@ -65,7 +79,7 @@ defmodule Pleroma.ListTest do user = insert(:user) other_user = insert(:user) third_user = insert(:user) - {:ok, list} = Pleroma.List.create("title", user) + {:ok, list} = Pleroma.List.create(%{title: "title"}, user) {:ok, list} = Pleroma.List.follow(list, other_user) {:ok, list} = Pleroma.List.follow(list, third_user) {:ok, following} = Pleroma.List.get_following(list) @@ -76,9 +90,9 @@ defmodule Pleroma.ListTest do test "getting all lists by an user" do user = insert(:user) other_user = insert(:user) - {:ok, list_one} = Pleroma.List.create("title", user) - {:ok, list_two} = Pleroma.List.create("other title", user) - {:ok, list_three} = Pleroma.List.create("third title", other_user) + {:ok, list_one} = Pleroma.List.create(%{title: "title"}, user) + {:ok, list_two} = Pleroma.List.create(%{title: "other title"}, user) + {:ok, list_three} = Pleroma.List.create(%{title: "third title"}, other_user) lists = Pleroma.List.for_user(user, %{}) assert list_one in lists assert list_two in lists @@ -88,9 +102,9 @@ defmodule Pleroma.ListTest do test "getting all lists the user is a member of" do user = insert(:user) other_user = insert(:user) - {:ok, list_one} = Pleroma.List.create("title", user) - {:ok, list_two} = Pleroma.List.create("other title", user) - {:ok, list_three} = Pleroma.List.create("third title", other_user) + {:ok, list_one} = Pleroma.List.create(%{title: "title"}, user) + {:ok, list_two} = Pleroma.List.create(%{title: "other title"}, user) + {:ok, list_three} = Pleroma.List.create(%{title: "third title"}, other_user) {:ok, list_one} = Pleroma.List.follow(list_one, other_user) {:ok, list_two} = Pleroma.List.follow(list_two, other_user) {:ok, list_three} = Pleroma.List.follow(list_three, user) @@ -106,8 +120,8 @@ defmodule Pleroma.ListTest do not_owner = insert(:user) member_1 = insert(:user) member_2 = insert(:user) - {:ok, owned_list} = Pleroma.List.create("owned", owner) - {:ok, not_owned_list} = Pleroma.List.create("not owned", not_owner) + {:ok, owned_list} = Pleroma.List.create(%{title: "owned"}, owner) + {:ok, not_owned_list} = Pleroma.List.create(%{title: "not owned"}, not_owner) {:ok, owned_list} = Pleroma.List.follow(owned_list, member_1) {:ok, owned_list} = Pleroma.List.follow(owned_list, member_2) {:ok, not_owned_list} = Pleroma.List.follow(not_owned_list, member_1) @@ -123,14 +137,14 @@ defmodule Pleroma.ListTest do test "get by ap_id" do user = insert(:user) - {:ok, list} = Pleroma.List.create("foo", user) + {:ok, list} = Pleroma.List.create(%{title: "foo"}, user) assert Pleroma.List.get_by_ap_id(list.ap_id) == list end test "memberships" do user = insert(:user) member = insert(:user) - {:ok, list} = Pleroma.List.create("foo", user) + {:ok, list} = Pleroma.List.create(%{title: "foo"}, user) {:ok, list} = Pleroma.List.follow(list, member) assert Pleroma.List.memberships(member) == [list.ap_id] @@ -140,7 +154,7 @@ defmodule Pleroma.ListTest do user = insert(:user) member = insert(:user) - {:ok, list} = Pleroma.List.create("foo", user) + {:ok, list} = Pleroma.List.create(%{title: "foo"}, user) {:ok, list} = Pleroma.List.follow(list, member) assert Pleroma.List.member?(list, member) diff --git a/test/pleroma/web/mastodon_api/controllers/list_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/list_controller_test.exs index 430b8b89d..bfda48be4 100644 --- a/test/pleroma/web/mastodon_api/controllers/list_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/list_controller_test.exs @@ -56,7 +56,7 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do %{user: user, conn: conn} = oauth_access(["write:lists"]) other_user = insert(:user) third_user = insert(:user) - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) assert %{} == conn @@ -77,7 +77,7 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do other_user = insert(:user) third_user = insert(:user) fourth_user = insert(:user) - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) {:ok, list} = Pleroma.List.follow(list, other_user) {:ok, list} = Pleroma.List.follow(list, third_user) {:ok, list} = Pleroma.List.follow(list, fourth_user) @@ -98,7 +98,7 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do %{user: user, conn: conn} = oauth_access(["write:lists"]) other_user = insert(:user) third_user = insert(:user) - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) {:ok, list} = Pleroma.List.follow(list, other_user) {:ok, list} = Pleroma.List.follow(list, third_user) @@ -115,7 +115,7 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do test "listing users in a list" do %{user: user, conn: conn} = oauth_access(["read:lists"]) other_user = insert(:user) - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) {:ok, list} = Pleroma.List.follow(list, other_user) conn = @@ -129,7 +129,7 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do test "retrieving a list" do %{user: user, conn: conn} = oauth_access(["read:lists"]) - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) conn = conn @@ -150,7 +150,7 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do test "renaming a list" do %{user: user, conn: conn} = oauth_access(["write:lists"]) - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) assert %{"title" => "newname"} = conn @@ -161,7 +161,7 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do test "validates title when renaming a list" do %{user: user, conn: conn} = oauth_access(["write:lists"]) - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) conn = conn @@ -175,7 +175,7 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do test "deleting a list" do %{user: user, conn: conn} = oauth_access(["write:lists"]) - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) conn = delete(conn, "/api/v1/lists/#{list.id}") diff --git a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs index 4d646509c..9808652d2 100644 --- a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs @@ -149,6 +149,31 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do |> get("/api/v1/timelines/home?remote=true&local=true") |> json_response_and_validate_schema(200) == [] end + + test "the home timeline excludes posts from users in exclusive lists", %{ + user: user, + conn: conn + } do + other_user1 = insert(:user) + other_user2 = insert(:user) + + {:ok, user, other_user1} = User.follow(user, other_user1) + {:ok, user, other_user2} = User.follow(user, other_user2) + + {:ok, list} = Pleroma.List.create(%{title: "foo", exclusive: true}, user) + {:ok, _list} = Pleroma.List.follow(list, other_user1) + + {:ok, _activity} = CommonAPI.post(other_user1, %{status: "hi"}) + {:ok, %{id: activity2_id}} = CommonAPI.post(other_user2, %{status: "hi too"}) + + response = + conn + |> assign(:user, user) + |> get("/api/v1/timelines/home") + |> json_response_and_validate_schema(200) + + assert [%{"id" => ^activity2_id}] = response + end end describe "public" do From 19025563e2107e5aa70b5d85aa9c1f9b20cba5d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Tue, 3 Mar 2026 00:29:46 +0100 Subject: [PATCH 054/127] fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../operations/remote_interaction_operation.ex | 10 ---------- lib/pleroma/web/router.ex | 3 ++- .../web/templates/static_fe/static_fe/profile.html.eex | 2 +- .../remote_interaction_controller_test.exs | 6 +++--- 4 files changed, 6 insertions(+), 15 deletions(-) diff --git a/lib/pleroma/web/api_spec/operations/remote_interaction_operation.ex b/lib/pleroma/web/api_spec/operations/remote_interaction_operation.ex index b5bd9d72f..54edbcf32 100644 --- a/lib/pleroma/web/api_spec/operations/remote_interaction_operation.ex +++ b/lib/pleroma/web/api_spec/operations/remote_interaction_operation.ex @@ -96,14 +96,4 @@ defmodule Pleroma.Web.ApiSpec.RemoteInteractionOperation do responses: %{200 => Operation.response("Web Page", "text/html", %Schema{type: :string})} } end - - def show_subscribe_form_operation do - %Operation{ - tags: ["Remote interaction"], - summary: "Show remote subscribe form", - operationId: "RemoteInteractionController.show_subscribe_form", - parameters: [], - responses: %{200 => Operation.response("Web Page", "text/html", %Schema{type: :string})} - } - end end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 22e82568a..eea0a0912 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -234,10 +234,11 @@ defmodule Pleroma.Web.Router do end scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do + pipe_through(:pleroma_api) + get("/emoji", UtilController, :emoji) get("/captcha", UtilController, :captcha) get("/healthcheck", UtilController, :healthcheck) - get("/federation_status", InstancesController, :show) end diff --git a/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex index a14ca305e..85bfd7b3a 100644 --- a/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex +++ b/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex @@ -2,7 +2,7 @@

<%= link instance_name(), to: "/" %>

-
+ diff --git a/test/pleroma/web/remote_interaction/remote_interaction_controller_test.exs b/test/pleroma/web/remote_interaction/remote_interaction_controller_test.exs index 116609013..9df86c0a2 100644 --- a/test/pleroma/web/remote_interaction/remote_interaction_controller_test.exs +++ b/test/pleroma/web/remote_interaction/remote_interaction_controller_test.exs @@ -428,7 +428,7 @@ defmodule Pleroma.Web.RemoteInteraction.RemoteInteractionControllerTest do avatar: %{"url" => [%{"href" => "https://remote.org/avatar.png"}]} }) - avatar_url = Pleroma.Web.PleromaAPI.RemoteFollowView.avatar_url(user) + avatar_url = Pleroma.Web.RemoteInteraction.RemoteInteractionView.avatar_url(user) assert avatar_url == "https://remote.org/avatar.png" end @@ -445,7 +445,7 @@ defmodule Pleroma.Web.RemoteInteraction.RemoteInteractionControllerTest do avatar: %{"url" => [%{"href" => "https://remote.org/avatar.png"}]} }) - avatar_url = Pleroma.Web.PleromaAPI.RemoteFollowView.avatar_url(user) + avatar_url = Pleroma.Web.RemoteInteraction.RemoteInteractionView.avatar_url(user) url = Pleroma.Web.Endpoint.url() assert String.starts_with?(avatar_url, url) @@ -460,7 +460,7 @@ defmodule Pleroma.Web.RemoteInteraction.RemoteInteractionControllerTest do avatar: %{"url" => [%{"href" => "#{Pleroma.Web.Endpoint.url()}/localuser/avatar.png"}]} }) - avatar_url = Pleroma.Web.PleromaAPI.RemoteFollowView.avatar_url(user) + avatar_url = Pleroma.Web.RemoteInteraction.RemoteInteractionView.avatar_url(user) assert avatar_url == "#{Pleroma.Web.Endpoint.url()}/localuser/avatar.png" end From ca38217898060775500ac9ebac3852e5a4c91e8f Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 3 Mar 2026 23:10:15 +0100 Subject: [PATCH 055/127] Fix AccountController Plug warning the URI path used in plug tests must start with "/", got: "api/v1/blocks" (plug 1.19.1) lib/plug/adapters/test/conn.ex:14: Plug.Adapters.Test.Conn.conn/4 (phoenix 1.7.14) lib/phoenix/test/conn_test.ex:236: Phoenix.ConnTest.dispatch_endpoint/5 (phoenix 1.7.14) lib/phoenix/test/conn_test.ex:225: Phoenix.ConnTest.dispatch/5 test/pleroma/web/mastodon_api/controllers/account_controller_test.exs:2099: Pleroma.Web.MastodonAPI.AccountControllerTest."test getting a list of blocks"/1 (ex_unit 1.19.5) lib/ex_unit/runner.ex:528: ExUnit.Runner.exec_test/2 (ex_unit 1.19.5) lib/ex_unit/capture_log.ex:121: ExUnit.CaptureLog.with_log/2 (ex_unit 1.19.5) lib/ex_unit/runner.ex:477: anonymous fn/3 in ExUnit.Runner.maybe_capture_log/3 (stdlib 7.2) timer.erl:599: :timer.tc/2 (ex_unit 1.19.5) lib/ex_unit/runner.ex:450: anonymous fn/6 in ExUnit.Runner.spawn_test_monitor/4 --- changelog.d/plug-test-typo.skip | 0 .../web/mastodon_api/controllers/account_controller_test.exs | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 changelog.d/plug-test-typo.skip diff --git a/changelog.d/plug-test-typo.skip b/changelog.d/plug-test-typo.skip new file mode 100644 index 000000000..e69de29bb diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs index ea98b53a8..14a8eb9a0 100644 --- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs @@ -2096,7 +2096,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do result = conn |> assign(:user, user) - |> get("api/v1/blocks") + |> get("/api/v1/blocks") |> json_response_and_validate_schema(200) assert [ From 848b3f5d5b6e004c51e18a4adb04d721fb89ea5c Mon Sep 17 00:00:00 2001 From: Yonle Date: Mon, 23 Feb 2026 20:57:11 +0700 Subject: [PATCH 056/127] reverse_proxy,endpoint,uploaded_media: add immutable cache-control flag --- lib/pleroma/reverse_proxy.ex | 2 +- lib/pleroma/web/endpoint.ex | 2 +- lib/pleroma/web/plugs/uploaded_media.ex | 2 +- test/pleroma/reverse_proxy_test.exs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/reverse_proxy.ex b/lib/pleroma/reverse_proxy.ex index bb55a4984..c7ee47c6e 100644 --- a/lib/pleroma/reverse_proxy.ex +++ b/lib/pleroma/reverse_proxy.ex @@ -12,7 +12,7 @@ defmodule Pleroma.ReverseProxy do @keep_resp_headers @resp_cache_headers ++ ~w(content-length content-type content-disposition content-encoding) ++ ~w(content-range accept-ranges vary) - @default_cache_control_header "public, max-age=1209600" + @default_cache_control_header "public, max-age=1209600, immutable" @valid_resp_codes [200, 206, 304] @max_read_duration :timer.seconds(30) @max_body_length :infinity diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index bab3c9fd0..13c655ee2 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -46,7 +46,7 @@ defmodule Pleroma.Web.Endpoint do plug(Pleroma.Web.Plugs.HTTPSecurityPlug) plug(Pleroma.Web.Plugs.UploadedMedia) - @static_cache_control "public, max-age=1209600" + @static_cache_control "public, max-age=1209600, immutable" @static_cache_disabled "public, no-cache" # InstanceStatic needs to be before Plug.Static to be able to override shipped-static files diff --git a/lib/pleroma/web/plugs/uploaded_media.ex b/lib/pleroma/web/plugs/uploaded_media.ex index abacf965b..5a3ea12aa 100644 --- a/lib/pleroma/web/plugs/uploaded_media.ex +++ b/lib/pleroma/web/plugs/uploaded_media.ex @@ -17,7 +17,7 @@ defmodule Pleroma.Web.Plugs.UploadedMedia do # no slashes @path "media" - @default_cache_control_header "public, max-age=1209600" + @default_cache_control_header "public, max-age=1209600, immutable" def init(_opts) do static_plug_opts = diff --git a/test/pleroma/reverse_proxy_test.exs b/test/pleroma/reverse_proxy_test.exs index 8dbe9c6bf..ec4470379 100644 --- a/test/pleroma/reverse_proxy_test.exs +++ b/test/pleroma/reverse_proxy_test.exs @@ -294,7 +294,7 @@ defmodule Pleroma.ReverseProxyTest do |> expect(:stream_body, fn _ -> :done end) conn = ReverseProxy.call(conn, "/cache") - assert {"cache-control", "public, max-age=1209600"} in conn.resp_headers + assert {"cache-control", "public, max-age=1209600, immutable"} in conn.resp_headers end end From 970e0f90441ff092fd6ee11ed0a87a06f4269373 Mon Sep 17 00:00:00 2001 From: Yonle Date: Mon, 2 Mar 2026 23:48:59 +0700 Subject: [PATCH 057/127] endpoint: set cache control for favicon.png Signed-off-by: Yonle --- lib/pleroma/web/endpoint.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 13c655ee2..f7c58f459 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -56,7 +56,7 @@ defmodule Pleroma.Web.Endpoint do Pleroma.Web.Plugs.InstanceStatic, at: "/", from: :pleroma, - only: ["emoji", "images"], + only: ["emoji", "images", "favicon.png"], gzip: true, cache_control_for_etags: @static_cache_control, headers: %{ From 8abd25950a483efcfa42ba113086dd6cc5730ebf Mon Sep 17 00:00:00 2001 From: Yonle Date: Wed, 4 Mar 2026 02:54:47 +0700 Subject: [PATCH 058/127] endpoint: use favicon plug --- lib/pleroma/web/endpoint.ex | 12 +++++- lib/pleroma/web/plugs/favicon.ex | 73 ++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 lib/pleroma/web/plugs/favicon.ex diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index f7c58f459..e769b11b1 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -48,6 +48,7 @@ defmodule Pleroma.Web.Endpoint do @static_cache_control "public, max-age=1209600, immutable" @static_cache_disabled "public, no-cache" + @favicon_cache_control "public, max=age=86400, immutable" # cache for a day # InstanceStatic needs to be before Plug.Static to be able to override shipped-static files # If you're adding new paths to `only:` you'll need to configure them in InstanceStatic as well @@ -56,7 +57,7 @@ defmodule Pleroma.Web.Endpoint do Pleroma.Web.Plugs.InstanceStatic, at: "/", from: :pleroma, - only: ["emoji", "images", "favicon.png"], + only: ["emoji", "images"], gzip: true, cache_control_for_etags: @static_cache_control, headers: %{ @@ -73,6 +74,15 @@ defmodule Pleroma.Web.Endpoint do } ) + plug(Pleroma.Web.Plugs.Favicon, + at: "/", + only: ["favicon.png"], + cache_control_for_etags: @favicon_cache_control, + headers: %{ + "cache-control" => @favicon_cache_control + } + ) + plug(Pleroma.Web.Plugs.FrontendStatic, at: "/", frontend_type: :primary, diff --git a/lib/pleroma/web/plugs/favicon.ex b/lib/pleroma/web/plugs/favicon.ex new file mode 100644 index 000000000..ad105c59a --- /dev/null +++ b/lib/pleroma/web/plugs/favicon.ex @@ -0,0 +1,73 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2026 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.Favicon do + @behaviour Plug + + @moduledoc """ + Serves favicon.png directly from the instance static directory, + bypassing the frontend-specific logic. + """ + + alias Pleroma.Web.Plugs.FrontendStatic + import Plug.Conn, only: [put_resp_header: 3] + + def init(opts) do + opts + |> Keyword.put(:from, "__unconfigured_favicon_static_plug") + |> Plug.Static.init() + end + + def call(conn, opts) do + if favicon_route?(conn.path_info) do + case find_favicon_dir(conn) do + {:ok, dir} -> + call_static(conn, opts, dir) + |> Plug.Conn.halt() + + :error -> + conn # Let the request keep going to a 404 + end + else + conn + end + end + + defp find_favicon_dir(conn) do + instance_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static") + instance_path = Path.join(instance_dir, "favicon.png") + + frontend_type = FrontendStatic.preferred_or_fallback(conn, :primary) + frontend_dir = FrontendStatic.file_path("", frontend_type) + frontend_path = if frontend_dir, do: Path.join(frontend_dir, "favicon.png"), else: nil + + priv_dir = Application.app_dir(:pleroma, "priv/static") + priv_path = Path.join(priv_dir, "favicon.png") + + cond do + File.exists?(instance_path) -> {:ok, instance_dir} + frontend_path && File.exists?(frontend_path) -> {:ok, frontend_dir} + File.exists?(priv_path) -> {:ok, priv_dir} + true -> :error + end + end + + defp favicon_route?(["favicon.png"]), do: true + defp favicon_route?(_), do: false + + defp call_static(conn, opts, from) do + opts = + opts + |> Map.put(:from, from) + |> Map.put(:content_types, false) + + conn = set_content_type(conn) + + Plug.Static.call(conn, opts) + end + + defp set_content_type(conn) do + put_resp_header(conn, "content-type", "image/png") + end +end From 4bc0b26abed50ca04041297f1f66467a8d812359 Mon Sep 17 00:00:00 2001 From: Yonle Date: Wed, 4 Mar 2026 02:57:53 +0700 Subject: [PATCH 059/127] changelog.d: add cache-control-immutable --- changelog.d/cache-control-immutable.add | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/cache-control-immutable.add diff --git a/changelog.d/cache-control-immutable.add b/changelog.d/cache-control-immutable.add new file mode 100644 index 000000000..516db67bf --- /dev/null +++ b/changelog.d/cache-control-immutable.add @@ -0,0 +1 @@ +Add immutable tag on cache-control header for several endpoints that's serving the same exact things. \ No newline at end of file From 0879dd39505bb9f577bd371a04f49b9beb2123ae Mon Sep 17 00:00:00 2001 From: Yonle Date: Wed, 4 Mar 2026 03:13:55 +0700 Subject: [PATCH 060/127] endpoint: reorder: handle favicon plug first --- lib/pleroma/web/endpoint.ex | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index e769b11b1..2bf64a1fd 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -65,15 +65,6 @@ defmodule Pleroma.Web.Endpoint do } ) - plug(Pleroma.Web.Plugs.InstanceStatic, - at: "/", - gzip: true, - cache_control_for_etags: @static_cache_disabled, - headers: %{ - "cache-control" => @static_cache_disabled - } - ) - plug(Pleroma.Web.Plugs.Favicon, at: "/", only: ["favicon.png"], @@ -83,6 +74,15 @@ defmodule Pleroma.Web.Endpoint do } ) + plug(Pleroma.Web.Plugs.InstanceStatic, + at: "/", + gzip: true, + cache_control_for_etags: @static_cache_disabled, + headers: %{ + "cache-control" => @static_cache_disabled + } + ) + plug(Pleroma.Web.Plugs.FrontendStatic, at: "/", frontend_type: :primary, From 96f252023e31549d934b42c098d6e62192f9f8d0 Mon Sep 17 00:00:00 2001 From: Yonle Date: Wed, 4 Mar 2026 22:49:54 +0700 Subject: [PATCH 061/127] constants: remove favicon.png from static_only_files --- lib/pleroma/constants.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex index f78b84099..178dd6094 100644 --- a/lib/pleroma/constants.ex +++ b/lib/pleroma/constants.ex @@ -29,7 +29,7 @@ defmodule Pleroma.Constants do const(static_only_files, do: - ~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc embed.js embed.css) + ~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js schemas doc embed.js embed.css) ) const(status_updatable_fields, From 89751296803ba60354c16335f3422421710ddb00 Mon Sep 17 00:00:00 2001 From: Yonle Date: Thu, 5 Mar 2026 02:07:39 +0700 Subject: [PATCH 062/127] webplug(favicon): remove check on url path. --- lib/pleroma/web/plugs/favicon.ex | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/lib/pleroma/web/plugs/favicon.ex b/lib/pleroma/web/plugs/favicon.ex index ad105c59a..32ef86cf8 100644 --- a/lib/pleroma/web/plugs/favicon.ex +++ b/lib/pleroma/web/plugs/favicon.ex @@ -20,17 +20,12 @@ defmodule Pleroma.Web.Plugs.Favicon do end def call(conn, opts) do - if favicon_route?(conn.path_info) do - case find_favicon_dir(conn) do - {:ok, dir} -> - call_static(conn, opts, dir) - |> Plug.Conn.halt() + case find_favicon_dir(conn) do + {:ok, dir} -> + call_static(conn, opts, dir) - :error -> - conn # Let the request keep going to a 404 - end - else - conn + :error -> + conn # Let the request keep going to a 404 end end @@ -53,9 +48,6 @@ defmodule Pleroma.Web.Plugs.Favicon do end end - defp favicon_route?(["favicon.png"]), do: true - defp favicon_route?(_), do: false - defp call_static(conn, opts, from) do opts = opts From d03ae43ee07a8d6467fe8b608ba69f2166a740e3 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 5 Mar 2026 10:28:09 +0100 Subject: [PATCH 063/127] Favicon Plug: Simplify and pass when not requesting favicon --- lib/pleroma/web/plugs/favicon.ex | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/pleroma/web/plugs/favicon.ex b/lib/pleroma/web/plugs/favicon.ex index 32ef86cf8..6564d2e00 100644 --- a/lib/pleroma/web/plugs/favicon.ex +++ b/lib/pleroma/web/plugs/favicon.ex @@ -10,7 +10,6 @@ defmodule Pleroma.Web.Plugs.Favicon do bypassing the frontend-specific logic. """ - alias Pleroma.Web.Plugs.FrontendStatic import Plug.Conn, only: [put_resp_header: 3] def init(opts) do @@ -19,8 +18,8 @@ defmodule Pleroma.Web.Plugs.Favicon do |> Plug.Static.init() end - def call(conn, opts) do - case find_favicon_dir(conn) do + def call(%{request_path: "/favicon.png"} = conn, opts) do + case find_favicon_dir() do {:ok, dir} -> call_static(conn, opts, dir) @@ -29,20 +28,19 @@ defmodule Pleroma.Web.Plugs.Favicon do end end - defp find_favicon_dir(conn) do + def call(conn, _) do + conn + end + + defp find_favicon_dir() do instance_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static") instance_path = Path.join(instance_dir, "favicon.png") - frontend_type = FrontendStatic.preferred_or_fallback(conn, :primary) - frontend_dir = FrontendStatic.file_path("", frontend_type) - frontend_path = if frontend_dir, do: Path.join(frontend_dir, "favicon.png"), else: nil - priv_dir = Application.app_dir(:pleroma, "priv/static") priv_path = Path.join(priv_dir, "favicon.png") cond do File.exists?(instance_path) -> {:ok, instance_dir} - frontend_path && File.exists?(frontend_path) -> {:ok, frontend_dir} File.exists?(priv_path) -> {:ok, priv_dir} true -> :error end From 2388964b141bb963d1ab37ccdd00985140a2c6aa Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 5 Mar 2026 11:37:02 +0100 Subject: [PATCH 064/127] Favicon Plug: Add tests --- test/pleroma/web/plugs/favicon_plug_test.exs | 69 ++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 test/pleroma/web/plugs/favicon_plug_test.exs diff --git a/test/pleroma/web/plugs/favicon_plug_test.exs b/test/pleroma/web/plugs/favicon_plug_test.exs new file mode 100644 index 000000000..36650e7bf --- /dev/null +++ b/test/pleroma/web/plugs/favicon_plug_test.exs @@ -0,0 +1,69 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2026 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.FaviconPlugTest do + use Pleroma.Web.ConnCase + + # import Mox + + # alias Pleroma.UnstubbedConfigMock, as: ConfigMock + + @dir "test/tmp/favicon_static" + + describe "default favicon" do + test "returns favicon", %{conn: conn} do + conn = get(conn, "/favicon.png") + etag = get_resp_header(conn, "etag") + + # etag changes when serving a different file + assert etag == ["\"72487CE\""] + assert response_content_type(conn, :png) + end + + test "returns correct cache-control", %{conn: conn} do + cache = + conn + |> get("/favicon.png") + |> get_resp_header("cache-control") + + assert cache == ["public, max=age=86400, immutable"] + end + end + + describe "custom favicon" do + setup do + Pleroma.Backports.mkdir_p!(@dir) + favicon_path = Path.join(@dir, "favicon.png") + + # Favicon plug should always return image/png + donor_image = "test/fixtures/image.gif" + image_stat = File.stat!(donor_image) + + # Preserve stat since that's what etag is based on + File.cp!(donor_image, favicon_path) + File.write_stat!(favicon_path, image_stat) + + clear_config([:instance, :static_dir], @dir) + + on_exit( fn -> File.rm_rf!(@dir) end) + end + + test "returns favicon", %{conn: conn} do + conn = get(conn, "/favicon.png") + etag = get_resp_header(conn, "etag") + + assert etag == ["\"215A3A4\""] + assert response_content_type(conn, :png) + end + + test "returns correct cache-control", %{conn: conn} do + cache = + conn + |> get("/favicon.png") + |> get_resp_header("cache-control") + + assert cache == ["public, max=age=86400, immutable"] + end + end +end From 662c9f36ac597c7f0bfbcdec8c12f2dfb87b8df0 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 5 Mar 2026 11:43:55 +0100 Subject: [PATCH 065/127] Favicon Plug: Update moduledoc and rename to adhere to convention --- lib/pleroma/web/endpoint.ex | 2 +- lib/pleroma/web/plugs/{favicon.ex => favicon_plug.ex} | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) rename lib/pleroma/web/plugs/{favicon.ex => favicon_plug.ex} (86%) diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 2bf64a1fd..81a9d3a09 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -65,7 +65,7 @@ defmodule Pleroma.Web.Endpoint do } ) - plug(Pleroma.Web.Plugs.Favicon, + plug(Pleroma.Web.Plugs.FaviconPlug, at: "/", only: ["favicon.png"], cache_control_for_etags: @favicon_cache_control, diff --git a/lib/pleroma/web/plugs/favicon.ex b/lib/pleroma/web/plugs/favicon_plug.ex similarity index 86% rename from lib/pleroma/web/plugs/favicon.ex rename to lib/pleroma/web/plugs/favicon_plug.ex index 6564d2e00..d50a03d83 100644 --- a/lib/pleroma/web/plugs/favicon.ex +++ b/lib/pleroma/web/plugs/favicon_plug.ex @@ -2,12 +2,13 @@ # Copyright © 2017-2026 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.Plugs.Favicon do +defmodule Pleroma.Web.Plugs.FaviconPlug do @behaviour Plug @moduledoc """ - Serves favicon.png directly from the instance static directory, - bypassing the frontend-specific logic. + This is a shim to call `Plug.Static` but with runtime `from` configuration for instance favicon. + + Serves default or custom favicon.png with cacheable cache-control. """ import Plug.Conn, only: [put_resp_header: 3] From d0db1f00c3d52cab72de6db300bb99d58b1f289a Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 5 Mar 2026 11:55:02 +0100 Subject: [PATCH 066/127] Favicon Plug: assert HTTP 200 status in tests --- test/pleroma/web/plugs/favicon_plug_test.exs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/pleroma/web/plugs/favicon_plug_test.exs b/test/pleroma/web/plugs/favicon_plug_test.exs index 36650e7bf..87d955ccc 100644 --- a/test/pleroma/web/plugs/favicon_plug_test.exs +++ b/test/pleroma/web/plugs/favicon_plug_test.exs @@ -17,16 +17,16 @@ defmodule Pleroma.Web.Plugs.FaviconPlugTest do etag = get_resp_header(conn, "etag") # etag changes when serving a different file + assert conn.status == 200 assert etag == ["\"72487CE\""] assert response_content_type(conn, :png) end test "returns correct cache-control", %{conn: conn} do - cache = - conn - |> get("/favicon.png") - |> get_resp_header("cache-control") + conn = get(conn, "/favicon.png") + cache = get_resp_header(conn, "cache-control") + assert conn.status == 200 assert cache == ["public, max=age=86400, immutable"] end end @@ -53,16 +53,16 @@ defmodule Pleroma.Web.Plugs.FaviconPlugTest do conn = get(conn, "/favicon.png") etag = get_resp_header(conn, "etag") + assert conn.status == 200 assert etag == ["\"215A3A4\""] assert response_content_type(conn, :png) end test "returns correct cache-control", %{conn: conn} do - cache = - conn - |> get("/favicon.png") - |> get_resp_header("cache-control") + conn = get(conn ,"/favicon.png") + cache = get_resp_header(conn, "cache-control") + assert conn.status == 200 assert cache == ["public, max=age=86400, immutable"] end end From 5f321b0b5b10d0a3324b588b8f9af01878ecac04 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 5 Mar 2026 12:49:20 +0100 Subject: [PATCH 067/127] Favicon Plug: Halt Plug pipeline when favicon not found --- lib/pleroma/web/plugs/favicon_plug.ex | 12 ++++++-- test/pleroma/web/plugs/favicon_plug_test.exs | 30 ++++++++------------ 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/pleroma/web/plugs/favicon_plug.ex b/lib/pleroma/web/plugs/favicon_plug.ex index d50a03d83..1f8b891a1 100644 --- a/lib/pleroma/web/plugs/favicon_plug.ex +++ b/lib/pleroma/web/plugs/favicon_plug.ex @@ -11,7 +11,9 @@ defmodule Pleroma.Web.Plugs.FaviconPlug do Serves default or custom favicon.png with cacheable cache-control. """ - import Plug.Conn, only: [put_resp_header: 3] + import Plug.Conn, only: [put_resp_header: 3, send_resp: 3, halt: 1] + + require Logger def init(opts) do opts @@ -25,7 +27,12 @@ defmodule Pleroma.Web.Plugs.FaviconPlug do call_static(conn, opts, dir) :error -> - conn # Let the request keep going to a 404 + # Favicon should always be available and this should never occur. + # If it does, halt the pipeline before having unintended side-effects. + Logger.error("No favicon.png found! Is the default favicon deleted?") + conn + |> send_resp(404, "Not found") + |> halt() end end @@ -54,7 +61,6 @@ defmodule Pleroma.Web.Plugs.FaviconPlug do |> Map.put(:content_types, false) conn = set_content_type(conn) - Plug.Static.call(conn, opts) end diff --git a/test/pleroma/web/plugs/favicon_plug_test.exs b/test/pleroma/web/plugs/favicon_plug_test.exs index 87d955ccc..62926e041 100644 --- a/test/pleroma/web/plugs/favicon_plug_test.exs +++ b/test/pleroma/web/plugs/favicon_plug_test.exs @@ -5,20 +5,21 @@ defmodule Pleroma.Web.Plugs.FaviconPlugTest do use Pleroma.Web.ConnCase - # import Mox - - # alias Pleroma.UnstubbedConfigMock, as: ConfigMock - @dir "test/tmp/favicon_static" + setup do + Pleroma.Backports.mkdir_p!(@dir) + + on_exit(fn -> File.rm_rf!(@dir) end) + end + describe "default favicon" do test "returns favicon", %{conn: conn} do conn = get(conn, "/favicon.png") - etag = get_resp_header(conn, "etag") + body_size = byte_size(conn.resp_body) - # etag changes when serving a different file assert conn.status == 200 - assert etag == ["\"72487CE\""] + assert body_size == 1583 assert response_content_type(conn, :png) end @@ -33,28 +34,21 @@ defmodule Pleroma.Web.Plugs.FaviconPlugTest do describe "custom favicon" do setup do - Pleroma.Backports.mkdir_p!(@dir) favicon_path = Path.join(@dir, "favicon.png") + donor_image = "test/fixtures/image.png" - # Favicon plug should always return image/png - donor_image = "test/fixtures/image.gif" - image_stat = File.stat!(donor_image) - - # Preserve stat since that's what etag is based on File.cp!(donor_image, favicon_path) - File.write_stat!(favicon_path, image_stat) - clear_config([:instance, :static_dir], @dir) - on_exit( fn -> File.rm_rf!(@dir) end) + on_exit(fn -> File.rm!(favicon_path) end) end test "returns favicon", %{conn: conn} do conn = get(conn, "/favicon.png") - etag = get_resp_header(conn, "etag") + body_size = byte_size(conn.resp_body) assert conn.status == 200 - assert etag == ["\"215A3A4\""] + assert body_size == 104426 assert response_content_type(conn, :png) end From 3760480813745c0f2d0b2fe1a8642400308e2be2 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 5 Mar 2026 11:48:37 +0100 Subject: [PATCH 068/127] lint --- lib/pleroma/web/plugs/favicon_plug.ex | 3 ++- test/pleroma/web/plugs/favicon_plug_test.exs | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/web/plugs/favicon_plug.ex b/lib/pleroma/web/plugs/favicon_plug.ex index 1f8b891a1..e2e1f1adb 100644 --- a/lib/pleroma/web/plugs/favicon_plug.ex +++ b/lib/pleroma/web/plugs/favicon_plug.ex @@ -30,6 +30,7 @@ defmodule Pleroma.Web.Plugs.FaviconPlug do # Favicon should always be available and this should never occur. # If it does, halt the pipeline before having unintended side-effects. Logger.error("No favicon.png found! Is the default favicon deleted?") + conn |> send_resp(404, "Not found") |> halt() @@ -40,7 +41,7 @@ defmodule Pleroma.Web.Plugs.FaviconPlug do conn end - defp find_favicon_dir() do + defp find_favicon_dir do instance_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static") instance_path = Path.join(instance_dir, "favicon.png") diff --git a/test/pleroma/web/plugs/favicon_plug_test.exs b/test/pleroma/web/plugs/favicon_plug_test.exs index 62926e041..520501250 100644 --- a/test/pleroma/web/plugs/favicon_plug_test.exs +++ b/test/pleroma/web/plugs/favicon_plug_test.exs @@ -8,9 +8,9 @@ defmodule Pleroma.Web.Plugs.FaviconPlugTest do @dir "test/tmp/favicon_static" setup do - Pleroma.Backports.mkdir_p!(@dir) + Pleroma.Backports.mkdir_p!(@dir) - on_exit(fn -> File.rm_rf!(@dir) end) + on_exit(fn -> File.rm_rf!(@dir) end) end describe "default favicon" do @@ -48,12 +48,12 @@ defmodule Pleroma.Web.Plugs.FaviconPlugTest do body_size = byte_size(conn.resp_body) assert conn.status == 200 - assert body_size == 104426 + assert body_size == 104_426 assert response_content_type(conn, :png) end test "returns correct cache-control", %{conn: conn} do - conn = get(conn ,"/favicon.png") + conn = get(conn, "/favicon.png") cache = get_resp_header(conn, "cache-control") assert conn.status == 200 From 499b2ed11810c418d5d42334f1e98aec28f93f88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 6 Mar 2026 17:23:54 +0100 Subject: [PATCH 069/127] remove unused alias MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- lib/pleroma/web/pleroma_api/controllers/util_controller.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/pleroma/web/pleroma_api/controllers/util_controller.ex b/lib/pleroma/web/pleroma_api/controllers/util_controller.ex index 70b0b9cf6..882e9476e 100644 --- a/lib/pleroma/web/pleroma_api/controllers/util_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/util_controller.ex @@ -7,7 +7,6 @@ defmodule Pleroma.Web.PleromaAPI.UtilController do require Logger - alias Pleroma.Activity alias Pleroma.Config alias Pleroma.Emoji alias Pleroma.Healthcheck From 87b4e3f3ff96e0860421562c0b956dbd3d716426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Tue, 10 Feb 2026 14:27:27 +0100 Subject: [PATCH 070/127] Avoid code duplication in UserView MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/user-view.ignore | 1 + .../web/activity_pub/views/user_view.ex | 74 ++++++++----------- 2 files changed, 32 insertions(+), 43 deletions(-) create mode 100644 changelog.d/user-view.ignore diff --git a/changelog.d/user-view.ignore b/changelog.d/user-view.ignore new file mode 100644 index 000000000..37e9a7e09 --- /dev/null +++ b/changelog.d/user-view.ignore @@ -0,0 +1 @@ +Avoid code duplication in UserView diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 4362db324..9eb8bb849 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -35,32 +35,14 @@ defmodule Pleroma.Web.ActivityPub.UserView do def render("endpoints.json", _), do: %{} def render("service.json", %{user: user}) do - {:ok, _, public_key} = Keys.keys_from_pem(user.keys) - public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key) - public_key = :public_key.pem_encode([public_key]) - - endpoints = render("endpoints.json", %{user: user}) - - %{ - "id" => user.ap_id, + Map.merge(common_actor_fields(user), %{ "type" => "Application", - "following" => "#{user.ap_id}/following", - "followers" => "#{user.ap_id}/followers", - "inbox" => "#{user.ap_id}/inbox", - "outbox" => "#{user.ap_id}/outbox", "name" => "Pleroma", "summary" => "An internal service actor for this Pleroma instance. No user-serviceable parts inside.", - "url" => user.ap_id, "manuallyApprovesFollowers" => false, - "publicKey" => %{ - "id" => "#{user.ap_id}#main-key", - "owner" => user.ap_id, - "publicKeyPem" => public_key - }, - "endpoints" => endpoints, "invisible" => User.invisible?(user) - } + }) |> Map.merge(Utils.make_json_ld_header()) end @@ -77,13 +59,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do end def render("user.json", %{user: user}) do - {:ok, _, public_key} = Keys.keys_from_pem(user.keys) - public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key) - public_key = :public_key.pem_encode([public_key]) user = User.sanitize_html(user) - endpoints = render("endpoints.json", %{user: user}) - emoji_tags = Transmogrifier.take_emoji_tags(user) fields = Enum.map(user.fields, &Map.put(&1, "type", "PropertyValue")) @@ -102,25 +79,9 @@ defmodule Pleroma.Web.ActivityPub.UserView do do: Date.to_iso8601(user.birthday), else: nil - %{ - "id" => user.ap_id, - "type" => user.actor_type, - "following" => "#{user.ap_id}/following", - "followers" => "#{user.ap_id}/followers", - "inbox" => "#{user.ap_id}/inbox", - "outbox" => "#{user.ap_id}/outbox", + Map.merge(common_actor_fields(user), %{ "featured" => "#{user.ap_id}/collections/featured", "preferredUsername" => user.nickname, - "name" => user.name, - "summary" => user.bio, - "url" => user.ap_id, - "manuallyApprovesFollowers" => user.is_locked, - "publicKey" => %{ - "id" => "#{user.ap_id}#main-key", - "owner" => user.ap_id, - "publicKeyPem" => public_key - }, - "endpoints" => endpoints, "attachment" => fields, "tag" => emoji_tags, # Note: key name is indeed "discoverable" (not an error) @@ -130,7 +91,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do "vcard:bday" => birthday, "webfinger" => "acct:#{User.full_nickname(user)}", "published" => Pleroma.Web.CommonAPI.Utils.to_masto_date(user.inserted_at) - } + }) |> Map.merge( maybe_make_image( &User.avatar_url/2, @@ -309,6 +270,33 @@ defmodule Pleroma.Web.ActivityPub.UserView do |> Map.merge(Utils.make_json_ld_header()) end + defp common_actor_fields(%User{} = user) do + endpoints = render("endpoints.json", %{user: user}) + + {:ok, _, public_key} = Keys.keys_from_pem(user.keys) + public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key) + public_key = :public_key.pem_encode([public_key]) + + %{ + "id" => user.ap_id, + "type" => user.actor_type, + "following" => "#{user.ap_id}/following", + "followers" => "#{user.ap_id}/followers", + "inbox" => "#{user.ap_id}/inbox", + "outbox" => "#{user.ap_id}/outbox", + "name" => user.name, + "summary" => user.bio, + "url" => user.ap_id, + "manuallyApprovesFollowers" => user.is_locked, + "endpoints" => endpoints, + "publicKey" => %{ + "id" => "#{user.ap_id}#main-key", + "owner" => user.ap_id, + "publicKeyPem" => public_key + } + } + end + defp maybe_put_total_items(map, false, _total), do: map defp maybe_put_total_items(map, true, total) do From 0592f111f6bc337256786a999252fe07bb80d4fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 6 Mar 2026 17:28:47 +0100 Subject: [PATCH 071/127] update tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../integration/mastodon_websocket_test.exs | 2 +- .../pleroma/web/activity_pub/activity_pub_test.exs | 2 +- test/pleroma/web/activity_pub/publisher_test.exs | 2 +- .../web/activity_pub/transmogrifier_test.exs | 2 +- test/pleroma/web/activity_pub/visibility_test.exs | 2 +- test/pleroma/web/common_api/utils_test.exs | 4 ++-- test/pleroma/web/common_api_test.exs | 2 +- .../controllers/account_controller_test.exs | 2 +- .../controllers/timeline_controller_test.exs | 12 ++++++------ .../web/mastodon_api/views/list_view_test.exs | 14 +++++++++----- .../web/mastodon_api/views/status_view_test.exs | 2 +- test/pleroma/web/streamer_test.exs | 10 +++++----- 12 files changed, 30 insertions(+), 26 deletions(-) diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs index 88f32762d..47f6f5f76 100644 --- a/test/pleroma/integration/mastodon_websocket_test.exs +++ b/test/pleroma/integration/mastodon_websocket_test.exs @@ -363,7 +363,7 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do test "accepts the 'list' stream", %{token: token, user: user} do posting_user = insert(:user) - {:ok, list} = Pleroma.List.create("test", user) + {:ok, list} = Pleroma.List.create(%{title: "test"}, user) Pleroma.List.follow(list, posting_user) assert {:ok, _} = start_socket("?stream=list&access_token=#{token.token}&list=#{list.id}") diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs index 73f53db56..a390cdfa8 100644 --- a/test/pleroma/web/activity_pub/activity_pub_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_test.exs @@ -1754,7 +1754,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do test "fetch_activities/2 returns activities addressed to a list " do user = insert(:user) member = insert(:user) - {:ok, list} = Pleroma.List.create("foo", user) + {:ok, list} = Pleroma.List.create(%{title: "foo"}, user) {:ok, list} = Pleroma.List.follow(list, member) {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) diff --git a/test/pleroma/web/activity_pub/publisher_test.exs b/test/pleroma/web/activity_pub/publisher_test.exs index 1f9e0bfe5..c0908655a 100644 --- a/test/pleroma/web/activity_pub/publisher_test.exs +++ b/test/pleroma/web/activity_pub/publisher_test.exs @@ -334,7 +334,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do test "activity with BCC is published to a list member." do actor = insert(:user) - {:ok, list} = Pleroma.List.create("list", actor) + {:ok, list} = Pleroma.List.create(%{title: "list"}, actor) list_member = insert(:user, %{local: false}) Pleroma.List.follow(list, list_member) diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs index b54196a3c..1e9be54ea 100644 --- a/test/pleroma/web/activity_pub/transmogrifier_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs @@ -581,7 +581,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do test "it strips BCC field" do user = insert(:user) - {:ok, list} = Pleroma.List.create("foo", user) + {:ok, list} = Pleroma.List.create(%{title: "foo"}, user) {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) diff --git a/test/pleroma/web/activity_pub/visibility_test.exs b/test/pleroma/web/activity_pub/visibility_test.exs index fd3dc83a1..f92f2df16 100644 --- a/test/pleroma/web/activity_pub/visibility_test.exs +++ b/test/pleroma/web/activity_pub/visibility_test.exs @@ -17,7 +17,7 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do following = insert(:user) unrelated = insert(:user) {:ok, following, user} = Pleroma.User.follow(following, user) - {:ok, list} = Pleroma.List.create("foo", user) + {:ok, list} = Pleroma.List.create(%{title: "foo"}, user) Pleroma.List.follow(list, unrelated) diff --git a/test/pleroma/web/common_api/utils_test.exs b/test/pleroma/web/common_api/utils_test.exs index 27b1da1e3..d0cbc3111 100644 --- a/test/pleroma/web/common_api/utils_test.exs +++ b/test/pleroma/web/common_api/utils_test.exs @@ -647,7 +647,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do describe "maybe_add_list_data/3" do test "adds list params when found user list" do user = insert(:user) - {:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", user) + {:ok, %Pleroma.List{} = list} = Pleroma.List.create(%{title: "title"}, user) assert Utils.maybe_add_list_data(%{additional: %{}, object: %{}}, user, {:list, list.id}) == %{ @@ -658,7 +658,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do test "returns original params when list not found" do user = insert(:user) - {:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", insert(:user)) + {:ok, %Pleroma.List{} = list} = Pleroma.List.create(%{title: "title"}, insert(:user)) assert Utils.maybe_add_list_data(%{additional: %{}, object: %{}}, user, {:list, list.id}) == %{additional: %{}, object: %{}} diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index 4eb057712..017fac696 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -759,7 +759,7 @@ defmodule Pleroma.Web.CommonAPITest do test "it allows to address a list" do user = insert(:user) - {:ok, list} = Pleroma.List.create("foo", user) + {:ok, list} = Pleroma.List.create(%{title: "foo"}, user) {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs index ea98b53a8..fb84569bc 100644 --- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs @@ -1815,7 +1815,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do test "returns lists to which the account belongs" do %{user: user, conn: conn} = oauth_access(["read:lists"]) other_user = insert(:user) - assert {:ok, %Pleroma.List{id: _list_id} = list} = Pleroma.List.create("Test List", user) + assert {:ok, %Pleroma.List{id: _list_id} = list} = Pleroma.List.create(%{title: "Test List"}, user) {:ok, %{following: _following}} = Pleroma.List.follow(list, other_user) assert [%{"id" => _list_id, "title" => "Test List"}] = diff --git a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs index 9808652d2..b685f4ff8 100644 --- a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs @@ -631,7 +631,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do {:ok, activity_two} = CommonAPI.post(other_user, %{status: "Marisa is stupid."}) {:ok, _} = CommonAPI.repeat(activity_one.id, other_user) - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) {:ok, list} = Pleroma.List.follow(list, other_user) conn = get(conn, "/api/v1/timelines/list/#{list.id}") @@ -643,7 +643,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do test "works with pagination", %{user: user, conn: conn} do other_user = insert(:user) - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) {:ok, list} = Pleroma.List.follow(list, other_user) Enum.each(1..30, fn i -> @@ -669,7 +669,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do other_user = insert(:user) {:ok, _activity_one} = CommonAPI.post(user, %{status: "Marisa is cute."}) {:ok, activity_two} = CommonAPI.post(other_user, %{status: "Marisa is cute."}) - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) {:ok, list} = Pleroma.List.follow(list, other_user) conn = get(conn, "/api/v1/timelines/list/#{list.id}") @@ -692,7 +692,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do visibility: "private" }) - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) {:ok, list} = Pleroma.List.follow(list, other_user) conn = get(conn, "/api/v1/timelines/list/#{list.id}") @@ -710,7 +710,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do {:ok, _} = CommonAPI.react_with_emoji(activity.id, user3, "🎅") User.mute(user, user3) - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) {:ok, list} = Pleroma.List.follow(list, user2) result = @@ -741,7 +741,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do end test "filtering", %{user: user, conn: conn} do - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) local_user = insert(:user) {:ok, local_activity} = CommonAPI.post(local_user, %{status: "Marisa is stupid."}) diff --git a/test/pleroma/web/mastodon_api/views/list_view_test.exs b/test/pleroma/web/mastodon_api/views/list_view_test.exs index bbf87bab2..ae0593b6b 100644 --- a/test/pleroma/web/mastodon_api/views/list_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/list_view_test.exs @@ -10,11 +10,12 @@ defmodule Pleroma.Web.MastodonAPI.ListViewTest do test "show" do user = insert(:user) title = "mortal enemies" - {:ok, list} = Pleroma.List.create(title, user) + {:ok, list} = Pleroma.List.create(%{title: title}, user) expected = %{ id: to_string(list.id), - title: title + title: title, + exclusive: false } assert expected == ListView.render("show.json", %{list: list}) @@ -23,10 +24,13 @@ defmodule Pleroma.Web.MastodonAPI.ListViewTest do test "index" do user = insert(:user) - {:ok, list} = Pleroma.List.create("my list", user) - {:ok, list2} = Pleroma.List.create("cofe", user) + {:ok, list} = Pleroma.List.create(%{title: "my list", exclusive: false}, user) + {:ok, list2} = Pleroma.List.create(%{title: "cofe", exclusive: true}, user) - assert [%{id: _, title: "my list"}, %{id: _, title: "cofe"}] = + assert [ + %{id: _, title: "my list", exclusive: false}, + %{id: _, title: "cofe", exclusive: true} + ] = ListView.render("index.json", lists: [list, list2]) end end 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 73cab817b..76123cd0f 100644 --- a/test/pleroma/web/mastodon_api/views/status_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs @@ -909,7 +909,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do test "visibility/list" do user = insert(:user) - {:ok, list} = Pleroma.List.create("foo", user) + {:ok, list} = Pleroma.List.create(%{title: "foo"}, user) {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) diff --git a/test/pleroma/web/streamer_test.exs b/test/pleroma/web/streamer_test.exs index f5008f6b9..b26bd1847 100644 --- a/test/pleroma/web/streamer_test.exs +++ b/test/pleroma/web/streamer_test.exs @@ -220,7 +220,7 @@ defmodule Pleroma.Web.StreamerTest do } do %{token: read_lists_token} = oauth_access(["read:lists"], user: user) %{token: invalid_token} = oauth_access(["irrelevant:scope"], user: user) - {:ok, list} = List.create("Test", user) + {:ok, list} = List.create(%{title: "Test"}, user) assert {:error, _} = Streamer.get_topic("list:#{list.id}", user, read_oauth_token) @@ -233,7 +233,7 @@ defmodule Pleroma.Web.StreamerTest do test "disallows list stream that are not owned by the user", %{user: user, token: oauth_token} do another_user = insert(:user) - {:ok, list} = List.create("Test", another_user) + {:ok, list} = List.create(%{title: "Test"}, another_user) assert {:error, _} = Streamer.get_topic("list:#{list.id}", user, oauth_token) assert {:error, _} = Streamer.get_topic("list", user, oauth_token, %{"list" => list.id}) @@ -803,7 +803,7 @@ defmodule Pleroma.Web.StreamerTest do {:ok, user_a, user_b} = User.follow(user_a, user_b) - {:ok, list} = List.create("Test", user_a) + {:ok, list} = List.create(%{title: "Test"}, user_a) {:ok, list} = List.follow(list, user_b) Streamer.get_topic_and_add_socket("list", user_a, user_a_token, %{"list" => list.id}) @@ -820,7 +820,7 @@ defmodule Pleroma.Web.StreamerTest do test "it doesn't send unwanted private posts to list", %{user: user_a, token: user_a_token} do user_b = insert(:user) - {:ok, list} = List.create("Test", user_a) + {:ok, list} = List.create(%{title: "Test"}, user_a) {:ok, list} = List.follow(list, user_b) Streamer.get_topic_and_add_socket("list", user_a, user_a_token, %{"list" => list.id}) @@ -839,7 +839,7 @@ defmodule Pleroma.Web.StreamerTest do {:ok, user_a, user_b} = User.follow(user_a, user_b) - {:ok, list} = List.create("Test", user_a) + {:ok, list} = List.create(%{title: "Test"}, user_a) {:ok, list} = List.follow(list, user_b) Streamer.get_topic_and_add_socket("list", user_a, user_a_token, %{"list" => list.id}) From 4e1ba489ec087b980e61ad705ab966953946ee90 Mon Sep 17 00:00:00 2001 From: shibao Date: Sun, 8 Mar 2026 11:16:33 +0000 Subject: [PATCH 072/127] fix 404s for missing static files --- lib/pleroma/web/plugs/instance_static.ex | 35 +++++++++++++-- .../web/plugs/instance_static_test.exs | 43 +++++++++++++++++++ 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/plugs/instance_static.ex b/lib/pleroma/web/plugs/instance_static.ex index d2a674d39..948188287 100644 --- a/lib/pleroma/web/plugs/instance_static.ex +++ b/lib/pleroma/web/plugs/instance_static.ex @@ -4,7 +4,7 @@ defmodule Pleroma.Web.Plugs.InstanceStatic do require Pleroma.Constants - import Plug.Conn, only: [put_resp_header: 3] + import Plug.Conn, only: [put_resp_header: 3, put_status: 2, send_resp: 3, halt: 1] @moduledoc """ This is a shim to call `Plug.Static` but with runtime `from` configuration. @@ -51,10 +51,37 @@ defmodule Pleroma.Web.Plugs.InstanceStatic do |> Map.put(:from, from) |> Map.put(:content_types, false) - conn = set_content_type(conn, conn.request_path) + conn = + conn + |> set_content_type(conn.request_path) + |> Plug.Static.call(opts) - # Call Plug.Static with our sanitized content-type - Plug.Static.call(conn, opts) + if conn.halted do + conn + else + path = String.trim_leading(conn.request_path, "/") + + if not File.exists?(file_path(path)) do + conn + |> put_status(:not_found) + |> send_404() + |> halt() + else + conn + end + end + end + + defp send_404(conn) do + if String.ends_with?(String.downcase(conn.request_path), ".json") do + conn + |> put_resp_header("content-type", "application/json") + |> send_resp(404, Jason.encode!(%{error: "not found"})) + else + conn + |> put_resp_header("content-type", "text/plain") + |> send_resp(404, "Not found") + end end defp set_content_type(conn, "/emoji/" <> filepath) do diff --git a/test/pleroma/web/plugs/instance_static_test.exs b/test/pleroma/web/plugs/instance_static_test.exs index b5a5a3334..017c49d1e 100644 --- a/test/pleroma/web/plugs/instance_static_test.exs +++ b/test/pleroma/web/plugs/instance_static_test.exs @@ -137,4 +137,47 @@ defmodule Pleroma.Web.Plugs.InstanceStaticTest do # It should be preserved because "image" is in the allowed_mime_types list assert content_type == "image/jpeg" end + + describe "404s for missing files in static-only paths" do + test "returns 404 for non-existent static-only JSON files" do + conn = get(build_conn(), "/static/non-existent.json") + + assert conn.status == 404 + assert ["application/json"] = get_resp_header(conn, "content-type") + assert Jason.decode!(conn.resp_body) == %{"error" => "not found"} + end + + test "returns 404 for non-existent static-only non-JSON files" do + conn = get(build_conn(), "/static/non-existent.txt") + + assert conn.status == 404 + assert conn.resp_body == "Not found" + assert ["text/plain"] = get_resp_header(conn, "content-type") + end + + test "returns 404 for non-existent .css files" do + conn = get(build_conn(), "/static/non-existent.css") + + assert conn.status == 404 + assert conn.resp_body == "Not found" + # Verifies that we forced text/plain for the error body, even though the path was .css + assert ["text/plain"] = get_resp_header(conn, "content-type") + end + + test "returns 404 for non-existent files without an extension" do + conn = get(build_conn(), "/static/non-existent") + + assert conn.status == 404 + assert conn.resp_body == "Not found" + assert ["text/plain"] = get_resp_header(conn, "content-type") + end + + test "returns 200 (falls through to SPA) for non-static-only paths" do + # /some-route is NOT in static_only_files, so it should still fall through to the SPA. + conn = get(build_conn(), "/some-route") + + assert conn.status == 200 + assert ["text/html; charset=utf-8"] = get_resp_header(conn, "content-type") + end + end end From bceb28b9416c34e066c31d5bbf4dac4366aa98ff Mon Sep 17 00:00:00 2001 From: shibao Date: Sun, 8 Mar 2026 11:19:14 +0000 Subject: [PATCH 073/127] add changelog note for missing static files fix --- changelog.d/missing-static-file.fix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/missing-static-file.fix diff --git a/changelog.d/missing-static-file.fix b/changelog.d/missing-static-file.fix new file mode 100644 index 000000000..c7ef805aa --- /dev/null +++ b/changelog.d/missing-static-file.fix @@ -0,0 +1 @@ +Fix 404 error codes for missing static files From a0131ff7338b34103e63a635f3778e5b2c58d8e9 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sun, 8 Mar 2026 21:19:49 +0100 Subject: [PATCH 074/127] credo: fix ordering of aliases missed in pleroma/pleroma!7841 --- changelog.d/credo-aliases-sort-fixes.skip | 0 lib/pleroma/web/o_auth/token_controller.ex | 2 +- lib/pleroma/web/pleroma_api/controllers/token_controller.ex | 2 +- lib/pleroma/web/preload/providers/instance.ex | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 changelog.d/credo-aliases-sort-fixes.skip diff --git a/changelog.d/credo-aliases-sort-fixes.skip b/changelog.d/credo-aliases-sort-fixes.skip new file mode 100644 index 000000000..e69de29bb diff --git a/lib/pleroma/web/o_auth/token_controller.ex b/lib/pleroma/web/o_auth/token_controller.ex index 37d08eea3..d2313a9cc 100644 --- a/lib/pleroma/web/o_auth/token_controller.ex +++ b/lib/pleroma/web/o_auth/token_controller.ex @@ -7,8 +7,8 @@ defmodule Pleroma.Web.OAuth.TokenController do alias Pleroma.User alias Pleroma.Web.OAuth.Token - alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.OAuth.TokenView + alias Pleroma.Web.Plugs.OAuthScopesPlug require Logger diff --git a/lib/pleroma/web/pleroma_api/controllers/token_controller.ex b/lib/pleroma/web/pleroma_api/controllers/token_controller.ex index 581dd569a..10eeb907e 100644 --- a/lib/pleroma/web/pleroma_api/controllers/token_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/token_controller.ex @@ -7,8 +7,8 @@ defmodule Pleroma.Web.PleromaAPI.TokenController do alias Pleroma.User alias Pleroma.Web.OAuth.Token - alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.PleromaAPI.TokenView + alias Pleroma.Web.Plugs.OAuthScopesPlug require Logger diff --git a/lib/pleroma/web/preload/providers/instance.ex b/lib/pleroma/web/preload/providers/instance.ex index d5417af30..f2a60393f 100644 --- a/lib/pleroma/web/preload/providers/instance.ex +++ b/lib/pleroma/web/preload/providers/instance.ex @@ -5,9 +5,9 @@ defmodule Pleroma.Web.Preload.Providers.Instance do alias Pleroma.Web.MastodonAPI.InstanceView alias Pleroma.Web.Nodeinfo.Nodeinfo + alias Pleroma.Web.PleromaAPI.UtilView alias Pleroma.Web.Plugs.InstanceStatic alias Pleroma.Web.Preload.Providers.Provider - alias Pleroma.Web.PleromaAPI.UtilView @behaviour Provider @instance_url "/api/v1/instance" From 23cc812366cc6ec765bcad008a5165bdd8101a01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 19 Mar 2026 15:19:51 +0100 Subject: [PATCH 075/127] Restore embed route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk Assisted-by: your mother Signed-off-by: nicole mikołajczyk --- changelog.d/restore-embeds.fix | 1 + lib/pleroma/web/embed_controller.ex | 1 + lib/pleroma/web/router.ex | 2 ++ 3 files changed, 4 insertions(+) create mode 100644 changelog.d/restore-embeds.fix diff --git a/changelog.d/restore-embeds.fix b/changelog.d/restore-embeds.fix new file mode 100644 index 000000000..5a2a1c4fe --- /dev/null +++ b/changelog.d/restore-embeds.fix @@ -0,0 +1 @@ +Restore embeds route diff --git a/lib/pleroma/web/embed_controller.ex b/lib/pleroma/web/embed_controller.ex index 2ca4501a6..8420f17a5 100644 --- a/lib/pleroma/web/embed_controller.ex +++ b/lib/pleroma/web/embed_controller.ex @@ -20,6 +20,7 @@ defmodule Pleroma.Web.EmbedController do conn |> delete_resp_header("x-frame-options") |> delete_resp_header("content-security-policy") + |> put_layout({Pleroma.Web.LayoutView, :embed}) |> render("show.html", activity: activity, author: User.sanitize_html(author), diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index eea0a0912..40c6f6d68 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -1032,6 +1032,8 @@ defmodule Pleroma.Web.Router do pipe_through(:pleroma_html) post("/auth/password", OAuth.PasswordController, :request) + + get("/embed/:id", EmbedController, :show) end scope "/proxy/", Pleroma.Web do From dfaabb48ef162a07302580ee60f014fcf5cffd59 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 16 Oct 2025 15:03:54 +0200 Subject: [PATCH 076/127] Elixir 1.19: Fix typing violation on struct updates in Pleroma.Marker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit warning: a struct for Pleroma.Marker is expected on struct update: %Pleroma.Marker{marker | user: user} but got type: dynamic() where "marker" was given the type: # type: dynamic() # from: lib/pleroma/marker.ex {:ok, marker} when defining the variable "marker", you must also pattern match on "%Pleroma.Marker{}". hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of: user = some_function() %User{user | name: "John Doe"} it is enough to write: %User{} = user = some_function() %{user | name: "John Doe"} typing violation found at: │ 81 │ {:ok, marker} -> %__MODULE__{marker | user: user} │ ~ │ └─ lib/pleroma/marker.ex:81:24: Pleroma.Marker.get_marker/2 --- lib/pleroma/marker.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/marker.ex b/lib/pleroma/marker.ex index 68b054e4d..4f645240e 100644 --- a/lib/pleroma/marker.ex +++ b/lib/pleroma/marker.ex @@ -78,7 +78,7 @@ defmodule Pleroma.Marker do defp get_marker(user, timeline) do case Repo.find_resource(get_query(user, timeline)) do - {:ok, marker} -> %__MODULE__{marker | user: user} + {:ok, %__MODULE__{} = marker} -> %__MODULE__{marker | user: user} _ -> %__MODULE__{timeline: timeline, user_id: user.id} end end From 1b9cd83d88776f4bc04f15628406d1fdd2850d9c Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 16 Oct 2025 15:06:02 +0200 Subject: [PATCH 077/127] Elixir 1.19: Fix typing violation on struct updates in MFA.Changeset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit warning: a struct for Pleroma.MFA.Settings is expected on struct update: %Pleroma.MFA.Settings{settings | enabled: false} but got type: dynamic() where "settings" was given the type: # type: dynamic() # from: lib/pleroma/mfa/changeset.ex:11:14 settings = Pleroma.MFA.fetch_settings(Ecto.Changeset.apply_changes(changeset)) when defining the variable "settings", you must also pattern match on "%Pleroma.MFA.Settings{}". hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of: user = some_function() %User{user | name: "John Doe"} it is enough to write: %User{} = user = some_function() %{user | name: "John Doe"} typing violation found at: │ 17 │ put_change(changeset, %Settings{settings | enabled: false}) │ ~ │ └─ lib/pleroma/mfa/changeset.ex:17:29: Pleroma.MFA.Changeset.disable/2 --- warning: a struct for Pleroma.MFA.Settings is expected on struct update: %Pleroma.MFA.Settings{ settings | totp: %Pleroma.MFA.Settings.TOTP{confirmed: false, delivery_type: "app", secret: nil} } but got type: dynamic() where "settings" was given the type: # type: dynamic() # from: lib/pleroma/mfa/changeset.ex:23:74 %Pleroma.User{multi_factor_authentication_settings: settings} = user when defining the variable "settings", you must also pattern match on "%Pleroma.MFA.Settings{}". hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of: user = some_function() %User{user | name: "John Doe"} it is enough to write: %User{} = user = some_function() %{user | name: "John Doe"} typing violation found at: │ 25 │ |> put_change(%Settings{settings | totp: %Settings.TOTP{}}) │ ~ │ └─ lib/pleroma/mfa/changeset.ex:25:19: Pleroma.MFA.Changeset.disable_totp/1 --- lib/pleroma/mfa/changeset.ex | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/pleroma/mfa/changeset.ex b/lib/pleroma/mfa/changeset.ex index 3ec3cfe91..890cb2193 100644 --- a/lib/pleroma/mfa/changeset.ex +++ b/lib/pleroma/mfa/changeset.ex @@ -8,7 +8,7 @@ defmodule Pleroma.MFA.Changeset do alias Pleroma.User def disable(%Ecto.Changeset{} = changeset, force \\ false) do - settings = + %Settings{} = settings = changeset |> Ecto.Changeset.apply_changes() |> MFA.fetch_settings() @@ -20,20 +20,20 @@ defmodule Pleroma.MFA.Changeset do end end - def disable_totp(%User{multi_factor_authentication_settings: settings} = user) do + def disable_totp(%User{multi_factor_authentication_settings: %Settings{} = settings} = user) do user |> put_change(%Settings{settings | totp: %Settings.TOTP{}}) end - def confirm_totp(%User{multi_factor_authentication_settings: settings} = user) do - totp_settings = %Settings.TOTP{settings.totp | confirmed: true} + def confirm_totp(%User{multi_factor_authentication_settings: %Settings{} = settings} = user) do + totp_settings = %Settings.TOTP{%Settings.TOTP{} = settings.totp | confirmed: true} user |> put_change(%Settings{settings | totp: totp_settings, enabled: true}) end def setup_totp(%User{} = user, attrs) do - mfa_settings = MFA.fetch_settings(user) + %Settings{} = mfa_settings = MFA.fetch_settings(user) totp_settings = %Settings.TOTP{} @@ -46,7 +46,7 @@ defmodule Pleroma.MFA.Changeset do def cast_backup_codes(%User{} = user, codes) do user |> put_change(%Settings{ - user.multi_factor_authentication_settings + %Settings{} = user.multi_factor_authentication_settings | backup_codes: codes }) end From 5b6af83e8658905420bc9d4f3b25a34667c39d39 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 16 Oct 2025 15:09:03 +0200 Subject: [PATCH 078/127] Elixir 1.19: Fix typing violation on struct updates in Pleroma.Upload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit warning: a struct for Pleroma.Upload is expected on struct update: %Pleroma.Upload{ upload | path: case upload.path do x when x === false or x === nil -> <> x -> x end } but got type: dynamic() where "upload" was given the type: # type: dynamic() # from: lib/pleroma/upload.ex:95:24 {:ok, upload} <- prepare_upload(upload, opts) when defining the variable "upload", you must also pattern match on "%Pleroma.Upload{}". hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of: user = some_function() %User{user | name: "John Doe"} it is enough to write: %User{} = user = some_function() %{user | name: "John Doe"} typing violation found at: │ 96 │ upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"}, │ ~ │ └─ lib/pleroma/upload.ex:96:19: Pleroma.Upload.store/2 --- lib/pleroma/upload.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex index 06d8005bc..350ff6cb3 100644 --- a/lib/pleroma/upload.ex +++ b/lib/pleroma/upload.ex @@ -93,7 +93,7 @@ defmodule Pleroma.Upload do def store(upload, opts \\ []) do opts = get_opts(opts) - with {:ok, upload} <- prepare_upload(upload, opts), + with {:ok, %__MODULE__{} = upload} <- prepare_upload(upload, opts), upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"}, {:ok, upload} <- Pleroma.Upload.Filter.filter(opts.filters, upload), description = get_description(upload), From 19e05b4a7bc55447598e11bf140785ed5e3435f8 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 16 Oct 2025 15:09:51 +0200 Subject: [PATCH 079/127] Elixir 1.19: Fix typing violation on struct updates in Web.ApiSpec.Cast* MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit warning: a struct for Plug.Conn is expected on struct update: %Plug.Conn{conn | query_params: query_params} but got type: dynamic() where "conn" was given the type: # type: dynamic() # from: lib/pleroma/web/api_spec/cast_and_validate.ex:109:43 conn when defining the variable "conn", you must also pattern match on "%Plug.Conn{}". hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of: user = some_function() %User{user | name: "John Doe"} it is enough to write: %User{} = user = some_function() %{user | name: "John Doe"} typing violation found at: │ 133 │ conn = %Conn{conn | query_params: query_params} │ ~ │ └─ lib/pleroma/web/api_spec/cast_and_validate.ex:133:16: Pleroma.Web.ApiSpec.CastAndValidate.cast_and_validate/6 --- lib/pleroma/web/api_spec/cast_and_validate.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/api_spec/cast_and_validate.ex b/lib/pleroma/web/api_spec/cast_and_validate.ex index 672d1c4a1..57ea8e9b3 100644 --- a/lib/pleroma/web/api_spec/cast_and_validate.ex +++ b/lib/pleroma/web/api_spec/cast_and_validate.ex @@ -106,7 +106,7 @@ defmodule Pleroma.Web.ApiSpec.CastAndValidate do OpenApiSpex.cast_and_validate(spec, operation, conn, content_type, cast_opts) end - defp cast_and_validate(spec, operation, conn, content_type, false = _strict, cast_opts) do + defp cast_and_validate(spec, operation, %Conn{} = conn, content_type, false = _strict, cast_opts) do case OpenApiSpex.cast_and_validate(spec, operation, conn, content_type) do {:ok, conn} -> {:ok, conn} From 958d250fe52711e3d69bba0eba77b06075ee8a80 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 16 Oct 2025 15:11:33 +0200 Subject: [PATCH 080/127] Elixir 1.19: Fix typing violation on struct updates in Web.ApiSpec.Rend* MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit warning: a struct for OpenApiSpex.Cast.Error is expected on struct update: %OpenApiSpex.Cast.Error{err | name: err.value} but got type: dynamic(%{..., name: nil, reason: :invalid_enum}) where "err" was given the type: # type: dynamic(%{..., name: nil, reason: :invalid_enum}) # from: lib/pleroma/web/api_spec/render_error.ex:20:45 %{name: nil, reason: :invalid_enum} = err when defining the variable "err", you must also pattern match on "%OpenApiSpex.Cast.Error{}". hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of: user = some_function() %User{user | name: "John Doe"} it is enough to write: %User{} = user = some_function() %{user | name: "John Doe"} typing violation found at: │ 21 │ %OpenApiSpex.Cast.Error{err | name: err.value} │ ~ │ └─ lib/pleroma/web/api_spec/render_error.ex:21:11: Pleroma.Web.ApiSpec.RenderError.call/2 --- lib/pleroma/web/api_spec/render_error.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/api_spec/render_error.ex b/lib/pleroma/web/api_spec/render_error.ex index 3539af6e4..acf510774 100644 --- a/lib/pleroma/web/api_spec/render_error.ex +++ b/lib/pleroma/web/api_spec/render_error.ex @@ -17,10 +17,10 @@ defmodule Pleroma.Web.ApiSpec.RenderError do def call(conn, errors) do errors = Enum.map(errors, fn - %{name: nil, reason: :invalid_enum} = err -> + %OpenApiSpex.Cast.Error{name: nil, reason: :invalid_enum} = err -> %OpenApiSpex.Cast.Error{err | name: err.value} - %{name: nil} = err -> + %OpenApiSpex.Cast.Error{name: nil} = err -> %OpenApiSpex.Cast.Error{err | name: List.last(err.path)} err -> From 8417629b4b51a378e2dd6788e7f43512e97734bc Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 16 Oct 2025 15:12:41 +0200 Subject: [PATCH 081/127] Elixir 1.19: Fix typing violation on struct updates in CommonAPI.Activity* MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit warning: a struct for Pleroma.Web.CommonAPI.ActivityDraft is expected on struct update: %Pleroma.Web.CommonAPI.ActivityDraft{draft | object: object} but got type: dynamic() where "draft" was given the type: # type: dynamic() # from: lib/pleroma/web/common_api/activity_draft.ex:91:22 draft when defining the variable "draft", you must also pattern match on "%Pleroma.Web.CommonAPI.ActivityDraft{}". hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of: user = some_function() %User{user | name: "John Doe"} it is enough to write: %User{} = user = some_function() %{user | name: "John Doe"} typing violation found at: │ 102 │ %__MODULE__{draft | object: object} │ ~ │ └─ lib/pleroma/web/common_api/activity_draft.ex:102:5: Pleroma.Web.CommonAPI.ActivityDraft.listen_object/1 --- lib/pleroma/web/common_api/activity_draft.ex | 46 ++++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index c0b98508c..c5959276a 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -88,7 +88,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do |> validate() end - defp listen_object(draft) do + defp listen_object(%__MODULE__{} = draft) do object = draft.params |> Map.take([:album, :artist, :title, :length]) @@ -102,20 +102,20 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do %__MODULE__{draft | object: object} end - defp put_params(draft, params) do + defp put_params(%__MODULE__{} = draft, params) do params = Map.put_new(params, :in_reply_to_status_id, params[:in_reply_to_id]) %__MODULE__{draft | params: params} end - defp status(%{params: %{status: status}} = draft) do + defp status(%__MODULE__{params: %{status: status}} = draft) do %__MODULE__{draft | status: String.trim(status)} end - defp summary(%{params: params} = draft) do + defp summary(%__MODULE__{params: params} = draft) do %__MODULE__{draft | summary: Map.get(params, :spoiler_text, "")} end - defp full_payload(%{status: status, summary: summary} = draft) do + defp full_payload(%__MODULE__{status: status, summary: summary} = draft) do full_payload = String.trim(status <> summary) case Utils.validate_character_limit(full_payload, draft.attachments) do @@ -124,7 +124,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do end end - defp attachments(%{params: params} = draft) do + defp attachments(%__MODULE__{params: params} = draft) do attachments = Utils.attachments_from_ids(params, draft.user) draft = %__MODULE__{draft | attachments: attachments} @@ -134,9 +134,9 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do end end - defp in_reply_to(%{params: %{in_reply_to_status_id: ""}} = draft), do: draft + defp in_reply_to(%__MODULE__{params: %{in_reply_to_status_id: ""}} = draft), do: draft - defp in_reply_to(%{params: %{in_reply_to_status_id: id}} = draft) when is_binary(id) do + defp in_reply_to(%__MODULE__{params: %{in_reply_to_status_id: id}} = draft) when is_binary(id) do # If a post was deleted all its activities (except the newly added Delete) are purged too, # thus lookup by Create db ID will yield nil just as if it never existed in the first place. # @@ -166,13 +166,13 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do end end - defp in_reply_to(%{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} = draft) do + defp in_reply_to(%__MODULE__{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} = draft) do %__MODULE__{draft | in_reply_to: in_reply_to} end defp in_reply_to(draft), do: draft - defp quote_post(%{params: %{quoted_status_id: id}} = draft) when not_empty_string(id) do + defp quote_post(%__MODULE__{params: %{quoted_status_id: id}} = draft) when not_empty_string(id) do case Activity.get_by_id_with_object(id) do %Activity{} = activity -> %__MODULE__{draft | quote_post: activity} @@ -188,12 +188,12 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do defp quote_post(draft), do: draft - defp in_reply_to_conversation(draft) do + defp in_reply_to_conversation(%__MODULE__{} = draft) do in_reply_to_conversation = Participation.get(draft.params[:in_reply_to_conversation_id]) %__MODULE__{draft | in_reply_to_conversation: in_reply_to_conversation} end - defp visibility(%{params: params} = draft) do + defp visibility(%__MODULE__{params: params} = draft) do case CommonAPI.get_visibility(params, draft.in_reply_to, draft.in_reply_to_conversation) do {visibility, "direct"} when visibility != "direct" -> add_error(draft, dgettext("errors", "The message visibility must be direct")) @@ -226,14 +226,14 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do defp quoting_visibility(draft), do: draft - defp expires_at(draft) do + defp expires_at(%__MODULE__{} = draft) do case CommonAPI.check_expiry_date(draft.params[:expires_in]) do {:ok, expires_at} -> %__MODULE__{draft | expires_at: expires_at} {:error, message} -> add_error(draft, message) end end - defp poll(draft) do + defp poll(%__MODULE__{} = draft) do case Utils.make_poll_data(draft.params) do {:ok, {poll, poll_emoji}} -> %__MODULE__{draft | extra: poll, emoji: Map.merge(draft.emoji, poll_emoji)} @@ -243,7 +243,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do end end - defp content(%{mentions: mentions} = draft) do + defp content(%__MODULE__{mentions: mentions} = draft) do {content_html, mentioned_users, tags} = Utils.make_content_html(draft) mentioned_ap_ids = @@ -257,22 +257,22 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do %__MODULE__{draft | content_html: content_html, mentions: mentions, tags: tags} end - defp to_and_cc(draft) do + defp to_and_cc(%__MODULE__{} = draft) do {to, cc} = Utils.get_to_and_cc(draft) %__MODULE__{draft | to: to, cc: cc} end - defp context(draft) do + defp context(%__MODULE__{} = draft) do context = Utils.make_context(draft.in_reply_to, draft.in_reply_to_conversation) %__MODULE__{draft | context: context} end - defp sensitive(draft) do + defp sensitive(%__MODULE__{} = draft) do sensitive = draft.params[:sensitive] %__MODULE__{draft | sensitive: sensitive} end - defp language(draft) do + defp language(%__MODULE__{} = draft) do language = with language <- draft.params[:language], true <- good_locale_code?(language) do @@ -284,7 +284,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do %__MODULE__{draft | language: language} end - defp object(draft) do + defp object(%__MODULE__{} = draft) do emoji = Map.merge(Pleroma.Emoji.Formatter.get_emoji_map(draft.full_payload), draft.emoji) # Sometimes people create posts with subject containing emoji, @@ -328,12 +328,12 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do %__MODULE__{draft | object: object} end - defp preview?(draft) do + defp preview?(%__MODULE__{} = draft) do preview? = Pleroma.Web.Utils.Params.truthy_param?(draft.params[:preview]) %__MODULE__{draft | preview?: preview?} end - defp changes(draft) do + defp changes(%__MODULE__{} = draft) do direct? = draft.visibility == "direct" additional = %{"cc" => draft.cc, "directMessage" => direct?} @@ -359,7 +359,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do defp with_valid(%{valid?: true} = draft, func), do: func.(draft) defp with_valid(draft, _func), do: draft - defp add_error(draft, message) do + defp add_error(%__MODULE__{} = draft, message) do %__MODULE__{draft | valid?: false, errors: [message | draft.errors]} end From 93e8f9d7d1dcc4fca58538616d79538fe30d48f1 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 9 Jan 2026 16:40:19 +0100 Subject: [PATCH 082/127] Elixir 1.19: Fix typing violations in ActivityPubTest --- test/pleroma/web/activity_pub/activity_pub_test.exs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs index 37ec87a9b..13146619a 100644 --- a/test/pleroma/web/activity_pub/activity_pub_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_test.exs @@ -1524,8 +1524,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do %{test_file: test_file} end - test "strips / from filename", %{test_file: file} do - file = %Plug.Upload{file | filename: "../../../../../nested/bad.jpg"} + test "strips / from filename", %{test_file: %Plug.Upload{} = file} do + file = %{file | filename: "../../../../../nested/bad.jpg"} {:ok, %Object{} = object} = ActivityPub.upload(file) [%{"href" => href}] = object.data["url"] assert Regex.match?(~r"/bad.jpg$", href) @@ -1786,10 +1786,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do {:ok, list} = Pleroma.List.create(%{title: "foo"}, user) {:ok, list} = Pleroma.List.follow(list, member) - {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) + {:ok, %Activity{} = activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) activity = Repo.preload(activity, :bookmark) - activity = %Activity{activity | thread_muted?: !!activity.thread_muted?} + activity = %{activity | thread_muted?: !!activity.thread_muted?} assert ActivityPub.fetch_activities([], %{user: user}) == [activity] end @@ -1989,7 +1989,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do assert User.following?(follower, old_user) assert User.following?(follower_move_opted_out, old_user) - assert {:ok, activity} = ActivityPub.move(old_user, new_user) + assert {:ok, %Activity{} = activity} = ActivityPub.move(old_user, new_user) assert %Activity{ actor: ^old_ap_id, @@ -2021,7 +2021,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do assert User.following?(follower_move_opted_out, old_user) refute User.following?(follower_move_opted_out, new_user) - activity = %Activity{activity | object: nil} + activity = %{activity | object: nil} assert [%Notification{activity: ^activity}] = Notification.for_user(follower) From b8a66c22b369fe52ed2305ee37828cd4f4060b79 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 9 Jan 2026 16:42:26 +0100 Subject: [PATCH 083/127] Elixir 1.19: Fix typing violation in MediaControllerTest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit warning: a struct for Plug.Upload is expected on struct update: %Plug.Upload{image | filename: "../../../../../nested/file.jpg"} but got type: dynamic() where "image" was given the type: # type: dynamic() # from: test/pleroma/web/mastodon_api/controllers/media_controller_test.exs:132:42 %{conn: conn, image: image} when defining the variable "image", you must also pattern match on "%Plug.Upload{}". hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of: user = some_function() %User{user | name: "John Doe"} it is enough to write: %User{} = user = some_function() %{user | name: "John Doe"} typing violation found at: │ 133 │ image = %Plug.Upload{ │ ~ │ └─ test/pleroma/web/mastodon_api/controllers/media_controller_test.exs:133:15: Pleroma.Web.MastodonAPI.MediaControllerTest."test Upload media Do not allow nested filename"/1 --- .../web/mastodon_api/controllers/media_controller_test.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs index ae86078d7..fc083fd0e 100644 --- a/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs @@ -129,8 +129,8 @@ defmodule Pleroma.Web.MastodonAPI.MediaControllerTest do assert :ok == File.rm(Path.absname("test/tmp/large_binary.data")) end - test "Do not allow nested filename", %{conn: conn, image: image} do - image = %Plug.Upload{ + test "Do not allow nested filename", %{conn: conn, image: %Plug.Upload{} = image} do + image = %{ image | filename: "../../../../../nested/file.jpg" } From ec294b30c1beaab4ef88f3195de36bbbc88b9c24 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 9 Jan 2026 16:44:37 +0100 Subject: [PATCH 084/127] Elixir 1.19: Fix typing violation in RepoTest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit warning: a struct for Pleroma.Web.OAuth.Token is expected on struct update: %Pleroma.Web.OAuth.Token{Pleroma.Factory.insert(:oauth_token) | user: user} but got type: dynamic() you must assign "Pleroma.Factory.insert(:oauth_token)" to variable and pattern match on "%Pleroma.Web.OAuth.Token{}". hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of: user = some_function() %User{user | name: "John Doe"} it is enough to write: %User{} = user = some_function() %{user | name: "John Doe"} typing violation found at: │ 27 │ token = %Pleroma.Web.OAuth.Token{insert(:oauth_token) | user: user} │ ~ │ └─ test/pleroma/repo_test.exs:27:15: Pleroma.RepoTest."test get_assoc/2 get assoc from preloaded data"/1 --- test/pleroma/repo_test.exs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/pleroma/repo_test.exs b/test/pleroma/repo_test.exs index 9c0f5d028..721175bda 100644 --- a/test/pleroma/repo_test.exs +++ b/test/pleroma/repo_test.exs @@ -24,7 +24,8 @@ defmodule Pleroma.RepoTest do describe "get_assoc/2" do test "get assoc from preloaded data" do user = %User{name: "Agent Smith"} - token = %Pleroma.Web.OAuth.Token{insert(:oauth_token) | user: user} + %Pleroma.Web.OAuth.Token{} = token = insert(:oauth_token) + token = %{token | user: user} assert Repo.get_assoc(token, :user) == {:ok, user} end From f4c28392e1841dd3789a53f553a1f3d47510ddd3 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 9 Jan 2026 16:48:55 +0100 Subject: [PATCH 085/127] Elixir 1.19: Fix typing violation in MarkerTest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit warning: a struct for Pleroma.Marker is expected on struct update: %Pleroma.Marker{refresh_record(marker) | unread_count: 2} but got type: dynamic() where "marker" was given the type: # type: dynamic() # from: test/pleroma/marker_test.exs:35:14 marker = Pleroma.Factory.insert(:marker, user: user) you must assign "refresh_record(marker)" to variable and pattern match on "%Pleroma.Marker{}". hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of: user = some_function() %User{user | name: "John Doe"} it is enough to write: %User{} = user = some_function() %{user | name: "John Doe"} typing violation found at: │ 43 │ ) == [%Marker{refresh_record(marker) | unread_count: 2}] │ ~ │ └─ test/pleroma/marker_test.exs:43:20: Pleroma.MarkerTest."test get_markers/2 returns user markers"/1 --- test/pleroma/marker_test.exs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/pleroma/marker_test.exs b/test/pleroma/marker_test.exs index 819dde9be..7f573ac7a 100644 --- a/test/pleroma/marker_test.exs +++ b/test/pleroma/marker_test.exs @@ -36,11 +36,12 @@ defmodule Pleroma.MarkerTest do insert(:notification, user: user, activity: insert(:note_activity)) insert(:notification, user: user, activity: insert(:note_activity)) insert(:marker, timeline: "home", user: user) + %Marker{} = refreshed_marker = refresh_record(marker) assert Marker.get_markers( user, ["notifications"] - ) == [%Marker{refresh_record(marker) | unread_count: 2}] + ) == [%{refreshed_marker | unread_count: 2}] end end From f60a317c2faee2bda15b1d1fc4c6cc9ff7ff8fb3 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 13 Jan 2026 14:58:32 +0100 Subject: [PATCH 086/127] Elixir 1.19: Only match once on structs Second match is not needed and a simple Map update is recommended by the compiler --- lib/pleroma/marker.ex | 2 +- lib/pleroma/web/api_spec/render_error.ex | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/marker.ex b/lib/pleroma/marker.ex index 4f645240e..fab24d183 100644 --- a/lib/pleroma/marker.ex +++ b/lib/pleroma/marker.ex @@ -78,7 +78,7 @@ defmodule Pleroma.Marker do defp get_marker(user, timeline) do case Repo.find_resource(get_query(user, timeline)) do - {:ok, %__MODULE__{} = marker} -> %__MODULE__{marker | user: user} + {:ok, %__MODULE__{} = marker} -> %{marker | user: user} _ -> %__MODULE__{timeline: timeline, user_id: user.id} end end diff --git a/lib/pleroma/web/api_spec/render_error.ex b/lib/pleroma/web/api_spec/render_error.ex index acf510774..2ba76f250 100644 --- a/lib/pleroma/web/api_spec/render_error.ex +++ b/lib/pleroma/web/api_spec/render_error.ex @@ -18,10 +18,10 @@ defmodule Pleroma.Web.ApiSpec.RenderError do errors = Enum.map(errors, fn %OpenApiSpex.Cast.Error{name: nil, reason: :invalid_enum} = err -> - %OpenApiSpex.Cast.Error{err | name: err.value} + %{err | name: err.value} %OpenApiSpex.Cast.Error{name: nil} = err -> - %OpenApiSpex.Cast.Error{err | name: List.last(err.path)} + %{err | name: List.last(err.path)} err -> err From 531041041a9137151022a6877e0bf1c24c15eff2 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 9 Jan 2026 17:01:36 +0100 Subject: [PATCH 087/127] Elixir 1.19: Fix deprecation warning when invoking ParallelCompiler warning: you must pass return_diagnostics: true when invoking Kernel.ParallelCompiler functions (elixir 1.19.5) lib/kernel/parallel_compiler.ex:324: Kernel.ParallelCompiler.spawn_workers/3 (pleroma 2.10.0-7-ga7a74d5e-elixir-1-19+test) lib/pleroma/html.ex:13: Pleroma.HTML.compile_scrubbers/0 (pleroma 2.10.0-7-ga7a74d5e-elixir-1-19+test) lib/pleroma/application.ex:48: Pleroma.Application.start/2 (kernel 10.5) application_master.erl:299: :application_master.start_it_old/4 warning: you must pass return_diagnostics: true when invoking Kernel.ParallelCompiler functions (elixir 1.19.5) lib/kernel/parallel_compiler.ex:324: Kernel.ParallelCompiler.spawn_workers/3 (pleroma 2.10.0-7-ga7a74d5e-elixir-1-19+test) lib/pleroma/application.ex:121: Pleroma.Application.load_custom_modules/0 (pleroma 2.10.0-7-ga7a74d5e-elixir-1-19+test) lib/pleroma/application.ex:60: Pleroma.Application.start/2 (kernel 10.5) application_master.erl:299: :application_master.start_it_old/4 --- lib/pleroma/utils.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/utils.ex b/lib/pleroma/utils.ex index 73001c987..61d122a47 100644 --- a/lib/pleroma/utils.ex +++ b/lib/pleroma/utils.ex @@ -17,7 +17,7 @@ defmodule Pleroma.Utils do dir |> File.ls!() |> Enum.map(&Path.join(dir, &1)) - |> Kernel.ParallelCompiler.compile() + |> Kernel.ParallelCompiler.compile(return_diagnostics: true) end @doc """ From bf86768e889b626ed4c8c8132c15fa189562bf30 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 9 Jan 2026 17:05:31 +0100 Subject: [PATCH 088/127] Elixir 1.19: Fix ConfigDBTest regex tests It is not possible match regexes anymore as this worked by accident previously. Instead, at least check that the sources of the regex (the regex itself) match. Notice the +1 difference in the regex Reference below. 1) test to_elixir_types/1 complex keyword with sigil (Pleroma.ConfigDBTest) test/pleroma/config_db_test.exs:460 Assertion with == failed code: assert ConfigDB.to_elixir_types([ %{"tuple" => [":federated_timeline_removal", []]}, %{"tuple" => [":reject", ["~r/comp[lL][aA][iI][nN]er/"]]}, %{"tuple" => [":replace", []]} ]) == [federated_timeline_removal: [], reject: [~r/comp[lL][aA][iI][nN]er/], replace: []] left: [federated_timeline_removal: [], reject: [%Regex{opts: [], re_pattern: {:re_pattern, 0, 0, 0, #Reference<0.230935836.591265794.259515>}, source: "comp[lL][aA][iI][nN]er"}], replace: []] right: [federated_timeline_removal: [], reject: [%Regex{opts: [], re_pattern: {:re_pattern, 0, 0, 0, #Reference<0.230935836.591265794.259516>}, source: "comp[lL][aA][iI][nN]er"}], replace: []] stacktrace: test/pleroma/config_db_test.exs:461: (test) --- test/pleroma/config_db_test.exs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/pleroma/config_db_test.exs b/test/pleroma/config_db_test.exs index d68e4e6fa..59beba48e 100644 --- a/test/pleroma/config_db_test.exs +++ b/test/pleroma/config_db_test.exs @@ -273,24 +273,24 @@ defmodule Pleroma.ConfigDBTest do end test "sigil" do - assert ConfigDB.to_elixir_types("~r[comp[lL][aA][iI][nN]er]") == ~r/comp[lL][aA][iI][nN]er/ + assert ConfigDB.to_elixir_types("~r[comp[lL][aA][iI][nN]er]").source == ~r/comp[lL][aA][iI][nN]er/.source end test "link sigil" do - assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/") == ~r/https:\/\/example.com/ + assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/").source == ~r/https:\/\/example.com/.source end test "link sigil with um modifiers" do - assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/um") == - ~r/https:\/\/example.com/um + assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/um").source == + ~r/https:\/\/example.com/um.source end test "link sigil with i modifier" do - assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/i") == ~r/https:\/\/example.com/i + assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/i").source == ~r/https:\/\/example.com/i.source end test "link sigil with s modifier" do - assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/s") == ~r/https:\/\/example.com/s + assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/s").source == ~r/https:\/\/example.com/s.source end test "raise if valid delimiter not found" do @@ -460,11 +460,11 @@ defmodule Pleroma.ConfigDBTest do test "complex keyword with sigil" do assert ConfigDB.to_elixir_types([ %{"tuple" => [":federated_timeline_removal", []]}, - %{"tuple" => [":reject", ["~r/comp[lL][aA][iI][nN]er/"]]}, + %{"tuple" => [":reject", [~r/comp[lL][aA][iI][nN]er/.source]]}, %{"tuple" => [":replace", []]} ]) == [ federated_timeline_removal: [], - reject: [~r/comp[lL][aA][iI][nN]er/], + reject: [~r/comp[lL][aA][iI][nN]er/.source], replace: [] ] end From 6a3b5b3218f2b28395d3e949b091dd0af14bdca7 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 9 Jan 2026 17:13:10 +0100 Subject: [PATCH 089/127] Elixir 1.19: Fix MRFTest regex tests It is no longer possible to match regexes. Instead at least match that the sources of the regexes (regexes themselves) are the same. Notice the +1 Reference number below. 2) test subdomain_match/2 wildcard domains with one subdomain (Pleroma.Web.ActivityPub.MRFTest) test/pleroma/web/activity_pub/mrf_test.exs:36 Assertion with == failed code: assert regexes == [~r/^(.*\.)*unsafe.tld$/i] left: [%Regex{opts: [:caseless], re_pattern: {:re_pattern, 1, 0, 0, #Reference<0.378940835.3277193222.129648>}, source: "^(.*\\.)*unsafe.tld$"}] right: [%Regex{opts: [:caseless], re_pattern: {:re_pattern, 1, 0, 0, #Reference<0.378940835.3277193222.129649>}, source: "^(.*\\.)*unsafe.tld$"}] stacktrace: test/pleroma/web/activity_pub/mrf_test.exs:39: (test) --- test/pleroma/web/activity_pub/mrf_test.exs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/test/pleroma/web/activity_pub/mrf_test.exs b/test/pleroma/web/activity_pub/mrf_test.exs index 25548e3da..6656a01b8 100644 --- a/test/pleroma/web/activity_pub/mrf_test.exs +++ b/test/pleroma/web/activity_pub/mrf_test.exs @@ -11,17 +11,21 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do alias Pleroma.Web.ActivityPub.MRF test "subdomains_regex/1" do - assert MRF.subdomains_regex(["unsafe.tld", "*.unsafe.tld"]) == [ - ~r/^unsafe.tld$/i, - ~r/^(.*\.)*unsafe.tld$/i + regexes = MRF.subdomains_regex(["unsafe.tld", "*.unsafe.tld"]) + matchable_regexes = Enum.map(regexes, fn r -> r.source end) + + assert matchable_regexes == [ + ~r/^unsafe.tld$/i.source, + ~r/^(.*\.)*unsafe.tld$/i.source ] end describe "subdomain_match/2" do test "common domains" do regexes = MRF.subdomains_regex(["unsafe.tld", "unsafe2.tld"]) + matchable_regexes = Enum.map(regexes, fn r -> r.source end) - assert regexes == [~r/^unsafe.tld$/i, ~r/^unsafe2.tld$/i] + assert matchable_regexes == [~r/^unsafe.tld$/i.source, ~r/^unsafe2.tld$/i.source] assert MRF.subdomain_match?(regexes, "unsafe.tld") assert MRF.subdomain_match?(regexes, "unsafe2.tld") @@ -31,8 +35,9 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do test "wildcard domains with one subdomain" do regexes = MRF.subdomains_regex(["*.unsafe.tld"]) + matchable_regexes = Enum.map(regexes, fn r -> r.source end) - assert regexes == [~r/^(.*\.)*unsafe.tld$/i] + assert matchable_regexes == [~r/^(.*\.)*unsafe.tld$/i.source] assert MRF.subdomain_match?(regexes, "unsafe.tld") assert MRF.subdomain_match?(regexes, "sub.unsafe.tld") @@ -42,8 +47,9 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do test "wildcard domains with two subdomains" do regexes = MRF.subdomains_regex(["*.unsafe.tld"]) + matchable_regexes = Enum.map(regexes, fn r -> r.source end) - assert regexes == [~r/^(.*\.)*unsafe.tld$/i] + assert matchable_regexes == [~r/^(.*\.)*unsafe.tld$/i.source] assert MRF.subdomain_match?(regexes, "unsafe.tld") assert MRF.subdomain_match?(regexes, "sub.sub.unsafe.tld") @@ -53,8 +59,9 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do test "matches are case-insensitive" do regexes = MRF.subdomains_regex(["UnSafe.TLD", "UnSAFE2.Tld"]) + matchable_regexes = Enum.map(regexes, fn r -> r.source end) - assert regexes == [~r/^UnSafe.TLD$/i, ~r/^UnSAFE2.Tld$/i] + assert matchable_regexes == [~r/^UnSafe.TLD$/i.source, ~r/^UnSAFE2.Tld$/i.source] assert MRF.subdomain_match?(regexes, "UNSAFE.TLD") assert MRF.subdomain_match?(regexes, "UNSAFE2.TLD") From a9ad6297b7a8444bad161256183b494d45ce92a7 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 9 Jan 2026 17:28:06 +0100 Subject: [PATCH 090/127] Elixir 1.19: Fix Mastodon StatusControllerTest DateTime difference --- .../web/mastodon_api/controllers/status_controller_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs index 298e92366..8477ae03f 100644 --- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -3068,7 +3068,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do |> json_response_and_validate_schema(:ok) {:ok, a_expires_at, 0} = DateTime.from_iso8601(a_expires_at) - assert DateTime.diff(expires_at, a_expires_at) == 0 + assert DateTime.diff(DateTime.truncate(expires_at, :second), DateTime.truncate(a_expires_at, :second)) == 0 %{conn: conn} = oauth_access(["read:statuses"]) From ee55764501077864e8112108a4df4c74c5f2d234 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 9 Jan 2026 20:16:30 +0100 Subject: [PATCH 091/127] lint --- lib/pleroma/mfa/changeset.ex | 7 ++++--- lib/pleroma/web/api_spec/cast_and_validate.ex | 9 ++++++++- lib/pleroma/web/common_api/activity_draft.ex | 10 +++++++--- test/pleroma/config_db_test.exs | 12 ++++++++---- test/pleroma/web/activity_pub/activity_pub_test.exs | 3 ++- .../controllers/status_controller_test.exs | 6 +++++- 6 files changed, 34 insertions(+), 13 deletions(-) diff --git a/lib/pleroma/mfa/changeset.ex b/lib/pleroma/mfa/changeset.ex index 890cb2193..2045c3a7c 100644 --- a/lib/pleroma/mfa/changeset.ex +++ b/lib/pleroma/mfa/changeset.ex @@ -8,7 +8,8 @@ defmodule Pleroma.MFA.Changeset do alias Pleroma.User def disable(%Ecto.Changeset{} = changeset, force \\ false) do - %Settings{} = settings = + %Settings{} = + settings = changeset |> Ecto.Changeset.apply_changes() |> MFA.fetch_settings() @@ -26,7 +27,7 @@ defmodule Pleroma.MFA.Changeset do end def confirm_totp(%User{multi_factor_authentication_settings: %Settings{} = settings} = user) do - totp_settings = %Settings.TOTP{%Settings.TOTP{} = settings.totp | confirmed: true} + totp_settings = %Settings.TOTP{(%Settings.TOTP{} = settings.totp) | confirmed: true} user |> put_change(%Settings{settings | totp: totp_settings, enabled: true}) @@ -46,7 +47,7 @@ defmodule Pleroma.MFA.Changeset do def cast_backup_codes(%User{} = user, codes) do user |> put_change(%Settings{ - %Settings{} = user.multi_factor_authentication_settings + (%Settings{} = user.multi_factor_authentication_settings) | backup_codes: codes }) end diff --git a/lib/pleroma/web/api_spec/cast_and_validate.ex b/lib/pleroma/web/api_spec/cast_and_validate.ex index 57ea8e9b3..95bd4d9cf 100644 --- a/lib/pleroma/web/api_spec/cast_and_validate.ex +++ b/lib/pleroma/web/api_spec/cast_and_validate.ex @@ -106,7 +106,14 @@ defmodule Pleroma.Web.ApiSpec.CastAndValidate do OpenApiSpex.cast_and_validate(spec, operation, conn, content_type, cast_opts) end - defp cast_and_validate(spec, operation, %Conn{} = conn, content_type, false = _strict, cast_opts) do + defp cast_and_validate( + spec, + operation, + %Conn{} = conn, + content_type, + false = _strict, + cast_opts + ) do case OpenApiSpex.cast_and_validate(spec, operation, conn, content_type) do {:ok, conn} -> {:ok, conn} diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index c5959276a..6c84ef656 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -136,7 +136,8 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do defp in_reply_to(%__MODULE__{params: %{in_reply_to_status_id: ""}} = draft), do: draft - defp in_reply_to(%__MODULE__{params: %{in_reply_to_status_id: id}} = draft) when is_binary(id) do + defp in_reply_to(%__MODULE__{params: %{in_reply_to_status_id: id}} = draft) + when is_binary(id) do # If a post was deleted all its activities (except the newly added Delete) are purged too, # thus lookup by Create db ID will yield nil just as if it never existed in the first place. # @@ -166,13 +167,16 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do end end - defp in_reply_to(%__MODULE__{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} = draft) do + defp in_reply_to( + %__MODULE__{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} = draft + ) do %__MODULE__{draft | in_reply_to: in_reply_to} end defp in_reply_to(draft), do: draft - defp quote_post(%__MODULE__{params: %{quoted_status_id: id}} = draft) when not_empty_string(id) do + defp quote_post(%__MODULE__{params: %{quoted_status_id: id}} = draft) + when not_empty_string(id) do case Activity.get_by_id_with_object(id) do %Activity{} = activity -> %__MODULE__{draft | quote_post: activity} diff --git a/test/pleroma/config_db_test.exs b/test/pleroma/config_db_test.exs index 59beba48e..98b56a146 100644 --- a/test/pleroma/config_db_test.exs +++ b/test/pleroma/config_db_test.exs @@ -273,11 +273,13 @@ defmodule Pleroma.ConfigDBTest do end test "sigil" do - assert ConfigDB.to_elixir_types("~r[comp[lL][aA][iI][nN]er]").source == ~r/comp[lL][aA][iI][nN]er/.source + assert ConfigDB.to_elixir_types("~r[comp[lL][aA][iI][nN]er]").source == + ~r/comp[lL][aA][iI][nN]er/.source end test "link sigil" do - assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/").source == ~r/https:\/\/example.com/.source + assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/").source == + ~r/https:\/\/example.com/.source end test "link sigil with um modifiers" do @@ -286,11 +288,13 @@ defmodule Pleroma.ConfigDBTest do end test "link sigil with i modifier" do - assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/i").source == ~r/https:\/\/example.com/i.source + assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/i").source == + ~r/https:\/\/example.com/i.source end test "link sigil with s modifier" do - assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/s").source == ~r/https:\/\/example.com/s.source + assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/s").source == + ~r/https:\/\/example.com/s.source end test "raise if valid delimiter not found" do diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs index 13146619a..9aafc41a5 100644 --- a/test/pleroma/web/activity_pub/activity_pub_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_test.exs @@ -1786,7 +1786,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do {:ok, list} = Pleroma.List.create(%{title: "foo"}, user) {:ok, list} = Pleroma.List.follow(list, member) - {:ok, %Activity{} = activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) + {:ok, %Activity{} = activity} = + CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) activity = Repo.preload(activity, :bookmark) activity = %{activity | thread_muted?: !!activity.thread_muted?} diff --git a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs index 8477ae03f..11e96a6ac 100644 --- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -3068,7 +3068,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do |> json_response_and_validate_schema(:ok) {:ok, a_expires_at, 0} = DateTime.from_iso8601(a_expires_at) - assert DateTime.diff(DateTime.truncate(expires_at, :second), DateTime.truncate(a_expires_at, :second)) == 0 + + assert DateTime.diff( + DateTime.truncate(expires_at, :second), + DateTime.truncate(a_expires_at, :second) + ) == 0 %{conn: conn} = oauth_access(["read:statuses"]) From 645211812e41a05cd1de3801030e0ddc3c756600 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 15 Jan 2026 16:03:44 +0100 Subject: [PATCH 092/127] Elixir 1.19 MRFTest: Replace matchable_regexes with regexes_match! func --- test/pleroma/web/activity_pub/mrf_test.exs | 26 +++++++++++----------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/test/pleroma/web/activity_pub/mrf_test.exs b/test/pleroma/web/activity_pub/mrf_test.exs index 6656a01b8..401d4ebb3 100644 --- a/test/pleroma/web/activity_pub/mrf_test.exs +++ b/test/pleroma/web/activity_pub/mrf_test.exs @@ -10,22 +10,25 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do alias Pleroma.Web.ActivityPub.MRF + defp regexes_match!([],[]), do: true + + defp regexes_match!([authority | authority_rest], [checked | checked_rest]) do + authority.source == checked.source and regexes_match!(authority_rest, checked_rest) + end + + defp regexes_match!(_, _), do: false + test "subdomains_regex/1" do regexes = MRF.subdomains_regex(["unsafe.tld", "*.unsafe.tld"]) - matchable_regexes = Enum.map(regexes, fn r -> r.source end) - assert matchable_regexes == [ - ~r/^unsafe.tld$/i.source, - ~r/^(.*\.)*unsafe.tld$/i.source - ] + assert regexes_match!(regexes, [~r/^unsafe.tld$/i, ~r/^(.*\.)*unsafe.tld$/i]) end describe "subdomain_match/2" do test "common domains" do regexes = MRF.subdomains_regex(["unsafe.tld", "unsafe2.tld"]) - matchable_regexes = Enum.map(regexes, fn r -> r.source end) - assert matchable_regexes == [~r/^unsafe.tld$/i.source, ~r/^unsafe2.tld$/i.source] + assert regexes_match!(regexes, [~r/^unsafe.tld$/i, ~r/^unsafe2.tld$/i]) assert MRF.subdomain_match?(regexes, "unsafe.tld") assert MRF.subdomain_match?(regexes, "unsafe2.tld") @@ -35,9 +38,8 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do test "wildcard domains with one subdomain" do regexes = MRF.subdomains_regex(["*.unsafe.tld"]) - matchable_regexes = Enum.map(regexes, fn r -> r.source end) - assert matchable_regexes == [~r/^(.*\.)*unsafe.tld$/i.source] + assert regexes_match!(regexes, [~r/^(.*\.)*unsafe.tld$/i]) assert MRF.subdomain_match?(regexes, "unsafe.tld") assert MRF.subdomain_match?(regexes, "sub.unsafe.tld") @@ -47,9 +49,8 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do test "wildcard domains with two subdomains" do regexes = MRF.subdomains_regex(["*.unsafe.tld"]) - matchable_regexes = Enum.map(regexes, fn r -> r.source end) - assert matchable_regexes == [~r/^(.*\.)*unsafe.tld$/i.source] + assert regexes_match!(regexes, [~r/^(.*\.)*unsafe.tld$/i]) assert MRF.subdomain_match?(regexes, "unsafe.tld") assert MRF.subdomain_match?(regexes, "sub.sub.unsafe.tld") @@ -59,9 +60,8 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do test "matches are case-insensitive" do regexes = MRF.subdomains_regex(["UnSafe.TLD", "UnSAFE2.Tld"]) - matchable_regexes = Enum.map(regexes, fn r -> r.source end) - assert matchable_regexes == [~r/^UnSafe.TLD$/i.source, ~r/^UnSAFE2.Tld$/i.source] + assert regexes_match!(regexes, [~r/^UnSafe.TLD$/i, ~r/^UnSAFE2.Tld$/i]) assert MRF.subdomain_match?(regexes, "UNSAFE.TLD") assert MRF.subdomain_match?(regexes, "UNSAFE2.TLD") From 750266f2e3d60ad2b83ad8c79b93f914063726c2 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sun, 22 Feb 2026 22:27:48 +0100 Subject: [PATCH 093/127] ActivityDraft: Add missing __MODULE__ matches and drop unneeded ones --- lib/pleroma/web/common_api/activity_draft.ex | 48 ++++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index 6c84ef656..16489663a 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -99,34 +99,34 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do |> Map.put("cc", draft.cc) |> Map.put("actor", draft.user.ap_id) - %__MODULE__{draft | object: object} + %{draft | object: object} end defp put_params(%__MODULE__{} = draft, params) do params = Map.put_new(params, :in_reply_to_status_id, params[:in_reply_to_id]) - %__MODULE__{draft | params: params} + %{draft | params: params} end defp status(%__MODULE__{params: %{status: status}} = draft) do - %__MODULE__{draft | status: String.trim(status)} + %{draft | status: String.trim(status)} end defp summary(%__MODULE__{params: params} = draft) do - %__MODULE__{draft | summary: Map.get(params, :spoiler_text, "")} + %{draft | summary: Map.get(params, :spoiler_text, "")} end defp full_payload(%__MODULE__{status: status, summary: summary} = draft) do full_payload = String.trim(status <> summary) case Utils.validate_character_limit(full_payload, draft.attachments) do - :ok -> %__MODULE__{draft | full_payload: full_payload} + :ok -> %{draft | full_payload: full_payload} {:error, message} -> add_error(draft, message) end end defp attachments(%__MODULE__{params: params} = draft) do attachments = Utils.attachments_from_ids(params, draft.user) - draft = %__MODULE__{draft | attachments: attachments} + draft = %{draft | attachments: attachments} case Utils.validate_attachments_count(attachments) do :ok -> draft @@ -149,7 +149,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do with %Activity{} = activity <- Activity.get_by_id(id), true <- Visibility.visible_for_user?(activity, draft.user), {_, type} when type in ["Create", "Announce"] <- {:type, activity.data["type"]} do - %__MODULE__{draft | in_reply_to: activity} + %{draft | in_reply_to: activity} else nil -> add_error(draft, dgettext("errors", "Cannot reply to a deleted status")) @@ -170,7 +170,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do defp in_reply_to( %__MODULE__{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} = draft ) do - %__MODULE__{draft | in_reply_to: in_reply_to} + %{draft | in_reply_to: in_reply_to} end defp in_reply_to(draft), do: draft @@ -179,14 +179,14 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do when not_empty_string(id) do case Activity.get_by_id_with_object(id) do %Activity{} = activity -> - %__MODULE__{draft | quote_post: activity} + %{draft | quote_post: activity} _ -> draft end end - defp quote_post(%{params: %{quote_id: id}} = draft) when not_empty_string(id) do + defp quote_post(%__MODULE__{params: %{quote_id: id}} = draft) when not_empty_string(id) do quote_post(%{draft | params: Map.put(draft.params, :quoted_status_id, id)}) end @@ -194,7 +194,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do defp in_reply_to_conversation(%__MODULE__{} = draft) do in_reply_to_conversation = Participation.get(draft.params[:in_reply_to_conversation_id]) - %__MODULE__{draft | in_reply_to_conversation: in_reply_to_conversation} + %{draft | in_reply_to_conversation: in_reply_to_conversation} end defp visibility(%__MODULE__{params: params} = draft) do @@ -203,7 +203,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do add_error(draft, dgettext("errors", "The message visibility must be direct")) {visibility, _} -> - %__MODULE__{draft | visibility: visibility} + %{draft | visibility: visibility} end end @@ -219,7 +219,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do false end - defp quoting_visibility(%{quote_post: %Activity{}} = draft) do + defp quoting_visibility(%__MODULE__{quote_post: %Activity{}} = draft) do with %Object{} = object <- Object.normalize(draft.quote_post, fetch: false), true <- can_quote?(draft, object, Visibility.get_visibility(object)) do draft @@ -232,7 +232,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do defp expires_at(%__MODULE__{} = draft) do case CommonAPI.check_expiry_date(draft.params[:expires_in]) do - {:ok, expires_at} -> %__MODULE__{draft | expires_at: expires_at} + {:ok, expires_at} -> %{draft | expires_at: expires_at} {:error, message} -> add_error(draft, message) end end @@ -240,7 +240,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do defp poll(%__MODULE__{} = draft) do case Utils.make_poll_data(draft.params) do {:ok, {poll, poll_emoji}} -> - %__MODULE__{draft | extra: poll, emoji: Map.merge(draft.emoji, poll_emoji)} + %{draft | extra: poll, emoji: Map.merge(draft.emoji, poll_emoji)} {:error, message} -> add_error(draft, message) @@ -258,22 +258,22 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do |> Kernel.++(mentioned_ap_ids) |> Utils.get_addressed_users(draft.params[:to]) - %__MODULE__{draft | content_html: content_html, mentions: mentions, tags: tags} + %{draft | content_html: content_html, mentions: mentions, tags: tags} end defp to_and_cc(%__MODULE__{} = draft) do {to, cc} = Utils.get_to_and_cc(draft) - %__MODULE__{draft | to: to, cc: cc} + %{draft | to: to, cc: cc} end defp context(%__MODULE__{} = draft) do context = Utils.make_context(draft.in_reply_to, draft.in_reply_to_conversation) - %__MODULE__{draft | context: context} + %{draft | context: context} end defp sensitive(%__MODULE__{} = draft) do sensitive = draft.params[:sensitive] - %__MODULE__{draft | sensitive: sensitive} + %{draft | sensitive: sensitive} end defp language(%__MODULE__{} = draft) do @@ -285,7 +285,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do _ -> LanguageDetector.detect(draft.content_html <> " " <> draft.summary) end - %__MODULE__{draft | language: language} + %{draft | language: language} end defp object(%__MODULE__{} = draft) do @@ -329,12 +329,12 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do |> Map.put("generator", draft.params[:generator]) |> Map.put("language", draft.language) - %__MODULE__{draft | object: object} + %{draft | object: object} end defp preview?(%__MODULE__{} = draft) do preview? = Pleroma.Web.Utils.Params.truthy_param?(draft.params[:preview]) - %__MODULE__{draft | preview?: preview?} + %{draft | preview?: preview?} end defp changes(%__MODULE__{} = draft) do @@ -357,14 +357,14 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do } |> Utils.maybe_add_list_data(draft.user, draft.visibility) - %__MODULE__{draft | changes: changes} + %{draft | changes: changes} end defp with_valid(%{valid?: true} = draft, func), do: func.(draft) defp with_valid(draft, _func), do: draft defp add_error(%__MODULE__{} = draft, message) do - %__MODULE__{draft | valid?: false, errors: [message | draft.errors]} + %{draft | valid?: false, errors: [message | draft.errors]} end defp validate(%{valid?: true} = draft), do: {:ok, draft} From 2937bb68b1d0d0b9c70fa40782f9cd772a56217d Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 25 Mar 2026 12:09:18 -0700 Subject: [PATCH 094/127] Fix MoveTokensExpirationIntoOban migration Pleroma.Workers.PurgeExpiredToken.enqueue/1 no longer exists, so this migration would fail. The enqueue/1 function is only used for this migration, so we can just include it in the migration module directly. --- ...0907092050_move_tokens_expiration_into_oban.exs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/priv/repo/migrations/20200907092050_move_tokens_expiration_into_oban.exs b/priv/repo/migrations/20200907092050_move_tokens_expiration_into_oban.exs index c140bc66a..0a55e4f71 100644 --- a/priv/repo/migrations/20200907092050_move_tokens_expiration_into_oban.exs +++ b/priv/repo/migrations/20200907092050_move_tokens_expiration_into_oban.exs @@ -21,7 +21,7 @@ defmodule Pleroma.Repo.Migrations.MoveTokensExpirationIntoOban do from(t in Pleroma.Web.OAuth.Token, where: t.valid_until > ^NaiveDateTime.utc_now()) |> Pleroma.Repo.stream() |> Stream.each(fn token -> - Pleroma.Workers.PurgeExpiredToken.enqueue(%{ + enqueue(%{ token_id: token.id, valid_until: DateTime.from_naive!(token.valid_until, "Etc/UTC"), mod: Pleroma.Web.OAuth.Token @@ -33,7 +33,7 @@ defmodule Pleroma.Repo.Migrations.MoveTokensExpirationIntoOban do from(t in Pleroma.MFA.Token, where: t.valid_until > ^NaiveDateTime.utc_now()) |> Pleroma.Repo.stream() |> Stream.each(fn token -> - Pleroma.Workers.PurgeExpiredToken.enqueue(%{ + enqueue(%{ token_id: token.id, valid_until: DateTime.from_naive!(token.valid_until, "Etc/UTC"), mod: Pleroma.MFA.Token @@ -41,4 +41,14 @@ defmodule Pleroma.Repo.Migrations.MoveTokensExpirationIntoOban do end) |> Stream.run() end + + @spec enqueue(%{token_id: integer(), valid_until: DateTime.t()}) :: + {:ok, Oban.Job.t()} | {:error, Ecto.Changeset.t()} + defp enqueue(args) do + {scheduled_at, args} = Map.pop(args, :valid_until) + + args + |> Pleroma.Workers.PurgeExpiredToken.new(scheduled_at: scheduled_at) + |> Oban.insert() + end end From f3f72048ac2a7c379cf31b2478eefc057de198f9 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 25 Mar 2026 12:19:33 -0700 Subject: [PATCH 095/127] Fix MoveActivityExpirationsToOban migration This never would have worked correctly. warning: Pleroma.Workers.PurgeExpiredActivity.enqueue/1 is undefined or private. Did you mean: * enqueue/2 --- ...0200825061316_move_activity_expirations_to_oban.exs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/priv/repo/migrations/20200825061316_move_activity_expirations_to_oban.exs b/priv/repo/migrations/20200825061316_move_activity_expirations_to_oban.exs index f15876180..b512313c7 100644 --- a/priv/repo/migrations/20200825061316_move_activity_expirations_to_oban.exs +++ b/priv/repo/migrations/20200825061316_move_activity_expirations_to_oban.exs @@ -23,10 +23,12 @@ defmodule Pleroma.Repo.Migrations.MoveActivityExpirationsToOban do |> Pleroma.Repo.stream() |> Stream.each(fn expiration -> with {:ok, expires_at} <- DateTime.from_naive(expiration.scheduled_at, "Etc/UTC") do - Pleroma.Workers.PurgeExpiredActivity.enqueue(%{ - activity_id: FlakeId.to_string(expiration.activity_id), - expires_at: expires_at - }) + Pleroma.Workers.PurgeExpiredActivity.enqueue( + %{ + activity_id: FlakeId.to_string(expiration.activity_id) + }, + scheduled_at: expires_at + ) end end) |> Stream.run() From e1a1e5c726fe0aef9accca30641160934d02143a Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 25 Mar 2026 12:22:58 -0700 Subject: [PATCH 096/127] Correct old migrations for expiring activities and user access tokens. --- changelog.d/old-migrations.fix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/old-migrations.fix diff --git a/changelog.d/old-migrations.fix b/changelog.d/old-migrations.fix new file mode 100644 index 000000000..49566c896 --- /dev/null +++ b/changelog.d/old-migrations.fix @@ -0,0 +1 @@ +Correct old migrations for expiring activities and user access tokens. From cbb715b978efe6f65c30b555341e0fa3191013cb Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 25 Mar 2026 12:36:16 -0700 Subject: [PATCH 097/127] No-op code correctness improvements detected by Elixir 1.19 compiler --- changelog.d/elixir-1.19-cherrypicks.change | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/elixir-1.19-cherrypicks.change diff --git a/changelog.d/elixir-1.19-cherrypicks.change b/changelog.d/elixir-1.19-cherrypicks.change new file mode 100644 index 000000000..7e56be008 --- /dev/null +++ b/changelog.d/elixir-1.19-cherrypicks.change @@ -0,0 +1 @@ +No-op code correctness improvements detected by Elixir 1.19 compiler From 711b33d81cd8ecbf83aa7eea9c4a76ab531713fe Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 25 Mar 2026 13:32:25 -0700 Subject: [PATCH 098/127] Fix CommonAPI.favorite/2 arg order --- test/pleroma/search_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/pleroma/search_test.exs b/test/pleroma/search_test.exs index cdd2a72bd..d777bcda2 100644 --- a/test/pleroma/search_test.exs +++ b/test/pleroma/search_test.exs @@ -54,7 +54,7 @@ defmodule Pleroma.SearchTest do assert_enqueued(worker: SearchIndexingWorker, args: args) - {:ok, fav_activity} = CommonAPI.favorite(user, activity.id) + {:ok, fav_activity} = CommonAPI.favorite(activity.id, user) args = %{"op" => "add_to_index", "activity" => fav_activity.id} From ea78e7683795dcda995e3755f0275fe6398f815f Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 25 Mar 2026 14:46:38 -0700 Subject: [PATCH 099/127] Fix add_to_index/1 to adhere to the typespec --- lib/pleroma/search.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/search.ex b/lib/pleroma/search.ex index 8477873f9..f78495c8d 100644 --- a/lib/pleroma/search.ex +++ b/lib/pleroma/search.ex @@ -18,7 +18,7 @@ defmodule Pleroma.Search do def add_to_index(%Activity{id: activity_id}) do case Activity.get_by_id_with_object(activity_id) do %Activity{} = preloaded -> add_to_index(preloaded) - _ -> :ok + _ -> {:ok, :noop} end end From f06a0eab50b9ed3e6d4ed4db714d5632225ff9f0 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 25 Mar 2026 14:47:39 -0700 Subject: [PATCH 100/127] Move object_to_search_data/1 to Pleroma.Search This standardizes this functionality within the Search module so it doesn't need to be imported by other search backends from Meilisearch Also integrate its filtering rules into Search.indexable?/1 for consistency --- changelog.d/search-indexing.skip | 0 lib/mix/tasks/pleroma/search/meilisearch.ex | 2 +- lib/pleroma/search.ex | 39 +++++++++++++++++++- lib/pleroma/search/meilisearch.ex | 40 ++------------------- lib/pleroma/search/qdrant_search.ex | 4 +-- 5 files changed, 43 insertions(+), 42 deletions(-) create mode 100644 changelog.d/search-indexing.skip diff --git a/changelog.d/search-indexing.skip b/changelog.d/search-indexing.skip new file mode 100644 index 000000000..e69de29bb diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex index edce9e871..facc38815 100644 --- a/lib/mix/tasks/pleroma/search/meilisearch.ex +++ b/lib/mix/tasks/pleroma/search/meilisearch.ex @@ -72,7 +72,7 @@ defmodule Mix.Tasks.Pleroma.Search.Meilisearch do query, timeout: :infinity ) - |> Stream.map(&Pleroma.Search.Meilisearch.object_to_search_data/1) + |> Stream.map(&Pleroma.Search.object_to_search_data/1) |> Stream.filter(fn o -> not is_nil(o) end) |> Stream.chunk_every(chunk_size) |> Stream.transform(0, fn objects, acc -> diff --git a/lib/pleroma/search.ex b/lib/pleroma/search.ex index f78495c8d..9cd2768c4 100644 --- a/lib/pleroma/search.ex +++ b/lib/pleroma/search.ex @@ -38,6 +38,43 @@ defmodule Pleroma.Search do search_module.healthcheck_endpoints() end - defp indexable?(%Activity{data: %{"type" => "Create"}}), do: true + def object_to_search_data(%Object{} = object) do + data = object.data + + content_str = + case data["content"] do + [nil | rest] -> to_string(rest) + str -> str + end + + content = + with {:ok, scrubbed} <- + FastSanitize.Sanitizer.scrub(content_str, Pleroma.HTML.Scrubber.SearchIndexing), + trimmed <- String.trim(scrubbed) do + trimmed + end + + # Make sure we have a non-empty string + if content != "" do + {:ok, published, _} = DateTime.from_iso8601(data["published"]) + + %{ + id: object.id, + content: content, + ap: data["id"], + published: published |> DateTime.to_unix() + } + end + end + + defp indexable?(%Activity{ + data: %{"type" => "Create"}, + object: %Object{ + data: %{"content" => content, "published" => published, "type" => "Note"} + } + }) + when not is_nil(content) and content not in ["", "."] and not is_nil(published), + do: true + defp indexable?(_), do: false end diff --git a/lib/pleroma/search/meilisearch.ex b/lib/pleroma/search/meilisearch.ex index 4541ef14a..dc10076e1 100644 --- a/lib/pleroma/search/meilisearch.ex +++ b/lib/pleroma/search/meilisearch.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Search.Meilisearch do alias Pleroma.Activity alias Pleroma.Config.Getting, as: Config alias Pleroma.Object + alias Pleroma.Search import Pleroma.Search.DatabaseSearch import Ecto.Query @@ -119,46 +120,9 @@ defmodule Pleroma.Search.Meilisearch do end end - def object_to_search_data(object) do - # Only index public or unlisted Notes - if not is_nil(object) and object.data["type"] == "Note" and - not is_nil(object.data["content"]) and - not is_nil(object.data["published"]) and - (Pleroma.Constants.as_public() in object.data["to"] or - Pleroma.Constants.as_public() in object.data["cc"]) and - object.data["content"] not in ["", "."] do - data = object.data - - content_str = - case data["content"] do - [nil | rest] -> to_string(rest) - str -> str - end - - content = - with {:ok, scrubbed} <- - FastSanitize.Sanitizer.scrub(content_str, Pleroma.HTML.Scrubber.SearchIndexing), - trimmed <- String.trim(scrubbed) do - trimmed - end - - # Make sure we have a non-empty string - if content != "" do - {:ok, published, _} = DateTime.from_iso8601(data["published"]) - - %{ - id: object.id, - content: content, - ap: data["id"], - published: published |> DateTime.to_unix() - } - end - end - end - @impl true def add_to_index(%Activity{object: %Object{} = object} = activity) do - search_data = object_to_search_data(object) + search_data = Search.object_to_search_data(object) result = meili_put( diff --git a/lib/pleroma/search/qdrant_search.ex b/lib/pleroma/search/qdrant_search.ex index 06f5b1983..4d57cfa88 100644 --- a/lib/pleroma/search/qdrant_search.ex +++ b/lib/pleroma/search/qdrant_search.ex @@ -5,11 +5,11 @@ defmodule Pleroma.Search.QdrantSearch do alias Pleroma.Activity alias Pleroma.Config.Getting, as: Config alias Pleroma.Object + alias Pleroma.Search alias __MODULE__.OpenAIClient alias __MODULE__.QdrantClient - import Pleroma.Search.Meilisearch, only: [object_to_search_data: 1] import Pleroma.Search.DatabaseSearch, only: [maybe_fetch: 3] @impl true @@ -84,7 +84,7 @@ defmodule Pleroma.Search.QdrantSearch do @impl true def add_to_index(%Activity{object: %Object{} = object} = activity) do - search_data = object_to_search_data(object) + search_data = Search.object_to_search_data(object) with {:ok, embedding} <- get_embedding(search_data.content), {:ok, %{status: 200}} <- From 5aa3c8a06e9b62f5fa6a90ae319c7a75cbbac4cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 23 Mar 2026 11:50:31 +0100 Subject: [PATCH 101/127] Federate `votersCount` correctly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk Assisted-by: your mother Signed-off-by: nicole mikołajczyk --- changelog.d/poll-voters-count.fix | 1 + lib/pleroma/object.ex | 8 ++++++++ .../object_validators/question_validator.ex | 1 + .../web/activity_pub/transmogrifier.ex | 7 +++++++ .../web/mastodon_api/views/poll_view.ex | 2 ++ .../transmogrifier/question_handling_test.exs | 19 +++++++++++++++++++ .../web/mastodon_api/views/poll_view_test.exs | 9 ++++++++- 7 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 changelog.d/poll-voters-count.fix diff --git a/changelog.d/poll-voters-count.fix b/changelog.d/poll-voters-count.fix new file mode 100644 index 000000000..2dbc81b5d --- /dev/null +++ b/changelog.d/poll-voters-count.fix @@ -0,0 +1 @@ +Federate `votersCount` correctly diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index d0cb16b79..f1e07a257 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -374,10 +374,18 @@ defmodule Pleroma.Object do voters = [actor | object.data["voters"] || []] |> Enum.uniq() + voters_count = + if Map.has_key?(object.data, "votersCount") do + object.data["votersCount"] + 1 + else + length(voters) + end + data = object.data |> Map.put(key, options) |> Map.put("voters", voters) + |> Map.put("votersCount", voters_count) object |> Object.change(%{data: data}) diff --git a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex index 21940f4f1..065c75910 100644 --- a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex @@ -28,6 +28,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do end field(:closed, ObjectValidators.DateTime) + field(:votersCount, :integer) field(:voters, {:array, ObjectValidators.ObjectID}, default: []) field(:nonAnonymous, :boolean) embeds_many(:anyOf, QuestionOptionsValidator) diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 6af5ee89d..4421da26c 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -783,6 +783,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def set_replies(obj_data), do: obj_data + defp set_voters_count(%{"voters" => [_ | _] = voters} = obj) do + Map.merge(obj, %{"votersCount" => length(voters)}) + end + + defp set_voters_count(obj), do: obj + # Prepares and sanitizes the object for federation. def prepare_object(object) do object @@ -795,6 +801,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do |> set_reply_to_uri |> set_quote_url |> set_replies + |> set_voters_count |> CommonFixes.maybe_add_content_map() |> strip_internal_fields |> strip_internal_tags diff --git a/lib/pleroma/web/mastodon_api/views/poll_view.ex b/lib/pleroma/web/mastodon_api/views/poll_view.ex index 1e3c9f36d..3b4271227 100644 --- a/lib/pleroma/web/mastodon_api/views/poll_view.ex +++ b/lib/pleroma/web/mastodon_api/views/poll_view.ex @@ -75,6 +75,8 @@ defmodule Pleroma.Web.MastodonAPI.PollView do length(voters) end + defp voters_count(%{data: %{"votersCount" => voters}}), do: voters + defp voters_count(_), do: 0 defp voted_and_own_votes(%{object: object} = params, options) do diff --git a/test/pleroma/web/activity_pub/transmogrifier/question_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/question_handling_test.exs index d31070546..2a4e78cf0 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/question_handling_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/question_handling_test.exs @@ -170,4 +170,23 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.QuestionHandlingTest do assert {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(data) end + + test "it displays voters count for a poll" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "???", + poll: %{expires_in: 10, options: ["yes", "no"]} + }) + + object = Object.normalize(activity, fetch: false) + {:ok, _, _} = CommonAPI.vote(object, other_user, [1]) + + {:ok, modified} = Transmogrifier.prepare_activity(activity.data) + + refute Map.has_key?(modified["object"], "voters") + assert modified["object"]["votersCount"] == 1 + end end diff --git a/test/pleroma/web/mastodon_api/views/poll_view_test.exs b/test/pleroma/web/mastodon_api/views/poll_view_test.exs index 6de001421..16281393d 100644 --- a/test/pleroma/web/mastodon_api/views/poll_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/poll_view_test.exs @@ -167,7 +167,14 @@ defmodule Pleroma.Web.MastodonAPI.PollViewTest do } = PollView.render("show.json", %{object: object}) end - test "that poll is non anonymous" do + test "displays correct voters count" do + object = Object.normalize("https://friends.grishka.me/posts/54642", fetch: true) + result = PollView.render("show.json", %{object: object}) + + assert result[:voters_count] == 14 + end + + test "detects that poll is non anonymous" do object = Object.normalize("https://friends.grishka.me/posts/54642", fetch: true) result = PollView.render("show.json", %{object: object}) From 799199f6b504d918bf55787149ec0e7240693164 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sun, 22 Feb 2026 20:50:39 +0100 Subject: [PATCH 102/127] DigestEmailsWorker: Change Oban queue to "background" The mailer queue has been long gone and that left Oban jobs always stuck in the "available" state that would never execute. --- lib/pleroma/workers/cron/digest_emails_worker.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/workers/cron/digest_emails_worker.ex b/lib/pleroma/workers/cron/digest_emails_worker.ex index b50b52a7b..5cb13f5f9 100644 --- a/lib/pleroma/workers/cron/digest_emails_worker.ex +++ b/lib/pleroma/workers/cron/digest_emails_worker.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Workers.Cron.DigestEmailsWorker do The worker to send digest emails. """ - use Oban.Worker, queue: "mailer" + use Oban.Worker, queue: :background alias Pleroma.Config alias Pleroma.Emails From c8baad165ba418f0aac9edcdc548381c9b5eb4fc Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 31 Mar 2026 16:04:55 +0200 Subject: [PATCH 103/127] lint: fix warnings throughout codebase --- changelog.d/lint-warnings.skip | 0 .../web/api_spec/operations/list_operation.ex | 10 ++++++++-- lib/pleroma/web/endpoint.ex | 3 ++- test/pleroma/web/activity_pub/mrf_test.exs | 2 +- .../controllers/account_controller_test.exs | 5 ++++- .../remote_interaction_controller_test.exs | 16 ++++++++++++---- 6 files changed, 27 insertions(+), 9 deletions(-) create mode 100644 changelog.d/lint-warnings.skip diff --git a/changelog.d/lint-warnings.skip b/changelog.d/lint-warnings.skip new file mode 100644 index 000000000..e69de29bb diff --git a/lib/pleroma/web/api_spec/operations/list_operation.ex b/lib/pleroma/web/api_spec/operations/list_operation.ex index d2e803178..87189edc2 100644 --- a/lib/pleroma/web/api_spec/operations/list_operation.ex +++ b/lib/pleroma/web/api_spec/operations/list_operation.ex @@ -172,7 +172,10 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do type: :object, properties: %{ title: %Schema{type: :string, description: "List title"}, - exclusive: %Schema{type: :boolean, description: "Whether members of the list should be removed from the “Home” feed"} + exclusive: %Schema{ + type: :boolean, + description: "Whether members of the list should be removed from the “Home” feed" + } }, required: [:title] }, @@ -188,7 +191,10 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do type: :object, properties: %{ title: %Schema{type: :string, description: "List title"}, - exclusive: %Schema{type: :boolean, description: "Whether members of the list should be removed from the “Home” feed"} + exclusive: %Schema{ + type: :boolean, + description: "Whether members of the list should be removed from the “Home” feed" + } } }, required: true diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 81a9d3a09..a5c04a0c4 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -48,7 +48,8 @@ defmodule Pleroma.Web.Endpoint do @static_cache_control "public, max-age=1209600, immutable" @static_cache_disabled "public, no-cache" - @favicon_cache_control "public, max=age=86400, immutable" # cache for a day + # cache for a day + @favicon_cache_control "public, max=age=86400, immutable" # InstanceStatic needs to be before Plug.Static to be able to override shipped-static files # If you're adding new paths to `only:` you'll need to configure them in InstanceStatic as well diff --git a/test/pleroma/web/activity_pub/mrf_test.exs b/test/pleroma/web/activity_pub/mrf_test.exs index 401d4ebb3..2d0f3b317 100644 --- a/test/pleroma/web/activity_pub/mrf_test.exs +++ b/test/pleroma/web/activity_pub/mrf_test.exs @@ -10,7 +10,7 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do alias Pleroma.Web.ActivityPub.MRF - defp regexes_match!([],[]), do: true + defp regexes_match!([], []), do: true defp regexes_match!([authority | authority_rest], [checked | checked_rest]) do authority.source == checked.source and regexes_match!(authority_rest, checked_rest) diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs index 67f292506..01e195842 100644 --- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs @@ -1815,7 +1815,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do test "returns lists to which the account belongs" do %{user: user, conn: conn} = oauth_access(["read:lists"]) other_user = insert(:user) - assert {:ok, %Pleroma.List{id: _list_id} = list} = Pleroma.List.create(%{title: "Test List"}, user) + + assert {:ok, %Pleroma.List{id: _list_id} = list} = + Pleroma.List.create(%{title: "Test List"}, user) + {:ok, %{following: _following}} = Pleroma.List.follow(list, other_user) assert [%{"id" => _list_id, "title" => "Test List"}] = diff --git a/test/pleroma/web/remote_interaction/remote_interaction_controller_test.exs b/test/pleroma/web/remote_interaction/remote_interaction_controller_test.exs index 9df86c0a2..9236d1bf8 100644 --- a/test/pleroma/web/remote_interaction/remote_interaction_controller_test.exs +++ b/test/pleroma/web/remote_interaction/remote_interaction_controller_test.exs @@ -83,7 +83,9 @@ defmodule Pleroma.Web.RemoteInteraction.RemoteInteractionControllerTest do response = conn - |> get(remote_interaction_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"})) + |> get( + remote_interaction_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"}) + ) |> html_response(200) assert response =~ "Log in to follow" @@ -114,7 +116,9 @@ defmodule Pleroma.Web.RemoteInteraction.RemoteInteractionControllerTest do response = conn |> assign(:user, user) - |> get(remote_interaction_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"})) + |> get( + remote_interaction_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"}) + ) |> html_response(200) assert response =~ "Remote follow" @@ -157,7 +161,9 @@ defmodule Pleroma.Web.RemoteInteraction.RemoteInteractionControllerTest do conn |> assign(:user, user) |> assign(:token, read_token) - |> post(remote_interaction_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) + |> post(remote_interaction_path(conn, :do_follow), %{ + "user" => %{"id" => user2.id} + }) |> response(200) assert response =~ "Error following account" @@ -496,7 +502,9 @@ defmodule Pleroma.Web.RemoteInteraction.RemoteInteractionControllerTest do ) assert redirected_to(conn) == - remote_interaction_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"}) + remote_interaction_path(conn, :follow, %{ + acct: "https://mastodon.social/users/emelie" + }) end end From eb69576154a29c8556a1e37b3138f359cd7e0e57 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 31 Mar 2026 16:21:04 +0200 Subject: [PATCH 104/127] fix test after embed route got added back --- test/pleroma/web/plugs/frontend_static_plug_test.exs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/pleroma/web/plugs/frontend_static_plug_test.exs b/test/pleroma/web/plugs/frontend_static_plug_test.exs index e1e331c06..b7c06eacd 100644 --- a/test/pleroma/web/plugs/frontend_static_plug_test.exs +++ b/test/pleroma/web/plugs/frontend_static_plug_test.exs @@ -105,6 +105,7 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do "nodeinfo", "manifest.json", "auth", + "embed", "proxy", "test", "user_exists", From a9fe2fe4d8fe976a2a96cd7d2e9f38fd4b8a857b Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 14 Feb 2026 15:22:34 +0100 Subject: [PATCH 105/127] Move main Woodpecker file to own directory --- .woodpecker.yml => .woodpecker/test.yaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .woodpecker.yml => .woodpecker/test.yaml (100%) diff --git a/.woodpecker.yml b/.woodpecker/test.yaml similarity index 100% rename from .woodpecker.yml rename to .woodpecker/test.yaml From 88a349f3ab30b9661daed66b529a094bcabb2737 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 14 Feb 2026 16:29:59 +0100 Subject: [PATCH 106/127] Woodpecker CI: Retry failed tests using pleroma.test_runner I didn't add the --cover option, but it would be useless right now anyway --- .woodpecker/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.woodpecker/test.yaml b/.woodpecker/test.yaml index 4ceb1cab5..10ca016c4 100644 --- a/.woodpecker/test.yaml +++ b/.woodpecker/test.yaml @@ -17,7 +17,7 @@ steps: - su testuser -c "HOME=/home/testuser mix local.hex --force" - su testuser -c "HOME=/home/testuser mix local.rebar --force" - su testuser -c "HOME=/home/testuser mix deps.get" - - su testuser -c "HOME=/home/testuser mix test" + - su testuser -c "HOME=/home/testuser mix pleroma.test_runner --preload-modules" services: postgres: From 1a0af1c0c074ee207fa3b377d534c6a028c9fb6a Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 14 Feb 2026 17:09:04 +0100 Subject: [PATCH 107/127] Woodpecker CI: Add check-changelog workflow --- .woodpecker/changelog.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .woodpecker/changelog.yaml diff --git a/.woodpecker/changelog.yaml b/.woodpecker/changelog.yaml new file mode 100644 index 000000000..4f38ce618 --- /dev/null +++ b/.woodpecker/changelog.yaml @@ -0,0 +1,9 @@ +when: + - event: pull_request + +steps: + check-changelog: + image: docker.io/alpine:3.23 + commands: + - apk add --no-cache git + - sh ./tools/check-changelog From 4493d0d187e668f422aa2c7b29392a92ff338edc Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 14 Feb 2026 17:19:24 +0100 Subject: [PATCH 108/127] Woodpecker CI: Update check-changelog script for Woodpecker --- tools/check-changelog | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/check-changelog b/tools/check-changelog index 5952aefcc..d09a68895 100644 --- a/tools/check-changelog +++ b/tools/check-changelog @@ -1,14 +1,14 @@ #!/bin/sh echo "adding ownership exception" -git config --global --add safe.directory $(pwd) +git config --global --add safe.directory "$(pwd)" echo "looking for change log" git remote add upstream https://git.pleroma.social/pleroma/pleroma.git -git fetch upstream ${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}:refs/remotes/upstream/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME +git fetch upstream ${CI_COMMIT_TARGET_BRANCH}:refs/remotes/upstream/${CI_COMMIT_TARGET_BRANCH} -git diff --raw --no-renames upstream/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME HEAD -- changelog.d | \ +git diff --raw --no-renames upstream/${CI_COMMIT_TARGET_BRANCH} HEAD -- changelog.d | \ grep ' A\t' | grep '\.\(skip\|add\|remove\|fix\|security\|change\)$' ret=$? From 2880aac61794bfa25601a0c2cbf0e8c949f6d4bf Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 14 Feb 2026 18:27:45 +0100 Subject: [PATCH 109/127] Woodpecker CI: Unit test using Elixir 1.15 and 1.18 --- ...est.yaml => unit-testing-elixir-1.15.yaml} | 10 ++++-- .woodpecker/unit-testing-elixir-1.18.yaml | 32 +++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) rename .woodpecker/{test.yaml => unit-testing-elixir-1.15.yaml} (87%) create mode 100644 .woodpecker/unit-testing-elixir-1.18.yaml diff --git a/.woodpecker/test.yaml b/.woodpecker/unit-testing-elixir-1.15.yaml similarity index 87% rename from .woodpecker/test.yaml rename to .woodpecker/unit-testing-elixir-1.15.yaml index 10ca016c4..cf1e638eb 100644 --- a/.woodpecker/test.yaml +++ b/.woodpecker/unit-testing-elixir-1.15.yaml @@ -1,9 +1,13 @@ when: - - event: - - pull_request + - event: pull_request + - event: push + branch: develop + +depends_on: + - changelog steps: - test: + unit-testing-elixir-1.15: image: elixir:1.15-alpine environment: MIX_ENV: test diff --git a/.woodpecker/unit-testing-elixir-1.18.yaml b/.woodpecker/unit-testing-elixir-1.18.yaml new file mode 100644 index 000000000..0e382f527 --- /dev/null +++ b/.woodpecker/unit-testing-elixir-1.18.yaml @@ -0,0 +1,32 @@ +when: + - event: pull_request + - event: push + branch: develop + +depends_on: + - changelog + +steps: + unit-testing-elixir-1.18: + image: elixir:1.18-alpine + environment: + MIX_ENV: test + DB_HOST: postgres + DB_PORT: 5432 + commands: + - apk add --no-cache build-base cmake exiftool ffmpeg file-dev git openssl + - adduser -D -h /home/testuser testuser + - mkdir -p /home/testuser/.mix /home/testuser/.hex + - chown -R testuser:testuser . /home/testuser + - su testuser -c "HOME=/home/testuser mix local.hex --force" + - su testuser -c "HOME=/home/testuser mix local.rebar --force" + - su testuser -c "HOME=/home/testuser mix deps.get" + - su testuser -c "HOME=/home/testuser mix pleroma.test_runner --preload-modules" + +services: + postgres: + image: postgres:13-alpine + environment: + POSTGRES_DB: pleroma_test + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres From 6f8233d780d16f9db0479849dd685b866fa096b2 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 14 Feb 2026 19:24:42 +0100 Subject: [PATCH 110/127] Woodpecker CI: Add linting pipeline --- .woodpecker/lint.yaml | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .woodpecker/lint.yaml diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml new file mode 100644 index 000000000..02b814223 --- /dev/null +++ b/.woodpecker/lint.yaml @@ -0,0 +1,36 @@ +when: + - event: pull_request + +depends_on: + - changelog + +steps: + mix-format: + image: &elixir-image + docker.io/elixir:1.15-alpine + commands: + - | + if ! mix format --check-formatted; then + touch fail.stamp + fi + + credo: + image: *elixir-image + environment: + MIX_ENV: test + commands: + - adduser -D -h /home/testuser testuser + - mkdir -p /home/testuser/.mix /home/testuser/.hex + - chown -R testuser:testuser . /home/testuser + - su testuser -c "HOME=/home/testuser mix local.hex --force" + - su testuser -c "HOME=/home/testuser mix local.rebar --force" + - su testuser -c "HOME=/home/testuser mix deps.get" + - | + if ! su testuser -c "HOME=/home/testuser mix analyze" && ! -f fail.stamp; then + touch fail.stamp + fi + + cycles: + image: *elixir-image + commands: + - mix xref graph --format cycles --label compile | awk '{print $0} END{exit ($0 != "No cycles found")}' From b67d7c110623115bb7baa63ecb78300e6c3fe24a Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 14 Feb 2026 19:38:51 +0100 Subject: [PATCH 111/127] changelog --- changelog.d/woodpecker-pr-pipeline.skip | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 changelog.d/woodpecker-pr-pipeline.skip diff --git a/changelog.d/woodpecker-pr-pipeline.skip b/changelog.d/woodpecker-pr-pipeline.skip new file mode 100644 index 000000000..e69de29bb From 0fd544722f7686cd2705fb4d48032d5e4d5111e5 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 14 Feb 2026 19:43:50 +0100 Subject: [PATCH 112/127] Woodpecker: Ensure correct workflow status in lint pipeline --- .woodpecker/lint.yaml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index 02b814223..910e335df 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -26,7 +26,7 @@ steps: - su testuser -c "HOME=/home/testuser mix local.rebar --force" - su testuser -c "HOME=/home/testuser mix deps.get" - | - if ! su testuser -c "HOME=/home/testuser mix analyze" && ! -f fail.stamp; then + if [ ! su testuser -c "HOME=/home/testuser mix analyze" && ! -f fail.stamp ]; then touch fail.stamp fi @@ -34,3 +34,14 @@ steps: image: *elixir-image commands: - mix xref graph --format cycles --label compile | awk '{print $0} END{exit ($0 != "No cycles found")}' + + ensure-status: + image: *elixir-image + commands: | + if [ -f fail.stamp ]; then + echo "One or more previous steps fails. Failing workflow... + exit 1 + else + echo "All steps passed" + exit 0 + fi From 8640fcef22c8d773ac7b0d13669545f66b1e98d3 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 14 Feb 2026 20:10:13 +0100 Subject: [PATCH 113/127] Woodpecker CI: Fix compile error on Elixir 1.18 due to wrong OTP --- .woodpecker/unit-testing-elixir-1.18.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.woodpecker/unit-testing-elixir-1.18.yaml b/.woodpecker/unit-testing-elixir-1.18.yaml index 0e382f527..91bcf1824 100644 --- a/.woodpecker/unit-testing-elixir-1.18.yaml +++ b/.woodpecker/unit-testing-elixir-1.18.yaml @@ -8,7 +8,7 @@ depends_on: steps: unit-testing-elixir-1.18: - image: elixir:1.18-alpine + image: elixir:1.18-otp-27-alpine environment: MIX_ENV: test DB_HOST: postgres From b224a2dacc793302242fc0738739415fa5484318 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 14 Feb 2026 20:10:49 +0100 Subject: [PATCH 114/127] Woodpecker CI: Don't immediately fail whole lint workflow with one error --- .woodpecker/lint.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index 910e335df..766a992eb 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -8,6 +8,7 @@ steps: mix-format: image: &elixir-image docker.io/elixir:1.15-alpine + failure: ignore commands: - | if ! mix format --check-formatted; then @@ -16,6 +17,7 @@ steps: credo: image: *elixir-image + failure: ignore environment: MIX_ENV: test commands: @@ -32,6 +34,7 @@ steps: cycles: image: *elixir-image + failure: ignore commands: - mix xref graph --format cycles --label compile | awk '{print $0} END{exit ($0 != "No cycles found")}' From 265d3eeebc15e6e63c6239ae9184e5626705c2d4 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 14 Feb 2026 20:22:49 +0100 Subject: [PATCH 115/127] Woodpecker CI: Fix syntax error in lint workflow --- .woodpecker/lint.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index 766a992eb..792540f71 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -42,7 +42,7 @@ steps: image: *elixir-image commands: | if [ -f fail.stamp ]; then - echo "One or more previous steps fails. Failing workflow... + echo "One or more previous steps fails. Failing workflow..." exit 1 else echo "All steps passed" From 56a25202b97c911e78a06a0bc20d585620582e49 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 14 Feb 2026 20:33:43 +0100 Subject: [PATCH 116/127] Woodpecker CI: Fix credo --- .woodpecker/lint.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index 792540f71..78f4a5164 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -21,6 +21,7 @@ steps: environment: MIX_ENV: test commands: + - apk add --no-cache build-base cmake exiftool ffmpeg file-dev git openssl - adduser -D -h /home/testuser testuser - mkdir -p /home/testuser/.mix /home/testuser/.hex - chown -R testuser:testuser . /home/testuser From b0de9bd3cdf9731bff858df640276ec70612a3e4 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 14 Feb 2026 20:36:27 +0100 Subject: [PATCH 117/127] Woodpecker CI: Make xref use fail stamp --- .woodpecker/lint.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index 78f4a5164..77f4ff589 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -37,7 +37,10 @@ steps: image: *elixir-image failure: ignore commands: - - mix xref graph --format cycles --label compile | awk '{print $0} END{exit ($0 != "No cycles found")}' + - | + if [ ! mix xref graph --format cycles --label compile | awk '{print $0} END{exit ($0 != "No cycles found")}' && -f fail.stamp ]; then + touch fail.stamp + fi ensure-status: image: *elixir-image From cdcc432f31e0245f6d849aeec4f7e438f1f8df91 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 14 Feb 2026 20:44:28 +0100 Subject: [PATCH 118/127] Woodpecker CI: Lint workflow, don't use brackets in shell tests --- .woodpecker/lint.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index 77f4ff589..5fa440f2f 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -29,7 +29,7 @@ steps: - su testuser -c "HOME=/home/testuser mix local.rebar --force" - su testuser -c "HOME=/home/testuser mix deps.get" - | - if [ ! su testuser -c "HOME=/home/testuser mix analyze" && ! -f fail.stamp ]; then + if ! su testuser -c "HOME=/home/testuser mix analyze" && ! test -f fail.stamp; then touch fail.stamp fi @@ -38,14 +38,14 @@ steps: failure: ignore commands: - | - if [ ! mix xref graph --format cycles --label compile | awk '{print $0} END{exit ($0 != "No cycles found")}' && -f fail.stamp ]; then - touch fail.stamp + if ! mix xref graph --format cycles --label compile | awk '{print $0} END{exit ($0 != "No cycles found")}' && test -f fail.stamp; then + touch fail.stamp fi ensure-status: image: *elixir-image commands: | - if [ -f fail.stamp ]; then + if test -f fail.stamp; then echo "One or more previous steps fails. Failing workflow..." exit 1 else From 08bf6c8fedbf2776bf87ad81731df9ab6ad85eb3 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 14 Feb 2026 21:02:04 +0100 Subject: [PATCH 119/127] Woodpecker CI: Explicitely exit with non-zero exit code on fail --- .woodpecker/lint.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index 5fa440f2f..6d4042890 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -13,6 +13,7 @@ steps: - | if ! mix format --check-formatted; then touch fail.stamp + exit 1 fi credo: @@ -31,6 +32,7 @@ steps: - | if ! su testuser -c "HOME=/home/testuser mix analyze" && ! test -f fail.stamp; then touch fail.stamp + exit 1 fi cycles: @@ -40,6 +42,7 @@ steps: - | if ! mix xref graph --format cycles --label compile | awk '{print $0} END{exit ($0 != "No cycles found")}' && test -f fail.stamp; then touch fail.stamp + exit 1 fi ensure-status: From 1fe0970b6446444910acdc3340dc9065c45f24c6 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 14 Feb 2026 21:21:11 +0100 Subject: [PATCH 120/127] woodpecker CI: Fix cycles in lint workflow --- .woodpecker/lint.yaml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index 6d4042890..2594b903a 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -30,7 +30,7 @@ steps: - su testuser -c "HOME=/home/testuser mix local.rebar --force" - su testuser -c "HOME=/home/testuser mix deps.get" - | - if ! su testuser -c "HOME=/home/testuser mix analyze" && ! test -f fail.stamp; then + if ! su testuser -c "HOME=/home/testuser mix analyze"; then touch fail.stamp exit 1 fi @@ -39,8 +39,15 @@ steps: image: *elixir-image failure: ignore commands: + - apk add --no-cache build-base cmake exiftool ffmpeg file-dev git openssl + - adduser -D -h /home/testuser testuser + - mkdir -p /home/testuser/.mix /home/testuser/.hex + - chown -R testuser:testuser . /home/testuser + - su testuser -c "HOME=/home/testuser mix local.hex --force" + - su testuser -c "HOME=/home/testuser mix local.rebar --force" + - su testuser -c "HOME=/home/testuser mix compile" - | - if ! mix xref graph --format cycles --label compile | awk '{print $0} END{exit ($0 != "No cycles found")}' && test -f fail.stamp; then + if ! su testuser -c "HOME=/home/testuser mix xref graph --format cycles --label compile | awk '{print $0} END{exit ($0 != \"No cycles found\")}'"; then touch fail.stamp exit 1 fi From 7bba485397fa40c748647ddec9201b6612c47b93 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Mon, 16 Feb 2026 16:17:36 +0100 Subject: [PATCH 121/127] Woodpecker CI: Disable cycles lint step for now since it always fails --- .woodpecker/lint.yaml | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index 2594b903a..5e9d9ff63 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -35,22 +35,22 @@ steps: exit 1 fi - cycles: - image: *elixir-image - failure: ignore - commands: - - apk add --no-cache build-base cmake exiftool ffmpeg file-dev git openssl - - adduser -D -h /home/testuser testuser - - mkdir -p /home/testuser/.mix /home/testuser/.hex - - chown -R testuser:testuser . /home/testuser - - su testuser -c "HOME=/home/testuser mix local.hex --force" - - su testuser -c "HOME=/home/testuser mix local.rebar --force" - - su testuser -c "HOME=/home/testuser mix compile" - - | - if ! su testuser -c "HOME=/home/testuser mix xref graph --format cycles --label compile | awk '{print $0} END{exit ($0 != \"No cycles found\")}'"; then - touch fail.stamp - exit 1 - fi + # cycles: + # image: *elixir-image + # failure: ignore + # commands: + # - apk add --no-cache build-base cmake exiftool ffmpeg file-dev git openssl + # - adduser -D -h /home/testuser testuser + # - mkdir -p /home/testuser/.mix /home/testuser/.hex + # - chown -R testuser:testuser . /home/testuser + # - su testuser -c "HOME=/home/testuser mix local.hex --force" + # - su testuser -c "HOME=/home/testuser mix local.rebar --force" + # - su testuser -c "HOME=/home/testuser mix compile" + # - | + # if ! su testuser -c "HOME=/home/testuser mix xref graph --format cycles --label compile | awk '{print $0} END{exit ($0 != \"No cycles found\")}'"; then + # touch fail.stamp + # exit 1 + # fi ensure-status: image: *elixir-image From 072dc39d831bd01d36e0ec9f15c43ac900983506 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Mon, 16 Feb 2026 16:18:15 +0100 Subject: [PATCH 122/127] Woodpecker CI: Don't depend on changelog in lint workflow --- .woodpecker/lint.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index 5e9d9ff63..db815ff93 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -1,9 +1,6 @@ when: - event: pull_request -depends_on: - - changelog - steps: mix-format: image: &elixir-image From 096c4ea9801f780c1a1455ff0fff4086ea7c1478 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Mon, 16 Feb 2026 16:20:24 +0100 Subject: [PATCH 123/127] Woodpecker CI: Run lint and unit tests also on push to default branch --- .woodpecker/lint.yaml | 2 ++ .woodpecker/unit-testing-elixir-1.15.yaml | 4 ++-- .woodpecker/unit-testing-elixir-1.18.yaml | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index db815ff93..d222de483 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -1,5 +1,7 @@ when: - event: pull_request + - event: push + branch: ${CI_REPO_DEFAULT_BRANCH} steps: mix-format: diff --git a/.woodpecker/unit-testing-elixir-1.15.yaml b/.woodpecker/unit-testing-elixir-1.15.yaml index cf1e638eb..75ca3b6a0 100644 --- a/.woodpecker/unit-testing-elixir-1.15.yaml +++ b/.woodpecker/unit-testing-elixir-1.15.yaml @@ -1,10 +1,10 @@ when: - event: pull_request - event: push - branch: develop + branch: ${CI_REPO_DEFAULT_BRANCH} depends_on: - - changelog + - lint steps: unit-testing-elixir-1.15: diff --git a/.woodpecker/unit-testing-elixir-1.18.yaml b/.woodpecker/unit-testing-elixir-1.18.yaml index 91bcf1824..01687cfb8 100644 --- a/.woodpecker/unit-testing-elixir-1.18.yaml +++ b/.woodpecker/unit-testing-elixir-1.18.yaml @@ -1,10 +1,10 @@ when: - event: pull_request - event: push - branch: develop + branch: ${CI_REPO_DEFAULT_BRANCH} depends_on: - - changelog + - lint steps: unit-testing-elixir-1.18: From fd7b809c5430aa557cb6711629ddf14a5d005fe5 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Mon, 16 Feb 2026 18:12:38 +0100 Subject: [PATCH 124/127] Woodpecker CI: Only run lint and unit tests when relevant files changed --- .woodpecker/lint.yaml | 2 ++ .woodpecker/unit-testing-elixir-1.15.yaml | 2 ++ .woodpecker/unit-testing-elixir-1.18.yaml | 2 ++ 3 files changed, 6 insertions(+) diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index d222de483..b96d584ee 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -1,7 +1,9 @@ when: - event: pull_request + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: push branch: ${CI_REPO_DEFAULT_BRANCH} + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] steps: mix-format: diff --git a/.woodpecker/unit-testing-elixir-1.15.yaml b/.woodpecker/unit-testing-elixir-1.15.yaml index 75ca3b6a0..84046be41 100644 --- a/.woodpecker/unit-testing-elixir-1.15.yaml +++ b/.woodpecker/unit-testing-elixir-1.15.yaml @@ -1,7 +1,9 @@ when: - event: pull_request + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: push branch: ${CI_REPO_DEFAULT_BRANCH} + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] depends_on: - lint diff --git a/.woodpecker/unit-testing-elixir-1.18.yaml b/.woodpecker/unit-testing-elixir-1.18.yaml index 01687cfb8..8dcec62fb 100644 --- a/.woodpecker/unit-testing-elixir-1.18.yaml +++ b/.woodpecker/unit-testing-elixir-1.18.yaml @@ -1,7 +1,9 @@ when: - event: pull_request + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: push branch: ${CI_REPO_DEFAULT_BRANCH} + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] depends_on: - lint From 01ced6bea2db989287c53795d7fb66c5c9138600 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 1 Apr 2026 11:59:23 -0700 Subject: [PATCH 125/127] Fix the daily email digest job which was not executing --- changelog.d/email_digest.fix | 1 + .../20260401185429_cleanup_stale_digest_email_jobs.exs | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 changelog.d/email_digest.fix create mode 100644 priv/repo/migrations/20260401185429_cleanup_stale_digest_email_jobs.exs diff --git a/changelog.d/email_digest.fix b/changelog.d/email_digest.fix new file mode 100644 index 000000000..cd15874a2 --- /dev/null +++ b/changelog.d/email_digest.fix @@ -0,0 +1 @@ +Fix the daily email digest job which was not executing diff --git a/priv/repo/migrations/20260401185429_cleanup_stale_digest_email_jobs.exs b/priv/repo/migrations/20260401185429_cleanup_stale_digest_email_jobs.exs new file mode 100644 index 000000000..882c7ec66 --- /dev/null +++ b/priv/repo/migrations/20260401185429_cleanup_stale_digest_email_jobs.exs @@ -0,0 +1,9 @@ +defmodule Pleroma.Repo.Migrations.CleanupStaleDigestEmailJobs do + use Ecto.Migration + + def up do + execute( + "DELETE from oban_jobs WHERE queue = 'mailer' AND worker = 'Pleroma.Workers.Cron.DigestEmailsWorker'" + ) + end +end From 00265751cc7242016e6ae6d8ff0408f30c90f263 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 3 Apr 2026 13:22:12 -0700 Subject: [PATCH 126/127] Update Bandit --- changelog.d/bandit.change | 1 + mix.exs | 2 +- mix.lock | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 changelog.d/bandit.change diff --git a/changelog.d/bandit.change b/changelog.d/bandit.change new file mode 100644 index 000000000..3831a02c2 --- /dev/null +++ b/changelog.d/bandit.change @@ -0,0 +1 @@ +Update Bandit to 1.10.4 diff --git a/mix.exs b/mix.exs index dc9fa31f5..ca365e9eb 100644 --- a/mix.exs +++ b/mix.exs @@ -197,7 +197,7 @@ defmodule Pleroma.Mixfile do {:elixir_make, "~> 0.7.8", override: true}, {:blurhash, "~> 0.1.0", hex: :rinpatch_blurhash}, {:exile, "~> 0.10.0"}, - {:bandit, "~> 1.5.7"}, + {:bandit, "~> 1.10"}, {:websock_adapter, "~> 0.5.8"}, {:oban_live_dashboard, "~> 0.1.1"}, {:multipart, "~> 0.4.0", optional: true}, diff --git a/mix.lock b/mix.lock index 8e1f684dc..a940d4011 100644 --- a/mix.lock +++ b/mix.lock @@ -1,7 +1,7 @@ %{ "accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm", "11b18c220bcc2eab63b5470c038ef10eb6783bcb1fcdb11aa4137defa5ac1bb8"}, "argon2_elixir": {:hex, :argon2_elixir, "4.1.3", "4f28318286f89453364d7fbb53e03d4563fd7ed2438a60237eba5e426e97785f", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "7c295b8d8e0eaf6f43641698f962526cdf87c6feb7d14bd21e599271b510608c"}, - "bandit": {:hex, :bandit, "1.5.7", "6856b1e1df4f2b0cb3df1377eab7891bec2da6a7fd69dc78594ad3e152363a50", [:mix], [{:hpax, "~> 1.0.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "f2dd92ae87d2cbea2fa9aa1652db157b6cba6c405cb44d4f6dd87abba41371cd"}, + "bandit": {:hex, :bandit, "1.10.4", "02b9734c67c5916a008e7eb7e2ba68aaea6f8177094a5f8d95f1fb99069aac17", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "a5faf501042ac1f31d736d9d4a813b3db4ef812e634583b6a457b0928798a51d"}, "base62": {:hex, :base62, "1.2.2", "85c6627eb609317b70f555294045895ffaaeb1758666ab9ef9ca38865b11e629", [:mix], [{:custom_base, "~> 0.2.1", [hex: :custom_base, repo: "hexpm", optional: false]}], "hexpm", "d41336bda8eaa5be197f1e4592400513ee60518e5b9f4dcf38f4b4dae6f377bb"}, "bbcode_pleroma": {:hex, :bbcode_pleroma, "0.2.0", "d36f5bca6e2f62261c45be30fa9b92725c0655ad45c99025cb1c3e28e25803ef", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "19851074419a5fedb4ef49e1f01b30df504bb5dbb6d6adfc135238063bebd1c3"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "2.3.1", "5114d780459a04f2b4aeef52307de23de961b69e13a5cd98a911e39fda13f420", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "42182d5f46764def15bf9af83739e3bf4ad22661b1c34fc3e88558efced07279"}, @@ -144,7 +144,7 @@ "telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.2.1", "c9755987d7b959b557084e6990990cb96a50d6482c683fb9622a63837f3cd3d8", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "5e2c599da4983c4f88a33e9571f1458bf98b0cf6ba930f1dc3a6e8cf45d5afb6"}, "telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"}, "tesla": {:hex, :tesla, "1.15.3", "3a2b5c37f09629b8dcf5d028fbafc9143c0099753559d7fe567eaabfbd9b8663", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "98bb3d4558abc67b92fb7be4cd31bb57ca8d80792de26870d362974b58caeda7"}, - "thousand_island": {:hex, :thousand_island, "1.3.14", "ad45ebed2577b5437582bcc79c5eccd1e2a8c326abf6a3464ab6c06e2055a34a", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d0d24a929d31cdd1d7903a4fe7f2409afeedff092d277be604966cd6aa4307ef"}, + "thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"}, "timex": {:hex, :timex, "3.7.7", "3ed093cae596a410759104d878ad7b38e78b7c2151c6190340835515d4a46b8a", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "0ec4b09f25fe311321f9fc04144a7e3affe48eb29481d7a5583849b6c4dfa0a7"}, "toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"}, "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"}, From 7582b71f4699dfedaac9ccdae163537034a86466 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 24 Mar 2026 11:56:28 -0700 Subject: [PATCH 127/127] Downgrade Hackney to 1.20.1, before connection performance regressions It appears the implementation of Happy Eyeballs in 1.22.0 is the origin of some pretty serious performance regressions that remain even in the latest Hackney 3.0 branch. Connection tests: === 1.22.0 === First call: 9434ms Second call: 14ms === 1.21.0 === First call: 228ms Second call: 16ms We went back further to 1.20.1 though because of reported problems with the mail client and ssl_options. That bug was not reproduced by a dev, though, but we'll trust it for now. --- changelog.d/hackney-downgrade.change | 1 + mix.exs | 2 +- mix.lock | 7 ++++--- 3 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 changelog.d/hackney-downgrade.change diff --git a/changelog.d/hackney-downgrade.change b/changelog.d/hackney-downgrade.change new file mode 100644 index 000000000..a98710692 --- /dev/null +++ b/changelog.d/hackney-downgrade.change @@ -0,0 +1 @@ +Downgrade Hackney to 1.20.1 diff --git a/mix.exs b/mix.exs index dc9fa31f5..f445ca7aa 100644 --- a/mix.exs +++ b/mix.exs @@ -211,7 +211,7 @@ defmodule Pleroma.Mixfile do {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, {:mock, "~> 0.3.9", only: :test}, {:covertool, "~> 2.0", only: :test}, - {:hackney, "~> 1.25.0", override: true}, + {:hackney, "~> 1.20.1", override: true}, {:mox, "~> 1.2", only: :test}, {:websockex, "~> 0.4.3", only: :test}, {:benchee, "~> 1.4", only: :benchmark}, diff --git a/mix.lock b/mix.lock index 8e1f684dc..6e12e9aa3 100644 --- a/mix.lock +++ b/mix.lock @@ -13,7 +13,7 @@ "captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "e7b7cc34cc16b383461b966484c297e4ec9aeef6", [ref: "e7b7cc34cc16b383461b966484c297e4ec9aeef6"]}, "castore": {:hex, :castore, "1.0.15", "8aa930c890fe18b6fe0a0cff27b27d0d4d231867897bd23ea772dee561f032a3", [:mix], [], "hexpm", "96ce4c69d7d5d7a0761420ef743e2f4096253931a3ba69e5ff8ef1844fe446d3"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, - "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"}, + "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, "comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"}, "concurrent_limiter": {:hex, :concurrent_limiter, "0.1.1", "43ae1dc23edda1ab03dd66febc739c4ff710d047bb4d735754909f9a474ae01c", [:mix], [{:telemetry, "~> 0.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "53968ff238c0fbb4d7ed76ddb1af0be6f3b2f77909f6796e249e737c505a16eb"}, @@ -59,7 +59,7 @@ "gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm", "29bd14a88030980849c7ed2447b8db6d6c9278a28b11a44cafe41b791205440f"}, "gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"}, "gun": {:hex, :gun, "2.2.0", "b8f6b7d417e277d4c2b0dc3c07dfdf892447b087f1cc1caff9c0f556b884e33d", [:make, :rebar3], [{:cowlib, ">= 2.15.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "76022700c64287feb4df93a1795cff6741b83fb37415c40c34c38d2a4645261a"}, - "hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"}, + "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, "http_signatures": {:hex, :http_signatures, "0.1.2", "ed1cc7043abcf5bb4f30d68fb7bad9d618ec1a45c4ff6c023664e78b67d9c406", [:mix], [], "hexpm", "f08aa9ac121829dae109d608d83c84b940ef2f183ae50f2dd1e9a8bc619d8be7"}, @@ -80,7 +80,7 @@ "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"}, - "mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"}, + "mimerl": {:hex, :mimerl, "1.5.0", "f35aca6f23242339b3666e0ac0702379e362b469d0aea167f6cc713547e777ed", [:rebar3], [], "hexpm", "db648ce065bae14ea84ca8b5dd123f42f49417cef693541110bf6f9e9be9ecc4"}, "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, "mochiweb": {:hex, :mochiweb, "2.18.0", "eb55f1db3e6e960fac4e6db4e2db9ec3602cc9f30b86cd1481d56545c3145d2e", [:rebar3], [], "hexpm"}, "mock": {:hex, :mock, "0.3.9", "10e44ad1f5962480c5c9b9fa779c6c63de9bd31997c8e04a853ec990a9d841af", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "9e1b244c4ca2551bb17bb8415eed89e40ee1308e0fbaed0a4fdfe3ec8a4adbd3"}, @@ -128,6 +128,7 @@ "prometheus_phx": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/prometheus-phx.git", "9cd8f248c9381ffedc799905050abce194a97514", [branch: "no-logging"]}, "prometheus_plugs": {:hex, :prometheus_plugs, "1.1.5", "25933d48f8af3a5941dd7b621c889749894d8a1082a6ff7c67cc99dec26377c5", [:mix], [{:accept, "~> 0.1", [hex: :accept, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}, {:prometheus_process_collector, "~> 1.1", [hex: :prometheus_process_collector, repo: "hexpm", optional: true]}], "hexpm", "0273a6483ccb936d79ca19b0ab629aef0dba958697c94782bb728b920dfc6a79"}, "quantile_estimator": {:hex, :quantile_estimator, "0.2.1", "ef50a361f11b5f26b5f16d0696e46a9e4661756492c981f7b2229ef42ff1cd15", [:rebar3], [], "hexpm", "282a8a323ca2a845c9e6f787d166348f776c1d4a41ede63046d72d422e3da946"}, + "quic": {:hex, :quic, "0.10.2", "4b390507a85f65ce47808f3df1a864e0baf9adb7a1b4ea9f4dcd66fe9d0cb166", [:rebar3], [], "hexpm", "7c196a66973c877a59768a5687f0a0610ff11817254d0a4e45cc4e3a16b1d00b"}, "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"}, "recon": {:hex, :recon, "2.5.6", "9052588e83bfedfd9b72e1034532aee2a5369d9d9343b61aeb7fbce761010741", [:mix, :rebar3], [], "hexpm", "96c6799792d735cc0f0fd0f86267e9d351e63339cbe03df9d162010cefc26bb0"}, "remote_ip": {:git, "https://git.pleroma.social/pleroma/remote_ip.git", "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8", [ref: "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8"]},