Added support for exclude_types, limit, and min_id in Mastodon
notifications. Unify Mastodon-compatible pagination logic.
This commit is contained in:
parent
1344e34ed3
commit
1588688a11
|
@ -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 @@ def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}),
|
|||
|
||||
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: []
|
||||
|
||||
|
|
|
@ -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 @@ def changeset(%Notification{} = notification, attrs) 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
|
||||
|
|
78
lib/pleroma/pagination.ex
Normal file
78
lib/pleroma/pagination.ex
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -502,7 +502,7 @@ def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) 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)
|
||||
|
|
|
@ -755,6 +755,96 @@ test "clearing all notifications", %{conn: conn} 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
|
||||
|
|
Loading…
Reference in a new issue