From b3b367b894d1605202625310e7d8b1ed6ed5eb13 Mon Sep 17 00:00:00 2001
From: "Haelwenn (lanodan) Monnier" <contact@hacktivis.me>
Date: Thu, 7 May 2020 21:52:45 +0200
Subject: [PATCH 1/4] Bugfix: Reuse Controller.Helper pagination for APC2S

---
 .../web/activity_pub/activity_pub_controller.ex    |  3 ++
 lib/pleroma/web/activity_pub/views/user_view.ex    | 34 ++++++---------
 lib/pleroma/web/controller_helper.ex               | 48 +++++++++++++--------
 .../controllers/timeline_controller.ex             |  4 +-
 .../activity_pub/activity_pub_controller_test.exs  | 50 +++++++++++++++++++++-
 test/web/activity_pub/views/user_view_test.exs     | 31 --------------
 6 files changed, 94 insertions(+), 76 deletions(-)

diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
index 28727d619..b624d4255 100644
--- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
@@ -21,6 +21,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
   alias Pleroma.Web.ActivityPub.UserView
   alias Pleroma.Web.ActivityPub.Utils
   alias Pleroma.Web.ActivityPub.Visibility
+  alias Pleroma.Web.ControllerHelper
   alias Pleroma.Web.Endpoint
   alias Pleroma.Web.FederatingPlug
   alias Pleroma.Web.Federator
@@ -251,6 +252,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
       |> put_view(UserView)
       |> render("activity_collection_page.json", %{
         activities: activities,
+        pagination: ControllerHelper.get_pagination_fields(conn, activities, %{"limit" => "10"}),
         iri: "#{user.ap_id}/outbox"
       })
     end
@@ -368,6 +370,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
     |> put_view(UserView)
     |> render("activity_collection_page.json", %{
       activities: activities,
+      pagination: ControllerHelper.get_pagination_fields(conn, activities, %{"limit" => "10"}),
       iri: "#{user.ap_id}/inbox"
     })
   end
diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex
index 34590b16d..4a02b09a1 100644
--- a/lib/pleroma/web/activity_pub/views/user_view.ex
+++ b/lib/pleroma/web/activity_pub/views/user_view.ex
@@ -213,34 +213,24 @@ defmodule Pleroma.Web.ActivityPub.UserView do
     |> Map.merge(Utils.make_json_ld_header())
   end
 
-  def render("activity_collection_page.json", %{activities: activities, iri: iri}) do
-    # this is sorted chronologically, so first activity is the newest (max)
-    {max_id, min_id, collection} =
-      if length(activities) > 0 do
-        {
-          Enum.at(activities, 0).id,
-          Enum.at(Enum.reverse(activities), 0).id,
-          Enum.map(activities, fn act ->
-            {:ok, data} = Transmogrifier.prepare_outgoing(act.data)
-            data
-          end)
-        }
-      else
-        {
-          0,
-          0,
-          []
-        }
-      end
+  def render("activity_collection_page.json", %{
+        activities: activities,
+        iri: iri,
+        pagination: pagination
+      }) do
+    collection =
+      Enum.map(activities, fn activity ->
+        {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
+        data
+      end)
 
     %{
-      "id" => "#{iri}?max_id=#{max_id}&page=true",
       "type" => "OrderedCollectionPage",
       "partOf" => iri,
-      "orderedItems" => collection,
-      "next" => "#{iri}?max_id=#{min_id}&page=true"
+      "orderedItems" => collection
     }
     |> Map.merge(Utils.make_json_ld_header())
+    |> Map.merge(pagination)
   end
 
   defp maybe_put_total_items(map, false, _total), do: map
diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex
index 5a1316a5f..2d35bb56c 100644
--- a/lib/pleroma/web/controller_helper.ex
+++ b/lib/pleroma/web/controller_helper.ex
@@ -5,6 +5,8 @@
 defmodule Pleroma.Web.ControllerHelper do
   use Pleroma.Web, :controller
 
+  alias Pleroma.Pagination
+
   # As in Mastodon API, per https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html
   @falsy_param_values [false, 0, "0", "f", "F", "false", "False", "FALSE", "off", "OFF"]
 
@@ -46,6 +48,16 @@ defmodule Pleroma.Web.ControllerHelper do
     do: conn
 
   def add_link_headers(conn, activities, extra_params) do
+    case get_pagination_fields(conn, activities, extra_params) do
+      %{"next" => next_url, "prev" => prev_url} ->
+        put_resp_header(conn, "link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
+
+      _ ->
+        conn
+    end
+  end
+
+  def get_pagination_fields(conn, activities, extra_params \\ %{}) do
     case List.last(activities) do
       %{id: max_id} ->
         params =
@@ -54,29 +66,29 @@ defmodule Pleroma.Web.ControllerHelper do
           |> Map.drop(["since_id", "max_id", "min_id"])
           |> Map.merge(extra_params)
 
-        limit =
-          params
-          |> Map.get("limit", "20")
-          |> String.to_integer()
-
         min_id =
-          if length(activities) <= limit do
-            activities
-            |> List.first()
-            |> Map.get(:id)
-          else
-            activities
-            |> Enum.at(limit * -1)
-            |> Map.get(:id)
-          end
+          activities
+          |> List.first()
+          |> Map.get(:id)
 
-        next_url = current_url(conn, Map.merge(params, %{max_id: max_id}))
-        prev_url = current_url(conn, Map.merge(params, %{min_id: min_id}))
+        fields = %{
+          "next" => current_url(conn, Map.put(params, :max_id, max_id)),
+          "prev" => current_url(conn, Map.put(params, :min_id, min_id))
+        }
 
-        put_resp_header(conn, "link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
+        #  Generating an `id` without already present pagination keys would
+        # need a query-restriction with an `q.id >= ^id` or `q.id <= ^id`
+        # instead of the `q.id > ^min_id` and `q.id < ^max_id`.
+        #  This is because we only have ids present inside of the page, while
+        # `min_id`, `since_id` and `max_id` requires to know one outside of it.
+        if Map.take(conn.params, Pagination.page_keys() -- ["limit", "order"]) != [] do
+          Map.put(fields, "id", current_url(conn, conn.params))
+        else
+          fields
+        end
 
       _ ->
-        conn
+        %{}
     end
   end
 
diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
index 958567510..c852082a5 100644
--- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
@@ -51,10 +51,8 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
       |> Map.put("reply_filtering_user", user)
       |> Map.put("user", user)
 
-    recipients = [user.ap_id | User.following(user)]
-
     activities =
-      recipients
+      [user.ap_id | User.following(user)]
       |> ActivityPub.fetch_activities(params)
       |> Enum.reverse()
 
diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs
index 24edab41a..3f48553c9 100644
--- a/test/web/activity_pub/activity_pub_controller_test.exs
+++ b/test/web/activity_pub/activity_pub_controller_test.exs
@@ -804,17 +804,63 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
   end
 
   describe "GET /users/:nickname/outbox" do
+    test "it paginates correctly", %{conn: conn} do
+      user = insert(:user)
+      conn = assign(conn, :user, user)
+      outbox_endpoint = user.ap_id <> "/outbox"
+
+      _posts =
+        for i <- 0..15 do
+          {:ok, activity} = CommonAPI.post(user, %{status: "post #{i}"})
+          activity
+        end
+
+      result =
+        conn
+        |> put_req_header("accept", "application/activity+json")
+        |> get(outbox_endpoint <> "?page=true")
+        |> json_response(200)
+
+      result_ids = Enum.map(result["orderedItems"], fn x -> x["id"] end)
+      assert length(result["orderedItems"]) == 10
+      assert length(result_ids) == 10
+      assert result["next"]
+      assert String.starts_with?(result["next"], outbox_endpoint)
+
+      result_next =
+        conn
+        |> put_req_header("accept", "application/activity+json")
+        |> get(result["next"])
+        |> json_response(200)
+
+      result_next_ids = Enum.map(result_next["orderedItems"], fn x -> x["id"] end)
+      assert length(result_next["orderedItems"]) == 6
+      assert length(result_next_ids) == 6
+      refute Enum.find(result_next_ids, fn x -> x in result_ids end)
+      refute Enum.find(result_ids, fn x -> x in result_next_ids end)
+      assert String.starts_with?(result["id"], outbox_endpoint)
+
+      result_next_again =
+        conn
+        |> put_req_header("accept", "application/activity+json")
+        |> get(result_next["id"])
+        |> json_response(200)
+
+      assert result_next == result_next_again
+    end
+
     test "it returns 200 even if there're no activities", %{conn: conn} do
       user = insert(:user)
+      outbox_endpoint = user.ap_id <> "/outbox"
 
       conn =
         conn
         |> assign(:user, user)
         |> put_req_header("accept", "application/activity+json")
-        |> get("/users/#{user.nickname}/outbox")
+        |> get(outbox_endpoint)
 
       result = json_response(conn, 200)
-      assert user.ap_id <> "/outbox" == result["id"]
+      assert outbox_endpoint == result["id"]
     end
 
     test "it returns a note activity in a collection", %{conn: conn} do
diff --git a/test/web/activity_pub/views/user_view_test.exs b/test/web/activity_pub/views/user_view_test.exs
index 20b0f223c..bec15a996 100644
--- a/test/web/activity_pub/views/user_view_test.exs
+++ b/test/web/activity_pub/views/user_view_test.exs
@@ -158,35 +158,4 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
       assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user})
     end
   end
-
-  test "activity collection page aginates correctly" do
-    user = insert(:user)
-
-    posts =
-      for i <- 0..25 do
-        {:ok, activity} = CommonAPI.post(user, %{status: "post #{i}"})
-        activity
-      end
-
-    # outbox sorts chronologically, newest first, with ten per page
-    posts = Enum.reverse(posts)
-
-    %{"next" => next_url} =
-      UserView.render("activity_collection_page.json", %{
-        iri: "#{user.ap_id}/outbox",
-        activities: Enum.take(posts, 10)
-      })
-
-    next_id = Enum.at(posts, 9).id
-    assert next_url =~ next_id
-
-    %{"next" => next_url} =
-      UserView.render("activity_collection_page.json", %{
-        iri: "#{user.ap_id}/outbox",
-        activities: Enum.take(Enum.drop(posts, 10), 10)
-      })
-
-    next_id = Enum.at(posts, 19).id
-    assert next_url =~ next_id
-  end
 end

From 2c18830d0dbd7f63cd20dcf5167254fede538930 Mon Sep 17 00:00:00 2001
From: "Haelwenn (lanodan) Monnier" <contact@hacktivis.me>
Date: Fri, 8 May 2020 03:08:11 +0200
Subject: [PATCH 2/4] Bugfix: router: allow basic_auth for outbox

---
 lib/pleroma/web/router.ex | 8 +-------
 1 file changed, 1 insertion(+), 7 deletions(-)

diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index e493a4153..d65af23d9 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -571,13 +571,6 @@ defmodule Pleroma.Web.Router do
     get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe)
   end
 
-  scope "/", Pleroma.Web.ActivityPub do
-    # XXX: not really ostatus
-    pipe_through(:ostatus)
-
-    get("/users/:nickname/outbox", ActivityPubController, :outbox)
-  end
-
   pipeline :ap_service_actor do
     plug(:accepts, ["activity+json", "json"])
   end
@@ -602,6 +595,7 @@ defmodule Pleroma.Web.Router do
     get("/api/ap/whoami", ActivityPubController, :whoami)
     get("/users/:nickname/inbox", ActivityPubController, :read_inbox)
 
+    get("/users/:nickname/outbox", ActivityPubController, :outbox)
     post("/users/:nickname/outbox", ActivityPubController, :update_outbox)
     post("/api/ap/upload_media", ActivityPubController, :upload_media)
 

From a43b435c0ad8a1198241fbd18e1a5f1be830f4b5 Mon Sep 17 00:00:00 2001
From: "Haelwenn (lanodan) Monnier" <contact@hacktivis.me>
Date: Fri, 8 May 2020 03:05:56 +0200
Subject: [PATCH 3/4] AP C2S: allow limit & order on outbox & read_inbox

---
 .../web/activity_pub/activity_pub_controller.ex    | 45 ++++++++++------------
 lib/pleroma/web/controller_helper.ex               |  2 +-
 .../activity_pub/activity_pub_controller_test.exs  |  6 +--
 3 files changed, 24 insertions(+), 29 deletions(-)

diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
index b624d4255..5b8441384 100644
--- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
@@ -231,28 +231,22 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
       when page? in [true, "true"] do
     with %User{} = user <- User.get_cached_by_nickname(nickname),
          {:ok, user} <- User.ensure_keys_present(user) do
-      activities =
-        if params["max_id"] do
-          ActivityPub.fetch_user_activities(user, for_user, %{
-            "max_id" => params["max_id"],
-            # This is a hack because postgres generates inefficient queries when filtering by
-            # 'Answer', poll votes will be hidden by the visibility filter in this case anyway
-            "include_poll_votes" => true,
-            "limit" => 10
-          })
-        else
-          ActivityPub.fetch_user_activities(user, for_user, %{
-            "limit" => 10,
-            "include_poll_votes" => true
-          })
-        end
+      # "include_poll_votes" is a hack because postgres generates inefficient
+      # queries when filtering by 'Answer', poll votes will be hidden by the
+      # visibility filter in this case anyway
+      params =
+        params
+        |> Map.drop(["nickname", "page"])
+        |> Map.put("include_poll_votes", true)
+
+      activities = ActivityPub.fetch_user_activities(user, for_user, params)
 
       conn
       |> put_resp_content_type("application/activity+json")
       |> put_view(UserView)
       |> render("activity_collection_page.json", %{
         activities: activities,
-        pagination: ControllerHelper.get_pagination_fields(conn, activities, %{"limit" => "10"}),
+        pagination: ControllerHelper.get_pagination_fields(conn, activities),
         iri: "#{user.ap_id}/outbox"
       })
     end
@@ -355,22 +349,23 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
         %{"nickname" => nickname, "page" => page?} = params
       )
       when page? in [true, "true"] do
+    params =
+      params
+      |> Map.drop(["nickname", "page"])
+      |> Map.put("blocking_user", user)
+      |> Map.put("user", user)
+
     activities =
-      if params["max_id"] do
-        ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{
-          "max_id" => params["max_id"],
-          "limit" => 10
-        })
-      else
-        ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{"limit" => 10})
-      end
+      [user.ap_id | User.following(user)]
+      |> ActivityPub.fetch_activities(params)
+      |> Enum.reverse()
 
     conn
     |> put_resp_content_type("application/activity+json")
     |> put_view(UserView)
     |> render("activity_collection_page.json", %{
       activities: activities,
-      pagination: ControllerHelper.get_pagination_fields(conn, activities, %{"limit" => "10"}),
+      pagination: ControllerHelper.get_pagination_fields(conn, activities),
       iri: "#{user.ap_id}/inbox"
     })
   end
diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex
index 2d35bb56c..9e5444817 100644
--- a/lib/pleroma/web/controller_helper.ex
+++ b/lib/pleroma/web/controller_helper.ex
@@ -63,8 +63,8 @@ defmodule Pleroma.Web.ControllerHelper do
         params =
           conn.params
           |> Map.drop(Map.keys(conn.path_params))
-          |> Map.drop(["since_id", "max_id", "min_id"])
           |> Map.merge(extra_params)
+          |> Map.drop(Pagination.page_keys() -- ["limit", "order"])
 
         min_id =
           activities
diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs
index 3f48553c9..e490a5744 100644
--- a/test/web/activity_pub/activity_pub_controller_test.exs
+++ b/test/web/activity_pub/activity_pub_controller_test.exs
@@ -810,7 +810,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
       outbox_endpoint = user.ap_id <> "/outbox"
 
       _posts =
-        for i <- 0..15 do
+        for i <- 0..25 do
           {:ok, activity} = CommonAPI.post(user, %{status: "post #{i}"})
           activity
         end
@@ -822,8 +822,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
         |> json_response(200)
 
       result_ids = Enum.map(result["orderedItems"], fn x -> x["id"] end)
-      assert length(result["orderedItems"]) == 10
-      assert length(result_ids) == 10
+      assert length(result["orderedItems"]) == 20
+      assert length(result_ids) == 20
       assert result["next"]
       assert String.starts_with?(result["next"], outbox_endpoint)
 

From 54bae06b4fa960eadb9918414f50b9ececc1faa4 Mon Sep 17 00:00:00 2001
From: Haelwenn <contact+git.pleroma.social@hacktivis.me>
Date: Fri, 5 Jun 2020 14:48:02 +0000
Subject: [PATCH 4/4] Create Pleroma.Maps.put_if_present(map, key, value,
 value_fun // &{:ok, &1})

Unifies all the similar functions to one and simplify some blocks with it.
---
 lib/pleroma/helpers/uri_helper.ex                  |  8 -----
 lib/pleroma/maps.ex                                | 15 +++++++++
 lib/pleroma/web/activity_pub/activity_pub.ex       | 20 +++---------
 lib/pleroma/web/activity_pub/transmogrifier.ex     | 17 ++++------
 lib/pleroma/web/activity_pub/utils.ex              | 18 +++++------
 .../web/admin_api/controllers/config_controller.ex |  5 ++-
 .../admin_api/controllers/oauth_app_controller.ex  | 14 ++-------
 lib/pleroma/web/controller_helper.ex               |  5 ---
 lib/pleroma/web/feed/tag_controller.ex             |  4 +--
 lib/pleroma/web/feed/user_controller.ex            |  4 +--
 .../mastodon_api/controllers/account_controller.ex | 36 ++++++++--------------
 lib/pleroma/web/mastodon_api/views/app_view.ex     |  6 +---
 .../mastodon_api/views/scheduled_activity_view.ex  |  8 ++---
 lib/pleroma/web/oauth/oauth_controller.ex          |  5 +--
 14 files changed, 59 insertions(+), 106 deletions(-)
 create mode 100644 lib/pleroma/maps.ex

diff --git a/lib/pleroma/helpers/uri_helper.ex b/lib/pleroma/helpers/uri_helper.ex
index 69d8c8fe0..6d205a636 100644
--- a/lib/pleroma/helpers/uri_helper.ex
+++ b/lib/pleroma/helpers/uri_helper.ex
@@ -17,14 +17,6 @@ defmodule Pleroma.Helpers.UriHelper do
     |> URI.to_string()
   end
 
-  def append_param_if_present(%{} = params, param_name, param_value) do
-    if param_value do
-      Map.put(params, param_name, param_value)
-    else
-      params
-    end
-  end
-
   def maybe_add_base("/" <> uri, base), do: Path.join([base, uri])
   def maybe_add_base(uri, _base), do: uri
 end
diff --git a/lib/pleroma/maps.ex b/lib/pleroma/maps.ex
new file mode 100644
index 000000000..ab2e32e2f
--- /dev/null
+++ b/lib/pleroma/maps.ex
@@ -0,0 +1,15 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Maps do
+  def put_if_present(map, key, value, value_function \\ &{:ok, &1}) when is_map(map) do
+    with false <- is_nil(key),
+         false <- is_nil(value),
+         {:ok, new_value} <- value_function.(value) do
+      Map.put(map, key, new_value)
+    else
+      _ -> map
+    end
+  end
+end
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 958f3e5af..75468f415 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
   alias Pleroma.Constants
   alias Pleroma.Conversation
   alias Pleroma.Conversation.Participation
+  alias Pleroma.Maps
   alias Pleroma.Notification
   alias Pleroma.Object
   alias Pleroma.Object.Containment
@@ -19,7 +20,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
   alias Pleroma.User
   alias Pleroma.Web.ActivityPub.MRF
   alias Pleroma.Web.ActivityPub.Transmogrifier
-  alias Pleroma.Web.ActivityPub.Utils
   alias Pleroma.Web.Streamer
   alias Pleroma.Web.WebFinger
   alias Pleroma.Workers.BackgroundWorker
@@ -161,12 +161,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
         })
 
       # Splice in the child object if we have one.
-      activity =
-        if not is_nil(object) do
-          Map.put(activity, :object, object)
-        else
-          activity
-        end
+      activity = Maps.put_if_present(activity, :object, object)
 
       BackgroundWorker.enqueue("fetch_data_for_activity", %{"activity_id" => activity.id})
 
@@ -328,7 +323,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
 
     with data <-
            %{"to" => to, "type" => type, "actor" => actor.ap_id, "object" => object}
-           |> Utils.maybe_put("id", activity_id),
+           |> Maps.put_if_present("id", activity_id),
          {:ok, activity} <- insert(data, local),
          _ <- notify_and_stream(activity),
          :ok <- maybe_federate(activity) do
@@ -348,7 +343,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
            "actor" => actor,
            "object" => object
          },
-         data <- Utils.maybe_put(data, "id", activity_id),
+         data <- Maps.put_if_present(data, "id", activity_id),
          {:ok, activity} <- insert(data, local),
          _ <- notify_and_stream(activity),
          :ok <- maybe_federate(activity) do
@@ -1225,12 +1220,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
   @spec upload(Upload.source(), keyword()) :: {:ok, Object.t()} | {:error, any()}
   def upload(file, opts \\ []) do
     with {:ok, data} <- Upload.store(file, opts) do
-      obj_data =
-        if opts[:actor] do
-          Map.put(data, "actor", opts[:actor])
-        else
-          data
-        end
+      obj_data = Maps.put_if_present(data, "actor", opts[:actor])
 
       Repo.insert(%Object{data: obj_data})
     end
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 8443c284c..fda1c71df 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
   alias Pleroma.Activity
   alias Pleroma.EarmarkRenderer
   alias Pleroma.FollowingRelationship
+  alias Pleroma.Maps
   alias Pleroma.Object
   alias Pleroma.Object.Containment
   alias Pleroma.Repo
@@ -208,12 +209,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
     |> Map.put("conversation", context)
   end
 
-  defp add_if_present(map, _key, nil), do: map
-
-  defp add_if_present(map, key, value) do
-    Map.put(map, key, value)
-  end
-
   def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
     attachments =
       Enum.map(attachment, fn data ->
@@ -241,13 +236,13 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
 
         attachment_url =
           %{"href" => href}
-          |> add_if_present("mediaType", media_type)
-          |> add_if_present("type", Map.get(url || %{}, "type"))
+          |> Maps.put_if_present("mediaType", media_type)
+          |> Maps.put_if_present("type", Map.get(url || %{}, "type"))
 
         %{"url" => [attachment_url]}
-        |> add_if_present("mediaType", media_type)
-        |> add_if_present("type", data["type"])
-        |> add_if_present("name", data["name"])
+        |> Maps.put_if_present("mediaType", media_type)
+        |> Maps.put_if_present("type", data["type"])
+        |> Maps.put_if_present("name", data["name"])
       end)
 
     Map.put(object, "attachment", attachments)
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index a76a699ee..5fce0ba63 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
   alias Ecto.UUID
   alias Pleroma.Activity
   alias Pleroma.Config
+  alias Pleroma.Maps
   alias Pleroma.Notification
   alias Pleroma.Object
   alias Pleroma.Repo
@@ -307,7 +308,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
       "cc" => cc,
       "context" => object.data["context"]
     }
-    |> maybe_put("id", activity_id)
+    |> Maps.put_if_present("id", activity_id)
   end
 
   def make_emoji_reaction_data(user, object, emoji, activity_id) do
@@ -477,7 +478,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
       "object" => followed_id,
       "state" => "pending"
     }
-    |> maybe_put("id", activity_id)
+    |> Maps.put_if_present("id", activity_id)
   end
 
   def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do
@@ -546,7 +547,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
       "cc" => [],
       "context" => object.data["context"]
     }
-    |> maybe_put("id", activity_id)
+    |> Maps.put_if_present("id", activity_id)
   end
 
   def make_announce_data(
@@ -563,7 +564,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
       "cc" => [Pleroma.Constants.as_public()],
       "context" => object.data["context"]
     }
-    |> maybe_put("id", activity_id)
+    |> Maps.put_if_present("id", activity_id)
   end
 
   def make_undo_data(
@@ -582,7 +583,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
       "cc" => [Pleroma.Constants.as_public()],
       "context" => context
     }
-    |> maybe_put("id", activity_id)
+    |> Maps.put_if_present("id", activity_id)
   end
 
   @spec add_announce_to_object(Activity.t(), Object.t()) ::
@@ -627,7 +628,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
       "to" => [followed.ap_id],
       "object" => follow_activity.data
     }
-    |> maybe_put("id", activity_id)
+    |> Maps.put_if_present("id", activity_id)
   end
 
   #### Block-related helpers
@@ -650,7 +651,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
       "to" => [blocked.ap_id],
       "object" => blocked.ap_id
     }
-    |> maybe_put("id", activity_id)
+    |> Maps.put_if_present("id", activity_id)
   end
 
   #### Create-related helpers
@@ -871,7 +872,4 @@ defmodule Pleroma.Web.ActivityPub.Utils do
     |> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data))
     |> Repo.all()
   end
-
-  def maybe_put(map, _key, nil), do: map
-  def maybe_put(map, key, value), do: Map.put(map, key, value)
 end
diff --git a/lib/pleroma/web/admin_api/controllers/config_controller.ex b/lib/pleroma/web/admin_api/controllers/config_controller.ex
index e221d9418..d6e2019bc 100644
--- a/lib/pleroma/web/admin_api/controllers/config_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/config_controller.ex
@@ -61,13 +61,12 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
                 value
               end
 
-            setting = %{
+            %{
               group: ConfigDB.convert(group),
               key: ConfigDB.convert(key),
               value: ConfigDB.convert(merged_value)
             }
-
-            if db, do: Map.put(setting, :db, db), else: setting
+            |> Pleroma.Maps.put_if_present(:db, db)
           end)
         end)
         |> List.flatten()
diff --git a/lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex b/lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex
index 04e629fc1..dca23ea73 100644
--- a/lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex
@@ -42,12 +42,7 @@ defmodule Pleroma.Web.AdminAPI.OAuthAppController do
   end
 
   def create(%{body_params: params} = conn, _) do
-    params =
-      if params[:name] do
-        Map.put(params, :client_name, params[:name])
-      else
-        params
-      end
+    params = Pleroma.Maps.put_if_present(params, :client_name, params[:name])
 
     case App.create(params) do
       {:ok, app} ->
@@ -59,12 +54,7 @@ defmodule Pleroma.Web.AdminAPI.OAuthAppController do
   end
 
   def update(%{body_params: params} = conn, %{id: id}) do
-    params =
-      if params[:name] do
-        Map.put(params, :client_name, params.name)
-      else
-        params
-      end
+    params = Pleroma.Maps.put_if_present(params, :client_name, params[:name])
 
     with {:ok, app} <- App.update(id, params) do
       render(conn, "show.json", app: app, admin: true)
diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex
index 5a1316a5f..bf832fe94 100644
--- a/lib/pleroma/web/controller_helper.ex
+++ b/lib/pleroma/web/controller_helper.ex
@@ -99,11 +99,6 @@ defmodule Pleroma.Web.ControllerHelper do
     render_error(conn, :not_implemented, "Can't display this activity")
   end
 
-  @spec put_if_exist(map(), atom() | String.t(), any) :: map()
-  def put_if_exist(map, _key, nil), do: map
-
-  def put_if_exist(map, key, value), do: Map.put(map, key, value)
-
   @doc """
   Returns true if request specifies to include embedded relationships in account objects.
   May only be used in selected account-related endpoints; has no effect for status- or
diff --git a/lib/pleroma/web/feed/tag_controller.ex b/lib/pleroma/web/feed/tag_controller.ex
index 8133f8480..4e86cfeb5 100644
--- a/lib/pleroma/web/feed/tag_controller.ex
+++ b/lib/pleroma/web/feed/tag_controller.ex
@@ -9,14 +9,12 @@ defmodule Pleroma.Web.Feed.TagController do
   alias Pleroma.Web.ActivityPub.ActivityPub
   alias Pleroma.Web.Feed.FeedView
 
-  import Pleroma.Web.ControllerHelper, only: [put_if_exist: 3]
-
   def feed(conn, %{"tag" => raw_tag} = params) do
     {format, tag} = parse_tag(raw_tag)
 
     activities =
       %{"type" => ["Create"], "tag" => tag}
-      |> put_if_exist("max_id", params["max_id"])
+      |> Pleroma.Maps.put_if_present("max_id", params["max_id"])
       |> ActivityPub.fetch_public_activities()
 
     conn
diff --git a/lib/pleroma/web/feed/user_controller.ex b/lib/pleroma/web/feed/user_controller.ex
index 5a6fc9de0..7c2e0d522 100644
--- a/lib/pleroma/web/feed/user_controller.ex
+++ b/lib/pleroma/web/feed/user_controller.ex
@@ -11,8 +11,6 @@ defmodule Pleroma.Web.Feed.UserController do
   alias Pleroma.Web.ActivityPub.ActivityPubController
   alias Pleroma.Web.Feed.FeedView
 
-  import Pleroma.Web.ControllerHelper, only: [put_if_exist: 3]
-
   plug(Pleroma.Plugs.SetFormatPlug when action in [:feed_redirect])
 
   action_fallback(:errors)
@@ -55,7 +53,7 @@ defmodule Pleroma.Web.Feed.UserController do
           "type" => ["Create"],
           "actor_id" => user.ap_id
         }
-        |> put_if_exist("max_id", params["max_id"])
+        |> Pleroma.Maps.put_if_present("max_id", params["max_id"])
         |> ActivityPub.fetch_public_or_unlisted_activities()
 
       conn
diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
index 97295a52f..5734bb854 100644
--- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
@@ -14,6 +14,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
       json_response: 3
     ]
 
+  alias Pleroma.Maps
   alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
   alias Pleroma.Plugs.OAuthScopesPlug
   alias Pleroma.Plugs.RateLimiter
@@ -160,23 +161,22 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
         :discoverable
       ]
       |> Enum.reduce(%{}, fn key, acc ->
-        add_if_present(acc, params, key, key, &{:ok, truthy_param?(&1)})
+        Maps.put_if_present(acc, key, params[key], &{:ok, truthy_param?(&1)})
       end)
-      |> add_if_present(params, :display_name, :name)
-      |> add_if_present(params, :note, :bio)
-      |> add_if_present(params, :avatar, :avatar)
-      |> add_if_present(params, :header, :banner)
-      |> add_if_present(params, :pleroma_background_image, :background)
-      |> add_if_present(
-        params,
-        :fields_attributes,
+      |> Maps.put_if_present(:name, params[:display_name])
+      |> Maps.put_if_present(:bio, params[:note])
+      |> Maps.put_if_present(:avatar, params[:avatar])
+      |> Maps.put_if_present(:banner, params[:header])
+      |> Maps.put_if_present(:background, params[:pleroma_background_image])
+      |> Maps.put_if_present(
         :raw_fields,
+        params[:fields_attributes],
         &{:ok, normalize_fields_attributes(&1)}
       )
-      |> add_if_present(params, :pleroma_settings_store, :pleroma_settings_store)
-      |> add_if_present(params, :default_scope, :default_scope)
-      |> add_if_present(params["source"], "privacy", :default_scope)
-      |> add_if_present(params, :actor_type, :actor_type)
+      |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
+      |> Maps.put_if_present(:default_scope, params[:default_scope])
+      |> Maps.put_if_present(:default_scope, params["source"]["privacy"])
+      |> Maps.put_if_present(:actor_type, params[:actor_type])
 
     changeset = User.update_changeset(user, user_params)
 
@@ -206,16 +206,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
     }
   end
 
-  defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
-    with true <- is_map(params),
-         true <- Map.has_key?(params, params_field),
-         {:ok, new_value} <- value_function.(Map.get(params, params_field)) do
-      Map.put(map, map_field, new_value)
-    else
-      _ -> map
-    end
-  end
-
   defp normalize_fields_attributes(fields) do
     if Enum.all?(fields, &is_tuple/1) do
       Enum.map(fields, fn {_, v} -> v end)
diff --git a/lib/pleroma/web/mastodon_api/views/app_view.ex b/lib/pleroma/web/mastodon_api/views/app_view.ex
index 36071cd25..e44272c6f 100644
--- a/lib/pleroma/web/mastodon_api/views/app_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/app_view.ex
@@ -45,10 +45,6 @@ defmodule Pleroma.Web.MastodonAPI.AppView do
   defp with_vapid_key(data) do
     vapid_key = Application.get_env(:web_push_encryption, :vapid_details, [])[:public_key]
 
-    if vapid_key do
-      Map.put(data, "vapid_key", vapid_key)
-    else
-      data
-    end
+    Pleroma.Maps.put_if_present(data, "vapid_key", vapid_key)
   end
 end
diff --git a/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex b/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex
index 458f6bc78..5b896bf3b 100644
--- a/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex
@@ -30,7 +30,7 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView do
   defp with_media_attachments(data, _), do: data
 
   defp status_params(params) do
-    data = %{
+    %{
       text: params["status"],
       sensitive: params["sensitive"],
       spoiler_text: params["spoiler_text"],
@@ -39,10 +39,6 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView do
       poll: params["poll"],
       in_reply_to_id: params["in_reply_to_id"]
     }
-
-    case params["media_ids"] do
-      nil -> data
-      media_ids -> Map.put(data, :media_ids, media_ids)
-    end
+    |> Pleroma.Maps.put_if_present(:media_ids, params["media_ids"])
   end
 end
diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex
index 7c804233c..c557778ca 100644
--- a/lib/pleroma/web/oauth/oauth_controller.ex
+++ b/lib/pleroma/web/oauth/oauth_controller.ex
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
   use Pleroma.Web, :controller
 
   alias Pleroma.Helpers.UriHelper
+  alias Pleroma.Maps
   alias Pleroma.MFA
   alias Pleroma.Plugs.RateLimiter
   alias Pleroma.Registration
@@ -108,7 +109,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
     if redirect_uri in String.split(app.redirect_uris) do
       redirect_uri = redirect_uri(conn, redirect_uri)
       url_params = %{access_token: token.token}
-      url_params = UriHelper.append_param_if_present(url_params, :state, params["state"])
+      url_params = Maps.put_if_present(url_params, :state, params["state"])
       url = UriHelper.append_uri_params(redirect_uri, url_params)
       redirect(conn, external: url)
     else
@@ -147,7 +148,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
     if redirect_uri in String.split(app.redirect_uris) do
       redirect_uri = redirect_uri(conn, redirect_uri)
       url_params = %{code: auth.token}
-      url_params = UriHelper.append_param_if_present(url_params, :state, auth_attrs["state"])
+      url_params = Maps.put_if_present(url_params, :state, auth_attrs["state"])
       url = UriHelper.append_uri_params(redirect_uri, url_params)
       redirect(conn, external: url)
     else