Added support for exclude_types, limit, and min_id in Mastodon
authoreugenijm <eugenijm@protonmail.com>
Mon, 18 Mar 2019 01:32:23 +0000 (04:32 +0300)
committereugenijm <eugenijm@protonmail.com>
Mon, 18 Mar 2019 08:27:27 +0000 (11:27 +0300)
notifications.

Unify Mastodon-compatible pagination logic.

lib/pleroma/activity.ex
lib/pleroma/notification.ex
lib/pleroma/pagination.ex [new file with mode: 0644]
lib/pleroma/web/mastodon_api/mastodon_api.ex
lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
test/web/mastodon_api/mastodon_api_controller_test.exs

index 79dc26b016de28a9b759626ee847b0936d0a04e0..de0e6668129841387eaa19e6a95d0af6bf3991ac 100644 (file)
@@ -22,6 +22,10 @@ defmodule Pleroma.Activity do
     "Like" => "favourite"
   }
 
+  @mastodon_to_ap_notification_types for {k, v} <- @mastodon_notification_types,
+                                         into: %{},
+                                         do: {v, k}
+
   schema "activities" do
     field(:data, :map)
     field(:local, :boolean, default: true)
@@ -126,6 +130,10 @@ defmodule Pleroma.Activity do
 
   def mastodon_notification_type(%Activity{}), do: nil
 
+  def from_mastodon_notification_type(type) do
+    Map.get(@mastodon_to_ap_notification_types, type)
+  end
+
   def all_by_actor_and_id(actor, status_ids \\ [])
   def all_by_actor_and_id(_actor, []), do: []
 
index 7651912754c63374b6487aa79443bc85107c7e76..a98649b6375bb1c9227c1e4246f88f3e66532fae 100644 (file)
@@ -7,6 +7,7 @@ defmodule Pleroma.Notification do
 
   alias Pleroma.Activity
   alias Pleroma.Notification
+  alias Pleroma.Pagination
   alias Pleroma.Repo
   alias Pleroma.User
   alias Pleroma.Web.CommonAPI
@@ -28,36 +29,17 @@ defmodule Pleroma.Notification do
     |> cast(attrs, [:seen])
   end
 
-  # TODO: Make generic and unify (see activity_pub.ex)
-  defp restrict_max(query, %{"max_id" => max_id}) do
-    from(activity in query, where: activity.id < ^max_id)
+  def for_user_query(user) do
+    Notification
+    |> where(user_id: ^user.id)
+    |> join(:inner, [n], activity in assoc(n, :activity))
+    |> preload(:activity)
   end
 
-  defp restrict_max(query, _), do: query
-
-  defp restrict_since(query, %{"since_id" => since_id}) do
-    from(activity in query, where: activity.id > ^since_id)
-  end
-
-  defp restrict_since(query, _), do: query
-
   def for_user(user, opts \\ %{}) do
-    query =
-      from(
-        n in Notification,
-        where: n.user_id == ^user.id,
-        order_by: [desc: n.id],
-        join: activity in assoc(n, :activity),
-        preload: [activity: activity],
-        limit: 20
-      )
-
-    query =
-      query
-      |> restrict_since(opts)
-      |> restrict_max(opts)
-
-    Repo.all(query)
+    user
+    |> for_user_query()
+    |> Pagination.fetch_paginated(opts)
   end
 
   def set_read_up_to(%{id: user_id} = _user, id) do
diff --git a/lib/pleroma/pagination.ex b/lib/pleroma/pagination.ex
new file mode 100644 (file)
index 0000000..7c864de
--- /dev/null
@@ -0,0 +1,78 @@
+defmodule Pleroma.Pagination do
+  @moduledoc """
+  Implements Mastodon-compatible pagination.
+  """
+
+  import Ecto.Query
+  import Ecto.Changeset
+
+  alias Pleroma.Repo
+
+  @default_limit 20
+
+  def fetch_paginated(query, params) do
+    options = cast_params(params)
+
+    query
+    |> paginate(options)
+    |> Repo.all()
+    |> enforce_order(options)
+  end
+
+  def paginate(query, options) do
+    query
+    |> restrict(:min_id, options)
+    |> restrict(:since_id, options)
+    |> restrict(:max_id, options)
+    |> restrict(:order, options)
+    |> restrict(:limit, options)
+  end
+
+  defp cast_params(params) do
+    param_types = %{
+      min_id: :string,
+      since_id: :string,
+      max_id: :string,
+      limit: :integer
+    }
+
+    changeset = cast({%{}, param_types}, params, Map.keys(param_types))
+    changeset.changes
+  end
+
+  defp restrict(query, :min_id, %{min_id: min_id}) do
+    where(query, [q], q.id > ^min_id)
+  end
+
+  defp restrict(query, :since_id, %{since_id: since_id}) do
+    where(query, [q], q.id > ^since_id)
+  end
+
+  defp restrict(query, :max_id, %{max_id: max_id}) do
+    where(query, [q], q.id < ^max_id)
+  end
+
+  defp restrict(query, :order, %{min_id: _}) do
+    order_by(query, [u], fragment("? asc nulls last", u.id))
+  end
+
+  defp restrict(query, :order, _options) do
+    order_by(query, [u], fragment("? desc nulls last", u.id))
+  end
+
+  defp restrict(query, :limit, options) do
+    limit = Map.get(options, :limit, @default_limit)
+
+    query
+    |> limit(^limit)
+  end
+
+  defp restrict(query, _, _), do: query
+
+  defp enforce_order(result, %{min_id: _}) do
+    result
+    |> Enum.reverse()
+  end
+
+  defp enforce_order(result, _), do: result
+end
index 54cb6c97a3bbb7374360ac63766a4d048e875a1b..08ea5f967c00652e60b8fdd16be254d87e473fbe 100644 (file)
@@ -2,61 +2,49 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
   import Ecto.Query
   import Ecto.Changeset
 
-  alias Pleroma.Repo
+  alias Pleroma.Activity
+  alias Pleroma.Notification
+  alias Pleroma.Pagination
   alias Pleroma.User
 
-  @default_limit 20
-
   def get_followers(user, params \\ %{}) do
     user
     |> User.get_followers_query()
-    |> paginate(params)
-    |> Repo.all()
+    |> Pagination.fetch_paginated(params)
   end
 
   def get_friends(user, params \\ %{}) do
     user
     |> User.get_friends_query()
-    |> paginate(params)
-    |> Repo.all()
+    |> Pagination.fetch_paginated(params)
   end
 
-  def paginate(query, params \\ %{}) do
+  def get_notifications(user, params \\ %{}) do
     options = cast_params(params)
 
-    query
-    |> restrict(:max_id, options)
-    |> restrict(:since_id, options)
-    |> restrict(:limit, options)
-    |> order_by([u], fragment("? desc nulls last", u.id))
+    user
+    |> Notification.for_user_query()
+    |> restrict(:exclude_types, options)
+    |> Pagination.fetch_paginated(params)
   end
 
-  def cast_params(params) do
+  defp cast_params(params) do
     param_types = %{
-      max_id: :string,
-      since_id: :string,
-      limit: :integer
+      exclude_types: {:array, :string}
     }
 
     changeset = cast({%{}, param_types}, params, Map.keys(param_types))
     changeset.changes
   end
 
-  defp restrict(query, :max_id, %{max_id: max_id}) do
-    query
-    |> where([q], q.id < ^max_id)
-  end
-
-  defp restrict(query, :since_id, %{since_id: since_id}) do
-    query
-    |> where([q], q.id > ^since_id)
-  end
-
-  defp restrict(query, :limit, options) do
-    limit = Map.get(options, :limit, @default_limit)
+  defp restrict(query, :exclude_types, %{exclude_types: mastodon_types = [_ | _]}) do
+    ap_types =
+      mastodon_types
+      |> Enum.map(&Activity.from_mastodon_notification_type/1)
+      |> Enum.filter(& &1)
 
     query
-    |> limit(^limit)
+    |> where([q, a], not fragment("? @> ARRAY[?->>'type']::varchar[]", ^ap_types, a.data))
   end
 
   defp restrict(query, _, _), do: query
index 952aa2453b4a87163d23b7f6f0fc80ab304ddc13..2eb1da56180ff9f453f0bdb048b664f99aa87a8f 100644 (file)
@@ -502,7 +502,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   end
 
   def notifications(%{assigns: %{user: user}} = conn, params) do
-    notifications = Notification.for_user(user, params)
+    notifications = MastodonAPI.get_notifications(user, params)
 
     conn
     |> add_link_headers(:notifications, notifications)
index 74bf057082e87b90d1da894210c1b7f26de8fcc6..b2302422b6aac7792f9872b0354e282237375d5b 100644 (file)
@@ -755,6 +755,96 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
       assert all = json_response(conn, 200)
       assert all == []
     end
+
+    test "paginates notifications using min_id, since_id, max_id, and limit", %{conn: conn} do
+      user = insert(:user)
+      other_user = insert(:user)
+
+      {:ok, activity1} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
+      {:ok, activity2} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
+      {:ok, activity3} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
+      {:ok, activity4} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
+
+      notification1_id = Repo.get_by(Notification, activity_id: activity1.id).id |> to_string()
+      notification2_id = Repo.get_by(Notification, activity_id: activity2.id).id |> to_string()
+      notification3_id = Repo.get_by(Notification, activity_id: activity3.id).id |> to_string()
+      notification4_id = Repo.get_by(Notification, activity_id: activity4.id).id |> to_string()
+
+      conn =
+        conn
+        |> assign(:user, user)
+
+      # min_id
+      conn_res =
+        conn
+        |> get("/api/v1/notifications?limit=2&min_id=#{notification1_id}")
+
+      result = json_response(conn_res, 200)
+      assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result
+
+      # since_id
+      conn_res =
+        conn
+        |> get("/api/v1/notifications?limit=2&since_id=#{notification1_id}")
+
+      result = json_response(conn_res, 200)
+      assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
+
+      # max_id
+      conn_res =
+        conn
+        |> get("/api/v1/notifications?limit=2&max_id=#{notification4_id}")
+
+      result = json_response(conn_res, 200)
+      assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result
+    end
+
+    test "filters notifications using exclude_types", %{conn: conn} do
+      user = insert(:user)
+      other_user = insert(:user)
+
+      {:ok, mention_activity} = CommonAPI.post(other_user, %{"status" => "hey @#{user.nickname}"})
+      {:ok, create_activity} = CommonAPI.post(user, %{"status" => "hey"})
+      {:ok, favorite_activity, _} = CommonAPI.favorite(create_activity.id, other_user)
+      {:ok, reblog_activity, _} = CommonAPI.repeat(create_activity.id, other_user)
+      {:ok, _, _, follow_activity} = CommonAPI.follow(other_user, user)
+
+      mention_notification_id =
+        Repo.get_by(Notification, activity_id: mention_activity.id).id |> to_string()
+
+      favorite_notification_id =
+        Repo.get_by(Notification, activity_id: favorite_activity.id).id |> to_string()
+
+      reblog_notification_id =
+        Repo.get_by(Notification, activity_id: reblog_activity.id).id |> to_string()
+
+      follow_notification_id =
+        Repo.get_by(Notification, activity_id: follow_activity.id).id |> to_string()
+
+      conn =
+        conn
+        |> assign(:user, user)
+
+      conn_res =
+        get(conn, "/api/v1/notifications", %{exclude_types: ["mention", "favourite", "reblog"]})
+
+      assert [%{"id" => ^follow_notification_id}] = json_response(conn_res, 200)
+
+      conn_res =
+        get(conn, "/api/v1/notifications", %{exclude_types: ["favourite", "reblog", "follow"]})
+
+      assert [%{"id" => ^mention_notification_id}] = json_response(conn_res, 200)
+
+      conn_res =
+        get(conn, "/api/v1/notifications", %{exclude_types: ["reblog", "follow", "mention"]})
+
+      assert [%{"id" => ^favorite_notification_id}] = json_response(conn_res, 200)
+
+      conn_res =
+        get(conn, "/api/v1/notifications", %{exclude_types: ["follow", "mention", "favourite"]})
+
+      assert [%{"id" => ^reblog_notification_id}] = json_response(conn_res, 200)
+    end
   end
 
   describe "reblogging" do