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 71f5a493f3be15308959a77ad368a61de48721e2 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 1 Jan 2026 10:02:37 +0400 Subject: [PATCH 006/127] Search: Better sorting for user searches. --- lib/pleroma/user/search.ex | 48 +++++++- test/pleroma/user/search_test.exs | 195 ++++++++++++++++++++++++++++++ 2 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 test/pleroma/user/search_test.exs diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex index 851745714..abc45f62a 100644 --- a/lib/pleroma/user/search.ex +++ b/lib/pleroma/user/search.ex @@ -4,6 +4,7 @@ defmodule Pleroma.User.Search do alias Pleroma.EctoType.ActivityPub.ObjectValidators.Uri, as: UriType + alias Pleroma.Instances.Instance alias Pleroma.Pagination alias Pleroma.User @@ -88,12 +89,13 @@ defmodule Pleroma.User.Search do |> filter_invisible_users() |> filter_internal_users() |> filter_blocked_domains(for_user) + |> filter_unreachable_users() |> fts_search(query_string) |> select_top_users(top_user_ids) |> trigram_rank(query_string) |> boost_search_rank(for_user, top_user_ids) |> subquery() - |> order_by(desc: :search_rank) + |> order_by_search_rank(for_user) |> maybe_restrict_local(for_user) |> maybe_restrict_accepting_chat_messages(capabilities) |> filter_deactivated_users() @@ -196,6 +198,14 @@ defmodule Pleroma.User.Search do defp filter_blocked_domains(query, _), do: query + defp filter_unreachable_users(query) do + from(u in query, + left_join: i in Instance, + on: i.host == fragment("substring(? from '.*://([^/]*)')", u.ap_id), + where: is_nil(i.unreachable_since) + ) + end + defp maybe_resolve(true, user, query) do case {limit(), user} do {:all, _} -> :noop @@ -236,6 +246,16 @@ defmodule Pleroma.User.Search do from(u in subquery(query), select_merge: %{ + search_type: + fragment( + """ + CASE WHEN (?) THEN 2 + WHEN (?) THEN 1 + ELSE 0 END + """, + u.id in ^top_user_ids, + u.id in ^friends_ids or u.id in ^followers_ids + ), search_rank: fragment( """ @@ -261,6 +281,14 @@ defmodule Pleroma.User.Search do defp boost_search_rank(query, _for_user, top_user_ids) do from(u in subquery(query), select_merge: %{ + search_type: + fragment( + """ + CASE WHEN (?) THEN 2 + ELSE 0 END + """, + u.id in ^top_user_ids + ), search_rank: fragment( """ @@ -273,4 +301,22 @@ defmodule Pleroma.User.Search do } ) end + + defp order_by_search_rank(query, %User{}) do + order_by( + query, + [u], + desc: u.search_type, + desc_nulls_last: + fragment( + "CASE WHEN ? = 1 THEN COALESCE(?, ?) ELSE NULL END", + u.search_type, + u.last_status_at, + u.last_active_at + ), + desc: u.search_rank + ) + end + + defp order_by_search_rank(query, _), do: order_by(query, desc: :search_rank) end diff --git a/test/pleroma/user/search_test.exs b/test/pleroma/user/search_test.exs new file mode 100644 index 000000000..c1aca90bc --- /dev/null +++ b/test/pleroma/user/search_test.exs @@ -0,0 +1,195 @@ +# Pleroma: A lightweight social networking server +# Copyright © Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.User.SearchTest do + use Pleroma.DataCase, async: false + + import Pleroma.Factory + + alias Pleroma.Instances + alias Pleroma.Repo + alias Pleroma.User + + describe "search/2 mention suggestions" do + test "prioritizes followed/follower users before others" do + user = insert(:user) + + related = + insert(:user, + local: false, + nickname: "hj@real.example", + ap_id: "https://real.example/users/hj", + last_status_at: ~N[2020-01-01 00:00:00] + ) + + other = insert(:user, nickname: "hj", last_status_at: ~N[2020-01-02 00:00:00]) + + {:ok, _related, _user} = User.follow(related, user) + + results = User.search("hj", for_user: user) |> Enum.map(& &1.id) + + assert results == [related.id, other.id] + end + + test "orders followed/follower users by most recent activity" do + user = insert(:user) + + older = + insert(:user, + local: false, + nickname: "ali@remote.example", + ap_id: "https://remote.example/users/ali", + last_status_at: ~N[2020-01-01 00:00:00] + ) + + newer = + insert(:user, + local: false, + nickname: "alia@remote.example", + ap_id: "https://remote.example/users/alia", + last_status_at: ~N[2020-01-02 00:00:00] + ) + + {:ok, _user, _older} = User.follow(user, older) + {:ok, _user, _newer} = User.follow(user, newer) + + assert [newer.id, older.id] == + User.search("ali", for_user: user) + |> Enum.map(& &1.id) + end + + test "groups followed/follower users first and sorts them by recency" do + user = insert(:user) + + following_newest = + insert(:user, + local: false, + nickname: "mentiontesta@related.example", + ap_id: "https://related.example/users/mentiontesta", + last_status_at: ~N[2020-01-03 00:00:00] + ) + + follower_middle = + insert(:user, + local: false, + nickname: "mentiontestb@related.example", + ap_id: "https://related.example/users/mentiontestb", + last_status_at: ~N[2020-01-02 00:00:00] + ) + + mutual_oldest = + insert(:user, + local: false, + nickname: "mentiontestc@related.example", + ap_id: "https://related.example/users/mentiontestc", + last_status_at: ~N[2020-01-01 00:00:00] + ) + + unrelated_newer = + insert(:user, + local: false, + nickname: "mentiontestd@unrelated.example", + ap_id: "https://unrelated.example/users/mentiontestd", + last_status_at: ~N[2020-01-04 00:00:00] + ) + + {:ok, _user, _following_newest} = User.follow(user, following_newest) + {:ok, _follower_middle, _user} = User.follow(follower_middle, user) + + {:ok, _user, _mutual_oldest} = User.follow(user, mutual_oldest) + {:ok, _mutual_oldest, _user} = User.follow(mutual_oldest, user) + + results = User.search("mentiontest", for_user: user) + + assert Enum.map(results, & &1.id) == + [following_newest.id, follower_middle.id, mutual_oldest.id, unrelated_newer.id] + end + + test "uses last_active_at when last_status_at is missing" do + user = insert(:user) + + older = + insert(:user, + local: false, + nickname: "activefallbacka@remote.example", + ap_id: "https://remote.example/users/activefallbacka", + last_status_at: nil, + last_active_at: ~N[2020-01-01 00:00:00] + ) + + newer = + insert(:user, + local: false, + nickname: "activefallbackb@remote.example", + ap_id: "https://remote.example/users/activefallbackb", + last_status_at: nil, + last_active_at: ~N[2020-01-02 00:00:00] + ) + + {:ok, _user, _older} = User.follow(user, older) + {:ok, _user, _newer} = User.follow(user, newer) + + assert [newer.id, older.id] == + User.search("activefallback", for_user: user) + |> Enum.map(& &1.id) + end + + test "does not return deactivated users even if related" do + user = insert(:user) + + active = + insert(:user, + local: false, + nickname: "deactivatedtesta@remote.example", + ap_id: "https://remote.example/users/deactivatedtesta", + last_status_at: ~N[2020-01-02 00:00:00] + ) + + deactivated = + insert(:user, + local: false, + nickname: "deactivatedtestb@remote.example", + ap_id: "https://remote.example/users/deactivatedtestb", + last_status_at: ~N[2020-01-03 00:00:00] + ) + + {:ok, _user, _active} = User.follow(user, active) + {:ok, _user, _deactivated} = User.follow(user, deactivated) + Repo.update!(Ecto.Changeset.change(deactivated, is_active: false)) + + results = User.search("deactivatedtest", for_user: user) |> Enum.map(& &1.id) + + assert results == [active.id] + end + + test "does not return users from unreachable instances" do + user = insert(:user) + + {:ok, _instance} = Instances.set_unreachable("dead.example") + + dead = + insert(:user, + local: false, + nickname: "ali@dead.example", + ap_id: "https://dead.example/users/ali", + last_status_at: ~N[2020-01-02 00:00:00] + ) + + alive = + insert(:user, + local: false, + nickname: "ali@alive.example", + ap_id: "https://alive.example/users/ali", + last_status_at: ~N[2020-01-02 00:00:00] + ) + + {:ok, _user, _alive} = User.follow(user, alive) + {:ok, _user, _dead} = User.follow(user, dead) + + results = User.search("ali", for_user: user) |> Enum.map(& &1.id) + + assert results == [alive.id] + end + end +end From 3903f12c78f9192ad27defe9738ca9669473c4c9 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 1 Jan 2026 10:04:55 +0400 Subject: [PATCH 007/127] Add changelog --- changelog.d/user-search-sorting.change | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/user-search-sorting.change diff --git a/changelog.d/user-search-sorting.change b/changelog.d/user-search-sorting.change new file mode 100644 index 000000000..def27e955 --- /dev/null +++ b/changelog.d/user-search-sorting.change @@ -0,0 +1 @@ +Improve user search / autocompletion ordering. From 9ede9b92d3604486fdf7189ef6d55c356ffb190a Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Tue, 6 Jan 2026 15:32:01 +0400 Subject: [PATCH 008/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 009/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 010/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 011/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 012/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 699a7e57e82e71b9e706ded28185ee490a903c6d Mon Sep 17 00:00:00 2001 From: Phantasm Date: Mon, 16 Feb 2026 17:04:00 +0100 Subject: [PATCH 013/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 23a4d68c97f710f753b42180ea9cbda067b192a3 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 28 Jan 2026 22:06:10 +0100 Subject: [PATCH 014/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 015/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 016/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 848b3f5d5b6e004c51e18a4adb04d721fb89ea5c Mon Sep 17 00:00:00 2001 From: Yonle Date: Mon, 23 Feb 2026 20:57:11 +0700 Subject: [PATCH 017/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 018/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 019/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 020/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 021/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 022/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 023/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 024/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 025/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 026/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 027/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 028/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 029/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 4e1ba489ec087b980e61ad705ab966953946ee90 Mon Sep 17 00:00:00 2001 From: shibao Date: Sun, 8 Mar 2026 11:16:33 +0000 Subject: [PATCH 030/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 031/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 032/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 033/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 711b33d81cd8ecbf83aa7eea9c4a76ab531713fe Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 25 Mar 2026 13:32:25 -0700 Subject: [PATCH 034/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 035/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 036/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 037/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 038/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 039/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 040/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 041/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 042/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 043/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 044/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 045/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 046/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 047/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 048/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 049/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 050/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 051/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 052/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 053/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 054/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 055/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 056/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 057/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 058/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 059/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 060/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 061/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 062/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 063/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"]}, From c2fb145c5f33ea1b4c3ad7dd4ce21588bedc5e95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 10 Apr 2026 21:33:20 +0200 Subject: [PATCH 064/127] litepub-0.1.jsonld cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/context-cleanup.skip | 1 + priv/static/schemas/litepub-0.1.jsonld | 59 +++++++++++++++----------- 2 files changed, 35 insertions(+), 25 deletions(-) create mode 100644 changelog.d/context-cleanup.skip diff --git a/changelog.d/context-cleanup.skip b/changelog.d/context-cleanup.skip new file mode 100644 index 000000000..ae609602d --- /dev/null +++ b/changelog.d/context-cleanup.skip @@ -0,0 +1 @@ +litepub-0.1.jsonld cleanup diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld index 3569165a4..4e82d4b01 100644 --- a/priv/static/schemas/litepub-0.1.jsonld +++ b/priv/static/schemas/litepub-0.1.jsonld @@ -4,46 +4,55 @@ "https://w3id.org/security/v1", "https://purl.archive.org/socialweb/webfinger", { - "Emoji": "toot:Emoji", + "as": "https://www.w3.org/ns/activitystreams#", + "ostatus": "http://ostatus.org#", + "schema": "http://schema.org#", + "vcard": "http://www.w3.org/2006/vcard/ns#", + + "fedibird": "http://fedibird.com/ns#", + "litepub": "http://litepub.social/ns#", + "sm": "http://smithereen.software/ns#", + "toot": "http://joinmastodon.org/ns#", + + "alsoKnownAs": { + "@id": "as:alsoKnownAs", + "@type": "@id" + }, "Hashtag": "as:Hashtag", - "PropertyValue": "schema:PropertyValue", + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "quoteUrl": "as:quoteUrl", + "sensitive": "as:sensitive", + "atomUri": "ostatus:atomUri", "conversation": { "@id": "ostatus:conversation", "@type": "@id" }, - "discoverable": "toot:discoverable", - "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", - "capabilities": "litepub:capabilities", - "ostatus": "http://ostatus.org#", - "schema": "http://schema.org#", - "toot": "http://joinmastodon.org/ns#", - "fedibird": "http://fedibird.com/ns#", + + "PropertyValue": "schema:PropertyValue", "value": "schema:value", - "sensitive": "as:sensitive", - "litepub": "http://litepub.social/ns#", - "invisible": "litepub:invisible", + + "quoteUri": "fedibird:quoteUri", + + "capabilities": "litepub:capabilities", + "ChatMessage": "litepub:ChatMessage", "directMessage": "litepub:directMessage", + "EmojiReact": "litepub:EmojiReact", + "formerRepresentations": "litepub:formerRepresentations", + "invisible": "litepub:invisible", "listMessage": { "@id": "litepub:listMessage", "@type": "@id" }, - "quoteUrl": "as:quoteUrl", - "quoteUri": "fedibird:quoteUri", "oauthRegistrationEndpoint": { "@id": "litepub:oauthRegistrationEndpoint", "@type": "@id" }, - "EmojiReact": "litepub:EmojiReact", - "ChatMessage": "litepub:ChatMessage", - "alsoKnownAs": { - "@id": "as:alsoKnownAs", - "@type": "@id" - }, - "vcard": "http://www.w3.org/2006/vcard/ns#", - "formerRepresentations": "litepub:formerRepresentations", - "sm": "http://smithereen.software/ns#", - "nonAnonymous": "sm:nonAnonymous" + + "nonAnonymous": "sm:nonAnonymous", + + "discoverable": "toot:discoverable", + "Emoji": "toot:Emoji" } ] -} +} \ No newline at end of file From fc5aea73ff094a4b44599d20c0498688f039d3f4 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 7 Apr 2026 14:25:48 +0200 Subject: [PATCH 065/127] Woodpecker CI: Add develop Docker image build pipeline --- .woodpecker/docker-develop.yaml | 45 +++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .woodpecker/docker-develop.yaml diff --git a/.woodpecker/docker-develop.yaml b/.woodpecker/docker-develop.yaml new file mode 100644 index 000000000..c50fd7eb1 --- /dev/null +++ b/.woodpecker/docker-develop.yaml @@ -0,0 +1,45 @@ +when: + - event: push + branch: ${CI_REPO_DEFAULT_BRANCH} + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + +matrix: + platform: + - linux/amd64 + - linux/arm64 + +# This is needed for the when clauses below. +# When the platform clause is fixed, this might not be needed anymore +labels: + platform: ${platform} + +steps: + docker-develop-amd64: + image: woodpeckerci/plugin-docker-buildx:6.0.4 + # when: + # - platform: linux/amd64 + # does not work even though it should according to docs + # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 + when: + - evaluate: platform == "linux/amd64" + settings: + repo: git.pleroma.social/pleroma/pleroma + tags: [latest-amd64, develop-amd64] + registry: git.pleroma.social + username: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + + docker-develop-arm64: + image: woodpeckerci/plugin-docker-buildx:6.0.4 + when: + - evaluate: platform == "linux/arm64" + settings: + repo: git.pleroma.social/pleroma/pleroma + tags: [latest-arm64, develop-arm64] + registry: git.pleroma.social + username: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password From 5351cd4ce98511113a7bb132f43b2b3c46c00041 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 7 Apr 2026 19:01:26 +0200 Subject: [PATCH 066/127] Woodpecker CI: Add OTP develop pipeline musl and glibc builds need to be split due to workspace polution. Workflow's steps share the same workspace, so two separate build steps can't be in the same workflow since they share the buld artifacts, deps. --- .woodpecker/otp-develop-musl.yaml | 85 ++++++++++++++++++++++++++++++ .woodpecker/otp-develop.yaml | 87 +++++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 .woodpecker/otp-develop-musl.yaml create mode 100644 .woodpecker/otp-develop.yaml diff --git a/.woodpecker/otp-develop-musl.yaml b/.woodpecker/otp-develop-musl.yaml new file mode 100644 index 000000000..74703e759 --- /dev/null +++ b/.woodpecker/otp-develop-musl.yaml @@ -0,0 +1,85 @@ +when: + - event: push + branch: ${CI_REPO_DEFAULT_BRANCH} + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + +matrix: + platform: + - linux/amd64 + - linux/arm64 + +# This is needed for the when clauses below. +# When the platform clause is fixed, this might not be needed anymore +labels: + platform: ${platform} + +steps: + otp-develop-amd64-musl: + image: docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 + # when: + # - platform: linux/amd64 + # does not work even though it should according to docs + # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 + when: + - evaluate: platform == "linux/amd64" + environment: + MIX_ENV: prod + VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS + commands: + - apk add git build-base cmake file-dev openssl vips-dev zip + - echo "import Config" > config/prod.secret.exs + - mix local.hex --force + - mix local.rebar --force + - mix deps.get --only prod + - mkdir release + - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} + - mix release --path release + - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip release + + upload-artifacts-amd64-musl: + image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + when: + - evaluate: platform == "linux/amd64" + settings: + user: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + owner: 'pleroma' + package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-amd64-musl + package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl + file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip + file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip + + otp-develop-arm64-musl: + image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 + when: + - evaluate: platform == "linux/arm64" + environment: + MIX_ENV: prod + VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS + commands: + - apk add git build-base cmake file-dev openssl vips-dev zip + - echo "import Config" > config/prod.secret.exs + - mix local.hex --force + - mix local.rebar --force + - mix deps.get --only prod + - mkdir release + - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} + - mix release --path release + - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip release + + upload-artifacts-arm64-musl: + image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + when: + - evaluate: platform == "linux/arm64" + settings: + user: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + owner: 'pleroma' + package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-arm64-musl + package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl + file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip + file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip diff --git a/.woodpecker/otp-develop.yaml b/.woodpecker/otp-develop.yaml new file mode 100644 index 000000000..20a81569d --- /dev/null +++ b/.woodpecker/otp-develop.yaml @@ -0,0 +1,87 @@ +when: + - event: push + branch: ${CI_REPO_DEFAULT_BRANCH} + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + +matrix: + platform: + - linux/amd64 + - linux/arm64 + +# This is needed for the when clauses below. +# When the platform clause is fixed, this might not be needed anymore +labels: + platform: ${platform} + +steps: + otp-develop-amd64: + image: docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 + # when: + # - platform: linux/amd64 + # does not work even though it should according to docs + # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 + when: + - evaluate: platform == "linux/amd64" + environment: + MIX_ENV: prod + VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS + DEBIAN_FRONTEND: noninteractive + commands: + - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip + - echo "import Config" > config/prod.secret.exs + - mix local.hex --force + - mix local.rebar --force + - mix deps.get --only prod + - mkdir release + - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} + - mix release --path release + - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip release + + upload-artifacts-amd64: + image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + when: + - evaluate: platform == "linux/amd64" + settings: + user: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + owner: 'pleroma' + package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-amd64 + package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64 + file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip + file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip + + otp-develop-arm64: + image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 + when: + - evaluate: platform == "linux/arm64" + environment: + MIX_ENV: prod + VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS + DEBIAN_FRONTEND: noninteractive + commands: + - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip + - echo "import Config" > config/prod.secret.exs + - mix local.hex --force + - mix local.rebar --force + - mix deps.get --only prod + - mkdir release + - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} + - mix release --path release + - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip release + + upload-artifacts-arm64: + image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + when: + - evaluate: platform == "linux/arm64" + settings: + user: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + owner: 'pleroma' + package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-arm64 + package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64 + file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip + file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip From d2f7c9252f5d4e92144ca434f9643728a836ed86 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 9 Apr 2026 21:47:51 +0200 Subject: [PATCH 067/127] Woodpecker CI Docker develop: Switch to kaniko --- .woodpecker/docker-develop.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.woodpecker/docker-develop.yaml b/.woodpecker/docker-develop.yaml index c50fd7eb1..92299ca7c 100644 --- a/.woodpecker/docker-develop.yaml +++ b/.woodpecker/docker-develop.yaml @@ -15,7 +15,7 @@ labels: steps: docker-develop-amd64: - image: woodpeckerci/plugin-docker-buildx:6.0.4 + image: woodpeckerci/plugin-kaniko:2.3.1 # when: # - platform: linux/amd64 # does not work even though it should according to docs @@ -23,7 +23,7 @@ steps: when: - evaluate: platform == "linux/amd64" settings: - repo: git.pleroma.social/pleroma/pleroma + repo: pleroma/pleroma tags: [latest-amd64, develop-amd64] registry: git.pleroma.social username: @@ -32,11 +32,11 @@ steps: from_secret: pleroma-ci-password docker-develop-arm64: - image: woodpeckerci/plugin-docker-buildx:6.0.4 + image: woodpeckerci/plugin-kaniko:2.3.1 when: - evaluate: platform == "linux/arm64" settings: - repo: git.pleroma.social/pleroma/pleroma + repo: pleroma/pleroma tags: [latest-arm64, develop-arm64] registry: git.pleroma.social username: From e2adc796c40d23881640533ed1c3250ae451d089 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 9 Apr 2026 23:34:49 +0200 Subject: [PATCH 068/127] Woodpecker CI: Multiplatform Docker image manifests --- .woodpecker/docker-develop-combine.yaml | 34 +++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .woodpecker/docker-develop-combine.yaml diff --git a/.woodpecker/docker-develop-combine.yaml b/.woodpecker/docker-develop-combine.yaml new file mode 100644 index 000000000..f79e6b59b --- /dev/null +++ b/.woodpecker/docker-develop-combine.yaml @@ -0,0 +1,34 @@ +when: + - event: push + branch: ${CI_REPO_DEFAULT_BRANCH} + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + +depends_on: + - docker-develop + +skip_clone: true + +steps: + docker-develop-combine: + image: docker.io/docker:cli + environment: + BUILD_ARCHES: "amd64 arm64" + REGISTRY: "git.fluffytail.org" + IMAGE_PATH: "git.fluffytail.org/pleroma-test/pleroma" + REGISTRY_USER: + from_secret: pleroma-ci-user + REGISTRY_PASSWORD: + from_secret: pleroma-ci-password + commands: + - set +x + - mkdir -p ~/.docker + - echo "{\"auths\":{\"$REGISTRY\":{\"username\":\"$REGISTRY_USER\",\"password\":\"$REGISTRY_PASSWORD\"}}}" > ~/.docker/config.json + - set -x + - IMAGES_DEVELOP=; for arch in $BUILD_ARCHES; do IMAGES_DEVELOP="$IMAGES_DEVELOP $IMAGE_PATH:develop-$arch"; done + - IMAGES_LATEST=; for arch in $BUILD_ARCHES; do IMAGES_LATEST="$IMAGES_LATEST $IMAGE_PATH:latest-$arch"; done + - echo $IMAGES_DEVELOP + - echo $IMAGES_LATEST + - docker manifest create $IMAGE_PATH:develop $IMAGES_DEVELOP + - docker manifest push $IMAGE_PATH:develop + - docker manifest create $IMAGE_PATH:latest $IMAGES_LATEST + - docker manifest push $IMAGE_PATH:latest From 67e7f788c9b6688cda039f9ee4473522367ced72 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sun, 12 Apr 2026 00:00:10 +0200 Subject: [PATCH 069/127] Woodpecker CI Docker Develop combine: Switch to plugin Replaces manual tagging handling with a plugin, mostly to avoid dealing with echoed out secrets in the job log, which should be censored automatically, but who knows when that breaks... --- .woodpecker/docker-develop-combine.yaml | 28 +++++++------------------ 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/.woodpecker/docker-develop-combine.yaml b/.woodpecker/docker-develop-combine.yaml index f79e6b59b..67da6ca12 100644 --- a/.woodpecker/docker-develop-combine.yaml +++ b/.woodpecker/docker-develop-combine.yaml @@ -10,25 +10,13 @@ skip_clone: true steps: docker-develop-combine: - image: docker.io/docker:cli - environment: - BUILD_ARCHES: "amd64 arm64" - REGISTRY: "git.fluffytail.org" - IMAGE_PATH: "git.fluffytail.org/pleroma-test/pleroma" - REGISTRY_USER: + image: git.fluffytail.org/phnt/wpc-docker-tagger:latest + settings: + registry: "git.fluffytail.org" + image: "pleroma-test/pleroma" + architectures: [amd64, arm64] + tags: [latest, develop] + username: from_secret: pleroma-ci-user - REGISTRY_PASSWORD: + password: from_secret: pleroma-ci-password - commands: - - set +x - - mkdir -p ~/.docker - - echo "{\"auths\":{\"$REGISTRY\":{\"username\":\"$REGISTRY_USER\",\"password\":\"$REGISTRY_PASSWORD\"}}}" > ~/.docker/config.json - - set -x - - IMAGES_DEVELOP=; for arch in $BUILD_ARCHES; do IMAGES_DEVELOP="$IMAGES_DEVELOP $IMAGE_PATH:develop-$arch"; done - - IMAGES_LATEST=; for arch in $BUILD_ARCHES; do IMAGES_LATEST="$IMAGES_LATEST $IMAGE_PATH:latest-$arch"; done - - echo $IMAGES_DEVELOP - - echo $IMAGES_LATEST - - docker manifest create $IMAGE_PATH:develop $IMAGES_DEVELOP - - docker manifest push $IMAGE_PATH:develop - - docker manifest create $IMAGE_PATH:latest $IMAGES_LATEST - - docker manifest push $IMAGE_PATH:latest From 13d6246ed9e77e5cd5f812c6b22859d8a310ee0e Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sun, 12 Apr 2026 22:19:56 +0200 Subject: [PATCH 070/127] Woodpecker CI: Cleanup develop releases CI code duplication --- .woodpecker/docker-develop.yaml | 23 ++++++++-------- .woodpecker/otp-develop-musl.yaml | 44 ++++++++++++++----------------- .woodpecker/otp-develop.yaml | 44 ++++++++++++++----------------- 3 files changed, 51 insertions(+), 60 deletions(-) diff --git a/.woodpecker/docker-develop.yaml b/.woodpecker/docker-develop.yaml index 92299ca7c..1a61b1626 100644 --- a/.woodpecker/docker-develop.yaml +++ b/.woodpecker/docker-develop.yaml @@ -13,6 +13,15 @@ matrix: labels: platform: ${platform} +variables: + docker_variables: &docker_variables + repo: pleroma/pleroma + registry: git.pleroma.social + username: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + steps: docker-develop-amd64: image: woodpeckerci/plugin-kaniko:2.3.1 @@ -23,23 +32,13 @@ steps: when: - evaluate: platform == "linux/amd64" settings: - repo: pleroma/pleroma + <<: *docker_variables tags: [latest-amd64, develop-amd64] - registry: git.pleroma.social - username: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password docker-develop-arm64: image: woodpeckerci/plugin-kaniko:2.3.1 when: - evaluate: platform == "linux/arm64" settings: - repo: pleroma/pleroma + <<: *docker_variables tags: [latest-arm64, develop-arm64] - registry: git.pleroma.social - username: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password diff --git a/.woodpecker/otp-develop-musl.yaml b/.woodpecker/otp-develop-musl.yaml index 74703e759..7ed567949 100644 --- a/.woodpecker/otp-develop-musl.yaml +++ b/.woodpecker/otp-develop-musl.yaml @@ -13,6 +13,22 @@ matrix: labels: platform: ${platform} +variables: + pleroma_build_cmds: &pleroma_build_cmds + - echo "import Config" > config/prod.secret.exs + - mix local.hex --force + - mix local.rebar --force + - mix deps.get --only prod + - mkdir release + - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} + - mix release --path release + artifacts_uploader_settings: &artifacts_uploader_settings + user: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + owner: 'pleroma' + steps: otp-develop-amd64-musl: image: docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 @@ -27,13 +43,7 @@ steps: VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS commands: - apk add git build-base cmake file-dev openssl vips-dev zip - - echo "import Config" > config/prod.secret.exs - - mix local.hex --force - - mix local.rebar --force - - mix deps.get --only prod - - mkdir release - - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} - - mix release --path release + - <<: *pleroma_build_cmds - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip release upload-artifacts-amd64-musl: @@ -41,11 +51,7 @@ steps: when: - evaluate: platform == "linux/amd64" settings: - user: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password - owner: 'pleroma' + <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-amd64-musl package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip @@ -60,13 +66,7 @@ steps: VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS commands: - apk add git build-base cmake file-dev openssl vips-dev zip - - echo "import Config" > config/prod.secret.exs - - mix local.hex --force - - mix local.rebar --force - - mix deps.get --only prod - - mkdir release - - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} - - mix release --path release + - <<: *pleroma_build_cmds - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip release upload-artifacts-arm64-musl: @@ -74,11 +74,7 @@ steps: when: - evaluate: platform == "linux/arm64" settings: - user: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password - owner: 'pleroma' + <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-arm64-musl package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip diff --git a/.woodpecker/otp-develop.yaml b/.woodpecker/otp-develop.yaml index 20a81569d..6cd76d450 100644 --- a/.woodpecker/otp-develop.yaml +++ b/.woodpecker/otp-develop.yaml @@ -13,6 +13,22 @@ matrix: labels: platform: ${platform} +variables: + pleroma_build_cmds: &pleroma_build_cmds + - echo "import Config" > config/prod.secret.exs + - mix local.hex --force + - mix local.rebar --force + - mix deps.get --only prod + - mkdir release + - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} + - mix release --path release + artifacts_uploader_settings: &artifacts_uploader_settings + user: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + owner: 'pleroma' + steps: otp-develop-amd64: image: docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 @@ -28,13 +44,7 @@ steps: DEBIAN_FRONTEND: noninteractive commands: - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - - echo "import Config" > config/prod.secret.exs - - mix local.hex --force - - mix local.rebar --force - - mix deps.get --only prod - - mkdir release - - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} - - mix release --path release + - <<: *pleroma_build_cmds - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip release upload-artifacts-amd64: @@ -42,11 +52,7 @@ steps: when: - evaluate: platform == "linux/amd64" settings: - user: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password - owner: 'pleroma' + <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-amd64 package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64 file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip @@ -62,13 +68,7 @@ steps: DEBIAN_FRONTEND: noninteractive commands: - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - - echo "import Config" > config/prod.secret.exs - - mix local.hex --force - - mix local.rebar --force - - mix deps.get --only prod - - mkdir release - - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} - - mix release --path release + - <<: *pleroma_build_cmds - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip release upload-artifacts-arm64: @@ -76,11 +76,7 @@ steps: when: - evaluate: platform == "linux/arm64" settings: - user: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password - owner: 'pleroma' + <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-arm64 package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64 file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip From f00c13602d734f4215e2ac6c54a6cbbff12e5d40 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sun, 12 Apr 2026 22:44:40 +0200 Subject: [PATCH 071/127] Woodpecker CI Develop: Also tag images using commit sha With the commit sha being present, `tags` now has to be a list instead of an array, otherwise Woodpecker raises a yaml compiler warning: yaml: line 17: did not find expected ',' or ']' --- .woodpecker/docker-develop-combine.yaml | 5 ++++- .woodpecker/docker-develop.yaml | 10 ++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.woodpecker/docker-develop-combine.yaml b/.woodpecker/docker-develop-combine.yaml index 67da6ca12..ebe5a502c 100644 --- a/.woodpecker/docker-develop-combine.yaml +++ b/.woodpecker/docker-develop-combine.yaml @@ -15,7 +15,10 @@ steps: registry: "git.fluffytail.org" image: "pleroma-test/pleroma" architectures: [amd64, arm64] - tags: [latest, develop] + tags: + - latest + - develop + - ${CI_COMMIT_SHA} username: from_secret: pleroma-ci-user password: diff --git a/.woodpecker/docker-develop.yaml b/.woodpecker/docker-develop.yaml index 1a61b1626..4f6bbdf12 100644 --- a/.woodpecker/docker-develop.yaml +++ b/.woodpecker/docker-develop.yaml @@ -33,7 +33,10 @@ steps: - evaluate: platform == "linux/amd64" settings: <<: *docker_variables - tags: [latest-amd64, develop-amd64] + tags: + - latest-amd64 + - develop-amd64 + - ${CI_COMMIT_SHA}-amd64 docker-develop-arm64: image: woodpeckerci/plugin-kaniko:2.3.1 @@ -41,4 +44,7 @@ steps: - evaluate: platform == "linux/arm64" settings: <<: *docker_variables - tags: [latest-arm64, develop-arm64] + tags: + - latest-arm64 + - develop-arm64 + - ${CI_COMMIT_SHA}-arm64 From e002650e23f4e1afe7d79585f8e6c6177a34f97d Mon Sep 17 00:00:00 2001 From: Phantasm Date: Mon, 13 Apr 2026 15:15:45 +0200 Subject: [PATCH 072/127] Woodpecker CI: Add Docker stable releases --- .woodpecker/docker-stable-combine.yaml | 27 +++++++++++++ .woodpecker/docker-stable.yaml | 52 ++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 .woodpecker/docker-stable-combine.yaml create mode 100644 .woodpecker/docker-stable.yaml diff --git a/.woodpecker/docker-stable-combine.yaml b/.woodpecker/docker-stable-combine.yaml new file mode 100644 index 000000000..e011750f1 --- /dev/null +++ b/.woodpecker/docker-stable-combine.yaml @@ -0,0 +1,27 @@ +when: + - event: push + branch: stable + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + - event: tag + branch: stable + +depends_on: + - docker-stable + +skip_clone: true + +steps: + docker-stable-combine: + image: git.fluffytail.org/phnt/wpc-docker-tagger:latest + settings: + registry: "git.fluffytail.org" + image: "pleroma-test/pleroma" + architectures: [amd64, arm64] + tags: + - latest + - stable + - ${CI_COMMIT_SHA} + username: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password diff --git a/.woodpecker/docker-stable.yaml b/.woodpecker/docker-stable.yaml new file mode 100644 index 000000000..6c5e31091 --- /dev/null +++ b/.woodpecker/docker-stable.yaml @@ -0,0 +1,52 @@ +when: + - event: push + branch: stable + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + - event: tag + branch: stable + +matrix: + platform: + - linux/amd64 + - linux/arm64 + +# This is needed for the when clauses below. +# When the platform clause is fixed, this might not be needed anymore +labels: + platform: ${platform} + +variables: + docker_variables: &docker_variables + repo: pleroma-test/pleroma + registry: git.fluffytail.org + username: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + +steps: + docker-stable-amd64: + image: woodpeckerci/plugin-kaniko:2.3.1 + # when: + # - platform: linux/amd64 + # does not work even though it should according to docs + # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 + when: + - evaluate: platform == "linux/amd64" + settings: + <<: *docker_variables + tags: + - latest-amd64 + - stable-amd64 + - ${CI_COMMIT_SHA}-amd64 + + docker-stable-arm64: + image: woodpeckerci/plugin-kaniko:2.3.1 + when: + - evaluate: platform == "linux/arm64" + settings: + <<: *docker_variables + tags: + - latest-arm64 + - stable-arm64 + - ${CI_COMMIT_SHA}-arm64 From 97a2e8c7647bbf40e554fb1ee40ef8160525dfd9 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Mon, 13 Apr 2026 17:17:28 +0200 Subject: [PATCH 073/127] Woodpecker CI: Tag stable docker release with version tag --- .woodpecker/docker-stable-combine.yaml | 16 +++++++++++-- .woodpecker/docker-stable.yaml | 33 +++++++++++++++++++++----- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/.woodpecker/docker-stable-combine.yaml b/.woodpecker/docker-stable-combine.yaml index e011750f1..e07e7a83c 100644 --- a/.woodpecker/docker-stable-combine.yaml +++ b/.woodpecker/docker-stable-combine.yaml @@ -13,11 +13,13 @@ skip_clone: true steps: docker-stable-combine: image: git.fluffytail.org/phnt/wpc-docker-tagger:latest - settings: + when: + - event: push + settings: &docker_settings registry: "git.fluffytail.org" image: "pleroma-test/pleroma" architectures: [amd64, arm64] - tags: + tags: &docker_tags - latest - stable - ${CI_COMMIT_SHA} @@ -25,3 +27,13 @@ steps: from_secret: pleroma-ci-user password: from_secret: pleroma-ci-password + + docker-stable-tag-combine: + image: git.fluffytail.org/phnt/wpc-docker-tagger:latest + when: + - event: tag + settings: + <<: *docker_settings + tags: + - <<: *docker_tags + - ${CI_COMMIT_TAG} diff --git a/.woodpecker/docker-stable.yaml b/.woodpecker/docker-stable.yaml index 6c5e31091..dcf98e31f 100644 --- a/.woodpecker/docker-stable.yaml +++ b/.woodpecker/docker-stable.yaml @@ -23,30 +23,51 @@ variables: from_secret: pleroma-ci-user password: from_secret: pleroma-ci-password + kaniko_image: &kaniko_image woodpeckerci/plugin-kaniko:2.3.1 steps: docker-stable-amd64: - image: woodpeckerci/plugin-kaniko:2.3.1 + image: *kaniko_image # when: # - platform: linux/amd64 # does not work even though it should according to docs # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 when: - - evaluate: platform == "linux/amd64" + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push"' settings: <<: *docker_variables - tags: + tags: &amd64_tags - latest-amd64 - stable-amd64 - ${CI_COMMIT_SHA}-amd64 - docker-stable-arm64: - image: woodpeckerci/plugin-kaniko:2.3.1 + docker-stable-tag-amd64: + image: *kaniko_image when: - - evaluate: platform == "linux/arm64" + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' settings: <<: *docker_variables tags: + - <<: *amd64_tags + - ${CI_COMMIT_TAG}-amd64 + + docker-stable-arm64: + image: *kaniko_image + when: + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push"' + settings: + <<: *docker_variables + tags: &arm64_tags - latest-arm64 - stable-arm64 - ${CI_COMMIT_SHA}-arm64 + + docker-stable-tag-arm64: + image: *kaniko_image + when: + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *docker_variables + tags: + - <<: *arm64_tags + - ${CI_COMMIT_TAG}-arm64 From 42eb9706a5cb2a76d9093852735459371d647fb1 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 17 Apr 2026 19:39:53 +0200 Subject: [PATCH 074/127] Woodpecker CI: Build stable OTP releases --- .woodpecker/otp-stable-musl.yaml | 83 +++++++++++++++++++++++++++++++ .woodpecker/otp-stable.yaml | 85 ++++++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 .woodpecker/otp-stable-musl.yaml create mode 100644 .woodpecker/otp-stable.yaml diff --git a/.woodpecker/otp-stable-musl.yaml b/.woodpecker/otp-stable-musl.yaml new file mode 100644 index 000000000..f97bde4ef --- /dev/null +++ b/.woodpecker/otp-stable-musl.yaml @@ -0,0 +1,83 @@ +when: + - event: push + branch: stable + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + - event: tag + branch: stable + +matrix: + platform: + - linux/amd64 + - linux/arm64 + +# This is needed for the when clauses below. +# When the platform clause is fixed, this might not be needed anymore +labels: + platform: ${platform} + +variables: + pleroma_build_cmds: &pleroma_build_cmds + - echo "import Config" > config/prod.secret.exs + - mix local.hex --force + - mix local.rebar --force + - mix deps.get --only prod + - mkdir release + - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} + - mix release --path release + artifacts_uploader_settings: &artifacts_uploader_settings + user: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + owner: 'pleroma-test' + +steps: + otp-stable-amd64-musl: + image: docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 + # when: + # - platform: linux/amd64 + # does not work even though it should according to docs + # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 + when: + - evaluate: platform == "linux/amd64" + environment: + MIX_ENV: prod + VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS + commands: + - apk add git build-base cmake file-dev openssl vips-dev zip + - <<: *pleroma_build_cmds + - zip -9rq pleroma-stable-${CI_COMMIT_SHA}-amd64-musl.zip release + + upload-artifacts-amd64-musl: + image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + when: + - evaluate: platform == "linux/amd64" + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-amd64-musl + package_version: stable-${CI_COMMIT_SHA}-amd64-musl + file_source: ./pleroma-stable-${CI_COMMIT_SHA}-amd64-musl.zip + file_name: ./pleroma-stable-${CI_COMMIT_SHA}-amd64-musl.zip + + otp-stable-arm64-musl: + image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 + when: + - evaluate: platform == "linux/arm64" + environment: + MIX_ENV: prod + VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS + commands: + - apk add git build-base cmake file-dev openssl vips-dev zip + - <<: *pleroma_build_cmds + - zip -9rq pleroma-stable-${CI_COMMIT_SHA}-arm64-musl.zip release + + upload-artifacts-arm64-musl: + image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + when: + - evaluate: platform == "linux/arm64" + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-arm64-musl + package_version: stable-${CI_COMMIT_SHA}-arm64-musl + file_source: ./pleroma-stable-${CI_COMMIT_SHA}-arm64-musl.zip + file_name: ./pleroma-stable-${CI_COMMIT_SHA}-arm64-musl.zip diff --git a/.woodpecker/otp-stable.yaml b/.woodpecker/otp-stable.yaml new file mode 100644 index 000000000..6219ff403 --- /dev/null +++ b/.woodpecker/otp-stable.yaml @@ -0,0 +1,85 @@ +when: + - event: push + branch: stable + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + - event: tag + branch: stable + +matrix: + platform: + - linux/amd64 + - linux/arm64 + +# This is needed for the when clauses below. +# When the platform clause is fixed, this might not be needed anymore +labels: + platform: ${platform} + +variables: + pleroma_build_cmds: &pleroma_build_cmds + - echo "import Config" > config/prod.secret.exs + - mix local.hex --force + - mix local.rebar --force + - mix deps.get --only prod + - mkdir release + - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} + - mix release --path release + artifacts_uploader_settings: &artifacts_uploader_settings + user: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + owner: 'pleroma-test' + +steps: + otp-stable-amd64: + image: docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 + # when: + # - platform: linux/amd64 + # does not work even though it should according to docs + # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 + when: + - evaluate: platform == "linux/amd64" + environment: + MIX_ENV: prod + VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS + DEBIAN_FRONTEND: noninteractive + commands: + - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip + - <<: *pleroma_build_cmds + - zip -9rq pleroma-stable-${CI_COMMIT_SHA}-amd64.zip release + + upload-artifacts-amd64: + image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + when: + - evaluate: platform == "linux/amd64" + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-amd64 + package_version: stable-${CI_COMMIT_SHA}-amd64 + file_source: ./pleroma-stable-${CI_COMMIT_SHA}-amd64.zip + file_name: ./pleroma-stable-${CI_COMMIT_SHA}-amd64.zip + + otp-stable-arm64: + image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 + when: + - evaluate: platform == "linux/arm64" + environment: + MIX_ENV: prod + VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS + DEBIAN_FRONTEND: noninteractive + commands: + - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip + - <<: *pleroma_build_cmds + - zip -9rq pleroma-stable-${CI_COMMIT_SHA}-arm64.zip release + + upload-artifacts-arm64: + image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + when: + - evaluate: platform == "linux/arm64" + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-arm64 + package_version: stable-${CI_COMMIT_SHA}-arm64 + file_source: ./pleroma-stable-${CI_COMMIT_SHA}-arm64.zip + file_name: ./pleroma-stable-${CI_COMMIT_SHA}-arm64.zip From eea01b54b7852628142dae3af9a280ee92620248 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 18 Apr 2026 22:28:40 +0200 Subject: [PATCH 075/127] Woodpecker CI: Allow running stable release jobs manually Also allows Docker images to be tagged with a version in manual jobs when CI_COMMIT_TAG is manually specified --- .woodpecker/docker-stable-combine.yaml | 4 ++++ .woodpecker/docker-stable.yaml | 6 ++++++ .woodpecker/otp-stable-musl.yaml | 2 ++ .woodpecker/otp-stable.yaml | 2 ++ 4 files changed, 14 insertions(+) diff --git a/.woodpecker/docker-stable-combine.yaml b/.woodpecker/docker-stable-combine.yaml index e07e7a83c..9f144d3f6 100644 --- a/.woodpecker/docker-stable-combine.yaml +++ b/.woodpecker/docker-stable-combine.yaml @@ -4,6 +4,8 @@ when: path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: tag branch: stable + - event: manual + branch: stable depends_on: - docker-stable @@ -15,6 +17,7 @@ steps: image: git.fluffytail.org/phnt/wpc-docker-tagger:latest when: - event: push + - evaluate: 'CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG == ""' settings: &docker_settings registry: "git.fluffytail.org" image: "pleroma-test/pleroma" @@ -32,6 +35,7 @@ steps: image: git.fluffytail.org/phnt/wpc-docker-tagger:latest when: - event: tag + - evaluate: 'CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG != ""' settings: <<: *docker_settings tags: diff --git a/.woodpecker/docker-stable.yaml b/.woodpecker/docker-stable.yaml index dcf98e31f..bd32b94d1 100644 --- a/.woodpecker/docker-stable.yaml +++ b/.woodpecker/docker-stable.yaml @@ -4,6 +4,8 @@ when: path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: tag branch: stable + - event: manual + branch: stable matrix: platform: @@ -34,6 +36,7 @@ steps: # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 when: - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG == ""' settings: <<: *docker_variables tags: &amd64_tags @@ -45,6 +48,7 @@ steps: image: *kaniko_image when: - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG != ""' settings: <<: *docker_variables tags: @@ -55,6 +59,7 @@ steps: image: *kaniko_image when: - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG == ""' settings: <<: *docker_variables tags: &arm64_tags @@ -66,6 +71,7 @@ steps: image: *kaniko_image when: - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG != ""' settings: <<: *docker_variables tags: diff --git a/.woodpecker/otp-stable-musl.yaml b/.woodpecker/otp-stable-musl.yaml index f97bde4ef..4663a4401 100644 --- a/.woodpecker/otp-stable-musl.yaml +++ b/.woodpecker/otp-stable-musl.yaml @@ -4,6 +4,8 @@ when: path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: tag branch: stable + - event: manual + branch: stable matrix: platform: diff --git a/.woodpecker/otp-stable.yaml b/.woodpecker/otp-stable.yaml index 6219ff403..afb23eada 100644 --- a/.woodpecker/otp-stable.yaml +++ b/.woodpecker/otp-stable.yaml @@ -4,6 +4,8 @@ when: path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: tag branch: stable + - event: manual + branch: stable matrix: platform: From dd29b9c11bdef53a81071f530b7416970baa8e70 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 18 Apr 2026 23:25:53 +0200 Subject: [PATCH 076/127] Woodpecker CI OTP: use CI_COMMIT_BRANCH variable instead of stable --- .woodpecker/otp-stable-musl.yaml | 20 ++++++++++---------- .woodpecker/otp-stable.yaml | 20 ++++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/.woodpecker/otp-stable-musl.yaml b/.woodpecker/otp-stable-musl.yaml index 4663a4401..96be0bf8e 100644 --- a/.woodpecker/otp-stable-musl.yaml +++ b/.woodpecker/otp-stable-musl.yaml @@ -48,7 +48,7 @@ steps: commands: - apk add git build-base cmake file-dev openssl vips-dev zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-stable-${CI_COMMIT_SHA}-amd64-musl.zip release + - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip release upload-artifacts-amd64-musl: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -56,10 +56,10 @@ steps: - evaluate: platform == "linux/amd64" settings: <<: *artifacts_uploader_settings - package_name: pleroma-otp-stable-amd64-musl - package_version: stable-${CI_COMMIT_SHA}-amd64-musl - file_source: ./pleroma-stable-${CI_COMMIT_SHA}-amd64-musl.zip - file_name: ./pleroma-stable-${CI_COMMIT_SHA}-amd64-musl.zip + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64-musl + package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl + file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip + file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip otp-stable-arm64-musl: image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 @@ -71,7 +71,7 @@ steps: commands: - apk add git build-base cmake file-dev openssl vips-dev zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-stable-${CI_COMMIT_SHA}-arm64-musl.zip release + - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip release upload-artifacts-arm64-musl: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -79,7 +79,7 @@ steps: - evaluate: platform == "linux/arm64" settings: <<: *artifacts_uploader_settings - package_name: pleroma-otp-stable-arm64-musl - package_version: stable-${CI_COMMIT_SHA}-arm64-musl - file_source: ./pleroma-stable-${CI_COMMIT_SHA}-arm64-musl.zip - file_name: ./pleroma-stable-${CI_COMMIT_SHA}-arm64-musl.zip + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64-musl + package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl + file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip + file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip diff --git a/.woodpecker/otp-stable.yaml b/.woodpecker/otp-stable.yaml index afb23eada..ae2670a79 100644 --- a/.woodpecker/otp-stable.yaml +++ b/.woodpecker/otp-stable.yaml @@ -49,7 +49,7 @@ steps: commands: - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-stable-${CI_COMMIT_SHA}-amd64.zip release + - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip release upload-artifacts-amd64: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -57,10 +57,10 @@ steps: - evaluate: platform == "linux/amd64" settings: <<: *artifacts_uploader_settings - package_name: pleroma-otp-stable-amd64 - package_version: stable-${CI_COMMIT_SHA}-amd64 - file_source: ./pleroma-stable-${CI_COMMIT_SHA}-amd64.zip - file_name: ./pleroma-stable-${CI_COMMIT_SHA}-amd64.zip + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64 + package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64 + file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip + file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip otp-stable-arm64: image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 @@ -73,7 +73,7 @@ steps: commands: - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-stable-${CI_COMMIT_SHA}-arm64.zip release + - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip release upload-artifacts-arm64: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -81,7 +81,7 @@ steps: - evaluate: platform == "linux/arm64" settings: <<: *artifacts_uploader_settings - package_name: pleroma-otp-stable-arm64 - package_version: stable-${CI_COMMIT_SHA}-arm64 - file_source: ./pleroma-stable-${CI_COMMIT_SHA}-arm64.zip - file_name: ./pleroma-stable-${CI_COMMIT_SHA}-arm64.zip + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64 + package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64 + file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip + file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip From 89a78d765cc231934608149f394389a9b90e840c Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 21 Apr 2026 16:09:15 +0200 Subject: [PATCH 077/127] Woodpecker CI: Unify Docker image workflows --- .woodpecker/docker-combine.yaml | 65 +++++++++++++++++++ .woodpecker/docker-develop-combine.yaml | 25 ------- .woodpecker/docker-develop.yaml | 50 -------------- .woodpecker/docker-stable-combine.yaml | 43 ------------ .../{docker-stable.yaml => docker.yaml} | 50 +++++++++----- 5 files changed, 100 insertions(+), 133 deletions(-) create mode 100644 .woodpecker/docker-combine.yaml delete mode 100644 .woodpecker/docker-develop-combine.yaml delete mode 100644 .woodpecker/docker-develop.yaml delete mode 100644 .woodpecker/docker-stable-combine.yaml rename .woodpecker/{docker-stable.yaml => docker.yaml} (58%) diff --git a/.woodpecker/docker-combine.yaml b/.woodpecker/docker-combine.yaml new file mode 100644 index 000000000..46401b0cb --- /dev/null +++ b/.woodpecker/docker-combine.yaml @@ -0,0 +1,65 @@ +when: + - event: push + branch: ${CI_REPO_DEFAULT_BRANCH} + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + - event: push + branch: stable + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + - event: tag + branch: stable + - event: manual + branch: stable + +depends_on: + - docker + +skip_clone: true + +steps: + docker-develop-combine: + image: git.fluffytail.org/phnt/wpc-docker-tagger:latest + when: + - event: push + branch: ${CI_REPO_DEFAULT_BRANCH} + settings: + registry: "git.pleroma.social" + image: "pleroma/pleroma" + architectures: [amd64, arm64] + tags: + - latest + - develop + - ${CI_COMMIT_SHA} + username: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + + docker-stable-combine: + image: git.fluffytail.org/phnt/wpc-docker-tagger:latest + when: + - event: push + branch: stable + - evaluate: 'CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG == ""' + settings: &docker_settings + registry: "git.pleroma.social" + image: "pleroma/pleroma" + architectures: [amd64, arm64] + tags: &docker_tags + - latest + - stable + - ${CI_COMMIT_SHA} + username: + from_secret: pleroma-ci-user + password: + from_secret: pleroma-ci-password + + docker-stable-tag-combine: + image: git.fluffytail.org/phnt/wpc-docker-tagger:latest + when: + - event: tag + - evaluate: 'CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG != ""' + settings: + <<: *docker_settings + tags: + - <<: *docker_tags + - ${CI_COMMIT_TAG} diff --git a/.woodpecker/docker-develop-combine.yaml b/.woodpecker/docker-develop-combine.yaml deleted file mode 100644 index ebe5a502c..000000000 --- a/.woodpecker/docker-develop-combine.yaml +++ /dev/null @@ -1,25 +0,0 @@ -when: - - event: push - branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - -depends_on: - - docker-develop - -skip_clone: true - -steps: - docker-develop-combine: - image: git.fluffytail.org/phnt/wpc-docker-tagger:latest - settings: - registry: "git.fluffytail.org" - image: "pleroma-test/pleroma" - architectures: [amd64, arm64] - tags: - - latest - - develop - - ${CI_COMMIT_SHA} - username: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password diff --git a/.woodpecker/docker-develop.yaml b/.woodpecker/docker-develop.yaml deleted file mode 100644 index 4f6bbdf12..000000000 --- a/.woodpecker/docker-develop.yaml +++ /dev/null @@ -1,50 +0,0 @@ -when: - - event: push - branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - -matrix: - platform: - - linux/amd64 - - linux/arm64 - -# This is needed for the when clauses below. -# When the platform clause is fixed, this might not be needed anymore -labels: - platform: ${platform} - -variables: - docker_variables: &docker_variables - repo: pleroma/pleroma - registry: git.pleroma.social - username: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password - -steps: - docker-develop-amd64: - image: woodpeckerci/plugin-kaniko:2.3.1 - # when: - # - platform: linux/amd64 - # does not work even though it should according to docs - # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 - when: - - evaluate: platform == "linux/amd64" - settings: - <<: *docker_variables - tags: - - latest-amd64 - - develop-amd64 - - ${CI_COMMIT_SHA}-amd64 - - docker-develop-arm64: - image: woodpeckerci/plugin-kaniko:2.3.1 - when: - - evaluate: platform == "linux/arm64" - settings: - <<: *docker_variables - tags: - - latest-arm64 - - develop-arm64 - - ${CI_COMMIT_SHA}-arm64 diff --git a/.woodpecker/docker-stable-combine.yaml b/.woodpecker/docker-stable-combine.yaml deleted file mode 100644 index 9f144d3f6..000000000 --- a/.woodpecker/docker-stable-combine.yaml +++ /dev/null @@ -1,43 +0,0 @@ -when: - - event: push - branch: stable - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - - event: tag - branch: stable - - event: manual - branch: stable - -depends_on: - - docker-stable - -skip_clone: true - -steps: - docker-stable-combine: - image: git.fluffytail.org/phnt/wpc-docker-tagger:latest - when: - - event: push - - evaluate: 'CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG == ""' - settings: &docker_settings - registry: "git.fluffytail.org" - image: "pleroma-test/pleroma" - architectures: [amd64, arm64] - tags: &docker_tags - - latest - - stable - - ${CI_COMMIT_SHA} - username: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password - - docker-stable-tag-combine: - image: git.fluffytail.org/phnt/wpc-docker-tagger:latest - when: - - event: tag - - evaluate: 'CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG != ""' - settings: - <<: *docker_settings - tags: - - <<: *docker_tags - - ${CI_COMMIT_TAG} diff --git a/.woodpecker/docker-stable.yaml b/.woodpecker/docker.yaml similarity index 58% rename from .woodpecker/docker-stable.yaml rename to .woodpecker/docker.yaml index bd32b94d1..c1299b4fe 100644 --- a/.woodpecker/docker-stable.yaml +++ b/.woodpecker/docker.yaml @@ -1,4 +1,7 @@ when: + - event: push + branch: ${CI_REPO_DEFAULT_BRANCH} + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: push branch: stable path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] @@ -13,14 +16,13 @@ matrix: - linux/arm64 # This is needed for the when clauses below. -# When the platform clause is fixed, this might not be needed anymore labels: platform: ${platform} variables: docker_variables: &docker_variables - repo: pleroma-test/pleroma - registry: git.fluffytail.org + repo: pleroma/pleroma + registry: git.pleroma.social username: from_secret: pleroma-ci-user password: @@ -28,15 +30,33 @@ variables: kaniko_image: &kaniko_image woodpeckerci/plugin-kaniko:2.3.1 steps: + docker-develop-amd64: + image: woodpeckerci/plugin-kaniko:2.3.1 + when: + - evaluate: 'platform == "linux/amd64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + settings: + <<: *docker_variables + tags: + - latest-amd64 + - develop-amd64 + - ${CI_COMMIT_SHA}-amd64 + + docker-develop-arm64: + image: woodpeckerci/plugin-kaniko:2.3.1 + when: + - evaluate: 'platform == "linux/arm64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + settings: + <<: *docker_variables + tags: + - latest-arm64 + - develop-arm64 + - ${CI_COMMIT_SHA}-arm64 + docker-stable-amd64: image: *kaniko_image - # when: - # - platform: linux/amd64 - # does not work even though it should according to docs - # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 when: - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push"' - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG == ""' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG == ""' settings: <<: *docker_variables tags: &amd64_tags @@ -47,8 +67,8 @@ steps: docker-stable-tag-amd64: image: *kaniko_image when: - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG != ""' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG != ""' settings: <<: *docker_variables tags: @@ -58,8 +78,8 @@ steps: docker-stable-arm64: image: *kaniko_image when: - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push"' - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG == ""' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG == ""' settings: <<: *docker_variables tags: &arm64_tags @@ -70,8 +90,8 @@ steps: docker-stable-tag-arm64: image: *kaniko_image when: - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_TAG != ""' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG != ""' settings: <<: *docker_variables tags: From d8b8cbbb8d74938159bdf50c190bf54eba276557 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 21 Apr 2026 18:33:39 +0200 Subject: [PATCH 078/127] Woodpecker CI: Shorten commit sha to eight chars This will hopefully help with avoiding: https://github.com/woodpecker-ci/woodpecker/issues/5450 --- .woodpecker/docker-combine.yaml | 4 ++-- .woodpecker/docker.yaml | 8 ++++---- .woodpecker/otp-develop-musl.yaml | 16 ++++++++-------- .woodpecker/otp-develop.yaml | 16 ++++++++-------- .woodpecker/otp-stable-musl.yaml | 16 ++++++++-------- .woodpecker/otp-stable.yaml | 16 ++++++++-------- 6 files changed, 38 insertions(+), 38 deletions(-) diff --git a/.woodpecker/docker-combine.yaml b/.woodpecker/docker-combine.yaml index 46401b0cb..c69bcf1cf 100644 --- a/.woodpecker/docker-combine.yaml +++ b/.woodpecker/docker-combine.yaml @@ -28,7 +28,7 @@ steps: tags: - latest - develop - - ${CI_COMMIT_SHA} + - ${CI_COMMIT_SHA:0:8} username: from_secret: pleroma-ci-user password: @@ -47,7 +47,7 @@ steps: tags: &docker_tags - latest - stable - - ${CI_COMMIT_SHA} + - ${CI_COMMIT_SHA:0:8} username: from_secret: pleroma-ci-user password: diff --git a/.woodpecker/docker.yaml b/.woodpecker/docker.yaml index c1299b4fe..66efe6903 100644 --- a/.woodpecker/docker.yaml +++ b/.woodpecker/docker.yaml @@ -39,7 +39,7 @@ steps: tags: - latest-amd64 - develop-amd64 - - ${CI_COMMIT_SHA}-amd64 + - ${CI_COMMIT_SHA:0:8}-amd64 docker-develop-arm64: image: woodpeckerci/plugin-kaniko:2.3.1 @@ -50,7 +50,7 @@ steps: tags: - latest-arm64 - develop-arm64 - - ${CI_COMMIT_SHA}-arm64 + - ${CI_COMMIT_SHA:0:8}-arm64 docker-stable-amd64: image: *kaniko_image @@ -62,7 +62,7 @@ steps: tags: &amd64_tags - latest-amd64 - stable-amd64 - - ${CI_COMMIT_SHA}-amd64 + - ${CI_COMMIT_SHA:0:8}-amd64 docker-stable-tag-amd64: image: *kaniko_image @@ -85,7 +85,7 @@ steps: tags: &arm64_tags - latest-arm64 - stable-arm64 - - ${CI_COMMIT_SHA}-arm64 + - ${CI_COMMIT_SHA:0:8}-arm64 docker-stable-tag-arm64: image: *kaniko_image diff --git a/.woodpecker/otp-develop-musl.yaml b/.woodpecker/otp-develop-musl.yaml index 7ed567949..aff52ee30 100644 --- a/.woodpecker/otp-develop-musl.yaml +++ b/.woodpecker/otp-develop-musl.yaml @@ -44,7 +44,7 @@ steps: commands: - apk add git build-base cmake file-dev openssl vips-dev zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip release + - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip release upload-artifacts-amd64-musl: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -53,9 +53,9 @@ steps: settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-amd64-musl - package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl - file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip - file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip + package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl + file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip otp-develop-arm64-musl: image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 @@ -67,7 +67,7 @@ steps: commands: - apk add git build-base cmake file-dev openssl vips-dev zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip release + - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip release upload-artifacts-arm64-musl: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -76,6 +76,6 @@ steps: settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-arm64-musl - package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl - file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip - file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip + package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl + file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip diff --git a/.woodpecker/otp-develop.yaml b/.woodpecker/otp-develop.yaml index 6cd76d450..623d1d846 100644 --- a/.woodpecker/otp-develop.yaml +++ b/.woodpecker/otp-develop.yaml @@ -45,7 +45,7 @@ steps: commands: - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip release + - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip release upload-artifacts-amd64: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -54,9 +54,9 @@ steps: settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-amd64 - package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64 - file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip - file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip + package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64 + file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip + file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip otp-develop-arm64: image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 @@ -69,7 +69,7 @@ steps: commands: - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip release + - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip release upload-artifacts-arm64: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -78,6 +78,6 @@ steps: settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-arm64 - package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64 - file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip - file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip + package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64 + file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip + file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip diff --git a/.woodpecker/otp-stable-musl.yaml b/.woodpecker/otp-stable-musl.yaml index 96be0bf8e..3a3d8bb53 100644 --- a/.woodpecker/otp-stable-musl.yaml +++ b/.woodpecker/otp-stable-musl.yaml @@ -48,7 +48,7 @@ steps: commands: - apk add git build-base cmake file-dev openssl vips-dev zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip release + - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip release upload-artifacts-amd64-musl: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -57,9 +57,9 @@ steps: settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64-musl - package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl - file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip - file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64-musl.zip + package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl + file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip otp-stable-arm64-musl: image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 @@ -71,7 +71,7 @@ steps: commands: - apk add git build-base cmake file-dev openssl vips-dev zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip release + - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip release upload-artifacts-arm64-musl: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -80,6 +80,6 @@ steps: settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64-musl - package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl - file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip - file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64-musl.zip + package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl + file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip diff --git a/.woodpecker/otp-stable.yaml b/.woodpecker/otp-stable.yaml index ae2670a79..80bfc9f7e 100644 --- a/.woodpecker/otp-stable.yaml +++ b/.woodpecker/otp-stable.yaml @@ -49,7 +49,7 @@ steps: commands: - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip release + - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip release upload-artifacts-amd64: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -58,9 +58,9 @@ steps: settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64 - package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64 - file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip - file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-amd64.zip + package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64 + file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip + file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip otp-stable-arm64: image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 @@ -73,7 +73,7 @@ steps: commands: - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip release + - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip release upload-artifacts-arm64: image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 @@ -82,6 +82,6 @@ steps: settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64 - package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64 - file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip - file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA}-arm64.zip + package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64 + file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip + file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip From 5229e8ae6531d4b78828ad43ce17e2e075080c04 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Tue, 21 Apr 2026 21:40:56 +0200 Subject: [PATCH 079/127] Woodpecker CI: Unify OTP builds into a single worfklow --- .woodpecker/otp-develop-musl.yaml | 81 ------------------ .woodpecker/otp-develop.yaml | 83 ------------------- .../{otp-stable-musl.yaml => otp-musl.yaml} | 72 +++++++++------- .woodpecker/{otp-stable.yaml => otp.yaml} | 75 ++++++++++------- 4 files changed, 87 insertions(+), 224 deletions(-) delete mode 100644 .woodpecker/otp-develop-musl.yaml delete mode 100644 .woodpecker/otp-develop.yaml rename .woodpecker/{otp-stable-musl.yaml => otp-musl.yaml} (56%) rename .woodpecker/{otp-stable.yaml => otp.yaml} (54%) diff --git a/.woodpecker/otp-develop-musl.yaml b/.woodpecker/otp-develop-musl.yaml deleted file mode 100644 index aff52ee30..000000000 --- a/.woodpecker/otp-develop-musl.yaml +++ /dev/null @@ -1,81 +0,0 @@ -when: - - event: push - branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - -matrix: - platform: - - linux/amd64 - - linux/arm64 - -# This is needed for the when clauses below. -# When the platform clause is fixed, this might not be needed anymore -labels: - platform: ${platform} - -variables: - pleroma_build_cmds: &pleroma_build_cmds - - echo "import Config" > config/prod.secret.exs - - mix local.hex --force - - mix local.rebar --force - - mix deps.get --only prod - - mkdir release - - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} - - mix release --path release - artifacts_uploader_settings: &artifacts_uploader_settings - user: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password - owner: 'pleroma' - -steps: - otp-develop-amd64-musl: - image: docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 - # when: - # - platform: linux/amd64 - # does not work even though it should according to docs - # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 - when: - - evaluate: platform == "linux/amd64" - environment: - MIX_ENV: prod - VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS - commands: - - apk add git build-base cmake file-dev openssl vips-dev zip - - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip release - - upload-artifacts-amd64-musl: - image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 - when: - - evaluate: platform == "linux/amd64" - settings: - <<: *artifacts_uploader_settings - package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-amd64-musl - package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl - file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip - file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip - - otp-develop-arm64-musl: - image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 - when: - - evaluate: platform == "linux/arm64" - environment: - MIX_ENV: prod - VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS - commands: - - apk add git build-base cmake file-dev openssl vips-dev zip - - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip release - - upload-artifacts-arm64-musl: - image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 - when: - - evaluate: platform == "linux/arm64" - settings: - <<: *artifacts_uploader_settings - package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-arm64-musl - package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl - file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip - file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip diff --git a/.woodpecker/otp-develop.yaml b/.woodpecker/otp-develop.yaml deleted file mode 100644 index 623d1d846..000000000 --- a/.woodpecker/otp-develop.yaml +++ /dev/null @@ -1,83 +0,0 @@ -when: - - event: push - branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - -matrix: - platform: - - linux/amd64 - - linux/arm64 - -# This is needed for the when clauses below. -# When the platform clause is fixed, this might not be needed anymore -labels: - platform: ${platform} - -variables: - pleroma_build_cmds: &pleroma_build_cmds - - echo "import Config" > config/prod.secret.exs - - mix local.hex --force - - mix local.rebar --force - - mix deps.get --only prod - - mkdir release - - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} - - mix release --path release - artifacts_uploader_settings: &artifacts_uploader_settings - user: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password - owner: 'pleroma' - -steps: - otp-develop-amd64: - image: docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 - # when: - # - platform: linux/amd64 - # does not work even though it should according to docs - # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 - when: - - evaluate: platform == "linux/amd64" - environment: - MIX_ENV: prod - VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS - DEBIAN_FRONTEND: noninteractive - commands: - - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip release - - upload-artifacts-amd64: - image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 - when: - - evaluate: platform == "linux/amd64" - settings: - <<: *artifacts_uploader_settings - package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-amd64 - package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64 - file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip - file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip - - otp-develop-arm64: - image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 - when: - - evaluate: platform == "linux/arm64" - environment: - MIX_ENV: prod - VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS - DEBIAN_FRONTEND: noninteractive - commands: - - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip release - - upload-artifacts-arm64: - image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 - when: - - evaluate: platform == "linux/arm64" - settings: - <<: *artifacts_uploader_settings - package_name: pleroma-otp-${CI_REPO_DEFAULT_BRANCH}-arm64 - package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64 - file_source: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip - file_name: ./pleroma-${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip diff --git a/.woodpecker/otp-stable-musl.yaml b/.woodpecker/otp-musl.yaml similarity index 56% rename from .woodpecker/otp-stable-musl.yaml rename to .woodpecker/otp-musl.yaml index 3a3d8bb53..87ab0b93a 100644 --- a/.woodpecker/otp-stable-musl.yaml +++ b/.woodpecker/otp-musl.yaml @@ -1,4 +1,7 @@ when: + - event: push + branch: ${CI_REPO_DEFAULT_BRANCH} + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: push branch: stable path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] @@ -18,7 +21,8 @@ labels: platform: ${platform} variables: - pleroma_build_cmds: &pleroma_build_cmds + build_cmds: &build_cmds + - apk add git build-base cmake file-dev openssl vips-dev zip - echo "import Config" > config/prod.secret.exs - mix local.hex --force - mix local.rebar --force @@ -26,32 +30,54 @@ variables: - mkdir release - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} - mix release --path release + build_image_amd64: &build_image_amd64 docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 + build_image_arm64: &build_image_arm64 docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 + artifacts_uploader_image: &artifacts_uploader_image docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 artifacts_uploader_settings: &artifacts_uploader_settings user: from_secret: pleroma-ci-user password: from_secret: pleroma-ci-password - owner: 'pleroma-test' + owner: 'pleroma' + env: &env + MIX_ENV: prod + VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS steps: - otp-stable-amd64-musl: - image: docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 - # when: - # - platform: linux/amd64 - # does not work even though it should according to docs - # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 + otp-develop-amd64-musl: + image: *build_image_amd64 when: - - evaluate: platform == "linux/amd64" - environment: - MIX_ENV: prod - VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS - commands: - - apk add git build-base cmake file-dev openssl vips-dev zip - - <<: *pleroma_build_cmds + - evaluate: 'platform == "linux/amd64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + environment: *env + commands: &amd64_build + - <<: *build_cmds - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip release + otp-stable-amd64-musl: + image: *build_image_amd64 + when: + - evaluate: 'platform == "linux/amd64" && CI_COMMIT_BRANCH == "stable"' + environment: *env + commands: *amd64_build + + otp-develop-arm64-musl: + image: *build_image_arm64 + when: + - evaluate: 'platform == "linux/arm64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + environment: *env + commands: &arm64_build + - <<: *build_cmds + - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip release + + otp-stable-arm64-musl: + image: *build_image_arm64 + when: + - evaluate: 'platform == "linux/arm64" && CI_COMMIT_BRANCH == "stable"' + environment: *env + commands: *arm64_build + upload-artifacts-amd64-musl: - image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + image: *artifacts_uploader_image when: - evaluate: platform == "linux/amd64" settings: @@ -61,20 +87,8 @@ steps: file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip - otp-stable-arm64-musl: - image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 - when: - - evaluate: platform == "linux/arm64" - environment: - MIX_ENV: prod - VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS - commands: - - apk add git build-base cmake file-dev openssl vips-dev zip - - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip release - upload-artifacts-arm64-musl: - image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + image: *artifacts_uploader_image when: - evaluate: platform == "linux/arm64" settings: diff --git a/.woodpecker/otp-stable.yaml b/.woodpecker/otp.yaml similarity index 54% rename from .woodpecker/otp-stable.yaml rename to .woodpecker/otp.yaml index 80bfc9f7e..533143769 100644 --- a/.woodpecker/otp-stable.yaml +++ b/.woodpecker/otp.yaml @@ -1,4 +1,7 @@ when: + - event: push + branch: ${CI_REPO_DEFAULT_BRANCH} + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: push branch: stable path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] @@ -18,7 +21,8 @@ labels: platform: ${platform} variables: - pleroma_build_cmds: &pleroma_build_cmds + build_cmds: &build_cmds + - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - echo "import Config" > config/prod.secret.exs - mix local.hex --force - mix local.rebar --force @@ -26,33 +30,55 @@ variables: - mkdir release - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} - mix release --path release + build_image_amd64: &build_image_amd64 docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 + build_image_arm64: &build_image_arm64 docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 + artifacts_uploader_image: &artifacts_uploader_image docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 artifacts_uploader_settings: &artifacts_uploader_settings user: from_secret: pleroma-ci-user password: from_secret: pleroma-ci-password - owner: 'pleroma-test' + owner: 'pleroma' + env: &env + MIX_ENV: prod + VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS + DEBIAN_FRONTEND: noninteractive steps: - otp-stable-amd64: - image: docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 - # when: - # - platform: linux/amd64 - # does not work even though it should according to docs - # https://github.com/woodpecker-ci/woodpecker/discussions/5367#discussioncomment-13901342 + otp-develop-amd64: + image: *build_image_amd64 when: - - evaluate: platform == "linux/amd64" - environment: - MIX_ENV: prod - VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS - DEBIAN_FRONTEND: noninteractive - commands: - - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - - <<: *pleroma_build_cmds + - evaluate: 'platform == "linux/amd64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + environment: *env + commands: &amd64_build + - <<: *build_cmds - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip release + otp-stable-amd64: + image: *build_image_amd64 + when: + - evaluate: 'platform == "linux/amd64" && CI_COMMIT_BRANCH == "stable"' + environment: *env + commands: *amd64_build + + otp-develop-arm64: + image: *build_image_arm64 + when: + - evaluate: 'platform == "linux/arm64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + environment: *env + commands: &arm64_build + - <<: *build_cmds + - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip release + + otp-stable-arm64: + image: *build_image_arm64 + when: + - evaluate: 'platform == "linux/arm64" && CI_COMMIT_BRANCH == "stable"' + environment: *env + commands: *arm64_build + upload-artifacts-amd64: - image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + image: *artifacts_uploader_image when: - evaluate: platform == "linux/amd64" settings: @@ -62,21 +88,8 @@ steps: file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip - otp-stable-arm64: - image: docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 - when: - - evaluate: platform == "linux/arm64" - environment: - MIX_ENV: prod - VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS - DEBIAN_FRONTEND: noninteractive - commands: - - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - - <<: *pleroma_build_cmds - - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip release - upload-artifacts-arm64: - image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 + image: *artifacts_uploader_image when: - evaluate: platform == "linux/arm64" settings: From 2e968890de91bcc7df982c7ad5bac7ee8621505f Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 22 Apr 2026 11:35:34 +0200 Subject: [PATCH 080/127] Woodpecker CI: Remove branch requirement for tag Tag events don't have CI_COMMIT_BRANCH set, and neither can they be restricted to specific branches. The branch condition is ignored on tags. --- .woodpecker/docker-combine.yaml | 1 - .woodpecker/docker.yaml | 5 ++- .woodpecker/otp-musl.yaml | 59 ++++++++++++++++++++++++++++++--- .woodpecker/otp.yaml | 59 ++++++++++++++++++++++++++++++--- 4 files changed, 110 insertions(+), 14 deletions(-) diff --git a/.woodpecker/docker-combine.yaml b/.woodpecker/docker-combine.yaml index c69bcf1cf..e615ca7c2 100644 --- a/.woodpecker/docker-combine.yaml +++ b/.woodpecker/docker-combine.yaml @@ -6,7 +6,6 @@ when: branch: stable path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: tag - branch: stable - event: manual branch: stable diff --git a/.woodpecker/docker.yaml b/.woodpecker/docker.yaml index 66efe6903..0c4b18fbc 100644 --- a/.woodpecker/docker.yaml +++ b/.woodpecker/docker.yaml @@ -6,7 +6,6 @@ when: branch: stable path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: tag - branch: stable - event: manual branch: stable @@ -67,7 +66,7 @@ steps: docker-stable-tag-amd64: image: *kaniko_image when: - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG != ""' settings: <<: *docker_variables @@ -90,7 +89,7 @@ steps: docker-stable-tag-arm64: image: *kaniko_image when: - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG != ""' settings: <<: *docker_variables diff --git a/.woodpecker/otp-musl.yaml b/.woodpecker/otp-musl.yaml index 87ab0b93a..d611f9f13 100644 --- a/.woodpecker/otp-musl.yaml +++ b/.woodpecker/otp-musl.yaml @@ -6,7 +6,6 @@ when: branch: stable path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: tag - branch: stable - event: manual branch: stable @@ -56,10 +55,21 @@ steps: otp-stable-amd64-musl: image: *build_image_amd64 when: - - evaluate: 'platform == "linux/amd64" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"' environment: *env commands: *amd64_build + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + otp-stable-tag-amd64-musl: + image: *build_image_amd64 + when: + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' + environment: *env + commands: + - <<: *build_cmds + - zip -9rq pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip release + otp-develop-arm64-musl: image: *build_image_arm64 when: @@ -72,14 +82,27 @@ steps: otp-stable-arm64-musl: image: *build_image_arm64 when: - - evaluate: 'platform == "linux/arm64" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"' environment: *env commands: *arm64_build + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + otp-stable-tag-arm64-musl: + image: *build_image_arm64 + when: + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' + environment: *env + commands: + - <<: *build_cmds + - zip -9rq pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip release + upload-artifacts-amd64-musl: image: *artifacts_uploader_image when: - - evaluate: platform == "linux/amd64" + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"' settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64-musl @@ -87,13 +110,39 @@ steps: file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-artifacts-tag-amd64-musl: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-amd64-musl + package_version: stable-${CI_COMMIT_SHA:0:8}-amd64-musl + file_source: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + upload-artifacts-arm64-musl: image: *artifacts_uploader_image when: - - evaluate: platform == "linux/arm64" + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"' settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64-musl package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-artifacts-tag-arm64-musl: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-arm64-musl + package_version: stable-${CI_COMMIT_SHA:0:8}-arm64-musl + file_source: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip diff --git a/.woodpecker/otp.yaml b/.woodpecker/otp.yaml index 533143769..0d0b08bec 100644 --- a/.woodpecker/otp.yaml +++ b/.woodpecker/otp.yaml @@ -6,7 +6,6 @@ when: branch: stable path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: tag - branch: stable - event: manual branch: stable @@ -57,10 +56,21 @@ steps: otp-stable-amd64: image: *build_image_amd64 when: - - evaluate: 'platform == "linux/amd64" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"' environment: *env commands: *amd64_build + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + otp-stable-tag-amd64: + image: *build_image_amd64 + when: + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' + environment: *env + commands: + - <<: *build_cmds + - zip -9rq pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64.zip release + otp-develop-arm64: image: *build_image_arm64 when: @@ -73,14 +83,27 @@ steps: otp-stable-arm64: image: *build_image_arm64 when: - - evaluate: 'platform == "linux/arm64" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"' environment: *env commands: *arm64_build + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + otp-stable-tag-arm64: + image: *build_image_arm64 + when: + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' + environment: *env + commands: + - <<: *build_cmds + - zip -9rq pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64.zip release + upload-artifacts-amd64: image: *artifacts_uploader_image when: - - evaluate: platform == "linux/amd64" + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"' settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64 @@ -88,13 +111,39 @@ steps: file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-artifacts-tag-amd64: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-amd64 + package_version: stable-${CI_COMMIT_SHA:0:8}-amd64 + file_source: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64.zip + file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64.zip + upload-artifacts-arm64: image: *artifacts_uploader_image when: - - evaluate: platform == "linux/arm64" + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"' settings: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64 package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64 file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-artifacts-tag-arm64: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-arm64 + package_version: stable-${CI_COMMIT_SHA:0:8}-arm64 + file_source: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64.zip + file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64.zip From 16b7a95c481233e2b4e02e90128fedce817852f8 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 22 Apr 2026 15:31:46 +0200 Subject: [PATCH 081/127] Woodpecker CI: Run Docker image workflows also on Dockerfile changes --- .woodpecker/docker-combine.yaml | 4 ++-- .woodpecker/docker.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.woodpecker/docker-combine.yaml b/.woodpecker/docker-combine.yaml index e615ca7c2..147cb1d55 100644 --- a/.woodpecker/docker-combine.yaml +++ b/.woodpecker/docker-combine.yaml @@ -1,10 +1,10 @@ when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - event: push branch: stable - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - event: tag - event: manual branch: stable diff --git a/.woodpecker/docker.yaml b/.woodpecker/docker.yaml index 0c4b18fbc..f178fb840 100644 --- a/.woodpecker/docker.yaml +++ b/.woodpecker/docker.yaml @@ -1,10 +1,10 @@ when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - event: push branch: stable - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - event: tag - event: manual branch: stable From 209b9c0a1e9bd7c822213f1a4cad745605ae31b2 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 22 Apr 2026 21:04:31 +0200 Subject: [PATCH 082/127] Woodpecker CI: Shorten zip archive names further Hopefully this will also help with the workflows randomly failing to create the zip archive due to the Woodpecker bug. --- .woodpecker/otp-musl.yaml | 16 ++++++++-------- .woodpecker/otp.yaml | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.woodpecker/otp-musl.yaml b/.woodpecker/otp-musl.yaml index d611f9f13..44d44a662 100644 --- a/.woodpecker/otp-musl.yaml +++ b/.woodpecker/otp-musl.yaml @@ -50,7 +50,7 @@ steps: environment: *env commands: &amd64_build - <<: *build_cmds - - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip release + - zip -9rq ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip release otp-stable-amd64-musl: image: *build_image_amd64 @@ -68,7 +68,7 @@ steps: environment: *env commands: - <<: *build_cmds - - zip -9rq pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip release + - zip -9rq stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip release otp-develop-arm64-musl: image: *build_image_arm64 @@ -77,7 +77,7 @@ steps: environment: *env commands: &arm64_build - <<: *build_cmds - - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip release + - zip -9rq ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip release otp-stable-arm64-musl: image: *build_image_arm64 @@ -95,7 +95,7 @@ steps: environment: *env commands: - <<: *build_cmds - - zip -9rq pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip release + - zip -9rq stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip release upload-artifacts-amd64-musl: image: *artifacts_uploader_image @@ -107,7 +107,7 @@ steps: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64-musl package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl - file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable @@ -119,7 +119,7 @@ steps: <<: *artifacts_uploader_settings package_name: pleroma-otp-stable-amd64-musl package_version: stable-${CI_COMMIT_SHA:0:8}-amd64-musl - file_source: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + file_source: ./stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip upload-artifacts-arm64-musl: @@ -132,7 +132,7 @@ steps: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64-musl package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl - file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable @@ -144,5 +144,5 @@ steps: <<: *artifacts_uploader_settings package_name: pleroma-otp-stable-arm64-musl package_version: stable-${CI_COMMIT_SHA:0:8}-arm64-musl - file_source: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip diff --git a/.woodpecker/otp.yaml b/.woodpecker/otp.yaml index 0d0b08bec..008c77f95 100644 --- a/.woodpecker/otp.yaml +++ b/.woodpecker/otp.yaml @@ -51,7 +51,7 @@ steps: environment: *env commands: &amd64_build - <<: *build_cmds - - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip release + - zip -9rq ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip release otp-stable-amd64: image: *build_image_amd64 @@ -69,7 +69,7 @@ steps: environment: *env commands: - <<: *build_cmds - - zip -9rq pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64.zip release + - zip -9rq stable-${CI_COMMIT_SHA:0:8}-amd64.zip release otp-develop-arm64: image: *build_image_arm64 @@ -78,7 +78,7 @@ steps: environment: *env commands: &arm64_build - <<: *build_cmds - - zip -9rq pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip release + - zip -9rq ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip release otp-stable-arm64: image: *build_image_arm64 @@ -96,7 +96,7 @@ steps: environment: *env commands: - <<: *build_cmds - - zip -9rq pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64.zip release + - zip -9rq stable-${CI_COMMIT_SHA:0:8}-arm64.zip release upload-artifacts-amd64: image: *artifacts_uploader_image @@ -108,7 +108,7 @@ steps: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64 package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64 - file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable @@ -120,7 +120,7 @@ steps: <<: *artifacts_uploader_settings package_name: pleroma-otp-stable-amd64 package_version: stable-${CI_COMMIT_SHA:0:8}-amd64 - file_source: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64.zip + file_source: ./stable-${CI_COMMIT_SHA:0:8}-amd64.zip file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64.zip upload-artifacts-arm64: @@ -133,7 +133,7 @@ steps: <<: *artifacts_uploader_settings package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64 package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64 - file_source: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable @@ -145,5 +145,5 @@ steps: <<: *artifacts_uploader_settings package_name: pleroma-otp-stable-arm64 package_version: stable-${CI_COMMIT_SHA:0:8}-arm64 - file_source: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64.zip + file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm64.zip file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64.zip From 7f97e21910ea9435febda3c36a67a467af8e2993 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 24 Apr 2026 15:04:12 +0200 Subject: [PATCH 083/127] pleroma_ctl: Properly handle user arguments with whitespace When user supplied arguments to pleroma_ctl include whitespace that has been properly quoted, all arguments were sent to ReleaseTasks in one string, which then String.split/1 the input on any whitespace. This broke Mix tasks that accept certain user input like instance gen and user management. To fix this, pleroma_ctl now sends the arguments in list form. Additionally pleroma_ctl arguments now need to be pre-processed. Fixes pleroma/pleroma#7874 --- lib/pleroma/release_tasks.ex | 19 ++++++++++++++++++- rel/files/bin/pleroma_ctl | 10 +++++++--- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/release_tasks.ex b/lib/pleroma/release_tasks.ex index af2d35c8f..49400940f 100644 --- a/lib/pleroma/release_tasks.ex +++ b/lib/pleroma/release_tasks.ex @@ -5,7 +5,10 @@ defmodule Pleroma.ReleaseTasks do @repo Pleroma.Repo - def run(args) do + # TODO: Kept for some backwards compatibility with buggy pleroma_ctl, + # if a mismatch between pleroma_ctl and Pleroma accidentaly happens. + # Remove in the future. + def run(args) when is_binary(args) do [task | args] = String.split(args) case task do @@ -16,6 +19,20 @@ defmodule Pleroma.ReleaseTasks do end end + # HACK: Script arguments need to be received as a list, otherwise (quoted) arguments with + # whitespace will be broken. Previously the broken string form above was used, + # escaping in the shell does not help. + def run(args) when is_list(args) do + [task | args] = args + + case task do + "migrate" -> migrate(args) + "create" -> create() + "rollback" -> rollback(args) + task -> mix_task(task, args) + end + end + def find_module(task) do module_name = task diff --git a/rel/files/bin/pleroma_ctl b/rel/files/bin/pleroma_ctl index 6f0dba3a8..56dc13a06 100755 --- a/rel/files/bin/pleroma_ctl +++ b/rel/files/bin/pleroma_ctl @@ -137,7 +137,11 @@ else SCRIPT=$(realpath "$0") SCRIPTPATH=$(dirname "$SCRIPT") - FULL_ARGS="$*" + # HACK: Script arguments need to be sent as an array to Mix tasks, otherwise they will break (quoted) arguments with whitespace. + # Previously it was sent as string, which would get split on whitespace on the task side. + # Escaping does not help including non-POSIX printf %q + PREPARED_ARGS="" + for arg in "$@"; do PREPARED_ARGS="$PREPARED_ARGS \"$arg\","; done ACTION="$1" if [ $# -gt 0 ]; then @@ -154,8 +158,8 @@ else if [ "$ACTION" = "update" ]; then update "$@" elif [ "$ACTION" = "migrate" ] || [ "$ACTION" = "rollback" ] || [ "$ACTION" = "create" ] || [ "$ACTION $SUBACTION" = "instance gen" ] || [ "$PLEROMA_CTL_RPC_DISABLED" = true ]; then - "$SCRIPTPATH"/pleroma eval 'Pleroma.ReleaseTasks.run("'"$FULL_ARGS"'")' + "$SCRIPTPATH"/pleroma eval 'Pleroma.ReleaseTasks.run(['"${PREPARED_ARGS%%,}"'])' else - "$SCRIPTPATH"/pleroma rpc 'Pleroma.ReleaseTasks.run("'"$FULL_ARGS"'")' + "$SCRIPTPATH"/pleroma rpc 'Pleroma.ReleaseTasks.run(['"${PREPARED_ARGS%%,}"'])' fi fi From 95a33855d1b0363057112d3470dfb5d980c58eb5 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 24 Apr 2026 22:02:01 +0200 Subject: [PATCH 084/127] pleroma_ctl: Update update logic to Gitea API --- rel/files/bin/pleroma_ctl | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/rel/files/bin/pleroma_ctl b/rel/files/bin/pleroma_ctl index 56dc13a06..2e114ff50 100755 --- a/rel/files/bin/pleroma_ctl +++ b/rel/files/bin/pleroma_ctl @@ -78,12 +78,17 @@ update() { RELEASE_ROOT=$(dirname "$SCRIPTPATH") uri="https://git.pleroma.social" - project_id="2" + project_name="pleroma" + api_base="${uri}/api/v1/packages/${project_name}/generic" + package_base="${uri}/api/packages/${project_name}/generic" project_branch="${BRANCH:-$(detect_branch)}" flavour="${FLAVOUR:-$(detect_flavour)}" + # API responds in JSON, optimistically try to make it one object per line + ver=$(curl -s "${api_base}"/pleroma-otp-"${project_branch}"-"${flavour}"/-/latest | tr ',' '\n' | grep '"version":' | cut -d':' -f2 | tr -d '"') + file=$(curl -s "${api_base}"/pleroma-otp-"${project_branch}"-"${flavour}"/"${ver}"/files | tr ',' '\n' | grep '"name":' | cut -d':' -f2 | tr -d '"') + full_uri=${FULL_URI:-"${package_base}"/pleroma-otp-"${project_branch}"-"${flavour}"/"${ver}"/"${file}"} tmp="${TMP_DIR:-/tmp}" artifact="$tmp/pleroma.zip" - full_uri="${FULL_URI:-${uri}/api/v4/projects/${project_id}/jobs/artifacts/${project_branch}/download?job=${flavour}}" echo "Downloading the artifact from ${full_uri} to ${artifact}" curl "$full_uri" -o "${artifact}" echo "Unpacking ${artifact} to ${tmp}" From cafd75b072ef408cc30ddaecdd003827274cdcf8 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 24 Apr 2026 23:15:00 +0200 Subject: [PATCH 085/127] Woodpecker CI docker-combine: Hoist docker_settings anchor --- .woodpecker/docker-combine.yaml | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/.woodpecker/docker-combine.yaml b/.woodpecker/docker-combine.yaml index 147cb1d55..6fa9583ba 100644 --- a/.woodpecker/docker-combine.yaml +++ b/.woodpecker/docker-combine.yaml @@ -20,7 +20,7 @@ steps: when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} - settings: + settings: &docker_settings registry: "git.pleroma.social" image: "pleroma/pleroma" architectures: [amd64, arm64] @@ -39,18 +39,12 @@ steps: - event: push branch: stable - evaluate: 'CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG == ""' - settings: &docker_settings - registry: "git.pleroma.social" - image: "pleroma/pleroma" - architectures: [amd64, arm64] - tags: &docker_tags + settings: + <<: *docker_settings + tags: &stable_docker_tags - latest - stable - ${CI_COMMIT_SHA:0:8} - username: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password docker-stable-tag-combine: image: git.fluffytail.org/phnt/wpc-docker-tagger:latest @@ -60,5 +54,5 @@ steps: settings: <<: *docker_settings tags: - - <<: *docker_tags + - <<: *stable_docker_tags - ${CI_COMMIT_TAG} From 25e543d44d7846125b773ed81b59dcc27137afd5 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Fri, 24 Apr 2026 23:38:29 +0200 Subject: [PATCH 086/127] changelog --- changelog.d/woodpecker-release-pipeline.skip | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 changelog.d/woodpecker-release-pipeline.skip diff --git a/changelog.d/woodpecker-release-pipeline.skip b/changelog.d/woodpecker-release-pipeline.skip new file mode 100644 index 000000000..e69de29bb From a996d25b8459b5a8f369430106950a2ee9bdf881 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 25 Apr 2026 11:08:28 +0200 Subject: [PATCH 087/127] Woodpecker CI Docker: label workflow as high memory --- .woodpecker/docker.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.woodpecker/docker.yaml b/.woodpecker/docker.yaml index f178fb840..703f3bbfc 100644 --- a/.woodpecker/docker.yaml +++ b/.woodpecker/docker.yaml @@ -17,6 +17,7 @@ matrix: # This is needed for the when clauses below. labels: platform: ${platform} + memory: 'high' variables: docker_variables: &docker_variables From e4632eced36c368ec62f38bb4e05bc4cc6602559 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 25 Apr 2026 13:33:58 +0200 Subject: [PATCH 088/127] Woodpecker CI: Only run stable release pipelines on tag events Removes possible races when uploading images/bundles and purposeful pipeline failures when both a push and tag happened (OTP bundles do not allow overwriting). --- .woodpecker/docker-combine.yaml | 5 ----- .woodpecker/docker.yaml | 5 ----- .woodpecker/otp-musl.yaml | 11 ++--------- .woodpecker/otp.yaml | 11 ++--------- 4 files changed, 4 insertions(+), 28 deletions(-) diff --git a/.woodpecker/docker-combine.yaml b/.woodpecker/docker-combine.yaml index 6fa9583ba..be8583763 100644 --- a/.woodpecker/docker-combine.yaml +++ b/.woodpecker/docker-combine.yaml @@ -2,9 +2,6 @@ when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - - event: push - branch: stable - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - event: tag - event: manual branch: stable @@ -36,8 +33,6 @@ steps: docker-stable-combine: image: git.fluffytail.org/phnt/wpc-docker-tagger:latest when: - - event: push - branch: stable - evaluate: 'CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG == ""' settings: <<: *docker_settings diff --git a/.woodpecker/docker.yaml b/.woodpecker/docker.yaml index 703f3bbfc..317cb5fbb 100644 --- a/.woodpecker/docker.yaml +++ b/.woodpecker/docker.yaml @@ -2,9 +2,6 @@ when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - - event: push - branch: stable - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - event: tag - event: manual branch: stable @@ -55,7 +52,6 @@ steps: docker-stable-amd64: image: *kaniko_image when: - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG == ""' settings: <<: *docker_variables @@ -78,7 +74,6 @@ steps: docker-stable-arm64: image: *kaniko_image when: - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG == ""' settings: <<: *docker_variables diff --git a/.woodpecker/otp-musl.yaml b/.woodpecker/otp-musl.yaml index 44d44a662..c58eee66b 100644 --- a/.woodpecker/otp-musl.yaml +++ b/.woodpecker/otp-musl.yaml @@ -2,9 +2,6 @@ when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - - event: push - branch: stable - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: tag - event: manual branch: stable @@ -55,8 +52,7 @@ steps: otp-stable-amd64-musl: image: *build_image_amd64 when: - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' environment: *env commands: *amd64_build @@ -82,8 +78,7 @@ steps: otp-stable-arm64-musl: image: *build_image_arm64 when: - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' environment: *env commands: *arm64_build @@ -101,7 +96,6 @@ steps: image: *artifacts_uploader_image when: - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"' settings: <<: *artifacts_uploader_settings @@ -126,7 +120,6 @@ steps: image: *artifacts_uploader_image when: - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"' settings: <<: *artifacts_uploader_settings diff --git a/.woodpecker/otp.yaml b/.woodpecker/otp.yaml index 008c77f95..c01c2e557 100644 --- a/.woodpecker/otp.yaml +++ b/.woodpecker/otp.yaml @@ -2,9 +2,6 @@ when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - - event: push - branch: stable - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] - event: tag - event: manual branch: stable @@ -56,8 +53,7 @@ steps: otp-stable-amd64: image: *build_image_amd64 when: - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' environment: *env commands: *amd64_build @@ -83,8 +79,7 @@ steps: otp-stable-arm64: image: *build_image_arm64 when: - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' environment: *env commands: *arm64_build @@ -102,7 +97,6 @@ steps: image: *artifacts_uploader_image when: - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' - - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"' settings: <<: *artifacts_uploader_settings @@ -127,7 +121,6 @@ steps: image: *artifacts_uploader_image when: - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' - - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "stable"' - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"' settings: <<: *artifacts_uploader_settings From cb2271978ec55c0714d6396f73394957b3704abf Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 30 Apr 2026 00:17:59 +0200 Subject: [PATCH 089/127] UpdateValidator: fix tests --- .../object_validators/update_handling_test.exs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) 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 347c5b578..94c502ad6 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 @@ -29,7 +29,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do assert {:ok, _update, []} = ObjectValidator.validate(valid_update, []) end - test "returns an error if the object can't be updated by the actor", %{ + test "returns an error if the object can't be updated by the actor (different domain)", %{ valid_update: valid_update } do other_user = insert(:user, local: false) @@ -41,24 +41,26 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do assert {:error, _cng} = ObjectValidator.validate(update, []) end - test "validates as long as the object is same-origin with the actor", %{ + test "returns an error if the object can't be updated by the actor (same domain)", %{ + user: user, valid_update: valid_update } do - other_user = insert(:user) + user_ap_id = user.ap_id + user_domain = URI.parse(user_ap_id).host + other_user = insert(:user, local: false, domain: user_domain) update = valid_update |> Map.put("actor", other_user.ap_id) - assert {:ok, _update, []} = ObjectValidator.validate(update, []) + assert {:error, _cng} = ObjectValidator.validate(update, []) end - test "validates if the object is not of an Actor type" do - note = insert(:note) + test "validates if the object is not of an Actor type", %{user: user} do + note = insert(:note, user: user) updated_note = note.data |> Map.put("content", "edited content") - other_user = insert(:user) - {:ok, update, _} = Builder.update(other_user, updated_note) + {:ok, update, _} = Builder.update(user, updated_note) assert {:ok, _update, _} = ObjectValidator.validate(update, []) end From af6d12c0a5be10291c9e2bc73cb29bc24c29115a Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 30 Apr 2026 00:18:54 +0200 Subject: [PATCH 090/127] UpdateValidator: Check Actor owns Object or updates itself --- .../object_validators/update_validator.ex | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex index aab90235f..5586b74cf 100644 --- a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex @@ -75,15 +75,36 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator do end end - # For remote Updates, verify the host is the same. + # For remote Updates, verify the Actor is the same def validate_updating_rights_remote(cng) do with actor = get_field(cng, :actor), object = get_field(cng, :object), {:ok, object_id} <- ObjectValidators.ObjectID.cast(object), - actor_uri <- URI.parse(actor), - object_uri <- URI.parse(object_id), - true <- actor_uri.host == object_uri.host do - cng + entity <- + Object.normalize(object_id, fetch: false) || User.get_cached_by_ap_id(object_id) do + case entity do + # Actor must own Object to update it + %Object{} -> + if actor == entity.data["actor"] do + cng + else + cng + |> add_error(:object, "Can't be updated by this actor") + end + + # Actor must only be allowed to update itself + %User{} -> + if actor == entity.ap_id do + cng + else + cng + |> add_error(:object, "Can't be updated by this actor") + end + + true -> + cng + |> add_error(:object, "Update is neither for Object or Actor") + end else _e -> cng From da28a4c44109f1af944d23b87e16ea661b76d868 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 30 Apr 2026 00:58:43 +0200 Subject: [PATCH 091/127] ReceiverWorker: Add cancels on actor does not match signature test --- test/pleroma/workers/receiver_worker_test.exs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/test/pleroma/workers/receiver_worker_test.exs b/test/pleroma/workers/receiver_worker_test.exs index 12abc1a27..1c886a74e 100644 --- a/test/pleroma/workers/receiver_worker_test.exs +++ b/test/pleroma/workers/receiver_worker_test.exs @@ -302,4 +302,48 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end end end + + test "cancels when signature actor does not match payload actor" do + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + + note = insert(:note, user: bob, object_local: false) + + update = %{ + "type" => "Update", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/malicious-update", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data + } + + req_headers = [ + ["host", "example.com"], + ["date", "Thu, 25 Jul 2024 13:33:31 GMT"], + ["digest", "SHA-256=fake-digest"], + ["content-type", "application/activity+json"], + [ + "signature", + "keyId=\"https://example.com/users/alice#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" + ] + ] + + oban_job = %Oban.Job{ + args: %{ + "op" => "incoming_ap_doc", + "method" => "POST", + "params" => update, + "req_headers" => req_headers, + "request_path" => "/inbox", + "query_string" => "" + } + } + + with_mock Pleroma.Signature, [:passthrough], + refetch_public_key: fn _conn -> {:ok, :fake_public_key} end, + validate_signature: fn _conn -> true end do + assert {:cancel, :invalid_signature} = ReceiverWorker.perform(oban_job) + end + end end From 42683e79dfe89651d8f44feef6659a5ceaa78183 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Thu, 30 Apr 2026 01:34:14 +0200 Subject: [PATCH 092/127] ReceiverWorker: Check that signature matches actor --- lib/pleroma/workers/receiver_worker.ex | 14 ++++++++++++++ test/pleroma/workers/receiver_worker_test.exs | 4 ++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/workers/receiver_worker.ex b/lib/pleroma/workers/receiver_worker.ex index e2c950967..507e099c2 100644 --- a/lib/pleroma/workers/receiver_worker.ex +++ b/lib/pleroma/workers/receiver_worker.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Workers.ReceiverWorker do alias Pleroma.Signature alias Pleroma.User alias Pleroma.Web.Federator + alias Pleroma.Web.Plugs.MappedSignatureToIdentityPlug use Oban.Worker, queue: :federator_incoming, max_attempts: 5, unique: [period: :infinity] @@ -27,6 +28,7 @@ defmodule Pleroma.Workers.ReceiverWorker do req_headers = Enum.into(req_headers, [], &List.to_tuple(&1)) conn_data = %Plug.Conn{ + assigns: %{valid_signature: true}, method: method, params: params, req_headers: req_headers, @@ -37,6 +39,7 @@ defmodule Pleroma.Workers.ReceiverWorker do with {:ok, %User{}} <- User.get_or_fetch_by_ap_id(conn_data.params["actor"]), {:ok, _public_key} <- Signature.refetch_public_key(conn_data), {:signature, true} <- {:signature, Signature.validate_signature(conn_data)}, + {:same_actor, true} <- {:same_actor, validate_same_actor(conn_data)}, {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do unless Instances.reachable?(params["actor"]) do domain = URI.parse(params["actor"]).host @@ -67,6 +70,16 @@ defmodule Pleroma.Workers.ReceiverWorker do def timeout(_job), do: :timer.seconds(5) + defp validate_same_actor(conn_data) do + case MappedSignatureToIdentityPlug.call(conn_data, []) do + %Plug.Conn{assigns: %{valid_signature: true}} -> + true + + _ -> + false + end + end + defp process_errors({:error, {:error, _} = error}), do: process_errors(error) defp process_errors(errors) do @@ -85,6 +98,7 @@ defmodule Pleroma.Workers.ReceiverWorker do {:error, {:reject, _} = reason} -> {:cancel, reason} # HTTP Sigs {:signature, false} -> {:cancel, :invalid_signature} + {:same_actor, false} -> {:cancel, :actor_signature_mismatch} # Origin / URL validation failed somewhere possibly due to spoofing {:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed} # Unclear if this can be reached diff --git a/test/pleroma/workers/receiver_worker_test.exs b/test/pleroma/workers/receiver_worker_test.exs index 1c886a74e..bc027ad4c 100644 --- a/test/pleroma/workers/receiver_worker_test.exs +++ b/test/pleroma/workers/receiver_worker_test.exs @@ -304,7 +304,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels when signature actor does not match payload actor" do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") note = insert(:note, user: bob, object_local: false) @@ -343,7 +343,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do with_mock Pleroma.Signature, [:passthrough], refetch_public_key: fn _conn -> {:ok, :fake_public_key} end, validate_signature: fn _conn -> true end do - assert {:cancel, :invalid_signature} = ReceiverWorker.perform(oban_job) + assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) end end end From 80e72b79f57bad270c530d94527f760c14d8c152 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 30 Apr 2026 14:31:06 +0400 Subject: [PATCH 093/127] Add spoofing regression tests --- .gitignore | 3 + .../activity_pub_controller_test.exs | 68 +++++ .../update_handling_test.exs | 26 ++ ...mapped_signature_to_identity_plug_test.exs | 10 +- test/pleroma/workers/receiver_worker_test.exs | 239 ++++++++++++++++++ 5 files changed, 342 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 355cea069..d8e5ed553 100644 --- a/.gitignore +++ b/.gitignore @@ -56,6 +56,9 @@ pleroma.iml # asdf .tool-versions +# mise +mise.toml + # Editor temp files *~ *# diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index d5947186f..42cb35669 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -726,6 +726,74 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert Activity.get_by_ap_id(data["id"]) end + test "does not create a forged post after failed signature retry", %{conn: conn} do + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + object_id = "https://example.com/objects/inbox-forged-note" + + data = %{ + "type" => "Create", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/inbox-forged-create", + "context" => "https://example.com/contexts/inbox-forged-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => object_id, + "actor" => bob.ap_id, + "attributedTo" => bob.ap_id, + "context" => "https://example.com/contexts/inbox-forged-create", + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + conn = + conn + |> assign(:valid_signature, false) + |> put_req_header("content-type", "application/activity+json") + |> put_req_header("signature", "keyId=\"https://example.com/users/alice#main-key\"") + |> post("/inbox", data) + + assert "ok" == json_response(conn, 200) + + assert [{:cancel, :actor_signature_mismatch}] = + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + + refute Activity.get_by_ap_id(data["id"]) + refute Object.get_by_ap_id(object_id) + end + + test "does not create a forged like after failed signature retry", %{conn: conn} do + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + note = insert(:note) + + data = %{ + "type" => "Like", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/inbox-forged-like", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data["id"] + } + + conn = + conn + |> assign(:valid_signature, false) + |> put_req_header("content-type", "application/activity+json") + |> put_req_header("signature", "keyId=\"https://example.com/users/alice#main-key\"") + |> post("/inbox", data) + + assert "ok" == json_response(conn, 200) + + assert [{:cancel, :actor_signature_mismatch}] = + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + + refute Activity.get_by_ap_id(data["id"]) + end + test "accept follow activity", %{conn: conn} do clear_config([:instance, :federating], true) relay = Relay.get_actor() 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 94c502ad6..f04f9cc61 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 @@ -64,6 +64,32 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do assert {:ok, _update, _} = ObjectValidator.validate(update, []) end + + test "returns an error if the remote update target is unknown" do + remote_user = insert(:user, local: false, ap_id: "https://example.com/users/alice") + + update = %{ + "type" => "Update", + "actor" => remote_user.ap_id, + "id" => "https://example.com/activities/update-unknown-object", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => "https://example.com/objects/unknown", + "actor" => remote_user.ap_id, + "content" => "edited content", + "published" => "2024-07-25T13:33:31Z", + "updated" => "2024-07-25T13:34:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + assert {:error, %Ecto.Changeset{} = cng} = ObjectValidator.validate(update, local: false) + refute cng.valid? + assert Keyword.has_key?(cng.errors, :object) + end end describe "update note" do diff --git a/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs b/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs index 33eff1bc5..81c6b0c5d 100644 --- a/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs +++ b/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs @@ -47,13 +47,15 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlugTest do assert %{valid_signature: false} == conn.assigns end - @tag skip: "known breakage; the testsuite presently depends on it" test "it considers a mapped identity to be invalid when the identity cannot be found" do + actor = "http://niu.moe/users/rye" + conn = - build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"}) - |> set_signature("http://niu.moe/users/rye") + build_conn(:post, "/doesntmattter", %{"actor" => actor}) + |> set_signature(actor) |> MappedSignatureToIdentityPlug.call(%{}) - assert %{valid_signature: false} == conn.assigns + assert conn.assigns.valid_signature == false + refute Map.has_key?(conn.assigns, :user) end end diff --git a/test/pleroma/workers/receiver_worker_test.exs b/test/pleroma/workers/receiver_worker_test.exs index bc027ad4c..9dccd739b 100644 --- a/test/pleroma/workers/receiver_worker_test.exs +++ b/test/pleroma/workers/receiver_worker_test.exs @@ -14,6 +14,43 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do alias Pleroma.Web.Federator alias Pleroma.Workers.ReceiverWorker + defp mismatched_signature_headers do + [ + {"host", "example.com"}, + {"date", "Thu, 25 Jul 2024 13:33:31 GMT"}, + {"digest", "SHA-256=fake-digest"}, + {"content-type", "application/activity+json"}, + { + "signature", + "keyId=\"https://example.com/users/alice#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" + } + ] + end + + defp assert_mismatched_signature_cancelled(params) do + with_mocks [ + {Pleroma.Signature, [:passthrough], + [ + refetch_public_key: fn _conn -> {:ok, :fake_public_key} end, + validate_signature: fn _conn -> true end + ]}, + {Pleroma.Web.Federator, [:passthrough], + [perform: fn :incoming_ap_doc, _params -> {:ok, :processed} end]} + ] do + assert {:ok, oban_job} = + Federator.incoming_ap_doc(%{ + method: "POST", + req_headers: mismatched_signature_headers(), + request_path: "/inbox", + params: params, + query_string: "" + }) + + assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) + refute called(Pleroma.Web.Federator.perform(:incoming_ap_doc, :_)) + end + end + test "it does not retry MRF reject" do params = insert(:note).data @@ -346,4 +383,206 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) end end + + test "Federator preserves request metadata needed for ReceiverWorker signature checks" do + params = insert(:note_activity).data + + req_headers = [ + {"host", "example.com"}, + {"signature", "keyId=\"https://example.com/users/alice#main-key\""} + ] + + assert {:ok, oban_job} = + Federator.incoming_ap_doc(%{ + method: "POST", + req_headers: req_headers, + request_path: "/inbox", + params: params, + query_string: "foo=bar" + }) + + assert %{ + "method" => "POST", + "req_headers" => ^req_headers, + "request_path" => "/inbox", + "params" => ^params, + "query_string" => "foo=bar" + } = oban_job.args + end + + test "cancels signature actor mismatch through Federator-created jobs" do + _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + + note = insert(:note, user: bob, object_local: false) + + update = %{ + "type" => "Update", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/federator-malicious-update", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data + } + + assert_mismatched_signature_cancelled(update) + end + + test "cancels signature actor mismatch before processing a forged Create" do + _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + + create = %{ + "type" => "Create", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/forged-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => "https://example.com/objects/forged-note", + "actor" => bob.ap_id, + "attributedTo" => bob.ap_id, + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + assert_mismatched_signature_cancelled(create) + end + + test "cancels signature actor mismatch before actually creating a forged post" do + _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + + object_id = "https://example.com/objects/actually-forged-note" + + create = %{ + "type" => "Create", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/actually-forged-create", + "context" => "https://example.com/contexts/actually-forged-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => object_id, + "actor" => bob.ap_id, + "attributedTo" => bob.ap_id, + "context" => "https://example.com/contexts/actually-forged-create", + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + assert {:ok, oban_job} = + Federator.incoming_ap_doc(%{ + method: "POST", + req_headers: mismatched_signature_headers(), + request_path: "/inbox", + params: create, + query_string: "" + }) + + assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) + refute Pleroma.Object.get_by_ap_id(object_id) + end + + test "cancels signature actor mismatch before processing a forged Like" do + _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + note = insert(:note) + + like = %{ + "type" => "Like", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/forged-like", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data["id"] + } + + assert_mismatched_signature_cancelled(like) + end + + test "cancels signature actor mismatch before actually creating a forged Like" do + _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + note = insert(:note) + + like = %{ + "type" => "Like", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/actually-forged-like", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data["id"] + } + + assert {:ok, oban_job} = + Federator.incoming_ap_doc(%{ + method: "POST", + req_headers: mismatched_signature_headers(), + request_path: "/inbox", + params: like, + query_string: "" + }) + + assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) + refute Pleroma.Activity.get_by_ap_id(like["id"]) + end + + test "cancels signature actor mismatch before processing a forged Announce" do + _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + note = insert(:note) + + announce = %{ + "type" => "Announce", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/forged-announce", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data["id"] + } + + assert_mismatched_signature_cancelled(announce) + end + + test "cancels signature actor mismatch before processing a forged Follow" do + _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + followed = insert(:user) + + follow = %{ + "type" => "Follow", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/forged-follow", + "to" => [followed.ap_id], + "cc" => [], + "object" => followed.ap_id + } + + assert_mismatched_signature_cancelled(follow) + end + + test "cancels signature actor mismatch before processing a forged Undo" do + _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + + undo = %{ + "type" => "Undo", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/forged-undo", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => "https://example.com/activities/existing-bob-activity" + } + + assert_mismatched_signature_cancelled(undo) + end end From 9c540995b4ca1bb33e38692bdf7504f9f4cdffc5 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 30 Apr 2026 15:36:55 +0400 Subject: [PATCH 094/127] Use Mox in spoofing regression tests --- .../activity_pub_controller_test.exs | 86 ++++++++++++++++++ test/pleroma/workers/receiver_worker_test.exs | 91 ++++++++++--------- 2 files changed, 136 insertions(+), 41 deletions(-) diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index 42cb35669..8cadf6686 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -794,6 +794,92 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do refute Activity.get_by_ap_id(data["id"]) end + test "does not create a forged post signed by a different actor", %{conn: conn} do + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + object_id = "https://example.com/objects/inbox-signed-forged-note" + + data = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Create", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/inbox-signed-forged-create", + "context" => "https://example.com/contexts/inbox-signed-forged-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => object_id, + "actor" => bob.ap_id, + "attributedTo" => bob.ap_id, + "context" => "https://example.com/contexts/inbox-signed-forged-create", + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end) + + conn = + conn + |> put_req_header("content-type", "application/activity+json") + |> put_req_header("date", "Thu, 25 Jul 2024 13:33:31 GMT") + |> put_req_header("digest", "SHA-256=fake-digest") + |> put_req_header( + "signature", + "keyId=\"#{alice.ap_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" + ) + |> post("/inbox", data) + + assert conn.assigns.valid_signature == false + assert "ok" == json_response(conn, 200) + + assert [{:cancel, :actor_signature_mismatch}] = + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + + refute Activity.get_by_ap_id(data["id"]) + refute Object.get_by_ap_id(object_id) + end + + test "does not create a forged like signed by a different actor", %{conn: conn} do + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + note = insert(:note) + + data = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Like", + "actor" => bob.ap_id, + "id" => "https://example.com/activities/inbox-signed-forged-like", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data["id"] + } + + Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end) + + conn = + conn + |> put_req_header("content-type", "application/activity+json") + |> put_req_header("date", "Thu, 25 Jul 2024 13:33:31 GMT") + |> put_req_header("digest", "SHA-256=fake-digest") + |> put_req_header( + "signature", + "keyId=\"#{alice.ap_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" + ) + |> post("/inbox", data) + + assert conn.assigns.valid_signature == false + assert "ok" == json_response(conn, 200) + + assert [{:cancel, :actor_signature_mismatch}] = + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + + refute Activity.get_by_ap_id(data["id"]) + end + test "accept follow activity", %{conn: conn} do clear_config([:instance, :federating], true) relay = Relay.get_actor() diff --git a/test/pleroma/workers/receiver_worker_test.exs b/test/pleroma/workers/receiver_worker_test.exs index 9dccd739b..5e18fe771 100644 --- a/test/pleroma/workers/receiver_worker_test.exs +++ b/test/pleroma/workers/receiver_worker_test.exs @@ -11,6 +11,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do alias Pleroma.User alias Pleroma.Web.CommonAPI + alias Pleroma.Web.ActivityPub.UserView alias Pleroma.Web.Federator alias Pleroma.Workers.ReceiverWorker @@ -27,28 +28,34 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do ] end - defp assert_mismatched_signature_cancelled(params) do - with_mocks [ - {Pleroma.Signature, [:passthrough], - [ - refetch_public_key: fn _conn -> {:ok, :fake_public_key} end, - validate_signature: fn _conn -> true end - ]}, - {Pleroma.Web.Federator, [:passthrough], - [perform: fn :incoming_ap_doc, _params -> {:ok, :processed} end]} - ] do - assert {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: mismatched_signature_headers(), - request_path: "/inbox", - params: params, - query_string: "" - }) + defp expect_signature_from(%User{} = signer) do + signer_json = UserView.render("user.json", %{user: signer}) |> Map.delete("featured") - assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) - refute called(Pleroma.Web.Federator.perform(:incoming_ap_doc, :_)) - end + Tesla.Mock.mock(fn + %{url: url} when url == signer.ap_id -> + %Tesla.Env{ + status: 200, + body: Jason.encode!(signer_json), + headers: HttpRequestMock.activitypub_object_headers() + } + end) + + Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end) + end + + defp assert_mismatched_signature_cancelled(params, signer) do + expect_signature_from(signer) + + assert {:ok, oban_job} = + Federator.incoming_ap_doc(%{ + method: "POST", + req_headers: mismatched_signature_headers(), + request_path: "/inbox", + params: params, + query_string: "" + }) + + assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) end test "it does not retry MRF reject" do @@ -341,7 +348,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels when signature actor does not match payload actor" do - _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") note = insert(:note, user: bob, object_local: false) @@ -377,11 +384,9 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do } } - with_mock Pleroma.Signature, [:passthrough], - refetch_public_key: fn _conn -> {:ok, :fake_public_key} end, - validate_signature: fn _conn -> true end do - assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) - end + expect_signature_from(alice) + + assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) end test "Federator preserves request metadata needed for ReceiverWorker signature checks" do @@ -411,7 +416,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels signature actor mismatch through Federator-created jobs" do - _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") note = insert(:note, user: bob, object_local: false) @@ -425,11 +430,11 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do "object" => note.data } - assert_mismatched_signature_cancelled(update) + assert_mismatched_signature_cancelled(update, alice) end test "cancels signature actor mismatch before processing a forged Create" do - _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") create = %{ @@ -450,11 +455,11 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do } } - assert_mismatched_signature_cancelled(create) + assert_mismatched_signature_cancelled(create, alice) end test "cancels signature actor mismatch before actually creating a forged post" do - _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") object_id = "https://example.com/objects/actually-forged-note" @@ -479,6 +484,8 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do } } + expect_signature_from(alice) + assert {:ok, oban_job} = Federator.incoming_ap_doc(%{ method: "POST", @@ -493,7 +500,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels signature actor mismatch before processing a forged Like" do - _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") note = insert(:note) @@ -506,11 +513,11 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do "object" => note.data["id"] } - assert_mismatched_signature_cancelled(like) + assert_mismatched_signature_cancelled(like, alice) end test "cancels signature actor mismatch before actually creating a forged Like" do - _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") note = insert(:note) @@ -523,6 +530,8 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do "object" => note.data["id"] } + expect_signature_from(alice) + assert {:ok, oban_job} = Federator.incoming_ap_doc(%{ method: "POST", @@ -537,7 +546,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels signature actor mismatch before processing a forged Announce" do - _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") note = insert(:note) @@ -550,11 +559,11 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do "object" => note.data["id"] } - assert_mismatched_signature_cancelled(announce) + assert_mismatched_signature_cancelled(announce, alice) end test "cancels signature actor mismatch before processing a forged Follow" do - _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") followed = insert(:user) @@ -567,11 +576,11 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do "object" => followed.ap_id } - assert_mismatched_signature_cancelled(follow) + assert_mismatched_signature_cancelled(follow, alice) end test "cancels signature actor mismatch before processing a forged Undo" do - _alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") + alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") undo = %{ @@ -583,6 +592,6 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do "object" => "https://example.com/activities/existing-bob-activity" } - assert_mismatched_signature_cancelled(undo) + assert_mismatched_signature_cancelled(undo, alice) end end From bd45704dba4eb82417daa449fc0af960867fcf9b Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 30 Apr 2026 17:21:40 +0400 Subject: [PATCH 095/127] Clarify cross-domain spoofing regressions --- .../activity_pub_controller_test.exs | 36 ++++---- test/pleroma/workers/receiver_worker_test.exs | 90 ++++++++++--------- 2 files changed, 68 insertions(+), 58 deletions(-) diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index 8cadf6686..2be5ca6df 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -727,14 +727,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do end test "does not create a forged post after failed signature retry", %{conn: conn} do - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") - object_id = "https://example.com/objects/inbox-forged-note" + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + object_id = "https://two.com/objects/inbox-forged-note" data = %{ "type" => "Create", "actor" => bob.ap_id, - "id" => "https://example.com/activities/inbox-forged-create", - "context" => "https://example.com/contexts/inbox-forged-create", + "id" => "https://two.com/activities/inbox-forged-create", + "context" => "https://two.com/contexts/inbox-forged-create", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], "object" => %{ @@ -742,7 +742,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do "id" => object_id, "actor" => bob.ap_id, "attributedTo" => bob.ap_id, - "context" => "https://example.com/contexts/inbox-forged-create", + "context" => "https://two.com/contexts/inbox-forged-create", "content" => "forged post", "published" => "2024-07-25T13:33:31Z", "to" => ["https://www.w3.org/ns/activitystreams#Public"], @@ -754,7 +754,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn |> assign(:valid_signature, false) |> put_req_header("content-type", "application/activity+json") - |> put_req_header("signature", "keyId=\"https://example.com/users/alice#main-key\"") + |> put_req_header("signature", "keyId=\"https://one.com/users/alice#main-key\"") |> post("/inbox", data) assert "ok" == json_response(conn, 200) @@ -767,13 +767,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do end test "does not create a forged like after failed signature retry", %{conn: conn} do - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") note = insert(:note) data = %{ "type" => "Like", "actor" => bob.ap_id, - "id" => "https://example.com/activities/inbox-forged-like", + "id" => "https://two.com/activities/inbox-forged-like", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], "object" => note.data["id"] @@ -783,7 +783,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn |> assign(:valid_signature, false) |> put_req_header("content-type", "application/activity+json") - |> put_req_header("signature", "keyId=\"https://example.com/users/alice#main-key\"") + |> put_req_header("signature", "keyId=\"https://one.com/users/alice#main-key\"") |> post("/inbox", data) assert "ok" == json_response(conn, 200) @@ -795,16 +795,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do end test "does not create a forged post signed by a different actor", %{conn: conn} do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") - object_id = "https://example.com/objects/inbox-signed-forged-note" + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + object_id = "https://two.com/objects/inbox-signed-forged-note" data = %{ "@context" => "https://www.w3.org/ns/activitystreams", "type" => "Create", "actor" => bob.ap_id, - "id" => "https://example.com/activities/inbox-signed-forged-create", - "context" => "https://example.com/contexts/inbox-signed-forged-create", + "id" => "https://two.com/activities/inbox-signed-forged-create", + "context" => "https://two.com/contexts/inbox-signed-forged-create", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], "object" => %{ @@ -812,7 +812,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do "id" => object_id, "actor" => bob.ap_id, "attributedTo" => bob.ap_id, - "context" => "https://example.com/contexts/inbox-signed-forged-create", + "context" => "https://two.com/contexts/inbox-signed-forged-create", "content" => "forged post", "published" => "2024-07-25T13:33:31Z", "to" => ["https://www.w3.org/ns/activitystreams#Public"], @@ -844,15 +844,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do end test "does not create a forged like signed by a different actor", %{conn: conn} do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") note = insert(:note) data = %{ "@context" => "https://www.w3.org/ns/activitystreams", "type" => "Like", "actor" => bob.ap_id, - "id" => "https://example.com/activities/inbox-signed-forged-like", + "id" => "https://two.com/activities/inbox-signed-forged-like", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], "object" => note.data["id"] diff --git a/test/pleroma/workers/receiver_worker_test.exs b/test/pleroma/workers/receiver_worker_test.exs index 5e18fe771..ec630eb7b 100644 --- a/test/pleroma/workers/receiver_worker_test.exs +++ b/test/pleroma/workers/receiver_worker_test.exs @@ -17,13 +17,13 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do defp mismatched_signature_headers do [ - {"host", "example.com"}, + {"host", "local.test"}, {"date", "Thu, 25 Jul 2024 13:33:31 GMT"}, {"digest", "SHA-256=fake-digest"}, {"content-type", "application/activity+json"}, { "signature", - "keyId=\"https://example.com/users/alice#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" + "keyId=\"https://one.com/users/alice#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" } ] end @@ -348,28 +348,33 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels when signature actor does not match payload actor" do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - note = insert(:note, user: bob, object_local: false) + note = + insert(:note, + user: bob, + object_local: false, + data: %{"id" => "https://two.com/objects/malicious-update-note"} + ) update = %{ "type" => "Update", "actor" => bob.ap_id, - "id" => "https://example.com/activities/malicious-update", + "id" => "https://two.com/activities/malicious-update", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], "object" => note.data } req_headers = [ - ["host", "example.com"], + ["host", "local.test"], ["date", "Thu, 25 Jul 2024 13:33:31 GMT"], ["digest", "SHA-256=fake-digest"], ["content-type", "application/activity+json"], [ "signature", - "keyId=\"https://example.com/users/alice#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" + "keyId=\"https://one.com/users/alice#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" ] ] @@ -393,8 +398,8 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do params = insert(:note_activity).data req_headers = [ - {"host", "example.com"}, - {"signature", "keyId=\"https://example.com/users/alice#main-key\""} + {"host", "local.test"}, + {"signature", "keyId=\"https://one.com/users/alice#main-key\""} ] assert {:ok, oban_job} = @@ -416,15 +421,20 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels signature actor mismatch through Federator-created jobs" do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - note = insert(:note, user: bob, object_local: false) + note = + insert(:note, + user: bob, + object_local: false, + data: %{"id" => "https://two.com/objects/federator-malicious-note"} + ) update = %{ "type" => "Update", "actor" => bob.ap_id, - "id" => "https://example.com/activities/federator-malicious-update", + "id" => "https://two.com/activities/federator-malicious-update", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], "object" => note.data @@ -434,18 +444,18 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels signature actor mismatch before processing a forged Create" do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") create = %{ "type" => "Create", "actor" => bob.ap_id, - "id" => "https://example.com/activities/forged-create", + "id" => "https://two.com/activities/forged-create", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], "object" => %{ "type" => "Note", - "id" => "https://example.com/objects/forged-note", + "id" => "https://two.com/objects/forged-note", "actor" => bob.ap_id, "attributedTo" => bob.ap_id, "content" => "forged post", @@ -459,16 +469,16 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels signature actor mismatch before actually creating a forged post" do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - object_id = "https://example.com/objects/actually-forged-note" + object_id = "https://two.com/objects/actually-forged-note" create = %{ "type" => "Create", "actor" => bob.ap_id, - "id" => "https://example.com/activities/actually-forged-create", - "context" => "https://example.com/contexts/actually-forged-create", + "id" => "https://two.com/activities/actually-forged-create", + "context" => "https://two.com/contexts/actually-forged-create", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], "object" => %{ @@ -476,7 +486,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do "id" => object_id, "actor" => bob.ap_id, "attributedTo" => bob.ap_id, - "context" => "https://example.com/contexts/actually-forged-create", + "context" => "https://two.com/contexts/actually-forged-create", "content" => "forged post", "published" => "2024-07-25T13:33:31Z", "to" => ["https://www.w3.org/ns/activitystreams#Public"], @@ -500,14 +510,14 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels signature actor mismatch before processing a forged Like" do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") note = insert(:note) like = %{ "type" => "Like", "actor" => bob.ap_id, - "id" => "https://example.com/activities/forged-like", + "id" => "https://two.com/activities/forged-like", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], "object" => note.data["id"] @@ -517,14 +527,14 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels signature actor mismatch before actually creating a forged Like" do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") note = insert(:note) like = %{ "type" => "Like", "actor" => bob.ap_id, - "id" => "https://example.com/activities/actually-forged-like", + "id" => "https://two.com/activities/actually-forged-like", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], "object" => note.data["id"] @@ -546,14 +556,14 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels signature actor mismatch before processing a forged Announce" do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") note = insert(:note) announce = %{ "type" => "Announce", "actor" => bob.ap_id, - "id" => "https://example.com/activities/forged-announce", + "id" => "https://two.com/activities/forged-announce", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], "object" => note.data["id"] @@ -563,14 +573,14 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels signature actor mismatch before processing a forged Follow" do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") followed = insert(:user) follow = %{ "type" => "Follow", "actor" => bob.ap_id, - "id" => "https://example.com/activities/forged-follow", + "id" => "https://two.com/activities/forged-follow", "to" => [followed.ap_id], "cc" => [], "object" => followed.ap_id @@ -580,16 +590,16 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end test "cancels signature actor mismatch before processing a forged Undo" do - alice = insert(:user, local: false, ap_id: "https://example.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://example.com/users/bob") + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") undo = %{ "type" => "Undo", "actor" => bob.ap_id, - "id" => "https://example.com/activities/forged-undo", + "id" => "https://two.com/activities/forged-undo", "to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => [], - "object" => "https://example.com/activities/existing-bob-activity" + "object" => "https://two.com/activities/existing-bob-activity" } assert_mismatched_signature_cancelled(undo, alice) From 7756f491d5a4d3528fc96e87f05554b2c2380ad4 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Fri, 1 May 2026 08:43:42 +0400 Subject: [PATCH 096/127] Split failed-signature inbox retries Route failed-signature ActivityPub inbox retries through a dedicated worker so legacy and malformed retry jobs fail closed before processing. --- .../activity_pub/activity_pub_controller.ex | 2 +- lib/pleroma/web/federator.ex | 16 +- lib/pleroma/workers/receiver_worker.ex | 76 +-- lib/pleroma/workers/signature_retry_worker.ex | 144 +++++ .../activity_pub_controller_test.exs | 37 +- test/pleroma/workers/receiver_worker_test.exs | 503 ++++-------------- .../workers/signature_retry_worker_test.exs | 469 ++++++++++++++++ 7 files changed, 786 insertions(+), 461 deletions(-) create mode 100644 lib/pleroma/workers/signature_retry_worker.ex create mode 100644 test/pleroma/workers/signature_retry_worker_test.exs diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 4f1613a07..2bfff6968 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -348,7 +348,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do end def inbox(%{assigns: %{valid_signature: false}} = conn, params) do - Federator.incoming_ap_doc(%{ + Federator.incoming_failed_signature_ap_doc(%{ method: conn.method, req_headers: conn.req_headers, request_path: conn.request_path, diff --git a/lib/pleroma/web/federator.ex b/lib/pleroma/web/federator.ex index 676fc5137..90cd2e54a 100644 --- a/lib/pleroma/web/federator.ex +++ b/lib/pleroma/web/federator.ex @@ -11,6 +11,7 @@ defmodule Pleroma.Web.Federator do alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Workers.PublisherWorker alias Pleroma.Workers.ReceiverWorker + alias Pleroma.Workers.SignatureRetryWorker require Logger @@ -35,12 +36,21 @@ defmodule Pleroma.Web.Federator do end # Client API - def incoming_ap_doc(%{params: params, req_headers: req_headers}) do - ReceiverWorker.new( + def incoming_failed_signature_ap_doc(%{ + method: method, + params: params, + req_headers: req_headers, + request_path: request_path, + query_string: query_string + }) do + SignatureRetryWorker.new( %{ - "op" => "incoming_ap_doc", + "op" => "incoming_failed_signature_ap_doc", + "method" => method, "req_headers" => req_headers, "params" => params, + "request_path" => request_path, + "query_string" => query_string, "timeout" => :timer.seconds(20) }, priority: 2 diff --git a/lib/pleroma/workers/receiver_worker.ex b/lib/pleroma/workers/receiver_worker.ex index 507e099c2..3afbe138d 100644 --- a/lib/pleroma/workers/receiver_worker.ex +++ b/lib/pleroma/workers/receiver_worker.ex @@ -4,55 +4,36 @@ defmodule Pleroma.Workers.ReceiverWorker do alias Pleroma.Instances - alias Pleroma.Signature - alias Pleroma.User alias Pleroma.Web.Federator - alias Pleroma.Web.Plugs.MappedSignatureToIdentityPlug + alias Pleroma.Workers.SignatureRetryWorker use Oban.Worker, queue: :federator_incoming, max_attempts: 5, unique: [period: :infinity] @impl true - - def perform(%Job{ - args: %{ - "op" => "incoming_ap_doc", - "method" => method, - "params" => params, - "req_headers" => req_headers, - "request_path" => request_path, - "query_string" => query_string - } - }) do - # Oban's serialization converts our tuple headers to lists. - # Revert it for the signature validation. - req_headers = Enum.into(req_headers, [], &List.to_tuple(&1)) - - conn_data = %Plug.Conn{ - assigns: %{valid_signature: true}, - method: method, - params: params, - req_headers: req_headers, - request_path: request_path, - query_string: query_string - } - - with {:ok, %User{}} <- User.get_or_fetch_by_ap_id(conn_data.params["actor"]), - {:ok, _public_key} <- Signature.refetch_public_key(conn_data), - {:signature, true} <- {:signature, Signature.validate_signature(conn_data)}, - {:same_actor, true} <- {:same_actor, validate_same_actor(conn_data)}, - {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do - unless Instances.reachable?(params["actor"]) do - domain = URI.parse(params["actor"]).host - Oban.insert(Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain})) - end - - {:ok, res} + def perform(%Job{args: %{"op" => "incoming_ap_doc", "params" => params} = args} = job) do + if signature_retry_job?(args) do + perform_signature_retry(job) else - e -> process_errors(e) + perform_incoming(params) end end - def perform(%Job{args: %{"op" => "incoming_ap_doc", "params" => params}}) do + def perform(%Job{args: %{"op" => "incoming_ap_doc"} = args} = job) do + if signature_retry_job?(args) do + perform_signature_retry(job) + else + process_errors(:missing_incoming_ap_doc_params) + end + end + + defp perform_signature_retry(%Job{args: args} = job) do + SignatureRetryWorker.perform(%Job{ + job + | args: Map.put(args, "op", "incoming_failed_signature_ap_doc") + }) + end + + defp perform_incoming(params) do with {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do unless Instances.reachable?(params["actor"]) do domain = URI.parse(params["actor"]).host @@ -65,21 +46,15 @@ defmodule Pleroma.Workers.ReceiverWorker do end end + defp signature_retry_job?(args) do + Enum.any?(~w(method req_headers request_path query_string), &Map.has_key?(args, &1)) + end + @impl true def timeout(%_{args: %{"timeout" => timeout}}), do: timeout def timeout(_job), do: :timer.seconds(5) - defp validate_same_actor(conn_data) do - case MappedSignatureToIdentityPlug.call(conn_data, []) do - %Plug.Conn{assigns: %{valid_signature: true}} -> - true - - _ -> - false - end - end - defp process_errors({:error, {:error, _} = error}), do: process_errors(error) defp process_errors(errors) do @@ -103,6 +78,7 @@ defmodule Pleroma.Workers.ReceiverWorker do {:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed} # Unclear if this can be reached {:error, {:side_effects, {:error, :no_object_actor}} = reason} -> {:cancel, reason} + :missing_incoming_ap_doc_params -> {:cancel, :missing_incoming_ap_doc_params} # Catchall {:error, _} = e -> e e -> {:error, e} diff --git a/lib/pleroma/workers/signature_retry_worker.ex b/lib/pleroma/workers/signature_retry_worker.ex new file mode 100644 index 000000000..56673a514 --- /dev/null +++ b/lib/pleroma/workers/signature_retry_worker.ex @@ -0,0 +1,144 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.SignatureRetryWorker do + alias Pleroma.Instances + alias Pleroma.Signature + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Utils + alias Pleroma.Web.Federator + alias Pleroma.Web.Plugs.MappedSignatureToIdentityPlug + + use Oban.Worker, queue: :federator_incoming, max_attempts: 5, unique: [period: :infinity] + + @impl true + def perform(%Job{ + args: %{ + "op" => "incoming_failed_signature_ap_doc", + "method" => method, + "params" => params, + "req_headers" => req_headers, + "request_path" => request_path, + "query_string" => query_string + } + }) + when is_binary(method) and is_map(params) and is_list(req_headers) and + is_binary(request_path) and is_binary(query_string) do + with {:ok, req_headers} <- normalize_req_headers(req_headers), + conn_data = %Plug.Conn{ + assigns: %{valid_signature: true}, + method: method, + params: params, + req_headers: req_headers, + request_path: request_path, + query_string: query_string + }, + actor_id = Utils.get_ap_id(params["actor"]), + {:signature_actor, {:ok, signature_actor_id}} <- + {:signature_actor, signature_actor_id(conn_data)}, + {:same_actor, true} <- {:same_actor, signature_actor_id == actor_id}, + {:ok, %User{}} <- User.get_or_fetch_by_ap_id(actor_id), + {:ok, _public_key} <- Signature.refetch_public_key(conn_data), + {:signature, true} <- {:signature, validate_signature(conn_data)}, + {:same_actor, true} <- {:same_actor, validate_same_actor(conn_data)}, + {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do + unless Instances.reachable?(params["actor"]) do + domain = URI.parse(params["actor"]).host + Oban.insert(Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain})) + end + + {:ok, res} + else + e -> process_errors(e) + end + end + + def perform(%Job{args: %{"op" => "incoming_failed_signature_ap_doc"}}) do + process_errors(:missing_signature_retry_metadata) + end + + def perform(%Job{}), do: process_errors(:missing_signature_retry_metadata) + + @impl true + def timeout(%_{args: %{"timeout" => timeout}}), do: timeout + + def timeout(_job), do: :timer.seconds(5) + + defp normalize_req_headers(req_headers) do + req_headers + |> Enum.reduce_while({:ok, []}, fn + {key, value}, {:ok, acc} when is_binary(key) and is_binary(value) -> + {:cont, {:ok, [{key, value} | acc]}} + + [key, value], {:ok, acc} when is_binary(key) and is_binary(value) -> + {:cont, {:ok, [{key, value} | acc]}} + + _, _ -> + {:halt, {:error, :invalid_signature_retry_metadata}} + end) + |> case do + {:ok, headers} -> {:ok, Enum.reverse(headers)} + error -> error + end + end + + defp validate_same_actor(conn_data) do + case MappedSignatureToIdentityPlug.call(conn_data, []) do + %Plug.Conn{assigns: %{valid_signature: true}} -> + true + + _ -> + false + end + end + + defp validate_signature(conn_data) do + Signature.validate_signature(conn_data) + rescue + _ -> false + catch + _, _ -> false + end + + defp signature_actor_id(conn_data) do + Signature.get_actor_id(conn_data) + rescue + _ -> {:error, :invalid_signature} + catch + _, _ -> {:error, :invalid_signature} + end + + defp process_errors({:error, {:error, _} = error}), do: process_errors(error) + + defp process_errors(errors) do + case errors do + # User fetch failures + {:error, :not_found} = reason -> {:cancel, reason} + {:error, :forbidden} = reason -> {:cancel, reason} + # Inactive user + {:error, {:user_active, false} = reason} -> {:cancel, reason} + # Validator will error and return a changeset error + # e.g., duplicate activities or if the object was deleted + {:error, {:validate, {:error, _changeset} = reason}} -> {:cancel, reason} + # Duplicate detection during Normalization + {:error, :already_present} -> {:cancel, :already_present} + # MRFs will return a reject + {:error, {:reject, _} = reason} -> {:cancel, reason} + # HTTP Sigs + {:signature_actor, {:error, _}} -> {:cancel, :invalid_signature} + {:signature, false} -> {:cancel, :invalid_signature} + {:same_actor, false} -> {:cancel, :actor_signature_mismatch} + # Origin / URL validation failed somewhere possibly due to spoofing + {:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed} + # Unclear if this can be reached + {:error, {:side_effects, {:error, :no_object_actor}} = reason} -> {:cancel, reason} + # Fail closed if the retry cannot reconstruct the original request. + :missing_signature_retry_metadata -> {:cancel, :missing_signature_retry_metadata} + {:error, :invalid_signature_retry_metadata} -> {:cancel, :invalid_signature_retry_metadata} + # Catchall + {:error, _} = e -> e + e -> {:error, e} + end + end +end diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index 2be5ca6df..62c1dd830 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -19,6 +19,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do alias Pleroma.Web.CommonAPI alias Pleroma.Web.Endpoint alias Pleroma.Workers.ReceiverWorker + alias Pleroma.Workers.SignatureRetryWorker import Pleroma.Factory @@ -36,6 +37,24 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do setup do: clear_config([:instance, :federating], true) + defp expect_signature_retry_from(%User{} = signer) do + signer_json = UserView.render("user.json", %{user: signer}) |> Map.delete("featured") + + Tesla.Mock.mock(fn + %{url: url} when url == signer.ap_id -> + %Tesla.Env{ + status: 200, + body: Jason.encode!(signer_json), + headers: HttpRequestMock.activitypub_object_headers() + } + + env -> + apply(HttpRequestMock, :request, [env]) + end) + + Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end) + end + describe "/relay" do setup do: clear_config([:instance, :allow_relay]) @@ -727,6 +746,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do end test "does not create a forged post after failed signature retry", %{conn: conn} do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") object_id = "https://two.com/objects/inbox-forged-note" @@ -750,6 +770,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do } } + expect_signature_retry_from(alice) + conn = conn |> assign(:valid_signature, false) @@ -760,13 +782,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert "ok" == json_response(conn, 200) assert [{:cancel, :actor_signature_mismatch}] = - ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker)) refute Activity.get_by_ap_id(data["id"]) refute Object.get_by_ap_id(object_id) end test "does not create a forged like after failed signature retry", %{conn: conn} do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") note = insert(:note) @@ -779,6 +802,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do "object" => note.data["id"] } + expect_signature_retry_from(alice) + conn = conn |> assign(:valid_signature, false) @@ -789,7 +814,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert "ok" == json_response(conn, 200) assert [{:cancel, :actor_signature_mismatch}] = - ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker)) refute Activity.get_by_ap_id(data["id"]) end @@ -820,7 +845,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do } } - Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end) + expect_signature_retry_from(alice) conn = conn @@ -837,7 +862,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert "ok" == json_response(conn, 200) assert [{:cancel, :actor_signature_mismatch}] = - ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker)) refute Activity.get_by_ap_id(data["id"]) refute Object.get_by_ap_id(object_id) @@ -858,7 +883,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do "object" => note.data["id"] } - Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end) + expect_signature_retry_from(alice) conn = conn @@ -875,7 +900,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert "ok" == json_response(conn, 200) assert [{:cancel, :actor_signature_mismatch}] = - ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker)) refute Activity.get_by_ap_id(data["id"]) end diff --git a/test/pleroma/workers/receiver_worker_test.exs b/test/pleroma/workers/receiver_worker_test.exs index ec630eb7b..67a3f902e 100644 --- a/test/pleroma/workers/receiver_worker_test.exs +++ b/test/pleroma/workers/receiver_worker_test.exs @@ -11,11 +11,9 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do alias Pleroma.User alias Pleroma.Web.CommonAPI - alias Pleroma.Web.ActivityPub.UserView - alias Pleroma.Web.Federator alias Pleroma.Workers.ReceiverWorker - defp mismatched_signature_headers do + defp signature_headers_for(%User{} = signer) do [ {"host", "local.test"}, {"date", "Thu, 25 Jul 2024 13:33:31 GMT"}, @@ -23,39 +21,15 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do {"content-type", "application/activity+json"}, { "signature", - "keyId=\"https://one.com/users/alice#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" + "keyId=\"#{signer.ap_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" } ] end - defp expect_signature_from(%User{} = signer) do - signer_json = UserView.render("user.json", %{user: signer}) |> Map.delete("featured") - - Tesla.Mock.mock(fn - %{url: url} when url == signer.ap_id -> - %Tesla.Env{ - status: 200, - body: Jason.encode!(signer_json), - headers: HttpRequestMock.activitypub_object_headers() - } - end) - - Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end) - end - - defp assert_mismatched_signature_cancelled(params, signer) do - expect_signature_from(signer) - - assert {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: mismatched_signature_headers(), - request_path: "/inbox", - params: params, - query_string: "" - }) - - assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) + defp perform_incoming(params) do + ReceiverWorker.perform(%Oban.Job{ + args: %{"op" => "incoming_ap_doc", "params" => params} + }) end test "it does not retry MRF reject" do @@ -125,16 +99,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do insert(:note_activity).data |> Map.put("actor", "https://springfield.social/users/bart") - {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: [], - request_path: "/inbox", - params: params, - query_string: "" - }) - - assert {:cancel, {:error, :forbidden}} = ReceiverWorker.perform(oban_job) + assert {:cancel, {:error, :forbidden}} = perform_incoming(params) end test "when request returns a 404" do @@ -142,16 +107,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do insert(:note_activity).data |> Map.put("actor", "https://springfield.social/users/troymcclure") - {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: [], - request_path: "/inbox", - params: params, - query_string: "" - }) - - assert {:cancel, {:error, :not_found}} = ReceiverWorker.perform(oban_job) + assert {:cancel, {:error, :not_found}} = perform_incoming(params) end test "when request returns a 410" do @@ -159,16 +115,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do insert(:note_activity).data |> Map.put("actor", "https://springfield.social/users/hankscorpio") - {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: [], - request_path: "/inbox", - params: params, - query_string: "" - }) - - assert {:cancel, {:error, :not_found}} = ReceiverWorker.perform(oban_job) + assert {:cancel, {:error, :not_found}} = perform_incoming(params) end test "when user account is disabled" do @@ -182,86 +129,16 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do {:ok, %User{}} = User.set_activation(user, false) - {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: [], - request_path: "/inbox", - params: params, - query_string: "" - }) - - assert {:cancel, {:user_active, false}} = ReceiverWorker.perform(oban_job) + assert {:cancel, {:user_active, false}} = perform_incoming(params) end end - test "it can validate the signature" do - Tesla.Mock.mock(fn - %{url: "https://phpc.social/users/denniskoch"} -> - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/denniskoch.json"), - headers: [{"content-type", "application/activity+json"}] - } - - %{url: "https://phpc.social/users/denniskoch/collections/featured"} -> - %Tesla.Env{ - status: 200, - headers: [{"content-type", "application/activity+json"}], - body: - File.read!("test/fixtures/users_mock/masto_featured.json") - |> String.replace("{{domain}}", "phpc.social") - |> String.replace("{{nickname}}", "denniskoch") - } - end) - - params = - File.read!("test/fixtures/receiver_worker_signature_activity.json") |> Jason.decode!() - - req_headers = [ - ["accept-encoding", "gzip"], - ["content-length", "5184"], - ["content-type", "application/activity+json"], - ["date", "Thu, 25 Jul 2024 13:33:31 GMT"], - ["digest", "SHA-256=ouge/6HP2/QryG6F3JNtZ6vzs/hSwMk67xdxe87eH7A="], - ["host", "bikeshed.party"], - [ - "signature", - "keyId=\"https://mastodon.social/users/bastianallgeier#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"ymE3vn5Iw50N6ukSp8oIuXJB5SBjGAGjBasdTDvn+ahZIzq2SIJfmVCsIIzyqIROnhWyQoTbavTclVojEqdaeOx+Ejz2wBnRBmhz5oemJLk4RnnCH0lwMWyzeY98YAvxi9Rq57Gojuv/1lBqyGa+rDzynyJpAMyFk17XIZpjMKuTNMCbjMDy76ILHqArykAIL/v1zxkgwxY/+ELzxqMpNqtZ+kQ29znNMUBB3eVZ/mNAHAz6o33Y9VKxM2jw+08vtuIZOusXyiHbRiaj2g5HtN2WBUw1MzzfRfHF2/yy7rcipobeoyk5RvP5SyHV3WrIeZ3iyoNfmv33y8fxllF0EA==\"" - ], - [ - "user-agent", - "http.rb/5.2.0 (Mastodon/4.3.0-nightly.2024-07-25; +https://mastodon.social/)" - ] - ] - - {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: req_headers, - request_path: "/inbox", - params: params, - query_string: "" - }) - - assert {:ok, %Pleroma.Activity{}} = ReceiverWorker.perform(oban_job) - end - test "cancels due to origin containment" do params = insert(:note_activity).data |> Map.put("id", "https://notorigindomain.com/activity") - {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: [], - request_path: "/inbox", - params: params, - query_string: "" - }) - - assert {:cancel, :origin_containment_failed} = ReceiverWorker.perform(oban_job) + assert {:cancel, :origin_containment_failed} = perform_incoming(params) end test "canceled due to deleted object" do @@ -277,16 +154,98 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do } end) - {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: [], - request_path: "/inbox", - params: params, - query_string: "" - }) + assert {:cancel, _} = perform_incoming(params) + end - assert {:cancel, _} = ReceiverWorker.perform(oban_job) + test "delegates legacy failed-signature metadata jobs instead of processing them as trusted" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + object_id = "https://two.com/objects/legacy-forged-note" + + create = %{ + "type" => "Create", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/legacy-forged-create", + "context" => "https://two.com/contexts/legacy-forged-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => object_id, + "actor" => bob.ap_id, + "attributedTo" => bob.ap_id, + "context" => "https://two.com/contexts/legacy-forged-create", + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + assert {:cancel, :actor_signature_mismatch} = + ReceiverWorker.perform(%Oban.Job{ + args: %{ + "op" => "incoming_ap_doc", + "method" => "POST", + "params" => create, + "req_headers" => signature_headers_for(alice), + "request_path" => "/inbox", + "query_string" => "" + } + }) + + refute Pleroma.Activity.get_by_ap_id(create["id"]) + refute Pleroma.Object.get_by_ap_id(object_id) + end + + test "fails closed for the old persisted failed-signature job shape" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + object_id = "https://two.com/objects/old-shape-forged-note" + + create = %{ + "type" => "Create", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/old-shape-forged-create", + "context" => "https://two.com/contexts/old-shape-forged-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => object_id, + "actor" => bob.ap_id, + "attributedTo" => bob.ap_id, + "context" => "https://two.com/contexts/old-shape-forged-create", + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + assert {:cancel, :missing_signature_retry_metadata} = + ReceiverWorker.perform(%Oban.Job{ + args: %{ + "op" => "incoming_ap_doc", + "params" => create, + "req_headers" => signature_headers_for(alice), + "timeout" => 20_000 + } + }) + + refute Pleroma.Activity.get_by_ap_id(create["id"]) + refute Pleroma.Object.get_by_ap_id(object_id) + end + + test "fails closed for malformed legacy metadata jobs without params" do + assert {:cancel, :missing_signature_retry_metadata} = + ReceiverWorker.perform(%Oban.Job{ + args: %{ + "op" => "incoming_ap_doc", + "req_headers" => [], + "timeout" => 20_000 + } + }) end describe "Server reachability:" do @@ -346,262 +305,4 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do end end end - - test "cancels when signature actor does not match payload actor" do - alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - - note = - insert(:note, - user: bob, - object_local: false, - data: %{"id" => "https://two.com/objects/malicious-update-note"} - ) - - update = %{ - "type" => "Update", - "actor" => bob.ap_id, - "id" => "https://two.com/activities/malicious-update", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [], - "object" => note.data - } - - req_headers = [ - ["host", "local.test"], - ["date", "Thu, 25 Jul 2024 13:33:31 GMT"], - ["digest", "SHA-256=fake-digest"], - ["content-type", "application/activity+json"], - [ - "signature", - "keyId=\"https://one.com/users/alice#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" - ] - ] - - oban_job = %Oban.Job{ - args: %{ - "op" => "incoming_ap_doc", - "method" => "POST", - "params" => update, - "req_headers" => req_headers, - "request_path" => "/inbox", - "query_string" => "" - } - } - - expect_signature_from(alice) - - assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) - end - - test "Federator preserves request metadata needed for ReceiverWorker signature checks" do - params = insert(:note_activity).data - - req_headers = [ - {"host", "local.test"}, - {"signature", "keyId=\"https://one.com/users/alice#main-key\""} - ] - - assert {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: req_headers, - request_path: "/inbox", - params: params, - query_string: "foo=bar" - }) - - assert %{ - "method" => "POST", - "req_headers" => ^req_headers, - "request_path" => "/inbox", - "params" => ^params, - "query_string" => "foo=bar" - } = oban_job.args - end - - test "cancels signature actor mismatch through Federator-created jobs" do - alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - - note = - insert(:note, - user: bob, - object_local: false, - data: %{"id" => "https://two.com/objects/federator-malicious-note"} - ) - - update = %{ - "type" => "Update", - "actor" => bob.ap_id, - "id" => "https://two.com/activities/federator-malicious-update", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [], - "object" => note.data - } - - assert_mismatched_signature_cancelled(update, alice) - end - - test "cancels signature actor mismatch before processing a forged Create" do - alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - - create = %{ - "type" => "Create", - "actor" => bob.ap_id, - "id" => "https://two.com/activities/forged-create", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [], - "object" => %{ - "type" => "Note", - "id" => "https://two.com/objects/forged-note", - "actor" => bob.ap_id, - "attributedTo" => bob.ap_id, - "content" => "forged post", - "published" => "2024-07-25T13:33:31Z", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [] - } - } - - assert_mismatched_signature_cancelled(create, alice) - end - - test "cancels signature actor mismatch before actually creating a forged post" do - alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - - object_id = "https://two.com/objects/actually-forged-note" - - create = %{ - "type" => "Create", - "actor" => bob.ap_id, - "id" => "https://two.com/activities/actually-forged-create", - "context" => "https://two.com/contexts/actually-forged-create", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [], - "object" => %{ - "type" => "Note", - "id" => object_id, - "actor" => bob.ap_id, - "attributedTo" => bob.ap_id, - "context" => "https://two.com/contexts/actually-forged-create", - "content" => "forged post", - "published" => "2024-07-25T13:33:31Z", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [] - } - } - - expect_signature_from(alice) - - assert {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: mismatched_signature_headers(), - request_path: "/inbox", - params: create, - query_string: "" - }) - - assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) - refute Pleroma.Object.get_by_ap_id(object_id) - end - - test "cancels signature actor mismatch before processing a forged Like" do - alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - note = insert(:note) - - like = %{ - "type" => "Like", - "actor" => bob.ap_id, - "id" => "https://two.com/activities/forged-like", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [], - "object" => note.data["id"] - } - - assert_mismatched_signature_cancelled(like, alice) - end - - test "cancels signature actor mismatch before actually creating a forged Like" do - alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - note = insert(:note) - - like = %{ - "type" => "Like", - "actor" => bob.ap_id, - "id" => "https://two.com/activities/actually-forged-like", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [], - "object" => note.data["id"] - } - - expect_signature_from(alice) - - assert {:ok, oban_job} = - Federator.incoming_ap_doc(%{ - method: "POST", - req_headers: mismatched_signature_headers(), - request_path: "/inbox", - params: like, - query_string: "" - }) - - assert {:cancel, :actor_signature_mismatch} = ReceiverWorker.perform(oban_job) - refute Pleroma.Activity.get_by_ap_id(like["id"]) - end - - test "cancels signature actor mismatch before processing a forged Announce" do - alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - note = insert(:note) - - announce = %{ - "type" => "Announce", - "actor" => bob.ap_id, - "id" => "https://two.com/activities/forged-announce", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [], - "object" => note.data["id"] - } - - assert_mismatched_signature_cancelled(announce, alice) - end - - test "cancels signature actor mismatch before processing a forged Follow" do - alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - followed = insert(:user) - - follow = %{ - "type" => "Follow", - "actor" => bob.ap_id, - "id" => "https://two.com/activities/forged-follow", - "to" => [followed.ap_id], - "cc" => [], - "object" => followed.ap_id - } - - assert_mismatched_signature_cancelled(follow, alice) - end - - test "cancels signature actor mismatch before processing a forged Undo" do - alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") - bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") - - undo = %{ - "type" => "Undo", - "actor" => bob.ap_id, - "id" => "https://two.com/activities/forged-undo", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [], - "object" => "https://two.com/activities/existing-bob-activity" - } - - assert_mismatched_signature_cancelled(undo, alice) - end end diff --git a/test/pleroma/workers/signature_retry_worker_test.exs b/test/pleroma/workers/signature_retry_worker_test.exs new file mode 100644 index 000000000..02706ebad --- /dev/null +++ b/test/pleroma/workers/signature_retry_worker_test.exs @@ -0,0 +1,469 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.SignatureRetryWorkerTest do + use Pleroma.DataCase, async: false + use Oban.Testing, repo: Pleroma.Repo + + import Pleroma.Factory + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Signature + alias Pleroma.User + alias Pleroma.Web.ActivityPub.UserView + alias Pleroma.Web.Federator + alias Pleroma.Workers.SignatureRetryWorker + + defp signature_headers_for(%User{} = signer) do + [ + {"host", "local.test"}, + {"date", "Thu, 25 Jul 2024 13:33:31 GMT"}, + {"digest", "SHA-256=fake-digest"}, + {"content-type", "application/activity+json"}, + { + "signature", + "keyId=\"#{signer.ap_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\"" + } + ] + end + + defp stub_actor_fetch(%User{} = signer) do + signer_json = UserView.render("user.json", %{user: signer}) |> Map.delete("featured") + + Tesla.Mock.mock(fn + %{url: url} when url == signer.ap_id -> + %Tesla.Env{ + status: 200, + body: Jason.encode!(signer_json), + headers: HttpRequestMock.activitypub_object_headers() + } + end) + end + + defp expect_signature_from(%User{} = signer) do + stub_actor_fetch(signer) + Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end) + end + + defp enqueue_failed_signature(params, signer) do + Federator.incoming_failed_signature_ap_doc(%{ + method: "POST", + req_headers: signature_headers_for(signer), + request_path: "/inbox", + params: params, + query_string: "" + }) + end + + defp failed_signature_job(params, req_headers, opts \\ []) do + %Oban.Job{ + args: %{ + "op" => "incoming_failed_signature_ap_doc", + "method" => Keyword.get(opts, :method, "POST"), + "req_headers" => req_headers, + "request_path" => Keyword.get(opts, :request_path, "/inbox"), + "params" => params, + "query_string" => Keyword.get(opts, :query_string, "") + } + } + end + + defp assert_mismatched_signature_cancelled(params, signer) do + assert {:ok, oban_job} = enqueue_failed_signature(params, signer) + + assert {:cancel, :actor_signature_mismatch} = SignatureRetryWorker.perform(oban_job) + end + + test "Federator preserves request metadata for failed-signature retry jobs" do + params = insert(:note_activity).data + + req_headers = [ + {"host", "local.test"}, + {"signature", "keyId=\"https://one.com/users/alice#main-key\""} + ] + + assert {:ok, oban_job} = + Federator.incoming_failed_signature_ap_doc(%{ + method: "POST", + req_headers: req_headers, + request_path: "/inbox", + params: params, + query_string: "foo=bar" + }) + + assert oban_job.worker == "Pleroma.Workers.SignatureRetryWorker" + + assert %{ + "op" => "incoming_failed_signature_ap_doc", + "method" => "POST", + "req_headers" => ^req_headers, + "request_path" => "/inbox", + "params" => ^params, + "query_string" => "foo=bar" + } = oban_job.args + end + + test "cancels retry jobs without request metadata" do + params = insert(:note_activity).data + + assert {:cancel, :missing_signature_retry_metadata} = + SignatureRetryWorker.perform(%Oban.Job{ + args: %{"op" => "incoming_failed_signature_ap_doc", "params" => params} + }) + end + + test "cancels retry jobs with malformed serialized request headers" do + params = insert(:note_activity).data + + assert {:cancel, :invalid_signature_retry_metadata} = + SignatureRetryWorker.perform(failed_signature_job(params, [["signature"]])) + end + + test "cancels retry jobs without a signature header" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + params = insert(:note_activity, user: alice).data + + assert {:cancel, :invalid_signature} = + SignatureRetryWorker.perform(failed_signature_job(params, [{"host", "local.test"}])) + end + + test "cancels missing signature before fetching an unavailable payload actor" do + params = + insert(:note_activity).data + |> Map.put("actor", "https://unavailable.example/users/bob") + + assert {:cancel, :invalid_signature} = + SignatureRetryWorker.perform(failed_signature_job(params, [{"host", "local.test"}])) + end + + test "cancels signer mismatch before fetching an unavailable payload actor" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + + params = + insert(:note_activity).data + |> Map.put("actor", "https://unavailable.example/users/bob") + + assert {:cancel, :actor_signature_mismatch} = + SignatureRetryWorker.perform( + failed_signature_job(params, signature_headers_for(alice)) + ) + end + + test "cancels retry jobs with a signature header without keyId" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + params = insert(:note_activity, user: alice).data + + req_headers = [{"signature", "algorithm=\"rsa-sha256\",signature=\"fake-signature\""}] + + assert {:cancel, :invalid_signature} = + SignatureRetryWorker.perform(failed_signature_job(params, req_headers)) + end + + test "cancels retry jobs with an unparsable signature keyId" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + params = insert(:note_activity, user: alice).data + req_headers = [{"signature", "keyId=\"not an activitypub id\",signature=\"fake-signature\""}] + + assert {:cancel, :invalid_signature} = + SignatureRetryWorker.perform(failed_signature_job(params, req_headers)) + end + + test "cancels when the refetched key still cannot validate the signature" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + + create = %{ + "type" => "Create", + "actor" => alice.ap_id, + "id" => "https://one.com/activities/invalid-signature-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => "https://one.com/objects/invalid-signature-note", + "actor" => alice.ap_id, + "attributedTo" => alice.ap_id, + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + stub_actor_fetch(alice) + + assert {:ok, oban_job} = enqueue_failed_signature(create, alice) + assert {:cancel, :invalid_signature} = SignatureRetryWorker.perform(oban_job) + refute Activity.get_by_ap_id(create["id"]) + end + + test "processes the activity after refetching a valid matching signature" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + + create = %{ + "type" => "Create", + "actor" => alice.ap_id, + "id" => "https://one.com/activities/valid-signature-create", + "context" => "https://one.com/contexts/valid-signature-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => "https://one.com/objects/valid-signature-note", + "actor" => alice.ap_id, + "attributedTo" => alice.ap_id, + "context" => "https://one.com/contexts/valid-signature-create", + "content" => "valid post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + expect_signature_from(alice) + + assert {:ok, oban_job} = enqueue_failed_signature(create, alice) + assert {:ok, %Activity{}} = SignatureRetryWorker.perform(oban_job) + assert Activity.get_by_ap_id(create["id"]) + end + + test "processes the activity when a real signature validates with a query string" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + + create = %{ + "type" => "Create", + "actor" => alice.ap_id, + "id" => "https://one.com/activities/valid-query-signature-create", + "context" => "https://one.com/contexts/valid-query-signature-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => "https://one.com/objects/valid-query-signature-note", + "actor" => alice.ap_id, + "attributedTo" => alice.ap_id, + "context" => "https://one.com/contexts/valid-query-signature-create", + "content" => "valid signed post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + stub_actor_fetch(alice) + + date = "Thu, 25 Jul 2024 13:33:31 GMT" + digest = "SHA-256=fake-digest" + + signature = + Signature.sign(alice, %{ + "(request-target)" => "post /inbox?foo=bar", + "content-type" => "application/activity+json", + date: date, + digest: digest, + host: "local.test" + }) + + req_headers = [ + ["host", "local.test"], + ["date", date], + ["digest", digest], + ["content-type", "application/activity+json"], + ["signature", signature] + ] + + assert {:ok, %Activity{}} = + SignatureRetryWorker.perform( + failed_signature_job(create, req_headers, query_string: "foo=bar") + ) + + assert Activity.get_by_ap_id(create["id"]) + end + + test "cancels when signature actor does not match payload actor" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + + note = + insert(:note, + user: bob, + object_local: false, + data: %{"id" => "https://two.com/objects/malicious-update-note"} + ) + + update = %{ + "type" => "Update", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/malicious-update", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data + } + + assert_mismatched_signature_cancelled(update, alice) + end + + test "cancels signature actor mismatch through Federator-created jobs" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + + note = + insert(:note, + user: bob, + object_local: false, + data: %{"id" => "https://two.com/objects/federator-malicious-note"} + ) + + update = %{ + "type" => "Update", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/federator-malicious-update", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data + } + + assert_mismatched_signature_cancelled(update, alice) + end + + test "cancels signature actor mismatch before processing a forged Create" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + + create = %{ + "type" => "Create", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/forged-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => "https://two.com/objects/forged-note", + "actor" => bob.ap_id, + "attributedTo" => bob.ap_id, + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + assert_mismatched_signature_cancelled(create, alice) + end + + test "cancels signature actor mismatch before actually creating a forged post" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + + object_id = "https://two.com/objects/actually-forged-note" + + create = %{ + "type" => "Create", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/actually-forged-create", + "context" => "https://two.com/contexts/actually-forged-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => object_id, + "actor" => bob.ap_id, + "attributedTo" => bob.ap_id, + "context" => "https://two.com/contexts/actually-forged-create", + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + assert_mismatched_signature_cancelled(create, alice) + refute Object.get_by_ap_id(object_id) + end + + test "cancels signature actor mismatch before processing a forged Like" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + note = insert(:note) + + like = %{ + "type" => "Like", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/forged-like", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data["id"] + } + + assert_mismatched_signature_cancelled(like, alice) + end + + test "cancels signature actor mismatch before actually creating a forged Like" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + note = insert(:note) + + like = %{ + "type" => "Like", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/actually-forged-like", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data["id"] + } + + assert_mismatched_signature_cancelled(like, alice) + refute Activity.get_by_ap_id(like["id"]) + end + + test "cancels signature actor mismatch before processing a forged Announce" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + note = insert(:note) + + announce = %{ + "type" => "Announce", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/forged-announce", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => note.data["id"] + } + + assert_mismatched_signature_cancelled(announce, alice) + end + + test "cancels signature actor mismatch before processing a forged Follow" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + followed = insert(:user) + + follow = %{ + "type" => "Follow", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/forged-follow", + "to" => [followed.ap_id], + "cc" => [], + "object" => followed.ap_id + } + + assert_mismatched_signature_cancelled(follow, alice) + end + + test "cancels signature actor mismatch before processing a forged Undo" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + + undo = %{ + "type" => "Undo", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/forged-undo", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => "https://two.com/activities/existing-bob-activity" + } + + assert_mismatched_signature_cancelled(undo, alice) + end +end From 4337e0eb1b40942c7860b9d32e5ecf1ddafdc0ba Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Fri, 1 May 2026 12:33:26 +0400 Subject: [PATCH 097/127] Fail closed on unresolved signed payloads Reject unknown remote Update targets and invalidate signed payloads when their signer identity cannot be mapped, avoiding crashes and fail-open signature state. --- .../object_validators/update_validator.ex | 4 ++++ .../plugs/mapped_signature_to_identity_plug.ex | 4 ++-- .../object_validators/update_handling_test.exs | 17 +++++++++++++++++ .../mapped_signature_to_identity_plug_test.exs | 12 ++++++++++++ 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex index 5586b74cf..ad3c0e3e2 100644 --- a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex @@ -101,6 +101,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator do |> add_error(:object, "Can't be updated by this actor") end + nil -> + cng + |> add_error(:object, "Can't be updated by this actor") + true -> cng |> add_error(:object, "Update is neither for Object or Actor") diff --git a/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex b/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex index c6d531086..a688b1780 100644 --- a/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex +++ b/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex @@ -32,8 +32,8 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do # remove me once testsuite uses mapped capabilities instead of what we do now {:user, nil} -> Logger.debug("Failed to map identity from signature (lookup failure)") - Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{actor}") - conn + Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{inspect(actor)}") + assign(conn, :valid_signature, false) end 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 f04f9cc61..9dec315b3 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 @@ -90,6 +90,23 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do refute cng.valid? assert Keyword.has_key?(cng.errors, :object) end + + test "returns an error if the remote update target IRI is unknown" do + remote_user = insert(:user, local: false, ap_id: "https://example.com/users/alice") + + update = %{ + "type" => "Update", + "actor" => remote_user.ap_id, + "id" => "https://example.com/activities/update-unknown-object-iri", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => "https://example.com/objects/unknown-iri" + } + + assert {:error, %Ecto.Changeset{} = cng} = ObjectValidator.validate(update, local: false) + refute cng.valid? + assert Keyword.has_key?(cng.errors, :object) + end end describe "update note" do diff --git a/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs b/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs index 81c6b0c5d..df713762c 100644 --- a/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs +++ b/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs @@ -58,4 +58,16 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlugTest do assert conn.assigns.valid_signature == false refute Map.has_key?(conn.assigns, :user) end + + test "it considers a mapped identity to be invalid when embedded actor identity cannot be found" do + actor = "http://niu.moe/users/rye" + + conn = + build_conn(:post, "/doesntmattter", %{"actor" => %{"id" => actor}}) + |> set_signature(actor) + |> MappedSignatureToIdentityPlug.call(%{}) + + assert conn.assigns.valid_signature == false + refute Map.has_key?(conn.assigns, :user) + end end From 99b614a52e9e804f8e3e642fe9104a18cbcfe3e1 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Fri, 1 May 2026 13:39:59 +0400 Subject: [PATCH 098/127] Add spoofing fixes changelog entry --- changelog.d/activitypub-spoofing.security | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/activitypub-spoofing.security diff --git a/changelog.d/activitypub-spoofing.security b/changelog.d/activitypub-spoofing.security new file mode 100644 index 000000000..3e6baffb6 --- /dev/null +++ b/changelog.d/activitypub-spoofing.security @@ -0,0 +1 @@ +ActivityPub: Fixed failed-signature inbox retry handling and signer identity checks to prevent spoofed remote activities from being processed From a35aa6551e79539f9452455cc54aeed63c9ba8d4 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 2 May 2026 10:39:49 +0400 Subject: [PATCH 099/127] Fix Woodpecker path filters --- .woodpecker/lint.yaml | 4 ++-- .woodpecker/unit-testing-elixir-1.15.yaml | 4 ++-- .woodpecker/unit-testing-elixir-1.18.yaml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index b96d584ee..54de53873 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -1,9 +1,9 @@ when: - event: pull_request - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ] - event: push branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + 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 84046be41..43f30262a 100644 --- a/.woodpecker/unit-testing-elixir-1.15.yaml +++ b/.woodpecker/unit-testing-elixir-1.15.yaml @@ -1,9 +1,9 @@ when: - event: pull_request - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ] - event: push branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + 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 8dcec62fb..1963b36a5 100644 --- a/.woodpecker/unit-testing-elixir-1.18.yaml +++ b/.woodpecker/unit-testing-elixir-1.18.yaml @@ -1,9 +1,9 @@ when: - event: pull_request - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ] - event: push branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ] depends_on: - lint From 3dbc570471862ad1edad323f65215f04fa0b83bf Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 2 May 2026 11:57:04 +0400 Subject: [PATCH 100/127] Woodpecker CI: Publish update-compatible OTP releases --- .woodpecker/docker-combine.yaml | 2 +- .woodpecker/docker.yaml | 2 +- .woodpecker/otp-musl.yaml | 140 +++++++++++++- .woodpecker/otp.yaml | 140 +++++++++++++- rel/files/bin/pleroma_ctl | 23 ++- test/pleroma/pleroma_ctl_test.exs | 293 ++++++++++++++++++++++++++++++ 6 files changed, 578 insertions(+), 22 deletions(-) create mode 100644 test/pleroma/pleroma_ctl_test.exs diff --git a/.woodpecker/docker-combine.yaml b/.woodpecker/docker-combine.yaml index be8583763..161987d64 100644 --- a/.woodpecker/docker-combine.yaml +++ b/.woodpecker/docker-combine.yaml @@ -1,7 +1,7 @@ when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] + path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - event: tag - event: manual branch: stable diff --git a/.woodpecker/docker.yaml b/.woodpecker/docker.yaml index 317cb5fbb..4053be91d 100644 --- a/.woodpecker/docker.yaml +++ b/.woodpecker/docker.yaml @@ -1,7 +1,7 @@ when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] + path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - event: tag - event: manual branch: stable diff --git a/.woodpecker/otp-musl.yaml b/.woodpecker/otp-musl.yaml index c58eee66b..e0fb0fb99 100644 --- a/.woodpecker/otp-musl.yaml +++ b/.woodpecker/otp-musl.yaml @@ -1,7 +1,7 @@ when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ] - event: tag - event: manual branch: stable @@ -9,6 +9,7 @@ when: matrix: platform: - linux/amd64 + - linux/arm - linux/arm64 # This is needed for the when clauses below. @@ -27,6 +28,7 @@ variables: - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} - mix release --path release build_image_amd64: &build_image_amd64 docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 + build_image_arm: &build_image_arm docker.io/arm32v7/elixir:1.17.3-alpine build_image_arm64: &build_image_arm64 docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 artifacts_uploader_image: &artifacts_uploader_image docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 artifacts_uploader_settings: &artifacts_uploader_settings @@ -66,6 +68,32 @@ steps: - <<: *build_cmds - zip -9rq stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip release + otp-develop-arm-musl: + image: *build_image_arm + when: + - evaluate: 'platform == "linux/arm" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + environment: *env + commands: &arm_build + - <<: *build_cmds + - zip -9rq ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm-musl.zip release + + otp-stable-arm-musl: + image: *build_image_arm + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' + environment: *env + commands: *arm_build + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + otp-stable-tag-arm-musl: + image: *build_image_arm + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"' + environment: *env + commands: + - <<: *build_cmds + - zip -9rq stable-${CI_COMMIT_SHA:0:8}-arm-musl.zip release + otp-develop-arm64-musl: image: *build_image_arm64 when: @@ -102,7 +130,20 @@ steps: package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64-musl package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip - file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + + upload-latest-amd64-musl: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64-musl + package_version: latest + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + file_name: pleroma.zip + update: 'true' # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable upload-artifacts-tag-amd64-musl: @@ -114,7 +155,70 @@ steps: package_name: pleroma-otp-stable-amd64-musl package_version: stable-${CI_COMMIT_SHA:0:8}-amd64-musl file_source: ./stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip - file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-latest-tag-amd64-musl: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-amd64-musl + package_version: latest + file_source: ./stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + file_name: pleroma.zip + update: 'true' + + upload-artifacts-arm-musl: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm-musl + package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm-musl + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm-musl.zip + file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm-musl.zip + + upload-latest-arm-musl: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm-musl + package_version: latest + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm-musl.zip + file_name: pleroma.zip + update: 'true' + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-artifacts-tag-arm-musl: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-arm-musl + package_version: stable-${CI_COMMIT_SHA:0:8}-arm-musl + file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm-musl.zip + file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-arm-musl.zip + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-latest-tag-arm-musl: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-arm-musl + package_version: latest + file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm-musl.zip + file_name: pleroma.zip + update: 'true' upload-artifacts-arm64-musl: image: *artifacts_uploader_image @@ -126,7 +230,20 @@ steps: package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64-musl package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip - file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + + upload-latest-arm64-musl: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64-musl + package_version: latest + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + file_name: pleroma.zip + update: 'true' # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable upload-artifacts-tag-arm64-musl: @@ -138,4 +255,17 @@ steps: package_name: pleroma-otp-stable-arm64-musl package_version: stable-${CI_COMMIT_SHA:0:8}-arm64-musl file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip - file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-latest-tag-arm64-musl: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-arm64-musl + package_version: latest + file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + file_name: pleroma.zip + update: 'true' diff --git a/.woodpecker/otp.yaml b/.woodpecker/otp.yaml index c01c2e557..3c8b71814 100644 --- a/.woodpecker/otp.yaml +++ b/.woodpecker/otp.yaml @@ -1,7 +1,7 @@ when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} - path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] + path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ] - event: tag - event: manual branch: stable @@ -9,6 +9,7 @@ when: matrix: platform: - linux/amd64 + - linux/arm - linux/arm64 # This is needed for the when clauses below. @@ -27,6 +28,7 @@ variables: - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH} - mix release --path release build_image_amd64: &build_image_amd64 docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 + build_image_arm: &build_image_arm docker.io/arm32v7/elixir:1.17.3 build_image_arm64: &build_image_arm64 docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 artifacts_uploader_image: &artifacts_uploader_image docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 artifacts_uploader_settings: &artifacts_uploader_settings @@ -67,6 +69,32 @@ steps: - <<: *build_cmds - zip -9rq stable-${CI_COMMIT_SHA:0:8}-amd64.zip release + otp-develop-arm: + image: *build_image_arm + when: + - evaluate: 'platform == "linux/arm" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + environment: *env + commands: &arm_build + - <<: *build_cmds + - zip -9rq ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm.zip release + + otp-stable-arm: + image: *build_image_arm + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' + environment: *env + commands: *arm_build + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + otp-stable-tag-arm: + image: *build_image_arm + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"' + environment: *env + commands: + - <<: *build_cmds + - zip -9rq stable-${CI_COMMIT_SHA:0:8}-arm.zip release + otp-develop-arm64: image: *build_image_arm64 when: @@ -103,7 +131,20 @@ steps: package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64 package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64 file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip - file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip + file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip + + upload-latest-amd64: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64 + package_version: latest + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip + file_name: pleroma.zip + update: 'true' # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable upload-artifacts-tag-amd64: @@ -115,7 +156,70 @@ steps: package_name: pleroma-otp-stable-amd64 package_version: stable-${CI_COMMIT_SHA:0:8}-amd64 file_source: ./stable-${CI_COMMIT_SHA:0:8}-amd64.zip - file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64.zip + file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64.zip + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-latest-tag-amd64: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-amd64 + package_version: latest + file_source: ./stable-${CI_COMMIT_SHA:0:8}-amd64.zip + file_name: pleroma.zip + update: 'true' + + upload-artifacts-arm: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm + package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm.zip + file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm.zip + + upload-latest-arm: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm + package_version: latest + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm.zip + file_name: pleroma.zip + update: 'true' + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-artifacts-tag-arm: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-arm + package_version: stable-${CI_COMMIT_SHA:0:8}-arm + file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm.zip + file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-arm.zip + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-latest-tag-arm: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-arm + package_version: latest + file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm.zip + file_name: pleroma.zip + update: 'true' upload-artifacts-arm64: image: *artifacts_uploader_image @@ -127,7 +231,20 @@ steps: package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64 package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64 file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip - file_name: ./pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip + file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip + + upload-latest-arm64: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64 + package_version: latest + file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip + file_name: pleroma.zip + update: 'true' # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable upload-artifacts-tag-arm64: @@ -139,4 +256,17 @@ steps: package_name: pleroma-otp-stable-arm64 package_version: stable-${CI_COMMIT_SHA:0:8}-arm64 file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm64.zip - file_name: ./pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64.zip + file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64.zip + + # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable + upload-latest-tag-arm64: + image: *artifacts_uploader_image + when: + - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' + settings: + <<: *artifacts_uploader_settings + package_name: pleroma-otp-stable-arm64 + package_version: latest + file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm64.zip + file_name: pleroma.zip + update: 'true' diff --git a/rel/files/bin/pleroma_ctl b/rel/files/bin/pleroma_ctl index 2e114ff50..f5d3f321b 100755 --- a/rel/files/bin/pleroma_ctl +++ b/rel/files/bin/pleroma_ctl @@ -79,18 +79,18 @@ update() { RELEASE_ROOT=$(dirname "$SCRIPTPATH") uri="https://git.pleroma.social" project_name="pleroma" - api_base="${uri}/api/v1/packages/${project_name}/generic" package_base="${uri}/api/packages/${project_name}/generic" - project_branch="${BRANCH:-$(detect_branch)}" - flavour="${FLAVOUR:-$(detect_flavour)}" - # API responds in JSON, optimistically try to make it one object per line - ver=$(curl -s "${api_base}"/pleroma-otp-"${project_branch}"-"${flavour}"/-/latest | tr ',' '\n' | grep '"version":' | cut -d':' -f2 | tr -d '"') - file=$(curl -s "${api_base}"/pleroma-otp-"${project_branch}"-"${flavour}"/"${ver}"/files | tr ',' '\n' | grep '"name":' | cut -d':' -f2 | tr -d '"') - full_uri=${FULL_URI:-"${package_base}"/pleroma-otp-"${project_branch}"-"${flavour}"/"${ver}"/"${file}"} + if [ -n "$FULL_URI" ]; then + full_uri="$FULL_URI" + else + project_branch="${BRANCH:-$(detect_branch)}" + flavour="${FLAVOUR:-$(detect_flavour)}" + full_uri="${package_base}"/pleroma-otp-"${project_branch}"-"${flavour}"/latest/pleroma.zip + fi tmp="${TMP_DIR:-/tmp}" artifact="$tmp/pleroma.zip" echo "Downloading the artifact from ${full_uri} to ${artifact}" - curl "$full_uri" -o "${artifact}" + curl -fL "$full_uri" -o "${artifact}" echo "Unpacking ${artifact} to ${tmp}" unzip -q "$artifact" -d "$tmp" echo "Copying files over to $RELEASE_ROOT" @@ -144,9 +144,12 @@ else # HACK: Script arguments need to be sent as an array to Mix tasks, otherwise they will break (quoted) arguments with whitespace. # Previously it was sent as string, which would get split on whitespace on the task side. - # Escaping does not help including non-POSIX printf %q + # Encode as Elixir binary literals to avoid string escaping and interpolation issues. PREPARED_ARGS="" - for arg in "$@"; do PREPARED_ARGS="$PREPARED_ARGS \"$arg\","; done + for arg in "$@"; do + bytes=$(printf '%s' "$arg" | od -An -v -tu1 | tr -s '[:space:]' ',' | sed 's/^,//; s/,$//') + PREPARED_ARGS="$PREPARED_ARGS <<$bytes>>," + done ACTION="$1" if [ $# -gt 0 ]; then diff --git a/test/pleroma/pleroma_ctl_test.exs b/test/pleroma/pleroma_ctl_test.exs new file mode 100644 index 000000000..d96396399 --- /dev/null +++ b/test/pleroma/pleroma_ctl_test.exs @@ -0,0 +1,293 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.PleromaCtlTest do + use ExUnit.Case, async: false + + @pleroma_ctl Path.expand("../../rel/files/bin/pleroma_ctl", __DIR__) + + setup do + tmp_dir = + Path.join(System.tmp_dir!(), "pleroma_ctl_test_#{System.unique_integer([:positive])}") + + release_root = Path.join(tmp_dir, "release") + bin_dir = Path.join(release_root, "bin") + + File.mkdir_p!(bin_dir) + File.cp!(@pleroma_ctl, Path.join(bin_dir, "pleroma_ctl")) + File.chmod!(Path.join(bin_dir, "pleroma_ctl"), 0o755) + + on_exit(fn -> File.rm_rf!(tmp_dir) end) + + {:ok, tmp_dir: tmp_dir, release_root: release_root, bin_dir: bin_dir} + end + + test "update downloads branch-scoped latest OTP package", %{ + tmp_dir: tmp_dir, + bin_dir: bin_dir, + release_root: release_root + } do + stubs_dir = Path.join(tmp_dir, "stubs") + curl_args_path = Path.join(tmp_dir, "curl.args") + File.mkdir_p!(stubs_dir) + write_update_stubs(stubs_dir, curl_args_path, "unused", "glibc") + + update_tmp_dir = Path.join(tmp_dir, "update_tmp") + File.mkdir_p!(update_tmp_dir) + + {output, status} = + System.cmd( + Path.join(bin_dir, "pleroma_ctl"), + [ + "update", + "--branch", + "develop", + "--flavour", + "amd64", + "--tmp-dir", + update_tmp_dir, + "--no-rm" + ], + env: [{"PATH", stubs_dir <> ":" <> System.get_env("PATH", "")}], + stderr_to_stdout: true + ) + + assert status == 0, output + assert File.exists?(Path.join(release_root, "bin/marker")) + + assert ["-fL", url, "-o", artifact_path] = + curl_args_path + |> File.read!() + |> String.split("\n", trim: true) + + assert url == + "https://git.pleroma.social/api/packages/pleroma/generic/pleroma-otp-develop-amd64/latest/pleroma.zip" + + assert artifact_path == Path.join(update_tmp_dir, "pleroma.zip") + end + + test "update detects stable branch and local flavour", %{ + tmp_dir: tmp_dir, + bin_dir: bin_dir, + release_root: release_root + } do + stubs_dir = Path.join(tmp_dir, "stubs") + curl_args_path = Path.join(tmp_dir, "curl.args") + File.mkdir_p!(stubs_dir) + write_update_stubs(stubs_dir, curl_args_path, "x86_64", "glibc") + write_start_erl_data(release_root, "2.10.0") + + {output, status} = + System.cmd( + Path.join(bin_dir, "pleroma_ctl"), + ["update", "--tmp-dir", create_update_tmp_dir(tmp_dir), "--no-rm"], + env: [{"PATH", stubs_dir <> ":" <> System.get_env("PATH", "")}], + stderr_to_stdout: true + ) + + assert status == 0, output + + assert curl_url(curl_args_path) == + "https://git.pleroma.social/api/packages/pleroma/generic/pleroma-otp-stable-amd64/latest/pleroma.zip" + end + + test "update detects develop branch and musl arm flavour", %{ + tmp_dir: tmp_dir, + bin_dir: bin_dir, + release_root: release_root + } do + stubs_dir = Path.join(tmp_dir, "stubs") + curl_args_path = Path.join(tmp_dir, "curl.args") + File.mkdir_p!(stubs_dir) + write_update_stubs(stubs_dir, curl_args_path, "armv7l", "musl") + write_start_erl_data(release_root, "2.10.0.develop") + + {output, status} = + System.cmd( + Path.join(bin_dir, "pleroma_ctl"), + ["update", "--tmp-dir", create_update_tmp_dir(tmp_dir), "--no-rm"], + env: [{"PATH", stubs_dir <> ":" <> System.get_env("PATH", "")}], + stderr_to_stdout: true + ) + + assert status == 0, output + + assert curl_url(curl_args_path) == + "https://git.pleroma.social/api/packages/pleroma/generic/pleroma-otp-develop-arm-musl/latest/pleroma.zip" + end + + test "update with zip URL bypasses branch and flavour detection", %{ + tmp_dir: tmp_dir, + bin_dir: bin_dir, + release_root: release_root + } do + stubs_dir = Path.join(tmp_dir, "stubs") + curl_args_path = Path.join(tmp_dir, "curl.args") + File.mkdir_p!(stubs_dir) + write_update_stubs(stubs_dir, curl_args_path, "unsupported-arch", "unsupported-libc") + write_start_erl_data(release_root, "2.10.0.custombranch") + + custom_url = "https://example.test/custom.zip" + + {output, status} = + System.cmd( + Path.join(bin_dir, "pleroma_ctl"), + [ + "update", + "--zip-url", + custom_url, + "--tmp-dir", + create_update_tmp_dir(tmp_dir), + "--no-rm" + ], + env: [{"PATH", stubs_dir <> ":" <> System.get_env("PATH", "")}], + stderr_to_stdout: true + ) + + assert status == 0, output + assert curl_url(curl_args_path) == custom_url + end + + test "passes arguments with spaces and Elixir string metacharacters", %{ + tmp_dir: tmp_dir, + bin_dir: bin_dir + } do + capture_path = Path.join(tmp_dir, "captured_args") + eval_path = Path.join(tmp_dir, "pleroma_ctl_eval.exs") + + write_executable(Path.join(bin_dir, "pleroma"), """ + #!/bin/sh + { + printf '%s\n' 'defmodule Pleroma.ReleaseTasks do' + printf '%s\n' ' def run(args), do: File.write!(System.fetch_env!("PLEROMA_CTL_CAPTURE"), :erlang.term_to_binary(args))' + printf '%s\n' 'end' + printf '%s\n' "$2" + } > "$PLEROMA_CTL_EVAL_FILE" + + exec elixir "$PLEROMA_CTL_EVAL_FILE" + """) + + {output, status} = + System.cmd( + Path.join(bin_dir, "pleroma_ctl"), + [ + "user", + "", + "has space", + ~s(has "quote"), + ~s(has \\ backslash), + ~S(#{:not_interpolated}) + ], + env: [ + {"PLEROMA_CTL_CAPTURE", capture_path}, + {"PLEROMA_CTL_EVAL_FILE", eval_path} + ], + stderr_to_stdout: true + ) + + assert status == 0, output + + assert capture_path + |> File.read!() + |> :erlang.binary_to_term() == [ + "user", + "", + "has space", + ~s(has "quote"), + ~s(has \\ backslash), + ~S(#{:not_interpolated}) + ] + end + + defp write_executable(path, contents) do + File.write!(path, contents) + File.chmod!(path, 0o755) + end + + defp write_start_erl_data(release_root, version) do + releases_dir = Path.join(release_root, "releases") + File.mkdir_p!(releases_dir) + File.write!(Path.join(releases_dir, "start_erl.data"), "erts-15.0 #{version}\n") + end + + defp create_update_tmp_dir(tmp_dir) do + update_tmp_dir = Path.join(tmp_dir, "update_tmp") + File.mkdir_p!(update_tmp_dir) + update_tmp_dir + end + + defp write_update_stubs(stubs_dir, curl_args_path, arch, libc) do + write_executable(Path.join(stubs_dir, "curl"), """ + #!/bin/sh + printf '%s\n' "$@" > "#{curl_args_path}" + + while [ $# -gt 0 ]; do + case "$1" in + -o) + artifact="$2" + shift 2 + ;; + *) + shift + ;; + esac + done + + : > "$artifact" + """) + + write_executable(Path.join(stubs_dir, "unzip"), """ + #!/bin/sh + while [ $# -gt 0 ]; do + case "$1" in + -d) + dest="$2" + shift 2 + ;; + *) + shift + ;; + esac + done + + mkdir -p "$dest/release/bin" + printf 'marker' > "$dest/release/bin/marker" + """) + + write_executable(Path.join(stubs_dir, "uname"), """ + #!/bin/sh + printf '%s\n' '#{arch}' + """) + + write_executable(Path.join(stubs_dir, "getconf"), getconf_stub(libc)) + + write_executable(Path.join(stubs_dir, "ldd"), """ + #!/bin/sh + printf '%s\n' 'musl libc (mock)' + """) + end + + defp getconf_stub("glibc") do + """ + #!/bin/sh + printf '%s\n' 'glibc 2.40' + """ + end + + defp getconf_stub(_libc) do + """ + #!/bin/sh + exit 1 + """ + end + + defp curl_url(curl_args_path) do + ["-fL", url, "-o", _artifact_path] = + curl_args_path + |> File.read!() + |> String.split("\n", trim: true) + + url + end +end From 47e6dbfadece58a277280a9910b340ec589e6507 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 2 May 2026 13:38:56 +0400 Subject: [PATCH 101/127] Woodpecker CI: Work around script entrypoint truncation --- .woodpecker/changelog.yaml | 7 +++++++ .woodpecker/lint.yaml | 9 +++++++++ .woodpecker/otp-musl.yaml | 13 +++++++++++++ .woodpecker/otp.yaml | 13 +++++++++++++ .woodpecker/unit-testing-elixir-1.15.yaml | 7 +++++++ .woodpecker/unit-testing-elixir-1.18.yaml | 7 +++++++ 6 files changed, 56 insertions(+) diff --git a/.woodpecker/changelog.yaml b/.woodpecker/changelog.yaml index 4f38ce618..1d65e8e9f 100644 --- a/.woodpecker/changelog.yaml +++ b/.woodpecker/changelog.yaml @@ -1,9 +1,16 @@ when: - event: pull_request +variables: + script_file_entrypoint: &script_file_entrypoint + - /bin/sh + - -c + - 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh' + steps: check-changelog: image: docker.io/alpine:3.23 + entrypoint: *script_file_entrypoint commands: - apk add --no-cache git - sh ./tools/check-changelog diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index b96d584ee..95325d779 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -5,10 +5,17 @@ when: branch: ${CI_REPO_DEFAULT_BRANCH} path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] +variables: + script_file_entrypoint: &script_file_entrypoint + - /bin/sh + - -c + - 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh' + steps: mix-format: image: &elixir-image docker.io/elixir:1.15-alpine + entrypoint: *script_file_entrypoint failure: ignore commands: - | @@ -19,6 +26,7 @@ steps: credo: image: *elixir-image + entrypoint: *script_file_entrypoint failure: ignore environment: MIX_ENV: test @@ -55,6 +63,7 @@ steps: ensure-status: image: *elixir-image + entrypoint: *script_file_entrypoint commands: | if test -f fail.stamp; then echo "One or more previous steps fails. Failing workflow..." diff --git a/.woodpecker/otp-musl.yaml b/.woodpecker/otp-musl.yaml index e0fb0fb99..877860255 100644 --- a/.woodpecker/otp-musl.yaml +++ b/.woodpecker/otp-musl.yaml @@ -18,6 +18,10 @@ labels: platform: ${platform} variables: + script_file_entrypoint: &script_file_entrypoint + - /bin/sh + - -c + - 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh' build_cmds: &build_cmds - apk add git build-base cmake file-dev openssl vips-dev zip - echo "import Config" > config/prod.secret.exs @@ -44,6 +48,7 @@ variables: steps: otp-develop-amd64-musl: image: *build_image_amd64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/amd64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' environment: *env @@ -53,6 +58,7 @@ steps: otp-stable-amd64-musl: image: *build_image_amd64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' environment: *env @@ -61,6 +67,7 @@ steps: # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable otp-stable-tag-amd64-musl: image: *build_image_amd64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' environment: *env @@ -70,6 +77,7 @@ steps: otp-develop-arm-musl: image: *build_image_arm + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' environment: *env @@ -79,6 +87,7 @@ steps: otp-stable-arm-musl: image: *build_image_arm + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' environment: *env @@ -87,6 +96,7 @@ steps: # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable otp-stable-tag-arm-musl: image: *build_image_arm + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"' environment: *env @@ -96,6 +106,7 @@ steps: otp-develop-arm64-musl: image: *build_image_arm64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' environment: *env @@ -105,6 +116,7 @@ steps: otp-stable-arm64-musl: image: *build_image_arm64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' environment: *env @@ -113,6 +125,7 @@ steps: # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable otp-stable-tag-arm64-musl: image: *build_image_arm64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' environment: *env diff --git a/.woodpecker/otp.yaml b/.woodpecker/otp.yaml index 3c8b71814..766e42e03 100644 --- a/.woodpecker/otp.yaml +++ b/.woodpecker/otp.yaml @@ -18,6 +18,10 @@ labels: platform: ${platform} variables: + script_file_entrypoint: &script_file_entrypoint + - /bin/sh + - -c + - 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh' build_cmds: &build_cmds - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip - echo "import Config" > config/prod.secret.exs @@ -45,6 +49,7 @@ variables: steps: otp-develop-amd64: image: *build_image_amd64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/amd64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' environment: *env @@ -54,6 +59,7 @@ steps: otp-stable-amd64: image: *build_image_amd64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' environment: *env @@ -62,6 +68,7 @@ steps: # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable otp-stable-tag-amd64: image: *build_image_amd64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"' environment: *env @@ -71,6 +78,7 @@ steps: otp-develop-arm: image: *build_image_arm + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' environment: *env @@ -80,6 +88,7 @@ steps: otp-stable-arm: image: *build_image_arm + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' environment: *env @@ -88,6 +97,7 @@ steps: # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable otp-stable-tag-arm: image: *build_image_arm + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"' environment: *env @@ -97,6 +107,7 @@ steps: otp-develop-arm64: image: *build_image_arm64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"' environment: *env @@ -106,6 +117,7 @@ steps: otp-stable-arm64: image: *build_image_arm64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"' environment: *env @@ -114,6 +126,7 @@ steps: # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable otp-stable-tag-arm64: image: *build_image_arm64 + entrypoint: *script_file_entrypoint when: - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"' environment: *env diff --git a/.woodpecker/unit-testing-elixir-1.15.yaml b/.woodpecker/unit-testing-elixir-1.15.yaml index 84046be41..9d5cf7102 100644 --- a/.woodpecker/unit-testing-elixir-1.15.yaml +++ b/.woodpecker/unit-testing-elixir-1.15.yaml @@ -8,9 +8,16 @@ when: depends_on: - lint +variables: + script_file_entrypoint: &script_file_entrypoint + - /bin/sh + - -c + - 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh' + steps: unit-testing-elixir-1.15: image: elixir:1.15-alpine + entrypoint: *script_file_entrypoint environment: MIX_ENV: test DB_HOST: postgres diff --git a/.woodpecker/unit-testing-elixir-1.18.yaml b/.woodpecker/unit-testing-elixir-1.18.yaml index 8dcec62fb..8b0451ef0 100644 --- a/.woodpecker/unit-testing-elixir-1.18.yaml +++ b/.woodpecker/unit-testing-elixir-1.18.yaml @@ -8,9 +8,16 @@ when: depends_on: - lint +variables: + script_file_entrypoint: &script_file_entrypoint + - /bin/sh + - -c + - 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh' + steps: unit-testing-elixir-1.18: image: elixir:1.18-otp-27-alpine + entrypoint: *script_file_entrypoint environment: MIX_ENV: test DB_HOST: postgres From ee18feef7c2a3e0953880b7d7350d49aa463fc55 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 2 May 2026 15:04:40 +0400 Subject: [PATCH 102/127] Woodpecker CI: Allow manual develop release runs --- .woodpecker/docker-combine.yaml | 4 ++++ .woodpecker/docker.yaml | 2 ++ .woodpecker/otp-musl.yaml | 2 ++ .woodpecker/otp.yaml | 2 ++ 4 files changed, 10 insertions(+) diff --git a/.woodpecker/docker-combine.yaml b/.woodpecker/docker-combine.yaml index 161987d64..c6493df1c 100644 --- a/.woodpecker/docker-combine.yaml +++ b/.woodpecker/docker-combine.yaml @@ -3,6 +3,8 @@ when: branch: ${CI_REPO_DEFAULT_BRANCH} path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - event: tag + - event: manual + branch: ${CI_REPO_DEFAULT_BRANCH} - event: manual branch: stable @@ -17,6 +19,8 @@ steps: when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} + - event: manual + branch: ${CI_REPO_DEFAULT_BRANCH} settings: &docker_settings registry: "git.pleroma.social" image: "pleroma/pleroma" diff --git a/.woodpecker/docker.yaml b/.woodpecker/docker.yaml index 4053be91d..abc6bfa3b 100644 --- a/.woodpecker/docker.yaml +++ b/.woodpecker/docker.yaml @@ -3,6 +3,8 @@ when: branch: ${CI_REPO_DEFAULT_BRANCH} path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ] - event: tag + - event: manual + branch: ${CI_REPO_DEFAULT_BRANCH} - event: manual branch: stable diff --git a/.woodpecker/otp-musl.yaml b/.woodpecker/otp-musl.yaml index 877860255..6f007a127 100644 --- a/.woodpecker/otp-musl.yaml +++ b/.woodpecker/otp-musl.yaml @@ -3,6 +3,8 @@ when: branch: ${CI_REPO_DEFAULT_BRANCH} path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ] - event: tag + - event: manual + branch: ${CI_REPO_DEFAULT_BRANCH} - event: manual branch: stable diff --git a/.woodpecker/otp.yaml b/.woodpecker/otp.yaml index 766e42e03..7e1bad611 100644 --- a/.woodpecker/otp.yaml +++ b/.woodpecker/otp.yaml @@ -3,6 +3,8 @@ when: branch: ${CI_REPO_DEFAULT_BRANCH} path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ] - event: tag + - event: manual + branch: ${CI_REPO_DEFAULT_BRANCH} - event: manual branch: stable From 9fdad779b5f316d9a5c3c5f8d966a8c4c99fc40d Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 2 May 2026 16:12:47 +0400 Subject: [PATCH 103/127] Woodpecker CI: Run Docker manifest combine on amd64 --- .woodpecker/docker-combine.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.woodpecker/docker-combine.yaml b/.woodpecker/docker-combine.yaml index c6493df1c..6c91d26e9 100644 --- a/.woodpecker/docker-combine.yaml +++ b/.woodpecker/docker-combine.yaml @@ -13,6 +13,9 @@ depends_on: skip_clone: true +labels: + platform: linux/amd64 + steps: docker-develop-combine: image: git.fluffytail.org/phnt/wpc-docker-tagger:latest From 50651284a2988a5e806a0e802ad2257a9595f3a2 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 2 May 2026 17:21:37 +0400 Subject: [PATCH 104/127] Woodpecker CI: Run generic workflows on amd64 --- .woodpecker/changelog.yaml | 3 +++ .woodpecker/lint.yaml | 3 +++ .woodpecker/unit-testing-elixir-1.15.yaml | 3 +++ .woodpecker/unit-testing-elixir-1.18.yaml | 3 +++ 4 files changed, 12 insertions(+) diff --git a/.woodpecker/changelog.yaml b/.woodpecker/changelog.yaml index 1d65e8e9f..64062f17e 100644 --- a/.woodpecker/changelog.yaml +++ b/.woodpecker/changelog.yaml @@ -1,6 +1,9 @@ when: - event: pull_request +labels: + platform: linux/amd64 + variables: script_file_entrypoint: &script_file_entrypoint - /bin/sh diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index 95325d779..7b2a1683c 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -5,6 +5,9 @@ when: branch: ${CI_REPO_DEFAULT_BRANCH} path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] +labels: + platform: linux/amd64 + variables: script_file_entrypoint: &script_file_entrypoint - /bin/sh diff --git a/.woodpecker/unit-testing-elixir-1.15.yaml b/.woodpecker/unit-testing-elixir-1.15.yaml index 9d5cf7102..5c6be764d 100644 --- a/.woodpecker/unit-testing-elixir-1.15.yaml +++ b/.woodpecker/unit-testing-elixir-1.15.yaml @@ -8,6 +8,9 @@ when: depends_on: - lint +labels: + platform: linux/amd64 + variables: script_file_entrypoint: &script_file_entrypoint - /bin/sh diff --git a/.woodpecker/unit-testing-elixir-1.18.yaml b/.woodpecker/unit-testing-elixir-1.18.yaml index 8b0451ef0..2a40f41f7 100644 --- a/.woodpecker/unit-testing-elixir-1.18.yaml +++ b/.woodpecker/unit-testing-elixir-1.18.yaml @@ -8,6 +8,9 @@ when: depends_on: - lint +labels: + platform: linux/amd64 + variables: script_file_entrypoint: &script_file_entrypoint - /bin/sh From 4acd8c4e7204e3eee50568f23f53bff053b9206e Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 2 May 2026 21:08:04 +0400 Subject: [PATCH 105/127] Log failed-signature retry rejections --- lib/pleroma/workers/signature_retry_worker.ex | 224 +++++++++++++----- .../workers/signature_retry_worker_test.exs | 100 +++++++- 2 files changed, 257 insertions(+), 67 deletions(-) diff --git a/lib/pleroma/workers/signature_retry_worker.ex b/lib/pleroma/workers/signature_retry_worker.ex index 56673a514..2c4c097dd 100644 --- a/lib/pleroma/workers/signature_retry_worker.ex +++ b/lib/pleroma/workers/signature_retry_worker.ex @@ -10,6 +10,8 @@ defmodule Pleroma.Workers.SignatureRetryWorker do alias Pleroma.Web.Federator alias Pleroma.Web.Plugs.MappedSignatureToIdentityPlug + require Logger + use Oban.Worker, queue: :federator_incoming, max_attempts: 5, unique: [period: :infinity] @impl true @@ -25,37 +27,55 @@ defmodule Pleroma.Workers.SignatureRetryWorker do }) when is_binary(method) and is_map(params) and is_list(req_headers) and is_binary(request_path) and is_binary(query_string) do - with {:ok, req_headers} <- normalize_req_headers(req_headers), - conn_data = %Plug.Conn{ - assigns: %{valid_signature: true}, - method: method, - params: params, - req_headers: req_headers, - request_path: request_path, - query_string: query_string - }, - actor_id = Utils.get_ap_id(params["actor"]), - {:signature_actor, {:ok, signature_actor_id}} <- - {:signature_actor, signature_actor_id(conn_data)}, - {:same_actor, true} <- {:same_actor, signature_actor_id == actor_id}, - {:ok, %User{}} <- User.get_or_fetch_by_ap_id(actor_id), - {:ok, _public_key} <- Signature.refetch_public_key(conn_data), - {:signature, true} <- {:signature, validate_signature(conn_data)}, - {:same_actor, true} <- {:same_actor, validate_same_actor(conn_data)}, - {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do - unless Instances.reachable?(params["actor"]) do - domain = URI.parse(params["actor"]).host - Oban.insert(Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain})) - end + case normalize_req_headers(req_headers) do + {:ok, req_headers} -> + conn_data = %Plug.Conn{ + assigns: %{valid_signature: true}, + method: method, + params: params, + req_headers: req_headers, + request_path: request_path, + query_string: query_string + } - {:ok, res} - else - e -> process_errors(e) + signature_actor_result = signature_actor_id(conn_data) + + with actor_id = Utils.get_ap_id(params["actor"]), + {:signature_actor, {:ok, signature_actor_id}} <- + {:signature_actor, signature_actor_result}, + {:same_actor, true} <- {:same_actor, signature_actor_id == actor_id}, + {:ok, %User{}} <- User.get_or_fetch_by_ap_id(actor_id), + {:ok, _public_key} <- Signature.refetch_public_key(conn_data), + {:signature, true} <- {:signature, validate_signature(conn_data)}, + {:same_actor, true} <- {:same_actor, validate_same_actor(conn_data)}, + {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do + unless Instances.reachable?(params["actor"]) do + domain = URI.parse(params["actor"]).host + Oban.insert(Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain})) + end + + {:ok, res} + else + e -> process_errors(e, retry_log_context(params, request_path, signature_actor_result)) + end + + e -> + process_errors(e, retry_log_context(params, request_path, nil)) end end - def perform(%Job{args: %{"op" => "incoming_failed_signature_ap_doc"}}) do - process_errors(:missing_signature_retry_metadata) + def perform(%Job{args: %{"op" => "incoming_failed_signature_ap_doc"} = args}) do + process_errors( + :missing_signature_retry_metadata, + retry_log_context(Map.get(args, "params"), Map.get(args, "request_path"), nil) + ) + end + + def perform(%Job{args: args}) when is_map(args) do + process_errors( + :missing_signature_retry_metadata, + retry_log_context(Map.get(args, "params"), Map.get(args, "request_path"), nil) + ) end def perform(%Job{}), do: process_errors(:missing_signature_retry_metadata) @@ -109,36 +129,126 @@ defmodule Pleroma.Workers.SignatureRetryWorker do _, _ -> {:error, :invalid_signature} end - defp process_errors({:error, {:error, _} = error}), do: process_errors(error) + defp process_errors(errors, context \\ %{}) - defp process_errors(errors) do - case errors do - # User fetch failures - {:error, :not_found} = reason -> {:cancel, reason} - {:error, :forbidden} = reason -> {:cancel, reason} - # Inactive user - {:error, {:user_active, false} = reason} -> {:cancel, reason} - # Validator will error and return a changeset error - # e.g., duplicate activities or if the object was deleted - {:error, {:validate, {:error, _changeset} = reason}} -> {:cancel, reason} - # Duplicate detection during Normalization - {:error, :already_present} -> {:cancel, :already_present} - # MRFs will return a reject - {:error, {:reject, _} = reason} -> {:cancel, reason} - # HTTP Sigs - {:signature_actor, {:error, _}} -> {:cancel, :invalid_signature} - {:signature, false} -> {:cancel, :invalid_signature} - {:same_actor, false} -> {:cancel, :actor_signature_mismatch} - # Origin / URL validation failed somewhere possibly due to spoofing - {:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed} - # Unclear if this can be reached - {:error, {:side_effects, {:error, :no_object_actor}} = reason} -> {:cancel, reason} - # Fail closed if the retry cannot reconstruct the original request. - :missing_signature_retry_metadata -> {:cancel, :missing_signature_retry_metadata} - {:error, :invalid_signature_retry_metadata} -> {:cancel, :invalid_signature_retry_metadata} - # Catchall - {:error, _} = e -> e - e -> {:error, e} - end + defp process_errors({:error, {:error, _} = error}, context), do: process_errors(error, context) + + defp process_errors(errors, context) do + result = + case errors do + # User fetch failures + {:error, :not_found} = reason -> + {:cancel, reason} + + {:error, :forbidden} = reason -> + {:cancel, reason} + + # Inactive user + {:error, {:user_active, false} = reason} -> + {:cancel, reason} + + # Validator will error and return a changeset error + # e.g., duplicate activities or if the object was deleted + {:error, {:validate, {:error, _changeset} = reason}} -> + {:cancel, reason} + + # Duplicate detection during Normalization + {:error, :already_present} -> + {:cancel, :already_present} + + # MRFs will return a reject + {:error, {:reject, _} = reason} -> + {:cancel, reason} + + # HTTP Sigs + {:signature_actor, {:error, _}} -> + {:cancel, :invalid_signature} + + {:signature, false} -> + {:cancel, :invalid_signature} + + {:same_actor, false} -> + {:cancel, :actor_signature_mismatch} + + # Origin / URL validation failed somewhere possibly due to spoofing + {:error, :origin_containment_failed} -> + {:cancel, :origin_containment_failed} + + # Unclear if this can be reached + {:error, {:side_effects, {:error, :no_object_actor}} = reason} -> + {:cancel, reason} + + # Fail closed if the retry cannot reconstruct the original request. + :missing_signature_retry_metadata -> + {:cancel, :missing_signature_retry_metadata} + + {:error, :invalid_signature_retry_metadata} -> + {:cancel, :invalid_signature_retry_metadata} + + # Catchall + {:error, _} = e -> + e + + e -> + {:error, e} + end + + log_signature_retry_rejection(result, context) + result end + + defp retry_log_context(params, request_path, signature_actor_result) when is_map(params) do + signature_actor = + case signature_actor_result do + {:ok, actor} when is_binary(actor) -> actor + actor when is_binary(actor) -> actor + _ -> nil + end + + %{ + activity_id: params["id"], + payload_actor: Utils.get_ap_id(params["actor"]), + request_path: request_path, + signature_actor: signature_actor, + type: params["type"] + } + end + + defp retry_log_context(_params, request_path, signature_actor_result) do + signature_actor = + case signature_actor_result do + {:ok, actor} when is_binary(actor) -> actor + actor when is_binary(actor) -> actor + _ -> nil + end + + %{ + activity_id: nil, + payload_actor: nil, + request_path: request_path, + signature_actor: signature_actor, + type: nil + } + end + + defp log_signature_retry_rejection({:cancel, reason}, context) + when reason in [ + :actor_signature_mismatch, + :invalid_signature, + :invalid_signature_retry_metadata, + :missing_signature_retry_metadata, + :origin_containment_failed + ] do + Logger.warning( + "Failed-signature inbox retry rejected " <> + "reason=#{inspect(reason)} " <> + "payload_actor=#{inspect(context[:payload_actor])} " <> + "signature_actor=#{inspect(context[:signature_actor])} " <> + "activity_id=#{inspect(context[:activity_id])} " <> + "type=#{inspect(context[:type])} " <> + "request_path=#{inspect(context[:request_path])}" + ) + end + + defp log_signature_retry_rejection(_result, _context), do: :ok end diff --git a/test/pleroma/workers/signature_retry_worker_test.exs b/test/pleroma/workers/signature_retry_worker_test.exs index 02706ebad..f4ec0e2e3 100644 --- a/test/pleroma/workers/signature_retry_worker_test.exs +++ b/test/pleroma/workers/signature_retry_worker_test.exs @@ -6,8 +6,11 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do use Pleroma.DataCase, async: false use Oban.Testing, repo: Pleroma.Repo + import ExUnit.CaptureLog import Pleroma.Factory + @moduletag capture_log: true + alias Pleroma.Activity alias Pleroma.Object alias Pleroma.Signature @@ -73,7 +76,9 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do defp assert_mismatched_signature_cancelled(params, signer) do assert {:ok, oban_job} = enqueue_failed_signature(params, signer) - assert {:cancel, :actor_signature_mismatch} = SignatureRetryWorker.perform(oban_job) + capture_log([level: :warning], fn -> + assert {:cancel, :actor_signature_mismatch} = SignatureRetryWorker.perform(oban_job) + end) end test "Federator preserves request metadata for failed-signature retry jobs" do @@ -108,25 +113,54 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do test "cancels retry jobs without request metadata" do params = insert(:note_activity).data - assert {:cancel, :missing_signature_retry_metadata} = - SignatureRetryWorker.perform(%Oban.Job{ - args: %{"op" => "incoming_failed_signature_ap_doc", "params" => params} - }) + log = + capture_log([level: :warning], fn -> + assert {:cancel, :missing_signature_retry_metadata} = + SignatureRetryWorker.perform(%Oban.Job{ + args: %{"op" => "incoming_failed_signature_ap_doc", "params" => params} + }) + end) + + assert log =~ "Failed-signature inbox retry rejected" + assert log =~ "reason=:missing_signature_retry_metadata" + assert log =~ "payload_actor=#{inspect(params["actor"])}" + assert log =~ "activity_id=#{inspect(params["id"])}" + assert log =~ "type=#{inspect(params["type"])}" + assert log =~ "request_path=nil" end test "cancels retry jobs with malformed serialized request headers" do params = insert(:note_activity).data - assert {:cancel, :invalid_signature_retry_metadata} = - SignatureRetryWorker.perform(failed_signature_job(params, [["signature"]])) + log = + capture_log([level: :warning], fn -> + assert {:cancel, :invalid_signature_retry_metadata} = + SignatureRetryWorker.perform(failed_signature_job(params, [["signature"]])) + end) + + assert log =~ "Failed-signature inbox retry rejected" + assert log =~ "reason=:invalid_signature_retry_metadata" + assert log =~ "signature_actor=nil" + assert log =~ "request_path=\"/inbox\"" end test "cancels retry jobs without a signature header" do alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") params = insert(:note_activity, user: alice).data - assert {:cancel, :invalid_signature} = - SignatureRetryWorker.perform(failed_signature_job(params, [{"host", "local.test"}])) + log = + capture_log([level: :warning], fn -> + assert {:cancel, :invalid_signature} = + SignatureRetryWorker.perform( + failed_signature_job(params, [{"host", "local.test"}]) + ) + end) + + assert log =~ "Failed-signature inbox retry rejected" + assert log =~ "reason=:invalid_signature" + assert log =~ "payload_actor=#{inspect(params["actor"])}" + assert log =~ "signature_actor=nil" + assert log =~ "request_path=\"/inbox\"" end test "cancels missing signature before fetching an unavailable payload actor" do @@ -194,7 +228,20 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do stub_actor_fetch(alice) assert {:ok, oban_job} = enqueue_failed_signature(create, alice) - assert {:cancel, :invalid_signature} = SignatureRetryWorker.perform(oban_job) + + log = + capture_log([level: :warning], fn -> + assert {:cancel, :invalid_signature} = SignatureRetryWorker.perform(oban_job) + end) + + assert log =~ "Failed-signature inbox retry rejected" + assert log =~ "reason=:invalid_signature" + assert log =~ "payload_actor=\"https://one.com/users/alice\"" + assert log =~ "signature_actor=\"https://one.com/users/alice\"" + assert log =~ "activity_id=\"https://one.com/activities/invalid-signature-create\"" + assert log =~ "type=\"Create\"" + assert log =~ "request_path=\"/inbox\"" + refute Activity.get_by_ap_id(create["id"]) end @@ -352,6 +399,39 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do assert_mismatched_signature_cancelled(create, alice) end + test "logs signature actor mismatch retry rejections" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + + create = %{ + "type" => "Create", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/logged-forged-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => "https://two.com/objects/logged-forged-note", + "actor" => bob.ap_id, + "attributedTo" => bob.ap_id, + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + log = assert_mismatched_signature_cancelled(create, alice) + + assert log =~ "Failed-signature inbox retry rejected" + assert log =~ "reason=:actor_signature_mismatch" + assert log =~ "payload_actor=\"https://two.com/users/bob\"" + assert log =~ "signature_actor=\"https://one.com/users/alice\"" + assert log =~ "activity_id=\"https://two.com/activities/logged-forged-create\"" + assert log =~ "type=\"Create\"" + assert log =~ "request_path=\"/inbox\"" + end + test "cancels signature actor mismatch before actually creating a forged post" do alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") From 00dd1b5103afce3e4e57cb6908d05ea6d94edeaf Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sun, 3 May 2026 10:19:33 +0400 Subject: [PATCH 106/127] Add failed-signature retry regression tests --- .../object_validators/update_validator.ex | 2 +- .../activity_pub_controller_test.exs | 33 +++++++++++++++++++ test/pleroma/workers/receiver_worker_test.exs | 16 +++++++++ .../workers/signature_retry_worker_test.exs | 25 ++++++++++++++ 4 files changed, 75 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex index ad3c0e3e2..4c0d9dff7 100644 --- a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex @@ -105,7 +105,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator do cng |> add_error(:object, "Can't be updated by this actor") - true -> + _ -> cng |> add_error(:object, "Update is neither for Object or Actor") end diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index 62c1dd830..b8af1e31b 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -819,6 +819,39 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do refute Activity.get_by_ap_id(data["id"]) end + test "does not delete an object after failed signature retry", %{conn: conn} do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + note = insert(:note) + object_id = note.data["id"] + + data = %{ + "type" => "Delete", + "actor" => bob.ap_id, + "id" => "https://two.com/activities/inbox-forged-delete", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => object_id + } + + expect_signature_retry_from(alice) + + conn = + conn + |> assign(:valid_signature, false) + |> put_req_header("content-type", "application/activity+json") + |> put_req_header("signature", "keyId=\"https://one.com/users/alice#main-key\"") + |> post("/inbox", data) + + assert "ok" == json_response(conn, 200) + + assert [{:cancel, :actor_signature_mismatch}] = + ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker)) + + refute Activity.get_by_ap_id(data["id"]) + assert %Object{data: %{"type" => "Note"}} = Object.get_by_ap_id(object_id) + end + test "does not create a forged post signed by a different actor", %{conn: conn} do alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") diff --git a/test/pleroma/workers/receiver_worker_test.exs b/test/pleroma/workers/receiver_worker_test.exs index 67a3f902e..ea05f38f1 100644 --- a/test/pleroma/workers/receiver_worker_test.exs +++ b/test/pleroma/workers/receiver_worker_test.exs @@ -237,6 +237,22 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do refute Pleroma.Object.get_by_ap_id(object_id) end + test "fails closed for legacy retry jobs missing one metadata field" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + params = insert(:note_activity).data + + assert {:cancel, :missing_signature_retry_metadata} = + ReceiverWorker.perform(%Oban.Job{ + args: %{ + "op" => "incoming_ap_doc", + "method" => "POST", + "params" => params, + "req_headers" => signature_headers_for(alice), + "request_path" => "/inbox" + } + }) + end + test "fails closed for malformed legacy metadata jobs without params" do assert {:cancel, :missing_signature_retry_metadata} = ReceiverWorker.perform(%Oban.Job{ diff --git a/test/pleroma/workers/signature_retry_worker_test.exs b/test/pleroma/workers/signature_retry_worker_test.exs index f4ec0e2e3..94dd5f6c1 100644 --- a/test/pleroma/workers/signature_retry_worker_test.exs +++ b/test/pleroma/workers/signature_retry_worker_test.exs @@ -399,6 +399,31 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do assert_mismatched_signature_cancelled(create, alice) end + test "cancels signature actor mismatch when payload actor is embedded" do + alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") + bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") + + create = %{ + "type" => "Create", + "actor" => %{"id" => bob.ap_id}, + "id" => "https://two.com/activities/embedded-actor-forged-create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "object" => %{ + "type" => "Note", + "id" => "https://two.com/objects/embedded-actor-forged-note", + "actor" => bob.ap_id, + "attributedTo" => bob.ap_id, + "content" => "forged post", + "published" => "2024-07-25T13:33:31Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [] + } + } + + assert_mismatched_signature_cancelled(create, alice) + end + test "logs signature actor mismatch retry rejections" do alice = insert(:user, local: false, ap_id: "https://one.com/users/alice") bob = insert(:user, local: false, ap_id: "https://two.com/users/bob") From 6ae02d71bd10006710e24a97bcb6b9a895ed50cd Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sun, 3 May 2026 10:33:42 +0400 Subject: [PATCH 107/127] Align inbox controller tests with signer mapping --- .../activity_pub_controller_test.exs | 59 ++++++++++++------- 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index b8af1e31b..3988c3912 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -37,6 +37,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do setup do: clear_config([:instance, :federating], true) + defp assign_valid_signature_for_actor(conn, %User{ap_id: actor_id}) do + assign_valid_signature_for_actor(conn, actor_id) + end + + defp assign_valid_signature_for_actor(conn, actor) do + actor_id = Utils.get_ap_id(actor) + + conn + |> assign(:valid_signature, true) + |> put_req_header("signature", "keyId=\"#{actor_id}#main-key\"") + end + defp expect_signature_retry_from(%User{} = signer) do signer_json = UserView.render("user.json", %{user: signer}) |> Map.delete("featured") @@ -707,7 +719,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn = conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/inbox", data) @@ -735,7 +747,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn = conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/inbox", data) @@ -954,7 +966,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert "ok" == conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(followed_relay) |> put_req_header("content-type", "application/activity+json") |> post("/inbox", accept) |> json_response(200) @@ -1034,16 +1046,19 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do test "Unknown activity types are discarded", %{conn: conn} do unknown_types = ["Poke", "Read", "Dazzle"] + actor = + insert(:user, local: false, ap_id: "https://unknown.mastodon.instance/users/somebody") + Enum.each(unknown_types, fn bad_type -> params = %{ "type" => bad_type, - "actor" => "https://unknown.mastodon.instance/users/somebody" + "actor" => actor.ap_id } |> Jason.encode!() conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(actor) |> put_req_header("content-type", "application/activity+json") |> post("/inbox", params) |> json_response(400) @@ -1112,7 +1127,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert "ok" == conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/inbox", data) |> json_response(200) @@ -1133,7 +1148,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert "ok" == conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/inbox", data) |> json_response(200) @@ -1202,7 +1217,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert "ok" == conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/inbox", data) |> json_response(200) @@ -1221,7 +1236,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert "ok" == conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/inbox", data) |> json_response(200) @@ -1252,7 +1267,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn = conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/inbox", data) @@ -1273,7 +1288,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn = conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/inbox", data) @@ -1294,7 +1309,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn = conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/inbox", data) @@ -1318,7 +1333,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn = conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/inbox", data) @@ -1345,7 +1360,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn = conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/inbox", data) @@ -1375,7 +1390,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn = conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{recipient.nickname}/inbox", data) @@ -1440,7 +1455,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do } conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{recipient.nickname}/inbox", data) |> json_response(200) @@ -1530,7 +1545,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do } conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{reported_user.nickname}/inbox", data) |> json_response(200) @@ -1584,7 +1599,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do } conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{reported_user.nickname}/inbox", data) |> json_response(200) @@ -1617,7 +1632,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn = conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/inbox", data) @@ -1640,7 +1655,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn = conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/inbox", data) @@ -1663,7 +1678,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do conn = conn - |> assign(:valid_signature, true) + |> assign_valid_signature_for_actor(data["actor"]) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/inbox", data) @@ -2783,6 +2798,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do setup do: clear_config([:media_proxy]) setup do: clear_config([Pleroma.Upload]) + # majic's libmagic port is unavailable on local Darwin runs; Linux CI still runs this test. + @tag :skip_darwin test "POST /api/ap/upload_media", %{conn: conn} do user = insert(:user) From 1a8d585cbfe8320ac6b35f86c05559592457f133 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sun, 3 May 2026 12:10:23 +0400 Subject: [PATCH 108/127] Woodpecker CI: Allow rerunning OTP package uploads --- .woodpecker/otp-musl.yaml | 6 ++++++ .woodpecker/otp.yaml | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/.woodpecker/otp-musl.yaml b/.woodpecker/otp-musl.yaml index 6f007a127..60558b893 100644 --- a/.woodpecker/otp-musl.yaml +++ b/.woodpecker/otp-musl.yaml @@ -146,6 +146,7 @@ steps: package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + update: 'true' upload-latest-amd64-musl: image: *artifacts_uploader_image @@ -171,6 +172,7 @@ steps: package_version: stable-${CI_COMMIT_SHA:0:8}-amd64-musl file_source: ./stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip + update: 'true' # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable upload-latest-tag-amd64-musl: @@ -196,6 +198,7 @@ steps: package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm-musl file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm-musl.zip file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm-musl.zip + update: 'true' upload-latest-arm-musl: image: *artifacts_uploader_image @@ -221,6 +224,7 @@ steps: package_version: stable-${CI_COMMIT_SHA:0:8}-arm-musl file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm-musl.zip file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-arm-musl.zip + update: 'true' # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable upload-latest-tag-arm-musl: @@ -246,6 +250,7 @@ steps: package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + update: 'true' upload-latest-arm64-musl: image: *artifacts_uploader_image @@ -271,6 +276,7 @@ steps: package_version: stable-${CI_COMMIT_SHA:0:8}-arm64-musl file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip + update: 'true' # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable upload-latest-tag-arm64-musl: diff --git a/.woodpecker/otp.yaml b/.woodpecker/otp.yaml index 7e1bad611..9a33c228e 100644 --- a/.woodpecker/otp.yaml +++ b/.woodpecker/otp.yaml @@ -147,6 +147,7 @@ steps: package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64 file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip + update: 'true' upload-latest-amd64: image: *artifacts_uploader_image @@ -172,6 +173,7 @@ steps: package_version: stable-${CI_COMMIT_SHA:0:8}-amd64 file_source: ./stable-${CI_COMMIT_SHA:0:8}-amd64.zip file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64.zip + update: 'true' # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable upload-latest-tag-amd64: @@ -197,6 +199,7 @@ steps: package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm.zip file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm.zip + update: 'true' upload-latest-arm: image: *artifacts_uploader_image @@ -222,6 +225,7 @@ steps: package_version: stable-${CI_COMMIT_SHA:0:8}-arm file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm.zip file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-arm.zip + update: 'true' # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable upload-latest-tag-arm: @@ -247,6 +251,7 @@ steps: package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64 file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip + update: 'true' upload-latest-arm64: image: *artifacts_uploader_image @@ -272,6 +277,7 @@ steps: package_version: stable-${CI_COMMIT_SHA:0:8}-arm64 file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm64.zip file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64.zip + update: 'true' # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable upload-latest-tag-arm64: From 6553ba24aa6b1a835e923019bb2e0f277abd85f9 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sun, 3 May 2026 16:56:01 +0400 Subject: [PATCH 109/127] Prepare 2.10.1 release --- CHANGELOG.md | 48 +++++++++++++++++++ changelog.d/assign-users.add | 1 - .../avatar-description-mastodon-api.change | 1 - changelog.d/bandit.change | 1 - changelog.d/bookmark-folders.ignore | 1 - changelog.d/boost-visibilities.add | 1 - changelog.d/cache-control-immutable.add | 1 - changelog.d/ci-artifacts.skip | 0 changelog.d/credo-aliases-sort-fixes.skip | 0 changelog.d/database-config-whitelist.add | 1 - changelog.d/elixir-1.19-cherrypicks.change | 1 - changelog.d/email_digest.fix | 1 - changelog.d/emoji-reaction-url-escape.fix | 1 - changelog.d/exclusive-lists.add | 1 - .../gopher-genserver-crash-on-boot.fix | 1 - changelog.d/hackney-downgrade.change | 1 - changelog.d/hackney-mediaproxy.change | 1 - changelog.d/hackney.change | 1 - changelog.d/hubzilla-alsoknownas.fix | 1 - changelog.d/inappropriate-docs.remove | 1 - changelog.d/instance-domain-blocks.add | 1 - changelog.d/instance-profile-fields.add | 1 - changelog.d/lint-warnings.skip | 0 changelog.d/live-dashboard-redirect.fix | 1 - changelog.d/map-side-effects.skip | 0 changelog.d/missing-static-file.fix | 1 - changelog.d/mix-exs-fix.skip | 0 changelog.d/mix-exs-update.skip | 0 .../oauth-registration-redirect_uris.fix | 1 - changelog.d/oban-web.add | 1 - changelog.d/old-migrations.fix | 1 - changelog.d/paginate-follow-requests.change | 1 - changelog.d/phoenix-livedashboard-move.change | 1 - changelog.d/plug-test-typo.skip | 0 changelog.d/poll-voters-count.fix | 1 - changelog.d/prune-hashtag-follow-3376.fix | 1 - changelog.d/rate-limiter-hardening.fix | 1 - changelog.d/reduce-flaky-tests.skip | 1 - changelog.d/relationship-expires-at.change | 1 - changelog.d/release-to-docker.add | 1 - changelog.d/restore-embeds.fix | 1 - .../reverseproxy-recursive-redirect.fix | 1 - changelog.d/search-indexing.change | 1 - changelog.d/search-indexing.skip | 0 changelog.d/twitter-api.skip | 0 changelog.d/update-comment.ignore | 1 - changelog.d/user-view.ignore | 1 - changelog.d/vix-0.36.0.fix | 1 - changelog.d/woodpecker-pr-pipeline.skip | 0 changelog.d/woodpecker-release-pipeline.skip | 0 mix.exs | 2 +- 51 files changed, 49 insertions(+), 39 deletions(-) delete mode 100644 changelog.d/assign-users.add delete mode 100644 changelog.d/avatar-description-mastodon-api.change delete mode 100644 changelog.d/bandit.change delete mode 100644 changelog.d/bookmark-folders.ignore delete mode 100644 changelog.d/boost-visibilities.add delete mode 100644 changelog.d/cache-control-immutable.add delete mode 100644 changelog.d/ci-artifacts.skip delete mode 100644 changelog.d/credo-aliases-sort-fixes.skip delete mode 100644 changelog.d/database-config-whitelist.add delete mode 100644 changelog.d/elixir-1.19-cherrypicks.change delete mode 100644 changelog.d/email_digest.fix delete mode 100644 changelog.d/emoji-reaction-url-escape.fix delete mode 100644 changelog.d/exclusive-lists.add delete mode 100644 changelog.d/gopher-genserver-crash-on-boot.fix delete mode 100644 changelog.d/hackney-downgrade.change delete mode 100644 changelog.d/hackney-mediaproxy.change delete mode 100644 changelog.d/hackney.change delete mode 100644 changelog.d/hubzilla-alsoknownas.fix delete mode 100644 changelog.d/inappropriate-docs.remove delete mode 100644 changelog.d/instance-domain-blocks.add delete mode 100644 changelog.d/instance-profile-fields.add delete mode 100644 changelog.d/lint-warnings.skip delete mode 100644 changelog.d/live-dashboard-redirect.fix delete mode 100644 changelog.d/map-side-effects.skip delete mode 100644 changelog.d/missing-static-file.fix delete mode 100644 changelog.d/mix-exs-fix.skip delete mode 100644 changelog.d/mix-exs-update.skip delete mode 100644 changelog.d/oauth-registration-redirect_uris.fix delete mode 100644 changelog.d/oban-web.add delete mode 100644 changelog.d/old-migrations.fix delete mode 100644 changelog.d/paginate-follow-requests.change delete mode 100644 changelog.d/phoenix-livedashboard-move.change delete mode 100644 changelog.d/plug-test-typo.skip delete mode 100644 changelog.d/poll-voters-count.fix delete mode 100644 changelog.d/prune-hashtag-follow-3376.fix delete mode 100644 changelog.d/rate-limiter-hardening.fix delete mode 100644 changelog.d/reduce-flaky-tests.skip delete mode 100644 changelog.d/relationship-expires-at.change delete mode 100644 changelog.d/release-to-docker.add delete mode 100644 changelog.d/restore-embeds.fix delete mode 100644 changelog.d/reverseproxy-recursive-redirect.fix delete mode 100644 changelog.d/search-indexing.change delete mode 100644 changelog.d/search-indexing.skip delete mode 100644 changelog.d/twitter-api.skip delete mode 100644 changelog.d/update-comment.ignore delete mode 100644 changelog.d/user-view.ignore delete mode 100644 changelog.d/vix-0.36.0.fix delete mode 100644 changelog.d/woodpecker-pr-pipeline.skip delete mode 100644 changelog.d/woodpecker-release-pipeline.skip diff --git a/CHANGELOG.md b/CHANGELOG.md index adc76c767..c7bb9b09d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,54 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## 2.10.1 + +### Changed + +- Move avatar_description and header_description fields to the account object +- Update Bandit to 1.10.4 +- No-op code correctness improvements detected by Elixir 1.19 compiler +- Downgrade Hackney to 1.20.1 +- Use a custom redirect handler to ensure MediaProxy redirects are followed with Hackney +- Update Hackney, the default HTTP client, to the latest release which supports Happy Eyeballs for improved IPv6 federation +- Paginate follow requests +- Moved Phoenix LiveDashboard to /pleroma/live_dashboard +- Add mute/block expiry to the relationship object +- Filter indexable activities before inserting indexing jobs into the queue. + +### Added + +- Allow assigning users to reports +- Allow fine-grained announce visibilities +- Add immutable tag on cache-control header for several endpoints that's serving the same exact things. +- Add reasonable defaults for :database_config_whitelist +- Support lists `exclusive` param +- Add v1/instance/domain_blocks endpoint +- Add /api/v2/instance profile fields limits info used by Mastodon +- Added Oban Web dashboard located at /pleroma/oban +- Add instructions on how to run a release in docker, to make it easier to run on older distros. + +### Fixed + +- Fix the daily email digest job which was not executing +- Encode custom emoji URLs in EmojiReact activity tags. +- Gopher: Fix Ranch listener not being stopped properly on Pleroma restart when database configuration is enabled +- Fix fetching Hubzilla Actors with alsoKnownAs as string +- Fix /phoenix/live_dashboard redirect not working when user added a path segment +- Fix 404 error codes for missing static files +- Fix OAuth app registration to accept `redirect_uris` as an array of strings (RFC 7591), while keeping backwards compatibility with string input. +- Correct old migrations for expiring activities and user access tokens. +- Federate `votersCount` correctly +- DB prune: Check if user follows hashtag with no objects before deletion +- Stop the rate limiter from crashing when run with wrong settings. +- Restore embeds route +- ReverseProxy: Recursively follow redirects until redirect_limit is reached +- Fix compilation with vips-8.18.0 with bumping to vix 0.36.0 + +### Removed + +- Docs: Removed outdated, incorrect, unmaintained and inappropriate installation documentation (Arch, NetBSD, NixOS) + ## 2.10 ### Security diff --git a/changelog.d/assign-users.add b/changelog.d/assign-users.add deleted file mode 100644 index f50ad94c6..000000000 --- a/changelog.d/assign-users.add +++ /dev/null @@ -1 +0,0 @@ -Allow assigning users to reports \ No newline at end of file diff --git a/changelog.d/avatar-description-mastodon-api.change b/changelog.d/avatar-description-mastodon-api.change deleted file mode 100644 index 6a454c01e..000000000 --- a/changelog.d/avatar-description-mastodon-api.change +++ /dev/null @@ -1 +0,0 @@ -Move avatar_description and header_description fields to the account object diff --git a/changelog.d/bandit.change b/changelog.d/bandit.change deleted file mode 100644 index 3831a02c2..000000000 --- a/changelog.d/bandit.change +++ /dev/null @@ -1 +0,0 @@ -Update Bandit to 1.10.4 diff --git a/changelog.d/bookmark-folders.ignore b/changelog.d/bookmark-folders.ignore deleted file mode 100644 index 8705ac00b..000000000 --- a/changelog.d/bookmark-folders.ignore +++ /dev/null @@ -1 +0,0 @@ -Various bookmark folders-related improvements diff --git a/changelog.d/boost-visibilities.add b/changelog.d/boost-visibilities.add deleted file mode 100644 index 317d9840d..000000000 --- a/changelog.d/boost-visibilities.add +++ /dev/null @@ -1 +0,0 @@ -Allow fine-grained announce visibilities diff --git a/changelog.d/cache-control-immutable.add b/changelog.d/cache-control-immutable.add deleted file mode 100644 index 516db67bf..000000000 --- a/changelog.d/cache-control-immutable.add +++ /dev/null @@ -1 +0,0 @@ -Add immutable tag on cache-control header for several endpoints that's serving the same exact things. \ No newline at end of file diff --git a/changelog.d/ci-artifacts.skip b/changelog.d/ci-artifacts.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/credo-aliases-sort-fixes.skip b/changelog.d/credo-aliases-sort-fixes.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/database-config-whitelist.add b/changelog.d/database-config-whitelist.add deleted file mode 100644 index a78960c98..000000000 --- a/changelog.d/database-config-whitelist.add +++ /dev/null @@ -1 +0,0 @@ -Add reasonable defaults for :database_config_whitelist \ No newline at end of file diff --git a/changelog.d/elixir-1.19-cherrypicks.change b/changelog.d/elixir-1.19-cherrypicks.change deleted file mode 100644 index 7e56be008..000000000 --- a/changelog.d/elixir-1.19-cherrypicks.change +++ /dev/null @@ -1 +0,0 @@ -No-op code correctness improvements detected by Elixir 1.19 compiler diff --git a/changelog.d/email_digest.fix b/changelog.d/email_digest.fix deleted file mode 100644 index cd15874a2..000000000 --- a/changelog.d/email_digest.fix +++ /dev/null @@ -1 +0,0 @@ -Fix the daily email digest job which was not executing diff --git a/changelog.d/emoji-reaction-url-escape.fix b/changelog.d/emoji-reaction-url-escape.fix deleted file mode 100644 index c3a1c8823..000000000 --- a/changelog.d/emoji-reaction-url-escape.fix +++ /dev/null @@ -1 +0,0 @@ -Encode custom emoji URLs in EmojiReact activity tags. diff --git a/changelog.d/exclusive-lists.add b/changelog.d/exclusive-lists.add deleted file mode 100644 index bbd722f07..000000000 --- a/changelog.d/exclusive-lists.add +++ /dev/null @@ -1 +0,0 @@ -Support lists `exclusive` param diff --git a/changelog.d/gopher-genserver-crash-on-boot.fix b/changelog.d/gopher-genserver-crash-on-boot.fix deleted file mode 100644 index 3b51662be..000000000 --- a/changelog.d/gopher-genserver-crash-on-boot.fix +++ /dev/null @@ -1 +0,0 @@ -Gopher: Fix Ranch listener not being stopped properly on Pleroma restart when database configuration is enabled diff --git a/changelog.d/hackney-downgrade.change b/changelog.d/hackney-downgrade.change deleted file mode 100644 index a98710692..000000000 --- a/changelog.d/hackney-downgrade.change +++ /dev/null @@ -1 +0,0 @@ -Downgrade Hackney to 1.20.1 diff --git a/changelog.d/hackney-mediaproxy.change b/changelog.d/hackney-mediaproxy.change deleted file mode 100644 index 10dfb0775..000000000 --- a/changelog.d/hackney-mediaproxy.change +++ /dev/null @@ -1 +0,0 @@ -Use a custom redirect handler to ensure MediaProxy redirects are followed with Hackney diff --git a/changelog.d/hackney.change b/changelog.d/hackney.change deleted file mode 100644 index 3158cfc77..000000000 --- a/changelog.d/hackney.change +++ /dev/null @@ -1 +0,0 @@ -Update Hackney, the default HTTP client, to the latest release which supports Happy Eyeballs for improved IPv6 federation diff --git a/changelog.d/hubzilla-alsoknownas.fix b/changelog.d/hubzilla-alsoknownas.fix deleted file mode 100644 index 2a2969807..000000000 --- a/changelog.d/hubzilla-alsoknownas.fix +++ /dev/null @@ -1 +0,0 @@ -Fix fetching Hubzilla Actors with alsoKnownAs as string diff --git a/changelog.d/inappropriate-docs.remove b/changelog.d/inappropriate-docs.remove deleted file mode 100644 index 699c9186a..000000000 --- a/changelog.d/inappropriate-docs.remove +++ /dev/null @@ -1 +0,0 @@ -Docs: Removed outdated, incorrect, unmaintained and inappropriate installation documentation (Arch, NetBSD, NixOS) diff --git a/changelog.d/instance-domain-blocks.add b/changelog.d/instance-domain-blocks.add deleted file mode 100644 index 85f01c5c2..000000000 --- a/changelog.d/instance-domain-blocks.add +++ /dev/null @@ -1 +0,0 @@ -Add v1/instance/domain_blocks endpoint diff --git a/changelog.d/instance-profile-fields.add b/changelog.d/instance-profile-fields.add deleted file mode 100644 index 712bd68d9..000000000 --- a/changelog.d/instance-profile-fields.add +++ /dev/null @@ -1 +0,0 @@ -Add /api/v2/instance profile fields limits info used by Mastodon diff --git a/changelog.d/lint-warnings.skip b/changelog.d/lint-warnings.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/live-dashboard-redirect.fix b/changelog.d/live-dashboard-redirect.fix deleted file mode 100644 index 10588d89e..000000000 --- a/changelog.d/live-dashboard-redirect.fix +++ /dev/null @@ -1 +0,0 @@ -Fix /phoenix/live_dashboard redirect not working when user added a path segment diff --git a/changelog.d/map-side-effects.skip b/changelog.d/map-side-effects.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/missing-static-file.fix b/changelog.d/missing-static-file.fix deleted file mode 100644 index c7ef805aa..000000000 --- a/changelog.d/missing-static-file.fix +++ /dev/null @@ -1 +0,0 @@ -Fix 404 error codes for missing static files diff --git a/changelog.d/mix-exs-fix.skip b/changelog.d/mix-exs-fix.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/mix-exs-update.skip b/changelog.d/mix-exs-update.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/oauth-registration-redirect_uris.fix b/changelog.d/oauth-registration-redirect_uris.fix deleted file mode 100644 index 76ace55df..000000000 --- a/changelog.d/oauth-registration-redirect_uris.fix +++ /dev/null @@ -1 +0,0 @@ -Fix OAuth app registration to accept `redirect_uris` as an array of strings (RFC 7591), while keeping backwards compatibility with string input. diff --git a/changelog.d/oban-web.add b/changelog.d/oban-web.add deleted file mode 100644 index c59e2ebca..000000000 --- a/changelog.d/oban-web.add +++ /dev/null @@ -1 +0,0 @@ -Added Oban Web dashboard located at /pleroma/oban diff --git a/changelog.d/old-migrations.fix b/changelog.d/old-migrations.fix deleted file mode 100644 index 49566c896..000000000 --- a/changelog.d/old-migrations.fix +++ /dev/null @@ -1 +0,0 @@ -Correct old migrations for expiring activities and user access tokens. diff --git a/changelog.d/paginate-follow-requests.change b/changelog.d/paginate-follow-requests.change deleted file mode 100644 index 1a88995b7..000000000 --- a/changelog.d/paginate-follow-requests.change +++ /dev/null @@ -1 +0,0 @@ -Paginate follow requests diff --git a/changelog.d/phoenix-livedashboard-move.change b/changelog.d/phoenix-livedashboard-move.change deleted file mode 100644 index 116b1523a..000000000 --- a/changelog.d/phoenix-livedashboard-move.change +++ /dev/null @@ -1 +0,0 @@ -Moved Phoenix LiveDashboard to /pleroma/live_dashboard diff --git a/changelog.d/plug-test-typo.skip b/changelog.d/plug-test-typo.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/poll-voters-count.fix b/changelog.d/poll-voters-count.fix deleted file mode 100644 index 2dbc81b5d..000000000 --- a/changelog.d/poll-voters-count.fix +++ /dev/null @@ -1 +0,0 @@ -Federate `votersCount` correctly diff --git a/changelog.d/prune-hashtag-follow-3376.fix b/changelog.d/prune-hashtag-follow-3376.fix deleted file mode 100644 index cdb4e9a79..000000000 --- a/changelog.d/prune-hashtag-follow-3376.fix +++ /dev/null @@ -1 +0,0 @@ -DB prune: Check if user follows hashtag with no objects before deletion diff --git a/changelog.d/rate-limiter-hardening.fix b/changelog.d/rate-limiter-hardening.fix deleted file mode 100644 index a3af8fcc4..000000000 --- a/changelog.d/rate-limiter-hardening.fix +++ /dev/null @@ -1 +0,0 @@ -Stop the rate limiter from crashing when run with wrong settings. diff --git a/changelog.d/reduce-flaky-tests.skip b/changelog.d/reduce-flaky-tests.skip deleted file mode 100644 index 0375762c0..000000000 --- a/changelog.d/reduce-flaky-tests.skip +++ /dev/null @@ -1 +0,0 @@ -Reduce the number of flaky tests by making them sync if they affect the global state, and silence noisy test output. diff --git a/changelog.d/relationship-expires-at.change b/changelog.d/relationship-expires-at.change deleted file mode 100644 index 286dba197..000000000 --- a/changelog.d/relationship-expires-at.change +++ /dev/null @@ -1 +0,0 @@ -Add mute/block expiry to the relationship object diff --git a/changelog.d/release-to-docker.add b/changelog.d/release-to-docker.add deleted file mode 100644 index 5fbf611a5..000000000 --- a/changelog.d/release-to-docker.add +++ /dev/null @@ -1 +0,0 @@ -Add instructions on how to run a release in docker, to make it easier to run on older distros. diff --git a/changelog.d/restore-embeds.fix b/changelog.d/restore-embeds.fix deleted file mode 100644 index 5a2a1c4fe..000000000 --- a/changelog.d/restore-embeds.fix +++ /dev/null @@ -1 +0,0 @@ -Restore embeds route diff --git a/changelog.d/reverseproxy-recursive-redirect.fix b/changelog.d/reverseproxy-recursive-redirect.fix deleted file mode 100644 index 744109fd6..000000000 --- a/changelog.d/reverseproxy-recursive-redirect.fix +++ /dev/null @@ -1 +0,0 @@ -ReverseProxy: Recursively follow redirects until redirect_limit is reached diff --git a/changelog.d/search-indexing.change b/changelog.d/search-indexing.change deleted file mode 100644 index 766934f3f..000000000 --- a/changelog.d/search-indexing.change +++ /dev/null @@ -1 +0,0 @@ -Filter indexable activities before inserting indexing jobs into the queue. diff --git a/changelog.d/search-indexing.skip b/changelog.d/search-indexing.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/twitter-api.skip b/changelog.d/twitter-api.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/update-comment.ignore b/changelog.d/update-comment.ignore deleted file mode 100644 index 733e813b3..000000000 --- a/changelog.d/update-comment.ignore +++ /dev/null @@ -1 +0,0 @@ -Update comment for prepare_object, rename prepare_outgoing diff --git a/changelog.d/user-view.ignore b/changelog.d/user-view.ignore deleted file mode 100644 index 37e9a7e09..000000000 --- a/changelog.d/user-view.ignore +++ /dev/null @@ -1 +0,0 @@ -Avoid code duplication in UserView diff --git a/changelog.d/vix-0.36.0.fix b/changelog.d/vix-0.36.0.fix deleted file mode 100644 index 43a8dd8f8..000000000 --- a/changelog.d/vix-0.36.0.fix +++ /dev/null @@ -1 +0,0 @@ -Fix compilation with vips-8.18.0 with bumping to vix 0.36.0 diff --git a/changelog.d/woodpecker-pr-pipeline.skip b/changelog.d/woodpecker-pr-pipeline.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/woodpecker-release-pipeline.skip b/changelog.d/woodpecker-release-pipeline.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/mix.exs b/mix.exs index 5d84c6e09..5da2ca657 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do def project do [ app: :pleroma, - version: version("2.10.0"), + version: version("2.10.1"), elixir: "~> 1.15", elixirc_paths: elixirc_paths(Mix.env()), compilers: Mix.compilers(), From 621d86a31da0900cc6ddd5bae1b6c65204b7b5c8 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sun, 3 May 2026 18:02:59 +0400 Subject: [PATCH 110/127] Validate WebFinger nicknames against actors --- lib/pleroma/web/activity_pub/activity_pub.ex | 106 +++++++++++++------ test/pleroma/user_test.exs | 95 +++++++++++++++-- 2 files changed, 161 insertions(+), 40 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 071d634db..0b513ee16 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1677,44 +1677,80 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do show_birthday = !!birthday - # if WebFinger request was already done, we probably have acct, otherwise - # we request WebFinger here - nickname = additional[:nickname_from_acct] || generate_nickname(data) + with {:ok, nickname} <- nickname_from_actor(data, additional) do + {:ok, + %{ + ap_id: data["id"], + uri: get_actor_url(data["url"]), + banner: normalize_image(data["image"]), + fields: fields, + emoji: emojis, + is_locked: is_locked, + is_discoverable: is_discoverable, + invisible: invisible, + avatar: normalize_image(data["icon"]), + name: data["name"], + follower_address: data["followers"], + following_address: data["following"], + featured_address: featured_address, + bio: data["summary"] || "", + actor_type: actor_type, + also_known_as: normalize_also_known_as(data["alsoKnownAs"]), + public_key: public_key, + inbox: data["inbox"], + shared_inbox: shared_inbox, + accepts_chat_messages: accepts_chat_messages, + birthday: birthday, + show_birthday: show_birthday, + pinned_objects: pinned_objects, + nickname: nickname + }} + end + end - %{ - ap_id: data["id"], - uri: get_actor_url(data["url"]), - banner: normalize_image(data["image"]), - fields: fields, - emoji: emojis, - is_locked: is_locked, - is_discoverable: is_discoverable, - invisible: invisible, - avatar: normalize_image(data["icon"]), - name: data["name"], - follower_address: data["followers"], - following_address: data["following"], - featured_address: featured_address, - bio: data["summary"] || "", - actor_type: actor_type, - also_known_as: normalize_also_known_as(data["alsoKnownAs"]), - public_key: public_key, - inbox: data["inbox"], - shared_inbox: shared_inbox, - accepts_chat_messages: accepts_chat_messages, - birthday: birthday, - show_birthday: show_birthday, - pinned_objects: pinned_objects, - nickname: nickname - } + defp nickname_from_actor(data, additional) do + generated = generated_nickname(data) + + case additional[:nickname_from_acct] do + ^generated when is_binary(generated) -> + {:ok, generated} + + acct when is_binary(acct) -> + with ^acct <- webfinger_nickname(data) do + {:ok, acct} + else + _ -> {:error, {:webfinger_actor_mismatch, acct, data["id"]}} + end + + _ -> + {:ok, generate_nickname(data)} + end + end + + defp generated_nickname(%{"preferredUsername" => username, "id" => ap_id}) + when is_binary(username) and is_binary(ap_id) do + case URI.parse(ap_id) do + %URI{host: host} when is_binary(host) -> "#{username}@#{host}" + _ -> nil + end + end + + defp generated_nickname(_), do: nil + + defp webfinger_nickname(data) do + with generated when is_binary(generated) <- generated_nickname(data), + {:ok, %{"subject" => "acct:" <> acct, "ap_id" => ap_id}} <- WebFinger.finger(generated), + true <- ap_id == data["id"] do + acct + end end defp generate_nickname(%{"preferredUsername" => username} = data) when is_binary(username) do - generated = "#{username}@#{URI.parse(data["id"]).host}" + generated = generated_nickname(data) if Config.get([WebFinger, :update_nickname_on_user_fetch]) do - case WebFinger.finger(generated) do - {:ok, %{"subject" => "acct:" <> acct}} -> acct + case webfinger_nickname(data) do + acct when is_binary(acct) -> acct _ -> generated end else @@ -1794,9 +1830,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp collection_private(_data), do: {:ok, true} def user_data_from_user_object(data, additional \\ []) do - with {:ok, data} <- MRF.filter(data) do - {:ok, object_to_user_data(data, additional)} + with {:ok, data} <- MRF.filter(data), + {:ok, data} <- object_to_user_data(data, additional) do + {:ok, data} else + {:error, _} = e -> e e -> {:error, e} end end diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs index b2533e9f1..de735cdb7 100644 --- a/test/pleroma/user_test.exs +++ b/test/pleroma/user_test.exs @@ -876,17 +876,17 @@ defmodule Pleroma.UserTest do describe "get_or_fetch/1 remote users with tld, while BE is running on a subdomain" do setup do: clear_config([Pleroma.Web.WebFinger, :update_nickname_on_user_fetch], true) - test "for mastodon" do - ap_id = "a@mastodon.example" - {:ok, fetched_user} = User.get_or_fetch(ap_id) + test "fetches a mastodon split-domain nickname" do + nickname = "a@mastodon.example" + {:ok, fetched_user} = User.get_or_fetch(nickname) assert fetched_user.ap_id == "https://sub.mastodon.example/users/a" assert fetched_user.nickname == "a@mastodon.example" end - test "for pleroma" do - ap_id = "a@pleroma.example" - {:ok, fetched_user} = User.get_or_fetch(ap_id) + test "fetches a pleroma split-domain nickname" do + nickname = "a@pleroma.example" + {:ok, fetched_user} = User.get_or_fetch(nickname) assert fetched_user.ap_id == "https://sub.pleroma.example/users/a" assert fetched_user.nickname == "a@pleroma.example" @@ -936,6 +936,89 @@ defmodule Pleroma.UserTest do assert fetched_user == "not found nonexistent" end + test "does not rename an existing remote actor from rogue WebFinger data" do + clear_config([Pleroma.Web.WebFinger, :update_nickname_on_user_fetch], true) + + actor_id = "https://legit-actor.example/users/alice" + + Tesla.Mock.mock(fn + %{url: "https://evil-webfinger.example/.well-known/host-meta"} -> + {:ok, %Tesla.Env{status: 404}} + + %{ + url: + "https://evil-webfinger.example/.well-known/webfinger?resource=acct:claimed@evil-webfinger.example" + } -> + Tesla.Mock.json(%{ + "subject" => "acct:claimed@evil-webfinger.example", + "links" => [ + %{ + "rel" => "self", + "type" => "application/activity+json", + "href" => actor_id + } + ] + }) + + %{url: ^actor_id} -> + {:ok, + %Tesla.Env{ + status: 200, + headers: [{"content-type", "application/activity+json"}], + body: + Jason.encode!(%{ + "id" => actor_id, + "type" => "Person", + "preferredUsername" => "alice", + "name" => "Alice", + "summary" => "", + "inbox" => "https://legit-actor.example/users/alice/inbox", + "outbox" => "https://legit-actor.example/users/alice/outbox", + "followers" => "https://legit-actor.example/users/alice/followers", + "following" => "https://legit-actor.example/users/alice/following" + }) + }} + + %{url: "https://legit-actor.example/.well-known/host-meta"} -> + {:ok, %Tesla.Env{status: 404}} + + %{ + url: + "https://legit-actor.example/.well-known/webfinger?resource=acct:alice@legit-actor.example" + } -> + Tesla.Mock.json(%{ + "subject" => "acct:alice@legit-actor.example", + "links" => [ + %{ + "rel" => "self", + "type" => "application/activity+json", + "href" => actor_id + } + ] + }) + end) + + assert {:error, {:webfinger_actor_mismatch, "claimed@evil-webfinger.example", ^actor_id}} = + ActivityPub.make_user_from_nickname("claimed@evil-webfinger.example") + + refute User.get_by_ap_id(actor_id) + refute User.get_by_nickname("claimed@evil-webfinger.example") + + orig_user = + insert(:user, + local: false, + nickname: "alice@legit-actor.example", + ap_id: actor_id + ) + + assert {:error, {:webfinger_actor_mismatch, "claimed@evil-webfinger.example", ^actor_id}} = + ActivityPub.make_user_from_nickname("claimed@evil-webfinger.example") + + assert {:error, _} = User.get_or_fetch_by_nickname("claimed@evil-webfinger.example") + assert User.get_by_id(orig_user.id).nickname == "alice@legit-actor.example" + refute User.get_by_nickname("claimed@evil-webfinger.example") + end + test "updates an existing user, if stale" do a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800) From 8ccdd98914e3ce11948d877b05a015bfddeebe4b Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sun, 3 May 2026 20:23:35 +0400 Subject: [PATCH 111/127] Prepare 2.10.2 release --- CHANGELOG.md | 6 ++++++ changelog.d/activitypub-spoofing.security | 1 - mix.exs | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) delete mode 100644 changelog.d/activitypub-spoofing.security diff --git a/CHANGELOG.md b/CHANGELOG.md index c7bb9b09d..f03be3f29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## 2.10.2 + +### Security + +- ActivityPub: Fixed failed-signature inbox retry handling and signer identity checks to prevent spoofed remote activities from being processed + ## 2.10.1 ### Changed diff --git a/changelog.d/activitypub-spoofing.security b/changelog.d/activitypub-spoofing.security deleted file mode 100644 index 3e6baffb6..000000000 --- a/changelog.d/activitypub-spoofing.security +++ /dev/null @@ -1 +0,0 @@ -ActivityPub: Fixed failed-signature inbox retry handling and signer identity checks to prevent spoofed remote activities from being processed diff --git a/mix.exs b/mix.exs index 5da2ca657..bb7bfb9da 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do def project do [ app: :pleroma, - version: version("2.10.1"), + version: version("2.10.2"), elixir: "~> 1.15", elixirc_paths: elixirc_paths(Mix.env()), compilers: Mix.compilers(), From aec0deef8b3018ddcced65c7bd883514922b7539 Mon Sep 17 00:00:00 2001 From: Yonle Date: Tue, 5 May 2026 13:49:29 +0700 Subject: [PATCH 112/127] poll_view: try to read votersCount first, and then manually count local voters. --- lib/pleroma/web/mastodon_api/views/poll_view.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/poll_view.ex b/lib/pleroma/web/mastodon_api/views/poll_view.ex index 3b4271227..67f874b46 100644 --- a/lib/pleroma/web/mastodon_api/views/poll_view.ex +++ b/lib/pleroma/web/mastodon_api/views/poll_view.ex @@ -71,12 +71,12 @@ defmodule Pleroma.Web.MastodonAPI.PollView do end) end + defp voters_count(%{data: %{"votersCount" => voters}}), do: voters + defp voters_count(%{data: %{"voters" => [_ | _] = voters}}) 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 From 727e9e77497f20e2b61bd6267555015d2c308696 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Wed, 6 May 2026 11:33:34 +0400 Subject: [PATCH 113/127] Fix votersCount inflation in multiple-choice polls increase_vote_count/3 was incrementing votersCount on every vote activity, causing inflation when a single voter picks multiple options. Now only increments when the actor is a new unique voter, and preserves existing votersCount otherwise. Also adds is_integer guard to voters_count/1 to handle nil safely, and adds tests for the voters_count clause ordering and edge cases. --- lib/pleroma/object.ex | 18 +- .../web/mastodon_api/views/poll_view.ex | 2 +- .../web/mastodon_api/views/poll_view_test.exs | 175 ++++++++++++++++++ 3 files changed, 189 insertions(+), 6 deletions(-) diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index f1e07a257..5e9314446 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -372,13 +372,21 @@ defmodule Pleroma.Object do option end) - voters = [actor | object.data["voters"] || []] |> Enum.uniq() + existing_voters = object.data["voters"] || [] + voters = [actor | existing_voters] |> Enum.uniq() + new_voter? = actor not in existing_voters + existing_voters_count = object.data["votersCount"] voters_count = - if Map.has_key?(object.data, "votersCount") do - object.data["votersCount"] + 1 - else - length(voters) + cond do + is_integer(existing_voters_count) and new_voter? -> + existing_voters_count + 1 + + is_integer(existing_voters_count) -> + existing_voters_count + + true -> + length(voters) end data = diff --git a/lib/pleroma/web/mastodon_api/views/poll_view.ex b/lib/pleroma/web/mastodon_api/views/poll_view.ex index 67f874b46..f047804e2 100644 --- a/lib/pleroma/web/mastodon_api/views/poll_view.ex +++ b/lib/pleroma/web/mastodon_api/views/poll_view.ex @@ -71,7 +71,7 @@ defmodule Pleroma.Web.MastodonAPI.PollView do end) end - defp voters_count(%{data: %{"votersCount" => voters}}), do: voters + defp voters_count(%{data: %{"votersCount" => voters}}) when is_integer(voters), do: voters defp voters_count(%{data: %{"voters" => [_ | _] = voters}}) do length(voters) 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 16281393d..6cb5934de 100644 --- a/test/pleroma/web/mastodon_api/views/poll_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/poll_view_test.exs @@ -180,4 +180,179 @@ defmodule Pleroma.Web.MastodonAPI.PollViewTest do assert result[:pleroma][:non_anonymous] == true end + + test "prefers votersCount over voters list when both are present" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Which flavor?", + poll: %{options: ["chocolate", "vanilla"], expires_in: 20} + }) + + object = Object.normalize(activity, fetch: false) + + voter = insert(:user) + {:ok, _, object} = CommonAPI.vote(object, voter, [0]) + + assert object.data["votersCount"] == 1 + assert length(object.data["voters"]) == 1 + + object = %{ + object + | data: Map.put(object.data, "votersCount", 42) + } + + result = PollView.render("show.json", %{object: object}) + + assert result[:voters_count] == 42 + end + + test "falls back to voters list when votersCount is absent" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Which flavor?", + poll: %{options: ["chocolate", "vanilla"], expires_in: 20} + }) + + object = Object.normalize(activity, fetch: false) + + voter = insert(:user) + {:ok, _, object} = CommonAPI.vote(object, voter, [0]) + + assert length(object.data["voters"]) == 1 + + data = Map.delete(object.data, "votersCount") + object = %{object | data: data} + + result = PollView.render("show.json", %{object: object}) + + assert result[:voters_count] == 1 + end + + test "returns 0 when both votersCount and voters are absent" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Which flavor?", + poll: %{options: ["chocolate", "vanilla"], expires_in: 20} + }) + + object = Object.normalize(activity, fetch: false) + + data = + object.data + |> Map.delete("votersCount") + |> Map.delete("voters") + + object = %{object | data: data} + + result = PollView.render("show.json", %{object: object}) + + assert result[:voters_count] == 0 + end + + test "returns 0 when voters list is empty" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Which flavor?", + poll: %{options: ["chocolate", "vanilla"], expires_in: 20} + }) + + object = Object.normalize(activity, fetch: false) + + data = + object.data + |> Map.delete("votersCount") + |> Map.put("voters", []) + + object = %{object | data: data} + + result = PollView.render("show.json", %{object: object}) + + assert result[:voters_count] == 0 + end + + test "does not inflate votersCount when same voter picks multiple options" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Pick several", + poll: %{options: ["a", "b", "c"], expires_in: 20, multiple: true} + }) + + object = Object.normalize(activity, fetch: false) + + voter = insert(:user) + {:ok, _, object} = CommonAPI.vote(object, voter, [0, 2]) + + assert object.data["votersCount"] == 1 + assert length(object.data["voters"]) == 1 + end + + test "preserves votersCount from remote source when existing voter picks another option" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Pick several", + poll: %{options: ["a", "b"], expires_in: 20, multiple: true} + }) + + object = Object.normalize(activity, fetch: false) + + voter = insert(:user) + {:ok, _, object} = CommonAPI.vote(object, voter, [0, 1]) + + object = %{object | data: Map.put(object.data, "votersCount", 14)} + + result = PollView.render("show.json", %{object: object}) + + assert result[:voters_count] == 14 + end + + test "returns 0 when votersCount is explicitly 0" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Pick one", + poll: %{options: ["a", "b"], expires_in: 20} + }) + + object = Object.normalize(activity, fetch: false) + + object = %{object | data: Map.put(object.data, "votersCount", 0)} + + result = PollView.render("show.json", %{object: object}) + + assert result[:voters_count] == 0 + end + + test "falls back to voters list when votersCount is nil" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Pick one", + poll: %{options: ["a", "b"], expires_in: 20} + }) + + object = Object.normalize(activity, fetch: false) + + voter = insert(:user) + {:ok, _, object} = CommonAPI.vote(object, voter, [0]) + + object = %{object | data: Map.put(object.data, "votersCount", nil)} + + result = PollView.render("show.json", %{object: object}) + + assert result[:voters_count] == length(object.data["voters"]) + end end From c62d1919867b287338f497d089acc81cd5b9a31b Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Wed, 6 May 2026 11:48:20 +0400 Subject: [PATCH 114/127] Add changelog for votersCount inflation fix --- changelog.d/poll-voters-count-inflation.fix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/poll-voters-count-inflation.fix diff --git a/changelog.d/poll-voters-count-inflation.fix b/changelog.d/poll-voters-count-inflation.fix new file mode 100644 index 000000000..7eae41f13 --- /dev/null +++ b/changelog.d/poll-voters-count-inflation.fix @@ -0,0 +1 @@ +Fix votersCount inflation when same voter picks multiple options From 4873991983ca396a86a7a86c2b423ff29936afa2 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 6 May 2026 15:40:47 +0200 Subject: [PATCH 115/127] Update Pleroma-FE build artifacts URL --- changelog.d/pleroma-fe-link.fix | 1 + config/config.exs | 2 +- lib/pleroma/frontend.ex | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog.d/pleroma-fe-link.fix diff --git a/changelog.d/pleroma-fe-link.fix b/changelog.d/pleroma-fe-link.fix new file mode 100644 index 000000000..de93f86dd --- /dev/null +++ b/changelog.d/pleroma-fe-link.fix @@ -0,0 +1 @@ +Updated Pleroma-FE build URL after Forgejo migration diff --git a/config/config.exs b/config/config.exs index 5bf2c5c2e..82bdd7750 100644 --- a/config/config.exs +++ b/config/config.exs @@ -775,7 +775,7 @@ config :pleroma, :frontends, "name" => "pleroma-fe", "git" => "https://git.pleroma.social/pleroma/pleroma-fe", "build_url" => - "https://git.pleroma.social/pleroma/pleroma-fe/-/jobs/artifacts/${ref}/download?job=build", + "https://git.pleroma.social/api/packages/pleroma/generic/pleroma-fe-builds/${ref}/latest.zip", "ref" => "develop" }, "fedi-fe" => %{ diff --git a/lib/pleroma/frontend.ex b/lib/pleroma/frontend.ex index e651d7d9d..e37a3fefe 100644 --- a/lib/pleroma/frontend.ex +++ b/lib/pleroma/frontend.ex @@ -75,8 +75,8 @@ defmodule Pleroma.Frontend do end defp download_build(frontend_info, dest) do - Logger.info("Downloading pre-built bundle for #{frontend_info["name"]}") url = String.replace(frontend_info["build_url"], "${ref}", frontend_info["ref"]) + Logger.info("Downloading pre-built bundle for #{frontend_info["name"]} from #{url}") with {:ok, %{status: 200, body: zip_body}} <- Pleroma.HTTP.get(url, [], pool: :media, recv_timeout: 120_000) do From 6b86e31e5d9d5d6c2175c12c3ea086de101ea33b Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 11 May 2026 14:53:06 +0400 Subject: [PATCH 116/127] Add backend MFM support --- config/config.exs | 3 +- config/description.exs | 11 +- lib/pleroma/formatter.ex | 11 ++ .../article_note_page_validator.ex | 113 ++++++++++++ .../object_validators/common_fields.ex | 1 + lib/pleroma/web/activity_pub/utils.ex | 3 +- lib/pleroma/web/common_api/activity_draft.ex | 7 +- lib/pleroma/web/common_api/utils.ex | 18 ++ mix.exs | 3 + mix.lock | 1 + priv/scrubbers/default.ex | 42 ++++- test/mix/tasks/pleroma/config_test.exs | 8 +- .../article_note_page_validator_test.exs | 165 ++++++++++++++++++ test/pleroma/web/activity_pub/utils_test.exs | 6 +- test/pleroma/web/common_api_test.exs | 41 +++++ 15 files changed, 423 insertions(+), 10 deletions(-) diff --git a/config/config.exs b/config/config.exs index 82bdd7750..2d38e3ebe 100644 --- a/config/config.exs +++ b/config/config.exs @@ -203,7 +203,8 @@ config :pleroma, :instance, "text/plain", "text/html", "text/markdown", - "text/bbcode" + "text/bbcode", + "text/x.misskeymarkdown" ], autofollowed_nicknames: [], autofollowing_nicknames: [], diff --git a/config/description.exs b/config/description.exs index c388d17c3..6e4348907 100644 --- a/config/description.exs +++ b/config/description.exs @@ -815,7 +815,8 @@ config :pleroma, :config_description, [ "text/plain", "text/html", "text/markdown", - "text/bbcode" + "text/bbcode", + "text/x.misskeymarkdown" ] }, %{ @@ -1394,7 +1395,13 @@ config :pleroma, :config_description, [ label: "Post Content Type", type: {:dropdown, :atom}, description: "Default post formatting option", - suggestions: ["text/plain", "text/html", "text/markdown", "text/bbcode"] + suggestions: [ + "text/plain", + "text/html", + "text/markdown", + "text/bbcode", + "text/x.misskeymarkdown" + ] }, %{ key: :redirectRootNoLogin, diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex index 11d5af2fb..4bf2f6b95 100644 --- a/lib/pleroma/formatter.ex +++ b/lib/pleroma/formatter.ex @@ -127,6 +127,13 @@ defmodule Pleroma.Formatter do Earmark.as_html!(text, %Earmark.Options{compact_output: true, smartypants: false}) end + def markdown_to_html(text, opts) do + Earmark.as_html!( + text, + %Earmark.Options{compact_output: true, smartypants: false} |> Map.merge(opts) + ) + end + def html_escape({text, mentions, hashtags}, type) do {html_escape(text, type), mentions, hashtags} end @@ -135,6 +142,10 @@ defmodule Pleroma.Formatter do HTML.filter_tags(text) end + def html_escape(text, "text/x.misskeymarkdown") do + HTML.filter_tags(text) + end + def html_escape(text, "text/plain") do Regex.split(@link_regex, text, include_captures: true) |> Enum.map_every(2, fn chunk -> diff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex index c0626ce4d..ec9259f95 100644 --- a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex @@ -6,6 +6,9 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do use Ecto.Schema alias Pleroma.EctoType.ActivityPub.ObjectValidators + alias Pleroma.HTML + alias Pleroma.User + alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations alias Pleroma.Web.ActivityPub.Transmogrifier @@ -26,6 +29,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do end field(:replies, {:array, ObjectValidators.ObjectID}, default: []) + field(:source, :map) end def cast_and_apply(data) do @@ -80,6 +84,113 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do def fix_attachments(data), do: data + defp remote_mention_resolver( + %{"id" => ap_id, "tag" => tags}, + "@" <> nickname = mention, + buffer, + opts, + acc + ) + when is_binary(ap_id) and is_list(tags) do + initial_host = + ap_id + |> URI.parse() + |> Map.get(:host) + + with mention_tag when not is_nil(mention_tag) <- + Enum.find(tags, &mention_tag?(&1, mention, initial_host)), + href when is_binary(href) <- mention_tag["href"], + %User{} = user <- User.get_cached_by_ap_id(href) do + link = Pleroma.Formatter.mention_from_user(user, opts) + {link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, user})}} + else + _ -> {buffer, acc} + end + end + + defp remote_mention_resolver(_object, _mention, buffer, _opts, acc), do: {buffer, acc} + + defp mention_tag?(%{"type" => "Mention", "name" => name}, mention, initial_host) + when is_binary(name) do + name == mention || mention == "#{name}@#{initial_host}" + end + + defp mention_tag?(_tag, _mention, _initial_host), do: false + + defp scrub_content(%{"content" => content} = object) when is_binary(content) do + Map.put(object, "content", HTML.filter_tags(content)) + end + + defp scrub_content(object), do: object + + defp mfm_parse_limit do + min(Pleroma.Config.get([:instance, :limit]), Pleroma.Config.get([:instance, :remote_limit])) + end + + defp normalize_source(%{"source" => source} = object) when is_binary(source) do + object + |> Map.put("source", %{"content" => source}) + |> normalize_source() + end + + defp normalize_source(%{"source" => source} = object) when is_map(source) do + source = + case source["content"] do + content when is_binary(content) -> + if String.length(content) <= mfm_parse_limit() do + source + else + Map.delete(source, "content") + end + + nil -> + source + + _ -> + Map.delete(source, "content") + end + + Map.put(object, "source", source) + end + + defp normalize_source(object), do: object + + defp fix_misskey_content(%{"htmlMfm" => true, "content" => content} = object) + when is_binary(content) do + Map.put(object, "content", HTML.filter_tags(content)) + end + + defp fix_misskey_content(%{"htmlMfm" => true} = object), do: object + + defp fix_misskey_content( + %{"source" => %{"mediaType" => "text/x.misskeymarkdown", "content" => content}} = object + ) + when is_binary(content) do + mention_handler = fn nick, buffer, opts, acc -> + remote_mention_resolver(object, nick, buffer, opts, acc) + end + + {linked, _mentions, _tags} = + Utils.format_input(content, "text/x.misskeymarkdown", mention_handler: mention_handler) + + Map.put(object, "content", linked) + end + + defp fix_misskey_content(%{"source" => %{"mediaType" => "text/x.misskeymarkdown"}} = object), + do: scrub_content(object) + + defp fix_misskey_content(%{"_misskey_content" => content} = object) when is_binary(content) do + object + |> Map.put("source", %{ + "content" => content, + "mediaType" => "text/x.misskeymarkdown" + }) + |> Map.delete("_misskey_content") + |> fix_misskey_content() + end + + defp fix_misskey_content(object), do: object + defp fix(data) do data |> CommonFixes.fix_actor() @@ -88,6 +199,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do |> fix_tag() |> fix_replies() |> fix_attachments() + |> normalize_source() + |> fix_misskey_content() |> CommonFixes.fix_quote_url() |> CommonFixes.fix_likes() |> Transmogrifier.fix_emoji() diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex index 22cf0cc05..9b8580200 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex @@ -32,6 +32,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do quote bind_quoted: binding() do field(:content, :string) field(:contentMap, ObjectValidators.ContentLanguageMap) + field(:htmlMfm, :boolean) field(:published, ObjectValidators.DateTime) field(:updated, ObjectValidators.DateTime) diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 43c0f456d..0af4ceaf5 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -120,7 +120,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do "https://www.w3.org/ns/activitystreams", "#{Endpoint.url()}/schemas/litepub-0.1.jsonld", %{ - "@language" => get_language(data) + "@language" => get_language(data), + "htmlMfm" => "https://w3id.org/fep/c16b#htmlMfm" } ] } diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index 16489663a..6072fff6b 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -317,6 +317,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do emoji = Map.merge(emoji, summary_emoji) + media_type = Utils.get_content_type(draft.params[:content_type]) {:ok, note_data, _meta} = Builder.note(draft) object = @@ -324,14 +325,18 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do |> Map.put("emoji", emoji) |> Map.put("source", %{ "content" => draft.status, - "mediaType" => Utils.get_content_type(draft.params[:content_type]) + "mediaType" => media_type }) + |> maybe_put("htmlMfm", true, media_type == "text/x.misskeymarkdown") |> Map.put("generator", draft.params[:generator]) |> Map.put("language", draft.language) %{draft | object: object} end + defp maybe_put(map, key, value, true), do: Map.put(map, key, value) + defp maybe_put(map, _key, _value, _condition), do: map + defp preview?(%__MODULE__{} = draft) do preview? = Pleroma.Web.Utils.Params.truthy_param?(draft.params[:preview]) %{draft | preview?: preview?} diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 32572a721..26034d685 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -322,6 +322,14 @@ defmodule Pleroma.Web.CommonAPI.Utils do |> Formatter.linkify(options) end + def format_input(text, "text/x.misskeymarkdown", options) do + text + |> Formatter.markdown_to_html(%{breaks: true}) + |> safe_mfm_to_html() + |> Formatter.linkify(options) + |> Formatter.html_escape("text/x.misskeymarkdown") + end + def format_input(text, "text/markdown", options) do text |> Formatter.mentions_escape(options) @@ -330,6 +338,16 @@ defmodule Pleroma.Web.CommonAPI.Utils do |> Formatter.html_escape("text/html") end + defp safe_mfm_to_html(html) do + html + |> MfmParser.Parser.parse() + |> MfmParser.Encoder.to_html() + rescue + _ -> html + catch + _, _ -> html + end + def format_naive_asctime(date) do date |> DateTime.from_naive!("Etc/UTC") |> format_asctime end diff --git a/mix.exs b/mix.exs index bb7bfb9da..88f98d54e 100644 --- a/mix.exs +++ b/mix.exs @@ -160,6 +160,9 @@ defmodule Pleroma.Mixfile do {:sweet_xml, "~> 0.7.5"}, {:earmark, "1.4.46"}, {:bbcode_pleroma, "~> 0.2.0"}, + {:mfm_parser, + git: "https://akkoma.dev/AkkomaGang/mfm-parser.git", + ref: "360a30267a847810a63ab48f606ba227b2ca05f0"}, {:cors_plug, "~> 2.0"}, {:web_push_encryption, "~> 0.3.1"}, {:swoosh, "~> 1.16.12"}, diff --git a/mix.lock b/mix.lock index dd5f65f11..976a856b0 100644 --- a/mix.lock +++ b/mix.lock @@ -79,6 +79,7 @@ "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, + "mfm_parser": {:git, "https://akkoma.dev/AkkomaGang/mfm-parser.git", "360a30267a847810a63ab48f606ba227b2ca05f0", [ref: "360a30267a847810a63ab48f606ba227b2ca05f0"]}, "mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"}, "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"}, diff --git a/priv/scrubbers/default.ex b/priv/scrubbers/default.ex index 0defdc74e..342ef9944 100644 --- a/priv/scrubbers/default.ex +++ b/priv/scrubbers/default.ex @@ -82,12 +82,50 @@ defmodule Pleroma.HTML.Scrubber.Default do "recipients-inline", "quote-inline", "invisible", - "ellipsis" + "ellipsis", + "mfm-center", + "mfm-flip", + "mfm-font", + "mfm-blur", + "mfm-rotate", + "mfm-x2", + "mfm-x3", + "mfm-x4", + "mfm-position", + "mfm-scale", + "mfm-fg", + "mfm-bg", + "mfm-jelly", + "mfm-twitch", + "mfm-shake", + "mfm-spin", + "mfm-jump", + "mfm-bounce", + "mfm-rainbow", + "mfm-tada", + "mfm-sparkle" ]) Meta.allow_tag_with_this_attribute_values(:p, "class", ["quote-inline"]) - Meta.allow_tag_with_these_attributes(:span, ["lang"]) + Meta.allow_tag_with_these_attributes(:span, [ + "lang", + "data-mfm-h", + "data-mfm-v", + "data-mfm-x", + "data-mfm-y", + "data-mfm-alternate", + "data-mfm-speed", + "data-mfm-deg", + "data-mfm-left", + "data-mfm-serif", + "data-mfm-monospace", + "data-mfm-cursive", + "data-mfm-fantasy", + "data-mfm-emoji", + "data-mfm-math", + "data-mfm-color" + ]) Meta.allow_tag_with_this_attribute_values(:code, "class", ["inline"]) diff --git a/test/mix/tasks/pleroma/config_test.exs b/test/mix/tasks/pleroma/config_test.exs index ef1adc235..f672d8c13 100644 --- a/test/mix/tasks/pleroma/config_test.exs +++ b/test/mix/tasks/pleroma/config_test.exs @@ -144,7 +144,13 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do quarantined_instances: [], managed_config: true, static_dir: "instance/static/", - allowed_post_formats: ["text/plain", "text/html", "text/markdown", "text/bbcode"], + allowed_post_formats: [ + "text/plain", + "text/html", + "text/markdown", + "text/bbcode", + "text/x.misskeymarkdown" + ], autofollowed_nicknames: [], max_pinned_statuses: 1, attachment_links: false, 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 c32811c5b..bf9c70fb6 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 @@ -149,6 +149,171 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest %{valid?: true} = ArticleNotePageValidator.cast_and_validate(note) end + test "a Misskey MFM note is rendered from source content" do + user = insert(:user, ap_id: "https://misskey.example/users/alice") + + note = %{ + "id" => "https://misskey.example/notes/1", + "type" => "Note", + "actor" => user.ap_id, + "attributedTo" => user.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "content" => "original content", + "context" => Utils.generate_context_id(), + "source" => %{ + "content" => "$[spin.speed=1s mfm goes here] ", + "mediaType" => "text/x.misskeymarkdown" + } + } + + %{valid?: true, changes: %{content: content, source: source}} = + ArticleNotePageValidator.cast_and_validate(note) + + assert source["mediaType"] == "text/x.misskeymarkdown" + assert content =~ ~s(class="mfm-spin") + assert content =~ ~s(data-mfm-speed="1s") + assert content =~ "mfm goes here" + refute content =~ "original content" + refute content =~ " "https://misskey.example/notes/3", + "type" => "Note", + "actor" => remote_user.ap_id, + "attributedTo" => remote_user.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "content" => "original content", + "context" => Utils.generate_context_id(), + "tag" => [ + %{ + "type" => "Mention", + "name" => "@local_user", + "href" => local_user.ap_id + }, + %{ + "type" => "Mention", + "name" => "@uncached", + "href" => "https://misskey.example/users/uncached" + } + ], + "source" => %{ + "content" => "@local_user @uncached $[spin hello]", + "mediaType" => "text/x.misskeymarkdown" + } + } + + %{valid?: true, changes: %{content: content}} = + ArticleNotePageValidator.cast_and_validate(note) + + assert content =~ local_user.ap_id + assert content =~ "@uncached" + end + + test "a Misskey MFM note drops oversized source content instead of parsing it" do + user = insert(:user, ap_id: "https://misskey.example/users/oversized") + + note = %{ + "id" => "https://misskey.example/notes/4", + "type" => "Note", + "actor" => user.ap_id, + "attributedTo" => user.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "content" => "safe fallback", + "context" => Utils.generate_context_id(), + "source" => %{ + "content" => String.duplicate("x", 5_001), + "mediaType" => "text/x.misskeymarkdown" + } + } + + %{valid?: true, changes: %{content: content, source: source}} = + ArticleNotePageValidator.cast_and_validate(note) + + assert content == "safe fallback" + refute Map.has_key?(source, "content") + end + + test "a note drops oversized non-MFM source content" do + user = insert(:user, ap_id: "https://example.com/users/source") + + note = %{ + "id" => "https://example.com/notes/1", + "type" => "Note", + "actor" => user.ap_id, + "attributedTo" => user.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "content" => "regular content", + "context" => Utils.generate_context_id(), + "source" => %{ + "content" => String.duplicate("x", 5_001), + "mediaType" => "text/markdown" + } + } + + %{valid?: true, changes: %{source: source}} = ArticleNotePageValidator.cast_and_validate(note) + + assert source == %{"mediaType" => "text/markdown"} + end + + test "a Misskey MFM note with legacy _misskey_content is rendered" do + user = insert(:user, ap_id: "https://misskey.example/users/legacy") + + note = %{ + "id" => "https://misskey.example/notes/5", + "type" => "Note", + "actor" => user.ap_id, + "attributedTo" => user.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "content" => "original content", + "context" => Utils.generate_context_id(), + "_misskey_content" => "$[spin legacy]" + } + + %{valid?: true, changes: %{content: content, source: source}} = + ArticleNotePageValidator.cast_and_validate(note) + + assert source == %{"content" => "$[spin legacy]", "mediaType" => "text/x.misskeymarkdown"} + assert content =~ ~s(class="mfm-spin") + assert content =~ "legacy" + end + + test "a Misskey MFM note with htmlMfm is scrubbed but not rendered from source content" do + user = insert(:user, ap_id: "https://misskey.example/users/bob") + + note = %{ + "id" => "https://misskey.example/notes/2", + "type" => "Note", + "actor" => user.ap_id, + "attributedTo" => user.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "content" => + "already rendered", + "htmlMfm" => true, + "context" => Utils.generate_context_id(), + "source" => %{ + "content" => String.duplicate("x", 5_001), + "mediaType" => "text/x.misskeymarkdown" + } + } + + %{valid?: true, changes: %{content: content, htmlMfm: true, source: source}} = + ArticleNotePageValidator.cast_and_validate(note) + + assert content == "already renderedalert('xss')" + refute Map.has_key?(source, "content") + end + test "a Note with validated likes collection validates" do insert(:user, ap_id: "https://pol.social/users/mkljczk") diff --git a/test/pleroma/web/activity_pub/utils_test.exs b/test/pleroma/web/activity_pub/utils_test.exs index 3b77f0867..93234a015 100644 --- a/test/pleroma/web/activity_pub/utils_test.exs +++ b/test/pleroma/web/activity_pub/utils_test.exs @@ -180,7 +180,8 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do "https://www.w3.org/ns/activitystreams", "http://localhost:4001/schemas/litepub-0.1.jsonld", %{ - "@language" => "und" + "@language" => "und", + "htmlMfm" => "https://w3id.org/fep/c16b#htmlMfm" } ] } @@ -192,7 +193,8 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do "https://www.w3.org/ns/activitystreams", "http://localhost:4001/schemas/litepub-0.1.jsonld", %{ - "@language" => "pl" + "@language" => "pl", + "htmlMfm" => "https://w3id.org/fep/c16b#htmlMfm" } ] } diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index 017fac696..ea1795c0b 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -709,6 +709,47 @@ defmodule Pleroma.Web.CommonAPITest do assert object.data["source"]["content"] == post end + test "it renders MFM posts and marks their ActivityPub representation" do + user = insert(:user) + + post = "

$[spin.speed=1s 13:37]

" + + {:ok, activity} = + CommonAPI.post(user, %{ + status: post, + content_type: "text/x.misskeymarkdown" + }) + + object = Object.normalize(activity, fetch: false) + + assert object.data["htmlMfm"] == true + + assert object.data["source"] == %{ + "content" => post, + "mediaType" => "text/x.misskeymarkdown" + } + + assert object.data["content"] =~ ~s(class="mfm-spin") + assert object.data["content"] =~ ~s(data-mfm-speed="1s") + assert object.data["content"] =~ "13:37" + refute object.data["content"] =~ "scrub-this" + end + + test "it falls back safely for malformed MFM" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "$[spin.speed=1s=boom malformed]", + content_type: "text/x.misskeymarkdown" + }) + + object = Object.normalize(activity, fetch: false) + + refute object.data["content"] =~ ~s(class="mfm-spin") + assert object.data["content"] =~ "malformed" + end + test "it does not allow replies to direct messages that are not direct messages themselves" do user = insert(:user) From c780298ce71577a1bffb755d6f183f049e6edf6e Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 11 May 2026 15:05:00 +0400 Subject: [PATCH 117/127] Add changelog for MFM support --- changelog.d/mfm-backend.add | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/mfm-backend.add diff --git a/changelog.d/mfm-backend.add b/changelog.d/mfm-backend.add new file mode 100644 index 000000000..f815c6828 --- /dev/null +++ b/changelog.d/mfm-backend.add @@ -0,0 +1 @@ +Add backend support for Misskey Markdown (MFM) posts From 47021b5aba4151284aefeeddbc8663ac80d5e19b Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 11 May 2026 16:37:59 +0400 Subject: [PATCH 118/127] Fix MFM validator alias ordering --- .../object_validators/article_note_page_validator.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex index ec9259f95..844ad2c8f 100644 --- a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex @@ -8,10 +8,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.HTML alias Pleroma.User - alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.CommonAPI.Utils import Ecto.Changeset From 592be493c825e324b9a161e984ad39d412575184 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 11 May 2026 19:12:03 +0400 Subject: [PATCH 119/127] Use published Pleroma MFM parser package --- mix.exs | 4 +--- mix.lock | 2 +- test/pleroma/web/common_api_test.exs | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/mix.exs b/mix.exs index 88f98d54e..db9a9b5ae 100644 --- a/mix.exs +++ b/mix.exs @@ -160,9 +160,7 @@ defmodule Pleroma.Mixfile do {:sweet_xml, "~> 0.7.5"}, {:earmark, "1.4.46"}, {:bbcode_pleroma, "~> 0.2.0"}, - {:mfm_parser, - git: "https://akkoma.dev/AkkomaGang/mfm-parser.git", - ref: "360a30267a847810a63ab48f606ba227b2ca05f0"}, + {:pleroma_mfm_parser, "~> 0.2.1"}, {:cors_plug, "~> 2.0"}, {:web_push_encryption, "~> 0.3.1"}, {:swoosh, "~> 1.16.12"}, diff --git a/mix.lock b/mix.lock index 976a856b0..af566967c 100644 --- a/mix.lock +++ b/mix.lock @@ -79,7 +79,6 @@ "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, - "mfm_parser": {:git, "https://akkoma.dev/AkkomaGang/mfm-parser.git", "360a30267a847810a63ab48f606ba227b2ca05f0", [ref: "360a30267a847810a63ab48f606ba227b2ca05f0"]}, "mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"}, "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"}, @@ -113,6 +112,7 @@ "phoenix_swoosh": {:hex, :phoenix_swoosh, "1.2.1", "b74ccaa8046fbc388a62134360ee7d9742d5a8ae74063f34eb050279de7a99e1", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "4000eeba3f9d7d1a6bf56d2bd56733d5cadf41a7f0d8ffe5bb67e7d667e204a2"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, + "pleroma_mfm_parser": {:hex, :pleroma_mfm_parser, "0.2.1", "87a40fec4e2e035447df34e1c98e1b816b6a448d49baea73eb2273531ac5ce66", [:mix], [], "hexpm", "ec4845461da95d63ab3d592086d65114c45057a1f2d48d768f3def45b0d7696f"}, "plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"}, "plug_cowboy": {:hex, :plug_cowboy, "2.7.4", "729c752d17cf364e2b8da5bdb34fb5804f56251e88bb602aff48ae0bd8673d11", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9b85632bd7012615bae0a5d70084deb1b25d2bcbb32cab82d1e9a1e023168aa3"}, "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index ea1795c0b..5d0ad4572 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -740,7 +740,7 @@ defmodule Pleroma.Web.CommonAPITest do {:ok, activity} = CommonAPI.post(user, %{ - status: "$[spin.speed=1s=boom malformed]", + status: "$[spin malformed", content_type: "text/x.misskeymarkdown" }) From 61feb3dfcdebf29186e214a92bd62c6cb1ac9583 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 11 May 2026 21:01:59 +0400 Subject: [PATCH 120/127] Update http_signatures to 0.1.3 --- mix.exs | 2 +- mix.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.exs b/mix.exs index db9a9b5ae..9c10cacc0 100644 --- a/mix.exs +++ b/mix.exs @@ -173,7 +173,7 @@ defmodule Pleroma.Mixfile do {:timex, "~> 3.7"}, {:ueberauth, "~> 0.10"}, {:linkify, "~> 0.5.3"}, - {:http_signatures, "~> 0.1.2"}, + {:http_signatures, "~> 0.1.3"}, {:telemetry, "~> 1.0.0", override: true}, {:poolboy, "~> 1.5"}, {:prom_ex, "~> 1.9"}, diff --git a/mix.lock b/mix.lock index af566967c..da0c1c60c 100644 --- a/mix.lock +++ b/mix.lock @@ -62,7 +62,7 @@ "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"}, + "http_signatures": {:hex, :http_signatures, "0.1.3", "19f26aee35b4684e37efdce3ac4638605e6e41a04368186bd39d2b6138a60ea9", [:mix], [{:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "20313a65516db88006f85b090f6f76cc5b04e9609b45943657e6781eb91174f4"}, "httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "inet_cidr": {:hex, :inet_cidr, "1.0.8", "d26bb7bdbdf21ae401ead2092bf2bb4bf57fe44a62f5eaa5025280720ace8a40", [:mix], [], "hexpm", "d5b26da66603bb56c933c65214c72152f0de9a6ea53618b56d63302a68f6a90e"}, From 7f4890b6a9beb60c8daeb59a54d19113386ebdd8 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 11 May 2026 21:12:34 +0400 Subject: [PATCH 121/127] Add changelog for http_signatures update --- changelog.d/http-signatures-0.1.3.fix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/http-signatures-0.1.3.fix diff --git a/changelog.d/http-signatures-0.1.3.fix b/changelog.d/http-signatures-0.1.3.fix new file mode 100644 index 000000000..3be1b9e9f --- /dev/null +++ b/changelog.d/http-signatures-0.1.3.fix @@ -0,0 +1 @@ +Fix compatibility with timestamped HTTP Signatures used by GoToSocial From 71afba4825eaa009e506a74e11dfece99dc08e39 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Tue, 12 May 2026 08:52:42 +0400 Subject: [PATCH 122/127] Signature: Treat HTTP signature errors as invalid --- lib/pleroma/signature.ex | 2 +- test/pleroma/signature_test.exs | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/signature.ex b/lib/pleroma/signature.ex index fca61799b..382cf9db3 100644 --- a/lib/pleroma/signature.ex +++ b/lib/pleroma/signature.ex @@ -104,7 +104,7 @@ defmodule Pleroma.Signature do |> put_req_header("(request-target)", request_target) |> put_req_header("@request-target", request_target) - @http_signatures_impl.validate_conn(conn) + @http_signatures_impl.validate_conn(conn) == true end @spec validate_signature(Plug.Conn.t()) :: boolean() diff --git a/test/pleroma/signature_test.exs b/test/pleroma/signature_test.exs index 572d7acc3..0c7c4c840 100644 --- a/test/pleroma/signature_test.exs +++ b/test/pleroma/signature_test.exs @@ -11,6 +11,7 @@ defmodule Pleroma.SignatureTest do import Mock alias Pleroma.Signature + alias Pleroma.StubbedHTTPSignaturesMock, as: HTTPSignaturesMock setup do mock(fn env -> apply(HttpRequestMock, :request, [env]) end) @@ -103,6 +104,18 @@ defmodule Pleroma.SignatureTest do end end + describe "validate_signature/1" do + test "treats HTTP signature errors as failed validation" do + conn = %Plug.Conn{method: "GET", request_path: "/inbox", req_headers: []} + + Mox.expect(HTTPSignaturesMock, :validate_conn, fn _conn -> + {:error, :request_target_header} + end) + + assert Signature.validate_signature(conn) == false + end + end + describe "key_id_to_actor_id/1" do test "it properly deduces the actor id for misskey" do assert Signature.key_id_to_actor_id("https://example.com/users/1234/publickey") == From 0cf865f0254f33fba312a9342c635b7300ea0291 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Tue, 12 May 2026 23:50:30 +0400 Subject: [PATCH 123/127] Reject third-party remote reports --- changelog.d/reject-third-party-reports.fix | 1 + lib/pleroma/web/activity_pub/transmogrifier.ex | 7 +++++++ .../web/activity_pub/transmogrifier_test.exs | 17 +++++++++++++++++ 3 files changed, 25 insertions(+) create mode 100644 changelog.d/reject-third-party-reports.fix diff --git a/changelog.d/reject-third-party-reports.fix b/changelog.d/reject-third-party-reports.fix new file mode 100644 index 000000000..7d4e87b94 --- /dev/null +++ b/changelog.d/reject-third-party-reports.fix @@ -0,0 +1 @@ +Reject incoming reports when both the reporter and reported account are remote diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 4421da26c..261272d08 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -430,6 +430,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end) end + defp reject_third_party_report(%User{local: false}, %User{local: false} = account) do + {:reject, "[Transmogrifier] third-party report: #{account.ap_id}"} + end + + defp reject_third_party_report(_, _), do: :ok + def handle_incoming(data, options \\ []) do data |> fix_recursive(&strip_internal_fields/1) @@ -447,6 +453,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do %User{} = actor <- User.get_cached_by_ap_id(actor), # Reduce the object list to find the reported user. %User{} = account <- get_reported(objects), + :ok <- reject_third_party_report(actor, account), # Remove the reported user from the object list. statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do %{ diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs index c1e01557d..b20f15cbd 100644 --- a/test/pleroma/web/activity_pub/transmogrifier_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs @@ -86,6 +86,23 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do assert activity.data["cc"] == [user.ap_id] end + test "it rejects Flag activities when both reporter and reported account are remote" do + reporter = insert(:user, local: false, domain: "mastodon.cat") + reported = insert(:user, local: false, domain: "nicecrew.digital") + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "actor" => reporter.ap_id, + "content" => "blocked AND reported!!!", + "object" => [reported.ap_id, "https://nicecrew.digital/objects/report-status"], + "type" => "Flag" + } + + assert {:reject, reason} = Transmogrifier.handle_incoming(message) + assert reason =~ "third-party report" + refute "Flag" |> Pleroma.Activity.Queries.by_type() |> Pleroma.Repo.one() + end + test "it accepts Move activities" do old_user = insert(:user) new_user = insert(:user) From 4d3aea1fce1a3d9b268aad2de8cae75b625ff5d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 13 May 2026 00:55:47 +0200 Subject: [PATCH 124/127] Handle reports with just actor ap id as the object MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- changelog.d/iceshrimpnet-reports-fix.fix | 1 + .../web/activity_pub/transmogrifier.ex | 1 + .../web/activity_pub/transmogrifier_test.exs | 20 +++++++++++++++++++ 3 files changed, 22 insertions(+) create mode 100644 changelog.d/iceshrimpnet-reports-fix.fix diff --git a/changelog.d/iceshrimpnet-reports-fix.fix b/changelog.d/iceshrimpnet-reports-fix.fix new file mode 100644 index 000000000..7487ae47e --- /dev/null +++ b/changelog.d/iceshrimpnet-reports-fix.fix @@ -0,0 +1 @@ +Handle reports with just actor ap id as the object diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 4421da26c..eba588cf3 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -444,6 +444,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do ) do with context <- data["context"] || Utils.generate_context_id(), content <- data["content"] || "", + objects <- List.wrap(objects), %User{} = actor <- User.get_cached_by_ap_id(actor), # Reduce the object list to find the reported user. %User{} = account <- get_reported(objects), diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs index c1e01557d..2e3da8598 100644 --- a/test/pleroma/web/activity_pub/transmogrifier_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs @@ -86,6 +86,26 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do assert activity.data["cc"] == [user.ap_id] end + test "it accepts Flag activities with just actor id as object" do + user = insert(:user) + other_user = insert(:user) + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "cc" => [user.ap_id], + "object" => user.ap_id, + "type" => "Flag", + "content" => "blocked AND reported!!!", + "actor" => other_user.ap_id + } + + assert {:ok, activity} = Transmogrifier.handle_incoming(message) + + assert activity.data["content"] == "blocked AND reported!!!" + assert activity.data["actor"] == other_user.ap_id + assert activity.data["cc"] == [user.ap_id] + end + test "it accepts Move activities" do old_user = insert(:user) new_user = insert(:user) From 9e16332d9d208abc4d3992fec27b317c932f7e07 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Wed, 13 May 2026 18:39:40 +0400 Subject: [PATCH 125/127] Update majic to 1.2.0 --- changelog.d/majic-1.2.0.change | 1 + mix.exs | 4 +++- mix.lock | 2 +- test/pleroma/dependency_version_test.exs | 16 ++++++++++++++++ 4 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 changelog.d/majic-1.2.0.change create mode 100644 test/pleroma/dependency_version_test.exs diff --git a/changelog.d/majic-1.2.0.change b/changelog.d/majic-1.2.0.change new file mode 100644 index 000000000..bc8d5eb3c --- /dev/null +++ b/changelog.d/majic-1.2.0.change @@ -0,0 +1 @@ +Update majic to 1.2.0. diff --git a/mix.exs b/mix.exs index 9c10cacc0..872929c2c 100644 --- a/mix.exs +++ b/mix.exs @@ -191,7 +191,9 @@ defmodule Pleroma.Mixfile do git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", ref: "e7b7cc34cc16b383461b966484c297e4ec9aeef6"}, {:restarter, path: "./restarter"}, - {:majic, "~> 1.1"}, + {:majic, "1.2.0", + git: "https://git.pleroma.social/pleroma-elixir-libraries/majic.git", + ref: "1f2df25cccdd9548fd59dbfa402c996268ae99ec"}, {:open_api_spex, "~> 3.22"}, {:ecto_psql_extras, "~> 0.8"}, {:vix, "~> 0.36"}, diff --git a/mix.lock b/mix.lock index da0c1c60c..780dea4c0 100644 --- a/mix.lock +++ b/mix.lock @@ -73,7 +73,7 @@ "linkify": {:hex, :linkify, "0.5.3", "5f8143d8f61f5ff08d3aeeff47ef6509492b4948d8f08007fbf66e4d2246a7f2", [:mix], [], "hexpm", "3ef35a1377d47c25506e07c1c005ea9d38d700699d92ee92825f024434258177"}, "logger_backends": {:hex, :logger_backends, "1.0.0", "09c4fad6202e08cb0fbd37f328282f16539aca380f512523ce9472b28edc6bdf", [:mix], [], "hexpm", "1faceb3e7ec3ef66a8f5746c5afd020e63996df6fd4eb8cdb789e5665ae6c9ce"}, "mail": {:hex, :mail, "0.3.1", "cb0a14e4ed8904e4e5a08214e686ccf6f9099346885db17d8c309381f865cc5c", [:mix], [], "hexpm", "1db701e89865c1d5fa296b2b57b1cd587587cca8d8a1a22892b35ef5a8e352a6"}, - "majic": {:hex, :majic, "1.1.1", "16092a3a3376cc5e13d207e82ec06e05a5561170465e50cc11cc4df8a5747938", [:make, :mix], [{:elixir_make, "~> 0.8.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "7fbb0372f0447b3f777056177d6ab3f009742e68474f850521ff56b84bd85b96"}, + "majic": {:git, "https://git.pleroma.social/pleroma-elixir-libraries/majic.git", "1f2df25cccdd9548fd59dbfa402c996268ae99ec", [ref: "1f2df25cccdd9548fd59dbfa402c996268ae99ec"]}, "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, "makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, diff --git a/test/pleroma/dependency_version_test.exs b/test/pleroma/dependency_version_test.exs new file mode 100644 index 000000000..0c9c1e939 --- /dev/null +++ b/test/pleroma/dependency_version_test.exs @@ -0,0 +1,16 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.DependencyVersionTest do + use ExUnit.Case, async: true + + test "uses majic 1.2" do + majic_version = + :majic + |> Application.spec(:vsn) + |> to_string() + + assert Version.match?(majic_version, "~> 1.2") + end +end From d8e3ea69b17189aad1a3d3e60587ccf16bd3154f Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Wed, 13 May 2026 19:00:16 +0400 Subject: [PATCH 126/127] Use Hex release for majic 1.2.0 --- mix.exs | 4 +--- mix.lock | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/mix.exs b/mix.exs index 872929c2c..2cf5137a5 100644 --- a/mix.exs +++ b/mix.exs @@ -191,9 +191,7 @@ defmodule Pleroma.Mixfile do git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", ref: "e7b7cc34cc16b383461b966484c297e4ec9aeef6"}, {:restarter, path: "./restarter"}, - {:majic, "1.2.0", - git: "https://git.pleroma.social/pleroma-elixir-libraries/majic.git", - ref: "1f2df25cccdd9548fd59dbfa402c996268ae99ec"}, + {:majic, "~> 1.2"}, {:open_api_spex, "~> 3.22"}, {:ecto_psql_extras, "~> 0.8"}, {:vix, "~> 0.36"}, diff --git a/mix.lock b/mix.lock index 780dea4c0..9214478c7 100644 --- a/mix.lock +++ b/mix.lock @@ -73,7 +73,7 @@ "linkify": {:hex, :linkify, "0.5.3", "5f8143d8f61f5ff08d3aeeff47ef6509492b4948d8f08007fbf66e4d2246a7f2", [:mix], [], "hexpm", "3ef35a1377d47c25506e07c1c005ea9d38d700699d92ee92825f024434258177"}, "logger_backends": {:hex, :logger_backends, "1.0.0", "09c4fad6202e08cb0fbd37f328282f16539aca380f512523ce9472b28edc6bdf", [:mix], [], "hexpm", "1faceb3e7ec3ef66a8f5746c5afd020e63996df6fd4eb8cdb789e5665ae6c9ce"}, "mail": {:hex, :mail, "0.3.1", "cb0a14e4ed8904e4e5a08214e686ccf6f9099346885db17d8c309381f865cc5c", [:mix], [], "hexpm", "1db701e89865c1d5fa296b2b57b1cd587587cca8d8a1a22892b35ef5a8e352a6"}, - "majic": {:git, "https://git.pleroma.social/pleroma-elixir-libraries/majic.git", "1f2df25cccdd9548fd59dbfa402c996268ae99ec", [ref: "1f2df25cccdd9548fd59dbfa402c996268ae99ec"]}, + "majic": {:hex, :majic, "1.2.0", "414b69c1460ece692fa892a0ed6669b8db6a44c42bb3071cb723854f22e7bd78", [:make, :mix], [{:elixir_make, "~> 0.8.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0bd0c65f8e4dcc595757d957577f6151b59246da9614ba87debfdc641ccc0514"}, "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, "makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, From d0c2d04356bbed3b2be2b9873a6c207f54021285 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Wed, 13 May 2026 19:10:15 +0400 Subject: [PATCH 127/127] Use Hex release for oban_plugins_lazarus --- changelog.d/oban-plugins-lazarus-hex.change | 1 + mix.exs | 4 +--- mix.lock | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) create mode 100644 changelog.d/oban-plugins-lazarus-hex.change diff --git a/changelog.d/oban-plugins-lazarus-hex.change b/changelog.d/oban-plugins-lazarus-hex.change new file mode 100644 index 000000000..9259df0f5 --- /dev/null +++ b/changelog.d/oban-plugins-lazarus-hex.change @@ -0,0 +1 @@ +Use the Hex package for oban_plugins_lazarus. diff --git a/mix.exs b/mix.exs index 2cf5137a5..946bf0ab0 100644 --- a/mix.exs +++ b/mix.exs @@ -137,9 +137,7 @@ defmodule Pleroma.Mixfile do {:tzdata, "~> 1.0.5"}, {:plug_cowboy, "~> 2.7"}, {:oban, "~> 2.19.4"}, - {:oban_plugins_lazarus, - git: "https://git.pleroma.social/pleroma/elixir-libraries/oban_plugins_lazarus.git", - ref: "e49fc355baaf0e435208bf5f534d31e26e897711"}, + {:oban_plugins_lazarus, "~> 0.1.1"}, {:oban_web, "~> 2.11"}, {:gettext, "~> 0.24"}, {:bcrypt_elixir, "~> 2.3"}, diff --git a/mix.lock b/mix.lock index 9214478c7..f33c22482 100644 --- a/mix.lock +++ b/mix.lock @@ -96,7 +96,7 @@ "oban": {:hex, :oban, "2.19.4", "045adb10db1161dceb75c254782f97cdc6596e7044af456a59decb6d06da73c1", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5fcc6219e6464525b808d97add17896e724131f498444a292071bf8991c99f97"}, "oban_live_dashboard": {:hex, :oban_live_dashboard, "0.1.1", "8aa4ceaf381c818f7d5c8185cc59942b8ac82ef0cf559881aacf8d3f8ac7bdd3", [:mix], [{:oban, "~> 2.15", [hex: :oban, repo: "hexpm", optional: false]}, {:phoenix_live_dashboard, "~> 0.7", [hex: :phoenix_live_dashboard, repo: "hexpm", optional: false]}], "hexpm", "16dc4ce9c9a95aa2e655e35ed4e675652994a8def61731a18af85e230e1caa63"}, "oban_met": {:hex, :oban_met, "1.0.5", "bb633ab06448dab2ef9194f6688d33b3d07fc3f2ad793a1a08f4dfbb2cc9fe50", [:mix], [{:oban, "~> 2.19", [hex: :oban, repo: "hexpm", optional: false]}], "hexpm", "64664d50805bbfd3903aeada1f3c39634652a87844797ee400b0bcc95a28f5ea"}, - "oban_plugins_lazarus": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/oban_plugins_lazarus.git", "e49fc355baaf0e435208bf5f534d31e26e897711", [ref: "e49fc355baaf0e435208bf5f534d31e26e897711"]}, + "oban_plugins_lazarus": {:hex, :oban_plugins_lazarus, "0.1.1", "a5141d569e5b9f3bec8f4a958231d2c538af097d3c1ad06274f096fc06956821", [:mix], [{:oban, "< 3.0.0", [hex: :oban, repo: "hexpm", optional: false]}], "hexpm", "7e9c51bc44d33f71c0a52cf0cd5c8d4c70380ede065c1042013f5123f2fc9729"}, "oban_web": {:hex, :oban_web, "2.11.6", "53933cb4253c4d9f1098ee311c06f07935259f0e564dcf2d66bae4cc98e317fe", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, "~> 2.19", [hex: :oban, repo: "hexpm", optional: false]}, {:oban_met, "~> 1.0", [hex: :oban_met, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}], "hexpm", "576d94b705688c313694c2c114ca21aa0f8f2ad1b9ca45c052c5ba316d3e8d10"}, "octo_fetch": {:hex, :octo_fetch, "0.4.0", "074b5ecbc08be10b05b27e9db08bc20a3060142769436242702931c418695b19", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "cf8be6f40cd519d7000bb4e84adcf661c32e59369ca2827c4e20042eda7a7fc6"}, "open_api_spex": {:hex, :open_api_spex, "3.22.0", "fbf90dc82681dc042a4ee79853c8e989efbba73d9e87439085daf849bbf8bc20", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "dd751ddbdd709bb4a5313e9a24530da6e66594773c7242a0c2592cbd9f589063"},