Merge branch 'masto-api-notifications' into 'develop'

Add support for exclude_types, limit and min_id in Mastodon notifications

See merge request pleroma/pleroma!949
This commit is contained in:
kaniini 2019-03-18 19:17:36 +00:00
commit aba4c27120
6 changed files with 204 additions and 58 deletions

View file

@ -22,6 +22,10 @@ defmodule Pleroma.Activity do
"Like" => "favourite" "Like" => "favourite"
} }
@mastodon_to_ap_notification_types for {k, v} <- @mastodon_notification_types,
into: %{},
do: {v, k}
schema "activities" do schema "activities" do
field(:data, :map) field(:data, :map)
field(:local, :boolean, default: true) 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 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, status_ids \\ [])
def all_by_actor_and_id(_actor, []), do: [] def all_by_actor_and_id(_actor, []), do: []

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Notification do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Pagination
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
@ -28,36 +29,17 @@ def changeset(%Notification{} = notification, attrs) do
|> cast(attrs, [:seen]) |> cast(attrs, [:seen])
end end
# TODO: Make generic and unify (see activity_pub.ex) def for_user_query(user) do
defp restrict_max(query, %{"max_id" => max_id}) do Notification
from(activity in query, where: activity.id < ^max_id) |> where(user_id: ^user.id)
|> join(:inner, [n], activity in assoc(n, :activity))
|> preload(:activity)
end 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 def for_user(user, opts \\ %{}) do
query = user
from( |> for_user_query()
n in Notification, |> Pagination.fetch_paginated(opts)
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)
end end
def set_read_up_to(%{id: user_id} = _user, id) do def set_read_up_to(%{id: user_id} = _user, id) do

78
lib/pleroma/pagination.ex Normal file
View 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

View file

@ -2,61 +2,49 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
import Ecto.Query import Ecto.Query
import Ecto.Changeset import Ecto.Changeset
alias Pleroma.Repo alias Pleroma.Activity
alias Pleroma.Notification
alias Pleroma.Pagination
alias Pleroma.User alias Pleroma.User
@default_limit 20
def get_followers(user, params \\ %{}) do def get_followers(user, params \\ %{}) do
user user
|> User.get_followers_query() |> User.get_followers_query()
|> paginate(params) |> Pagination.fetch_paginated(params)
|> Repo.all()
end end
def get_friends(user, params \\ %{}) do def get_friends(user, params \\ %{}) do
user user
|> User.get_friends_query() |> User.get_friends_query()
|> paginate(params) |> Pagination.fetch_paginated(params)
|> Repo.all()
end end
def paginate(query, params \\ %{}) do def get_notifications(user, params \\ %{}) do
options = cast_params(params) options = cast_params(params)
query user
|> restrict(:max_id, options) |> Notification.for_user_query()
|> restrict(:since_id, options) |> restrict(:exclude_types, options)
|> restrict(:limit, options) |> Pagination.fetch_paginated(params)
|> order_by([u], fragment("? desc nulls last", u.id))
end end
def cast_params(params) do defp cast_params(params) do
param_types = %{ param_types = %{
max_id: :string, exclude_types: {:array, :string}
since_id: :string,
limit: :integer
} }
changeset = cast({%{}, param_types}, params, Map.keys(param_types)) changeset = cast({%{}, param_types}, params, Map.keys(param_types))
changeset.changes changeset.changes
end end
defp restrict(query, :max_id, %{max_id: max_id}) do defp restrict(query, :exclude_types, %{exclude_types: mastodon_types = [_ | _]}) do
query ap_types =
|> where([q], q.id < ^max_id) mastodon_types
end |> Enum.map(&Activity.from_mastodon_notification_type/1)
|> Enum.filter(& &1)
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)
query query
|> limit(^limit) |> where([q, a], not fragment("? @> ARRAY[?->>'type']::varchar[]", ^ap_types, a.data))
end end
defp restrict(query, _, _), do: query defp restrict(query, _, _), do: query

View file

@ -502,7 +502,7 @@ def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
end end
def notifications(%{assigns: %{user: user}} = conn, params) do def notifications(%{assigns: %{user: user}} = conn, params) do
notifications = Notification.for_user(user, params) notifications = MastodonAPI.get_notifications(user, params)
conn conn
|> add_link_headers(:notifications, notifications) |> add_link_headers(:notifications, notifications)

View file

@ -755,6 +755,96 @@ test "clearing all notifications", %{conn: conn} do
assert all = json_response(conn, 200) assert all = json_response(conn, 200)
assert all == [] assert all == []
end 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 end
describe "reblogging" do describe "reblogging" do