diff options
| -rw-r--r-- | lib/pleroma/activity.ex | 8 | ||||
| -rw-r--r-- | lib/pleroma/notification.ex | 36 | ||||
| -rw-r--r-- | lib/pleroma/pagination.ex | 78 | ||||
| -rw-r--r-- | lib/pleroma/web/mastodon_api/mastodon_api.ex | 48 | ||||
| -rw-r--r-- | lib/pleroma/web/mastodon_api/mastodon_api_controller.ex | 2 | ||||
| -rw-r--r-- | test/web/mastodon_api/mastodon_api_controller_test.exs | 90 | 
6 files changed, 204 insertions, 58 deletions
diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index 79dc26b01..de0e66681 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -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: [] diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 765191275..a98649b63 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -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 index 000000000..7c864deef --- /dev/null +++ b/lib/pleroma/pagination.ex @@ -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 diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex index 54cb6c97a..08ea5f967 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex @@ -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 diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 952aa2453..2eb1da561 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -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) diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index 74bf05708..b2302422b 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -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  | 
