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:
commit
aba4c27120
|
@ -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: []
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue