Merge pull request 'Add grouped notifications API' (#7910) from issue-7905-grouped-notifications into develop
Reviewed-on: https://git.pleroma.social/pleroma/pleroma/pulls/7910 Reviewed-by: Phantasm <phnt@noreply.git.pleroma.social>
This commit is contained in:
commit
98cd1bbb9a
11 changed files with 1255 additions and 32 deletions
1
changelog.d/grouped-notifications-api.add
Normal file
1
changelog.d/grouped-notifications-api.add
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Add Mastodon-compatible grouped notifications API endpoints under `/api/v2/notifications` for newly-created notifications
|
||||||
|
|
@ -35,6 +35,7 @@ defmodule Pleroma.Notification do
|
||||||
# remember to add a migration to add it to the `notifications_type` enum
|
# remember to add a migration to add it to the `notifications_type` enum
|
||||||
# as well.
|
# as well.
|
||||||
field(:type, :string)
|
field(:type, :string)
|
||||||
|
field(:group_key, :string)
|
||||||
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
|
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
|
||||||
belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
|
belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
|
||||||
|
|
||||||
|
|
@ -49,7 +50,7 @@ defmodule Pleroma.Notification do
|
||||||
|> type_from_activity()
|
|> type_from_activity()
|
||||||
|
|
||||||
notification
|
notification
|
||||||
|> changeset(%{type: type})
|
|> changeset(%{type: type, group_key: grouped_notification_key(type, activity)})
|
||||||
|> Repo.update()
|
|> Repo.update()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -62,6 +63,99 @@ defmodule Pleroma.Notification do
|
||||||
|> Repo.aggregate(:count, :id)
|
|> Repo.aggregate(:count, :id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@groupable_notification_types ~w{favourite follow reblog}
|
||||||
|
@group_bucket_seconds 12 * 60 * 60
|
||||||
|
|
||||||
|
def groupable_notification_types, do: @groupable_notification_types
|
||||||
|
|
||||||
|
def group_notifications(notifications, grouped_types \\ nil) do
|
||||||
|
grouped_types = normalize_grouped_types(grouped_types)
|
||||||
|
|
||||||
|
{group_keys, grouped_notifications} =
|
||||||
|
Enum.reduce(notifications, {[], %{}}, fn notification,
|
||||||
|
{group_keys, grouped_notifications} ->
|
||||||
|
group_key = group_key(notification, grouped_types)
|
||||||
|
|
||||||
|
if Map.has_key?(grouped_notifications, group_key) do
|
||||||
|
{group_keys, Map.update!(grouped_notifications, group_key, &[notification | &1])}
|
||||||
|
else
|
||||||
|
{[group_key | group_keys], Map.put(grouped_notifications, group_key, [notification])}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
group_keys
|
||||||
|
|> Enum.reverse()
|
||||||
|
|> Enum.map(fn group_key ->
|
||||||
|
grouped_notifications
|
||||||
|
|> Map.fetch!(group_key)
|
||||||
|
|> Enum.reverse()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def group_key(notification, grouped_types \\ nil)
|
||||||
|
|
||||||
|
def group_key(%Notification{} = notification, nil) do
|
||||||
|
group_key(notification, @groupable_notification_types)
|
||||||
|
end
|
||||||
|
|
||||||
|
def group_key(%Notification{type: type, group_key: group_key} = notification, grouped_types) do
|
||||||
|
grouped_types = normalize_grouped_types(grouped_types)
|
||||||
|
|
||||||
|
if type in @groupable_notification_types and type in grouped_types and is_binary(group_key) do
|
||||||
|
group_key
|
||||||
|
else
|
||||||
|
ungrouped_group_key(notification)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def normalize_grouped_types(nil), do: @groupable_notification_types
|
||||||
|
def normalize_grouped_types(types) when is_list(types), do: types
|
||||||
|
def normalize_grouped_types(type), do: [type]
|
||||||
|
|
||||||
|
defp ungrouped_group_key(%Notification{id: id}), do: "ungrouped-#{id}"
|
||||||
|
|
||||||
|
defp grouped_notification_key(type, activity) when type in @groupable_notification_types do
|
||||||
|
with target_id when is_binary(target_id) <- group_target_id(type, activity) do
|
||||||
|
# Mastodon uses Redis to reuse a recent bucket for rolling 12h windows. Pleroma keeps keys
|
||||||
|
# deterministic and self-contained; clients must treat group_key as opaque either way.
|
||||||
|
"#{type}-#{target_id}-#{group_time_bucket(activity)}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp grouped_notification_key(_type, _activity), do: nil
|
||||||
|
|
||||||
|
defp group_time_bucket(%Activity{inserted_at: inserted_at}) when not is_nil(inserted_at) do
|
||||||
|
inserted_at
|
||||||
|
|> NaiveDateTime.to_erl()
|
||||||
|
|> :calendar.datetime_to_gregorian_seconds()
|
||||||
|
|> div(@group_bucket_seconds)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp group_time_bucket(_),
|
||||||
|
do: group_time_bucket(%Activity{inserted_at: NaiveDateTime.utc_now()})
|
||||||
|
|
||||||
|
defp group_target_id(type, activity) when type in ["favourite", "reblog"] do
|
||||||
|
with object_id when is_binary(object_id) <- object_id_for(activity),
|
||||||
|
%Activity{id: id} <- Activity.get_create_by_object_ap_id(object_id) do
|
||||||
|
to_string(id)
|
||||||
|
else
|
||||||
|
_ -> nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp group_target_id("follow", %{data: %{"object" => ap_id}}) do
|
||||||
|
case User.get_cached_by_ap_id(ap_id) do
|
||||||
|
%User{id: id} -> to_string(id)
|
||||||
|
_ -> nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp group_target_id(_, _), do: nil
|
||||||
|
|
||||||
|
defp object_id_for(%{data: %{"object" => %{"id" => id}}}) when is_binary(id), do: id
|
||||||
|
defp object_id_for(%{data: %{"object" => id}}) when is_binary(id), do: id
|
||||||
|
defp object_id_for(_), do: nil
|
||||||
|
|
||||||
@notification_types ~w{
|
@notification_types ~w{
|
||||||
favourite
|
favourite
|
||||||
follow
|
follow
|
||||||
|
|
@ -79,7 +173,7 @@ defmodule Pleroma.Notification do
|
||||||
|
|
||||||
def changeset(%Notification{} = notification, attrs) do
|
def changeset(%Notification{} = notification, attrs) do
|
||||||
notification
|
notification
|
||||||
|> cast(attrs, [:seen, :type])
|
|> cast(attrs, [:seen, :type, :group_key])
|
||||||
|> validate_inclusion(:type, @notification_types)
|
|> validate_inclusion(:type, @notification_types)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -458,7 +552,8 @@ defmodule Pleroma.Notification do
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
activity: activity,
|
activity: activity,
|
||||||
seen: mark_as_read?(activity, user),
|
seen: mark_as_read?(activity, user),
|
||||||
type: type
|
type: type,
|
||||||
|
group_key: grouped_notification_key(type, activity)
|
||||||
})
|
})
|
||||||
|> Marker.multi_set_last_read_id(user, "notifications")
|
|> Marker.multi_set_last_read_id(user, "notifications")
|
||||||
|> Repo.transaction()
|
|> Repo.transaction()
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Pleroma.Web.ApiSpec.NotificationOperation do
|
defmodule Pleroma.Web.ApiSpec.NotificationOperation do
|
||||||
alias OpenApiSpex.Operation
|
|
||||||
alias OpenApiSpex.Operation
|
alias OpenApiSpex.Operation
|
||||||
alias OpenApiSpex.Schema
|
alias OpenApiSpex.Schema
|
||||||
alias Pleroma.Web.ApiSpec.Schemas.Account
|
alias Pleroma.Web.ApiSpec.Schemas.Account
|
||||||
|
|
@ -91,6 +90,79 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def grouped_index_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Notifications"],
|
||||||
|
summary: "Retrieve grouped notifications",
|
||||||
|
description: "Notifications concerning the user, grouped by supported notification types.",
|
||||||
|
operationId: "NotificationController.grouped_index",
|
||||||
|
security: [%{"oAuth" => ["read:notifications"]}],
|
||||||
|
parameters: grouped_notification_params() ++ pagination_params(),
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Grouped notifications", "application/json", grouped_result()),
|
||||||
|
404 => Operation.response("Error", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def show_group_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Notifications"],
|
||||||
|
summary: "Retrieve a notification group",
|
||||||
|
operationId: "NotificationController.show_group",
|
||||||
|
security: [%{"oAuth" => ["read:notifications"]}],
|
||||||
|
parameters: [group_key_param()],
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Grouped notifications", "application/json", grouped_result()),
|
||||||
|
404 => Operation.response("Error", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def group_accounts_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Notifications"],
|
||||||
|
summary: "Retrieve notification group accounts",
|
||||||
|
operationId: "NotificationController.group_accounts",
|
||||||
|
security: [%{"oAuth" => ["read:notifications"]}],
|
||||||
|
parameters: [group_key_param()],
|
||||||
|
responses: %{
|
||||||
|
200 =>
|
||||||
|
Operation.response("Accounts", "application/json", %Schema{
|
||||||
|
type: :array,
|
||||||
|
items: Account
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def unread_count_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Notifications"],
|
||||||
|
summary: "Retrieve unread grouped notification count",
|
||||||
|
operationId: "NotificationController.unread_count",
|
||||||
|
security: [%{"oAuth" => ["read:notifications"]}],
|
||||||
|
parameters:
|
||||||
|
grouped_notification_params() ++
|
||||||
|
[
|
||||||
|
Operation.parameter(
|
||||||
|
:limit,
|
||||||
|
:query,
|
||||||
|
%Schema{type: :integer},
|
||||||
|
"Maximum number of notifications to count"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
responses: %{
|
||||||
|
200 =>
|
||||||
|
Operation.response("Unread notification group count", "application/json", %Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{count: %Schema{type: :integer}}
|
||||||
|
}),
|
||||||
|
404 => Operation.response("Error", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def clear_operation do
|
def clear_operation do
|
||||||
%Operation{
|
%Operation{
|
||||||
tags: ["Notifications"],
|
tags: ["Notifications"],
|
||||||
|
|
@ -114,6 +186,18 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def dismiss_group_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Notifications"],
|
||||||
|
summary: "Dismiss a notification group",
|
||||||
|
description: "Clear all notifications in a notification group from the server.",
|
||||||
|
operationId: "NotificationController.dismiss_group",
|
||||||
|
parameters: [group_key_param()],
|
||||||
|
security: [%{"oAuth" => ["write:notifications"]}],
|
||||||
|
responses: %{200 => empty_object_response()}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def dismiss_via_body_operation do
|
def dismiss_via_body_operation do
|
||||||
%Operation{
|
%Operation{
|
||||||
tags: ["Notifications"],
|
tags: ["Notifications"],
|
||||||
|
|
@ -184,7 +268,7 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
|
||||||
},
|
},
|
||||||
example: %{
|
example: %{
|
||||||
"id" => "34975861",
|
"id" => "34975861",
|
||||||
"group-key" => "ungrouped-34975861",
|
"group_key" => "ungrouped-34975861",
|
||||||
"type" => "mention",
|
"type" => "mention",
|
||||||
"created_at" => "2019-11-23T07:49:02.064Z",
|
"created_at" => "2019-11-23T07:49:02.064Z",
|
||||||
"account" => Account.schema().example,
|
"account" => Account.schema().example,
|
||||||
|
|
@ -194,6 +278,73 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp grouped_result do
|
||||||
|
%Schema{
|
||||||
|
title: "GroupedNotificationsResults",
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
accounts: %Schema{type: :array, items: Account},
|
||||||
|
statuses: %Schema{type: :array, items: Status},
|
||||||
|
notification_groups: %Schema{type: :array, items: notification_group()}
|
||||||
|
},
|
||||||
|
required: [:accounts, :statuses, :notification_groups]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp notification_group do
|
||||||
|
%Schema{
|
||||||
|
title: "NotificationGroup",
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
group_key: %Schema{type: :string},
|
||||||
|
notifications_count: %Schema{type: :integer},
|
||||||
|
type: notification_type(),
|
||||||
|
most_recent_notification_id: %Schema{type: :string},
|
||||||
|
page_min_id: %Schema{type: :string, nullable: true},
|
||||||
|
page_max_id: %Schema{type: :string, nullable: true},
|
||||||
|
latest_page_notification_at: %Schema{type: :string, format: :"date-time", nullable: true},
|
||||||
|
sample_account_ids: %Schema{type: :array, items: %Schema{type: :string}},
|
||||||
|
status_id: %Schema{type: :string, nullable: true}
|
||||||
|
},
|
||||||
|
required: [
|
||||||
|
:group_key,
|
||||||
|
:notifications_count,
|
||||||
|
:type,
|
||||||
|
:most_recent_notification_id,
|
||||||
|
:sample_account_ids
|
||||||
|
]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp grouped_notification_params do
|
||||||
|
[
|
||||||
|
Operation.parameter(
|
||||||
|
:exclude_types,
|
||||||
|
:query,
|
||||||
|
%Schema{type: :array, items: notification_type()},
|
||||||
|
"Array of types to exclude"
|
||||||
|
),
|
||||||
|
Operation.parameter(
|
||||||
|
:account_id,
|
||||||
|
:query,
|
||||||
|
%Schema{type: :string},
|
||||||
|
"Return only notifications received from this account"
|
||||||
|
),
|
||||||
|
Operation.parameter(
|
||||||
|
:types,
|
||||||
|
:query,
|
||||||
|
%Schema{type: :array, items: notification_type()},
|
||||||
|
"Include the notifications for activities with the given types"
|
||||||
|
),
|
||||||
|
Operation.parameter(
|
||||||
|
:grouped_types,
|
||||||
|
:query,
|
||||||
|
%Schema{type: :array, items: notification_type()},
|
||||||
|
"Notification types that may be grouped"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
defp notification_type do
|
defp notification_type do
|
||||||
%Schema{
|
%Schema{
|
||||||
type: :string,
|
type: :string,
|
||||||
|
|
@ -239,4 +390,11 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
|
||||||
required: true
|
required: true
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp group_key_param do
|
||||||
|
Operation.parameter(:group_key, :path, :string, "Notification group key",
|
||||||
|
example: "favourite-123",
|
||||||
|
required: true
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -55,16 +55,28 @@ defmodule Pleroma.Web.ControllerHelper do
|
||||||
|
|
||||||
# TODO: Only fetch the params from open_api_spex when everything is converted
|
# TODO: Only fetch the params from open_api_spex when everything is converted
|
||||||
@id_keys Pagination.page_keys() -- ["limit", "order"]
|
@id_keys Pagination.page_keys() -- ["limit", "order"]
|
||||||
|
@id_key_atoms Enum.map(@id_keys, &String.to_atom/1)
|
||||||
|
@drop_id_params_key :drop_id_params
|
||||||
defp build_pagination_fields(conn, min_id, max_id, extra_params, order) do
|
defp build_pagination_fields(conn, min_id, max_id, extra_params, order) do
|
||||||
|
drop_id_params? = Map.get(extra_params, @drop_id_params_key, false)
|
||||||
|
extra_params = Map.delete(extra_params, @drop_id_params_key)
|
||||||
|
|
||||||
|
path_param_keys =
|
||||||
|
conn.path_params
|
||||||
|
|> Map.keys()
|
||||||
|
|> Enum.flat_map(&[&1, String.to_existing_atom(&1)])
|
||||||
|
|
||||||
params =
|
params =
|
||||||
if Map.has_key?(conn.private, :open_api_spex) do
|
if Map.has_key?(conn.private, :open_api_spex) do
|
||||||
get_in(conn, [Access.key(:private), Access.key(:open_api_spex), Access.key(:params)])
|
get_in(conn, [Access.key(:private), Access.key(:open_api_spex), Access.key(:params)])
|
||||||
else
|
else
|
||||||
conn.params
|
conn.params
|
||||||
end
|
end
|
||||||
|> Map.drop(Map.keys(conn.path_params) |> Enum.map(&String.to_existing_atom/1))
|
|> Map.drop(path_param_keys)
|
||||||
|> Map.merge(extra_params)
|
|> Map.merge(extra_params)
|
||||||
|> Map.drop(@id_keys)
|
# Some OpenApiSpex routes cast cursor params to atoms. Keep the historical default for
|
||||||
|
# existing endpoints, but allow grouped notifications to opt out of stale request cursors.
|
||||||
|
|> Map.drop(@id_keys ++ if(drop_id_params?, do: @id_key_atoms, else: []))
|
||||||
|
|
||||||
{{next_id, nid}, {prev_id, pid}} =
|
{{next_id, nid}, {prev_id, pid}} =
|
||||||
if order == :desc,
|
if order == :desc,
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,17 @@
|
||||||
defmodule Pleroma.Web.MastodonAPI.NotificationController do
|
defmodule Pleroma.Web.MastodonAPI.NotificationController do
|
||||||
use Pleroma.Web, :controller
|
use Pleroma.Web, :controller
|
||||||
|
|
||||||
import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
|
import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2, add_link_headers: 3]
|
||||||
|
|
||||||
alias Pleroma.Notification
|
alias Pleroma.Notification
|
||||||
|
alias Pleroma.User
|
||||||
|
alias Pleroma.Web.MastodonAPI.AccountView
|
||||||
alias Pleroma.Web.MastodonAPI.MastodonAPI
|
alias Pleroma.Web.MastodonAPI.MastodonAPI
|
||||||
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
||||||
|
|
||||||
@oauth_read_actions [:show, :index]
|
# Mastodon's docs currently list write:notifications for group accounts, but the endpoint is
|
||||||
|
# read-only and Mastodon's implementation accepts read:notifications. Prefer least privilege.
|
||||||
|
@oauth_read_actions [:show, :index, :grouped_index, :show_group, :group_accounts, :unread_count]
|
||||||
|
|
||||||
plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
|
plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
|
||||||
|
|
||||||
|
|
@ -39,7 +43,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
|
||||||
|
|
||||||
# GET /api/v1/notifications
|
# GET /api/v1/notifications
|
||||||
def index(%{private: %{open_api_spex: %{params: %{account_id: account_id} = params}}} = conn, _) do
|
def index(%{private: %{open_api_spex: %{params: %{account_id: account_id} = params}}} = conn, _) do
|
||||||
case Pleroma.User.get_cached_by_id(account_id) do
|
case User.get_cached_by_id(account_id) do
|
||||||
%{ap_id: account_ap_id} ->
|
%{ap_id: account_ap_id} ->
|
||||||
params =
|
params =
|
||||||
params
|
params
|
||||||
|
|
@ -59,10 +63,108 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
|
||||||
do_get_notifications(conn, params)
|
do_get_notifications(conn, params)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# GET /api/v2/notifications
|
||||||
|
def grouped_index(
|
||||||
|
%{private: %{open_api_spex: %{params: %{account_id: account_id} = params}}} = conn,
|
||||||
|
_
|
||||||
|
) do
|
||||||
|
case User.get_cached_by_id(account_id) do
|
||||||
|
%{ap_id: account_ap_id} ->
|
||||||
|
params =
|
||||||
|
params
|
||||||
|
|> Map.delete(:account_id)
|
||||||
|
|> Map.put(:account_ap_id, account_ap_id)
|
||||||
|
|
||||||
|
do_get_grouped_notifications(conn, params)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
conn
|
||||||
|
|> put_status(:not_found)
|
||||||
|
|> json(%{"error" => "Account is not found"})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def grouped_index(%{private: %{open_api_spex: %{params: params}}} = conn, _) do
|
||||||
|
do_get_grouped_notifications(conn, params)
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /api/v2/notifications/:group_key
|
||||||
|
def show_group(
|
||||||
|
%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{group_key: group_key}}}} =
|
||||||
|
conn,
|
||||||
|
_
|
||||||
|
) do
|
||||||
|
{notifications, notification_group_counts, notification_group_bounds} =
|
||||||
|
MastodonAPI.get_notification_group_result(user, group_key, %{})
|
||||||
|
|
||||||
|
if Enum.empty?(notifications) do
|
||||||
|
conn
|
||||||
|
|> put_status(:not_found)
|
||||||
|
|> json(%{"error" => "Notification group is not found"})
|
||||||
|
else
|
||||||
|
grouped_types = if String.starts_with?(group_key, "ungrouped-"), do: [], else: nil
|
||||||
|
|
||||||
|
render(conn, "grouped_index.json",
|
||||||
|
notification_groups: [notifications],
|
||||||
|
notification_group_counts: notification_group_counts,
|
||||||
|
notification_group_bounds: notification_group_bounds,
|
||||||
|
for: user,
|
||||||
|
grouped_types: grouped_types,
|
||||||
|
include_page_metadata: false
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /api/v2/notifications/:group_key/accounts
|
||||||
|
def group_accounts(
|
||||||
|
%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{group_key: group_key}}}} =
|
||||||
|
conn,
|
||||||
|
_
|
||||||
|
) do
|
||||||
|
# Mastodon paginates this endpoint in code, but the public docs say it returns accounts of all
|
||||||
|
# notifications in the group and do not document cursor params here. Follow the documented API.
|
||||||
|
users = MastodonAPI.get_notification_group_accounts(user, group_key)
|
||||||
|
|
||||||
|
json(conn, AccountView.render("index.json", %{users: users, for: user}))
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /api/v2/notifications/unread_count
|
||||||
|
def unread_count(
|
||||||
|
%{private: %{open_api_spex: %{params: %{account_id: account_id} = params}}} = conn,
|
||||||
|
_
|
||||||
|
) do
|
||||||
|
case User.get_cached_by_id(account_id) do
|
||||||
|
%{ap_id: account_ap_id} ->
|
||||||
|
params =
|
||||||
|
params
|
||||||
|
|> Map.delete(:account_id)
|
||||||
|
|> Map.put(:account_ap_id, account_ap_id)
|
||||||
|
|
||||||
|
do_get_unread_group_count(conn, params)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
conn
|
||||||
|
|> put_status(:not_found)
|
||||||
|
|> json(%{"error" => "Account is not found"})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def unread_count(%{private: %{open_api_spex: %{params: params}}} = conn, _) do
|
||||||
|
do_get_unread_group_count(conn, params)
|
||||||
|
end
|
||||||
|
|
||||||
|
# POST /api/v2/notifications/:group_key/dismiss
|
||||||
|
def dismiss_group(
|
||||||
|
%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{group_key: group_key}}}} =
|
||||||
|
conn,
|
||||||
|
_
|
||||||
|
) do
|
||||||
|
MastodonAPI.dismiss_notification_group(user, group_key)
|
||||||
|
json(conn, %{})
|
||||||
|
end
|
||||||
|
|
||||||
defp do_get_notifications(%{assigns: %{user: user}} = conn, params) do
|
defp do_get_notifications(%{assigns: %{user: user}} = conn, params) do
|
||||||
params =
|
params = normalize_notification_params(params)
|
||||||
Map.new(params, fn {k, v} -> {to_string(k), v} end)
|
|
||||||
|> Map.put_new("types", Map.get(params, :include_types, @default_notification_types))
|
|
||||||
|
|
||||||
notifications = MastodonAPI.get_notifications(user, params)
|
notifications = MastodonAPI.get_notifications(user, params)
|
||||||
|
|
||||||
|
|
@ -74,6 +176,35 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp do_get_grouped_notifications(%{assigns: %{user: user}} = conn, params) do
|
||||||
|
params = normalize_notification_params(params)
|
||||||
|
|
||||||
|
{notification_groups, page_notifications, notification_group_counts,
|
||||||
|
notification_group_bounds} =
|
||||||
|
MastodonAPI.get_grouped_notification_page(user, params)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> add_link_headers(page_notifications, %{drop_id_params: true})
|
||||||
|
|> render("grouped_index.json",
|
||||||
|
notification_groups: notification_groups,
|
||||||
|
notification_group_counts: notification_group_counts,
|
||||||
|
notification_group_bounds: notification_group_bounds,
|
||||||
|
for: user,
|
||||||
|
grouped_types: params["grouped_types"]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_get_unread_group_count(%{assigns: %{user: user}} = conn, params) do
|
||||||
|
params = normalize_notification_params(params)
|
||||||
|
json(conn, %{count: MastodonAPI.unread_notification_group_count(user, params)})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp normalize_notification_params(params) do
|
||||||
|
params
|
||||||
|
|> Map.new(fn {k, v} -> {to_string(k), v} end)
|
||||||
|
|> Map.put_new("types", Map.get(params, :include_types, @default_notification_types))
|
||||||
|
end
|
||||||
|
|
||||||
# GET /api/v1/notifications/:id
|
# GET /api/v1/notifications/:id
|
||||||
def show(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
|
def show(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
|
||||||
with {:ok, notification} <- Notification.get(user, id) do
|
with {:ok, notification} <- Notification.get(user, id) do
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,16 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
alias Pleroma.Marker
|
||||||
alias Pleroma.Notification
|
alias Pleroma.Notification
|
||||||
alias Pleroma.Pagination
|
alias Pleroma.Pagination
|
||||||
|
alias Pleroma.Repo
|
||||||
alias Pleroma.ScheduledActivity
|
alias Pleroma.ScheduledActivity
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.Web.CommonAPI
|
alias Pleroma.Web.CommonAPI
|
||||||
|
|
||||||
|
@notification_group_sample_limit 8
|
||||||
|
|
||||||
@spec follow(User.t(), User.t(), map) :: {:ok, User.t()} | {:error, String.t()}
|
@spec follow(User.t(), User.t(), map) :: {:ok, User.t()} | {:error, String.t()}
|
||||||
def follow(follower, followed, params \\ %{}) do
|
def follow(follower, followed, params \\ %{}) do
|
||||||
result =
|
result =
|
||||||
|
|
@ -61,29 +65,356 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_notifications(user, params \\ %{}) do
|
def get_notifications(user, params \\ %{}) do
|
||||||
options =
|
user
|
||||||
cast_params(params) |> Map.update(:include_types, [], fn include_types -> include_types end)
|
|> notifications_query(params)
|
||||||
|
|> Pagination.fetch_paginated(params)
|
||||||
|
end
|
||||||
|
|
||||||
options =
|
def get_grouped_notification_page(user, params \\ %{}) do
|
||||||
if ("pleroma:report" not in options.include_types and
|
grouped_types =
|
||||||
User.privileged?(user, :reports_manage_reports)) or
|
params
|
||||||
User.privileged?(user, :reports_manage_reports) do
|
|> Map.get("grouped_types")
|
||||||
options
|
|> Notification.normalize_grouped_types()
|
||||||
else
|
|
||||||
options
|
query = notifications_query(user, params)
|
||||||
|> Map.update(:exclude_types, ["pleroma:report"], fn current_exclude_types ->
|
{order, cursor_filters} = group_pagination(params)
|
||||||
current_exclude_types ++ ["pleroma:report"]
|
|
||||||
end)
|
group_rows =
|
||||||
end
|
query
|
||||||
|
|> notification_group_rows(grouped_types, grouped_limit(params), order, cursor_filters)
|
||||||
|
|
||||||
|
group_rows = if order == :asc, do: Enum.reverse(group_rows), else: group_rows
|
||||||
|
|
||||||
|
page_notifications = representative_notifications(query, group_rows)
|
||||||
|
|
||||||
|
notification_groups =
|
||||||
|
Enum.map(group_rows, ¬ification_group_sample(user, params, &1.group_key))
|
||||||
|
|
||||||
|
{
|
||||||
|
notification_groups,
|
||||||
|
page_notifications,
|
||||||
|
notification_group_counts(group_rows),
|
||||||
|
notification_group_bounds(group_rows)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_notification_group_result(user, group_key, params \\ %{}) do
|
||||||
|
notifications = notification_group_sample(user, params, group_key)
|
||||||
|
metadata = notification_group_metadata(user, group_key, params)
|
||||||
|
|
||||||
|
if Enum.empty?(notifications) or is_nil(metadata) do
|
||||||
|
{[], %{}, %{}}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
notifications,
|
||||||
|
%{group_key => metadata.notifications_count},
|
||||||
|
%{group_key => Map.drop(metadata, [:notifications_count])}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_notification_group(user, group_key, params \\ %{})
|
||||||
|
|
||||||
|
def get_notification_group(user, "ungrouped-" <> notification_id, _params) do
|
||||||
|
case Notification.get(user, notification_id) do
|
||||||
|
{:ok, notification} -> [notification]
|
||||||
|
_ -> []
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_notification_group(user, group_key, params) do
|
||||||
|
notification_group_sample(user, params, group_key)
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_notification_group_accounts(user, "ungrouped-" <> notification_id) do
|
||||||
|
with {:ok, notification} <- Notification.get(user, notification_id),
|
||||||
|
%User{} = actor <- User.get_cached_by_ap_id(notification.activity.data["actor"]) do
|
||||||
|
[actor]
|
||||||
|
else
|
||||||
|
_ -> []
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_notification_group_accounts(user, group_key) do
|
||||||
|
user
|
||||||
|
|> notification_group_query(group_key, %{})
|
||||||
|
|> exclude(:preload)
|
||||||
|
|> distinct(true)
|
||||||
|
|> select([user_actor: user_actor], user_actor)
|
||||||
|
|> Repo.all()
|
||||||
|
end
|
||||||
|
|
||||||
|
def dismiss_notification_group(user, "ungrouped-" <> notification_id) do
|
||||||
|
Notification.destroy_multiple(user, [notification_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def dismiss_notification_group(%User{id: user_id}, group_key) do
|
||||||
|
Notification
|
||||||
|
|> where([n], n.user_id == ^user_id and n.group_key == ^group_key)
|
||||||
|
|> Repo.delete_all()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp group_pagination(params) do
|
||||||
|
cond do
|
||||||
|
min_id = Map.get(params, "min_id") ->
|
||||||
|
cursor_filters = [{:gt, min_id}]
|
||||||
|
|
||||||
|
cursor_filters =
|
||||||
|
case Map.get(params, "max_id") do
|
||||||
|
nil -> cursor_filters
|
||||||
|
max_id -> [{:lt, max_id} | cursor_filters]
|
||||||
|
end
|
||||||
|
|
||||||
|
{:asc, cursor_filters}
|
||||||
|
|
||||||
|
since_id = Map.get(params, "since_id") ->
|
||||||
|
{:desc, [{:gt, since_id}]}
|
||||||
|
|
||||||
|
max_id = Map.get(params, "max_id") ->
|
||||||
|
{:desc, [{:lt, max_id}]}
|
||||||
|
|
||||||
|
true ->
|
||||||
|
{:desc, []}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp grouped_limit(params) do
|
||||||
|
params
|
||||||
|
|> Map.get("limit", 40)
|
||||||
|
|> parse_limit(40)
|
||||||
|
|> min(80)
|
||||||
|
end
|
||||||
|
|
||||||
|
def unread_notification_group_count(user, params \\ %{}) do
|
||||||
|
grouped_types =
|
||||||
|
params
|
||||||
|
|> Map.get("grouped_types")
|
||||||
|
|> Notification.normalize_grouped_types()
|
||||||
|
|
||||||
|
limit = unread_count_limit(params)
|
||||||
|
|
||||||
|
user
|
||||||
|
|> notifications_query(params)
|
||||||
|
# The grouped API docs define unread by the notifications marker, not by Pleroma's per-row
|
||||||
|
# seen flag used by the v1 unread count. Keep this marker-based for Mastodon clients.
|
||||||
|
|> restrict_after_marker(notification_marker_last_read_id(user))
|
||||||
|
|> notification_group_rows(grouped_types, limit, :desc, [])
|
||||||
|
|> length()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp notification_group_rows(query, grouped_types, group_limit, :desc, cursor_filters) do
|
||||||
|
query
|
||||||
|
|> notification_group_keyed_query(grouped_types)
|
||||||
|
|> group_by([n], n.group_key)
|
||||||
|
|> apply_group_cursor_filters(cursor_filters)
|
||||||
|
|> select([n], %{
|
||||||
|
group_key: n.group_key,
|
||||||
|
representative_id: max(n.id),
|
||||||
|
notifications_count: count(n.id),
|
||||||
|
page_min_id: min(n.id),
|
||||||
|
page_max_id: max(n.id),
|
||||||
|
latest_page_notification_at: max(n.inserted_at)
|
||||||
|
})
|
||||||
|
|> order_by([n], desc: max(n.id))
|
||||||
|
|> limit(^group_limit)
|
||||||
|
|> Repo.all()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp notification_group_rows(query, grouped_types, group_limit, :asc, cursor_filters) do
|
||||||
|
query
|
||||||
|
|> notification_group_keyed_query(grouped_types)
|
||||||
|
|> group_by([n], n.group_key)
|
||||||
|
|> apply_group_cursor_filters(cursor_filters)
|
||||||
|
|> select([n], %{
|
||||||
|
group_key: n.group_key,
|
||||||
|
representative_id: max(n.id),
|
||||||
|
notifications_count: count(n.id),
|
||||||
|
page_min_id: min(n.id),
|
||||||
|
page_max_id: max(n.id),
|
||||||
|
latest_page_notification_at: max(n.inserted_at)
|
||||||
|
})
|
||||||
|
|> order_by([n], asc: max(n.id))
|
||||||
|
|> limit(^group_limit)
|
||||||
|
|> Repo.all()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp apply_group_cursor_filters(query, []), do: query
|
||||||
|
|
||||||
|
defp apply_group_cursor_filters(query, [{:gt, id} | rest]) do
|
||||||
|
query
|
||||||
|
|> having([n], max(n.id) > ^id)
|
||||||
|
|> apply_group_cursor_filters(rest)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp apply_group_cursor_filters(query, [{:lt, id} | rest]) do
|
||||||
|
query
|
||||||
|
|> having([n], max(n.id) < ^id)
|
||||||
|
|> apply_group_cursor_filters(rest)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp notification_group_keyed_query(query, grouped_types) do
|
||||||
|
query
|
||||||
|
|> exclude(:preload)
|
||||||
|
|> select([n], %{
|
||||||
|
id: n.id,
|
||||||
|
inserted_at: n.inserted_at,
|
||||||
|
group_key:
|
||||||
|
fragment(
|
||||||
|
"CASE WHEN ? IS NOT NULL AND ?::text = ANY(?) THEN ? ELSE 'ungrouped-' || ?::text END",
|
||||||
|
n.group_key,
|
||||||
|
n.type,
|
||||||
|
type(^grouped_types, {:array, :string}),
|
||||||
|
n.group_key,
|
||||||
|
n.id
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|> subquery()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp representative_notifications(_query, []), do: []
|
||||||
|
|
||||||
|
defp representative_notifications(query, group_rows) do
|
||||||
|
representative_ids = Enum.map(group_rows, & &1.representative_id)
|
||||||
|
|
||||||
|
notifications_by_id =
|
||||||
|
query
|
||||||
|
|> where([n], n.id in ^representative_ids)
|
||||||
|
|> Repo.all()
|
||||||
|
|> Map.new(&{to_string(&1.id), &1})
|
||||||
|
|
||||||
|
group_rows
|
||||||
|
|> Enum.map(&Map.get(notifications_by_id, to_string(&1.representative_id)))
|
||||||
|
|> Enum.filter(& &1)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp notification_group_counts(group_rows) do
|
||||||
|
Map.new(group_rows, &{&1.group_key, &1.notifications_count})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp notification_group_bounds(group_rows) do
|
||||||
|
Map.new(group_rows, fn row ->
|
||||||
|
{row.group_key,
|
||||||
|
%{
|
||||||
|
page_min_id: row.page_min_id,
|
||||||
|
page_max_id: row.page_max_id,
|
||||||
|
latest_page_notification_at: row.latest_page_notification_at
|
||||||
|
}}
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp notification_group_sample(user, _params, "ungrouped-" <> notification_id) do
|
||||||
|
case Notification.get(user, notification_id) do
|
||||||
|
{:ok, notification} -> [notification]
|
||||||
|
_ -> []
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp notification_group_sample(user, params, group_key) do
|
||||||
|
user
|
||||||
|
|> notification_group_query(group_key, params)
|
||||||
|
|> order_by([n], desc: n.id)
|
||||||
|
|> limit(^@notification_group_sample_limit)
|
||||||
|
|> Repo.all()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp notification_group_metadata(user, "ungrouped-" <> notification_id, _params) do
|
||||||
|
case Notification.get(user, notification_id) do
|
||||||
|
{:ok, notification} ->
|
||||||
|
%{
|
||||||
|
notifications_count: 1,
|
||||||
|
page_min_id: notification.id,
|
||||||
|
page_max_id: notification.id,
|
||||||
|
latest_page_notification_at: notification.inserted_at
|
||||||
|
}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp notification_group_metadata(user, group_key, params) do
|
||||||
|
user
|
||||||
|
|> notification_group_query(group_key, params)
|
||||||
|
|> exclude(:preload)
|
||||||
|
|> select([n], %{
|
||||||
|
notifications_count: count(n.id),
|
||||||
|
page_min_id: min(n.id),
|
||||||
|
page_max_id: max(n.id),
|
||||||
|
latest_page_notification_at: max(n.inserted_at)
|
||||||
|
})
|
||||||
|
|> Repo.one()
|
||||||
|
|> case do
|
||||||
|
%{notifications_count: 0} -> nil
|
||||||
|
metadata -> metadata
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp notification_group_query(user, group_key, params) do
|
||||||
|
user
|
||||||
|
|> notifications_query(params)
|
||||||
|
|> where([n], n.group_key == ^group_key)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp notification_marker_last_read_id(user) do
|
||||||
|
Marker
|
||||||
|
|> where([m], m.user_id == ^user.id and m.timeline == "notifications")
|
||||||
|
|> select([m], m.last_read_id)
|
||||||
|
|> Repo.one()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp restrict_after_marker(query, last_read_id)
|
||||||
|
when is_binary(last_read_id) and last_read_id != "" do
|
||||||
|
where(query, [n], n.id > ^last_read_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp restrict_after_marker(query, _last_read_id), do: query
|
||||||
|
|
||||||
|
defp notifications_query(user, params) do
|
||||||
|
options = notification_options(user, params)
|
||||||
|
|
||||||
user
|
user
|
||||||
|> Notification.for_user_query(options)
|
|> Notification.for_user_query(options)
|
||||||
|> restrict(:types, options)
|
|> restrict(:types, options)
|
||||||
|> restrict(:exclude_types, options)
|
|> restrict(:exclude_types, options)
|
||||||
|> restrict(:account_ap_id, options)
|
|> restrict(:account_ap_id, options)
|
||||||
|> Pagination.fetch_paginated(params)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp notification_options(user, params) do
|
||||||
|
options =
|
||||||
|
params
|
||||||
|
|> cast_params()
|
||||||
|
|> Map.update(:include_types, [], fn include_types -> include_types end)
|
||||||
|
|
||||||
|
if ("pleroma:report" not in options.include_types and
|
||||||
|
User.privileged?(user, :reports_manage_reports)) or
|
||||||
|
User.privileged?(user, :reports_manage_reports) do
|
||||||
|
options
|
||||||
|
else
|
||||||
|
options
|
||||||
|
|> Map.update(:exclude_types, ["pleroma:report"], fn current_exclude_types ->
|
||||||
|
current_exclude_types ++ ["pleroma:report"]
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp unread_count_limit(params) do
|
||||||
|
params
|
||||||
|
|> Map.get("limit", 100)
|
||||||
|
|> parse_limit(100)
|
||||||
|
|> min(1000)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_limit(limit, _default) when is_integer(limit) and limit > 0, do: limit
|
||||||
|
|
||||||
|
defp parse_limit(limit, default) when is_binary(limit) do
|
||||||
|
case Integer.parse(limit) do
|
||||||
|
{limit, _} when limit > 0 -> limit
|
||||||
|
_ -> default
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_limit(_, default), do: default
|
||||||
|
|
||||||
def get_scheduled_activities(user, params \\ %{}) do
|
def get_scheduled_activities(user, params \\ %{}) do
|
||||||
user
|
user
|
||||||
|> ScheduledActivity.for_user_query()
|
|> ScheduledActivity.for_user_query()
|
||||||
|
|
@ -95,6 +426,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
|
||||||
exclude_types: {:array, :string},
|
exclude_types: {:array, :string},
|
||||||
types: {:array, :string},
|
types: {:array, :string},
|
||||||
exclude_visibilities: {:array, :string},
|
exclude_visibilities: {:array, :string},
|
||||||
|
grouped_types: {:array, :string},
|
||||||
|
limit: :integer,
|
||||||
reblogs: :boolean,
|
reblogs: :boolean,
|
||||||
with_muted: :boolean,
|
with_muted: :boolean,
|
||||||
account_ap_id: :string,
|
account_ap_id: :string,
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,58 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
|
||||||
safe_render_many(notifications, NotificationView, "show.json", opts)
|
safe_render_many(notifications, NotificationView, "show.json", opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def render("grouped_index.json", %{notifications: notifications} = opts) do
|
||||||
|
grouped_types = Notification.normalize_grouped_types(opts[:grouped_types])
|
||||||
|
|
||||||
|
opts
|
||||||
|
|> Map.delete(:notifications)
|
||||||
|
|> Map.put(
|
||||||
|
:notification_groups,
|
||||||
|
Notification.group_notifications(notifications, grouped_types)
|
||||||
|
)
|
||||||
|
|> then(&render("grouped_index.json", &1))
|
||||||
|
end
|
||||||
|
|
||||||
|
def render(
|
||||||
|
"grouped_index.json",
|
||||||
|
%{notification_groups: notification_groups, for: reading_user} = opts
|
||||||
|
) do
|
||||||
|
grouped_types = Notification.normalize_grouped_types(opts[:grouped_types])
|
||||||
|
notification_group_counts = Map.get(opts, :notification_group_counts, %{})
|
||||||
|
notification_group_bounds = Map.get(opts, :notification_group_bounds, %{})
|
||||||
|
include_page_metadata = Map.get(opts, :include_page_metadata, true)
|
||||||
|
|
||||||
|
statuses =
|
||||||
|
notification_groups
|
||||||
|
|> Enum.map(&List.first/1)
|
||||||
|
|> Enum.map(&render("show.json", %{notification: &1, for: reading_user}))
|
||||||
|
|> Enum.map(& &1[:status])
|
||||||
|
|> Enum.filter(& &1)
|
||||||
|
|> Enum.uniq_by(& &1[:id])
|
||||||
|
|
||||||
|
actors =
|
||||||
|
notification_groups
|
||||||
|
|> List.flatten()
|
||||||
|
|> notification_actors()
|
||||||
|
|
||||||
|
%{
|
||||||
|
accounts: AccountView.render("index.json", %{users: actors, for: reading_user}),
|
||||||
|
statuses: statuses,
|
||||||
|
notification_groups:
|
||||||
|
Enum.map(
|
||||||
|
notification_groups,
|
||||||
|
&render_group(
|
||||||
|
&1,
|
||||||
|
reading_user,
|
||||||
|
grouped_types,
|
||||||
|
notification_group_counts,
|
||||||
|
notification_group_bounds,
|
||||||
|
include_page_metadata
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def render(
|
def render(
|
||||||
"show.json",
|
"show.json",
|
||||||
%{
|
%{
|
||||||
|
|
@ -95,7 +147,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
|
||||||
|
|
||||||
response = %{
|
response = %{
|
||||||
id: to_string(notification.id),
|
id: to_string(notification.id),
|
||||||
group_key: "ungrouped-" <> to_string(notification.id),
|
group_key: Notification.group_key(notification),
|
||||||
type: notification.type,
|
type: notification.type,
|
||||||
created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at),
|
created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at),
|
||||||
account: account,
|
account: account,
|
||||||
|
|
@ -137,6 +189,104 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
|
||||||
Map.put(response, :report, report_render)
|
Map.put(response, :report, report_render)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp render_group(
|
||||||
|
[%Notification{} = notification | _] = notifications,
|
||||||
|
_reading_user,
|
||||||
|
grouped_types,
|
||||||
|
notification_group_counts,
|
||||||
|
notification_group_bounds,
|
||||||
|
include_page_metadata
|
||||||
|
) do
|
||||||
|
latest_notification = List.first(notifications)
|
||||||
|
oldest_notification = List.last(notifications)
|
||||||
|
status_activity = status_activity_for_group(notifications, grouped_types)
|
||||||
|
group_key = Notification.group_key(notification, grouped_types)
|
||||||
|
bounds = Map.get(notification_group_bounds, group_key, %{})
|
||||||
|
|
||||||
|
response = %{
|
||||||
|
group_key: group_key,
|
||||||
|
notifications_count: Map.get(notification_group_counts, group_key, length(notifications)),
|
||||||
|
type: notification.type,
|
||||||
|
most_recent_notification_id:
|
||||||
|
bounds
|
||||||
|
|> Map.get(:page_max_id, latest_notification.id)
|
||||||
|
|> to_string(),
|
||||||
|
sample_account_ids:
|
||||||
|
notifications
|
||||||
|
|> notification_actors()
|
||||||
|
|> Enum.map(&to_string(&1.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
response =
|
||||||
|
if include_page_metadata do
|
||||||
|
Map.merge(response, %{
|
||||||
|
page_min_id:
|
||||||
|
bounds
|
||||||
|
|> Map.get(:page_min_id, oldest_notification.id)
|
||||||
|
|> to_string(),
|
||||||
|
page_max_id:
|
||||||
|
bounds
|
||||||
|
|> Map.get(:page_max_id, latest_notification.id)
|
||||||
|
|> to_string(),
|
||||||
|
latest_page_notification_at:
|
||||||
|
bounds
|
||||||
|
|> Map.get(:latest_page_notification_at, latest_notification.inserted_at)
|
||||||
|
|> CommonAPI.Utils.to_masto_date()
|
||||||
|
})
|
||||||
|
else
|
||||||
|
response
|
||||||
|
end
|
||||||
|
|
||||||
|
if status_activity do
|
||||||
|
Map.put(response, :status_id, to_string(status_activity.id))
|
||||||
|
else
|
||||||
|
response
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp status_activity_for_group([%Notification{} = notification | _], grouped_types) do
|
||||||
|
status_activity_for(notification, grouped_types)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp status_activity_for(%Notification{type: type, activity: activity}, _grouped_types)
|
||||||
|
when type in ["mention", "status", "poll"] do
|
||||||
|
activity
|
||||||
|
end
|
||||||
|
|
||||||
|
defp status_activity_for(%Notification{type: type} = notification, grouped_types)
|
||||||
|
when type in ["favourite", "reblog"] do
|
||||||
|
group_key = Notification.group_key(notification, grouped_types)
|
||||||
|
|
||||||
|
case String.split(group_key, "-", parts: 3) do
|
||||||
|
[^type, activity_id, _bucket] ->
|
||||||
|
case Activity.create_by_id_with_object(activity_id) do
|
||||||
|
%Activity{} = activity -> activity
|
||||||
|
_ -> parent_status_activity(notification.activity)
|
||||||
|
end
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
parent_status_activity(notification.activity)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp status_activity_for(%Notification{type: type, activity: activity}, _grouped_types)
|
||||||
|
when type in ["update", "pleroma:emoji_reaction"] do
|
||||||
|
parent_status_activity(activity)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp status_activity_for(_, _grouped_types), do: nil
|
||||||
|
|
||||||
|
defp parent_status_activity(activity) do
|
||||||
|
Activity.get_create_by_object_ap_id(object_id_for(activity))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp notification_actors(notifications) do
|
||||||
|
notifications
|
||||||
|
|> Enum.map(&User.get_cached_by_ap_id(&1.activity.data["actor"]))
|
||||||
|
|> Enum.filter(& &1)
|
||||||
|
|> Enum.uniq_by(& &1.id)
|
||||||
|
end
|
||||||
|
|
||||||
defp put_emoji(response, activity) do
|
defp put_emoji(response, activity) do
|
||||||
response
|
response
|
||||||
|> Map.put(:emoji, activity.data["content"])
|
|> Map.put(:emoji, activity.data["content"])
|
||||||
|
|
|
||||||
|
|
@ -844,6 +844,16 @@ defmodule Pleroma.Web.Router do
|
||||||
get("/directory", DirectoryController, :index)
|
get("/directory", DirectoryController, :index)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
scope "/api/v2", Pleroma.Web.MastodonAPI do
|
||||||
|
pipe_through(:authenticated_api)
|
||||||
|
|
||||||
|
get("/notifications", NotificationController, :grouped_index)
|
||||||
|
get("/notifications/unread_count", NotificationController, :unread_count)
|
||||||
|
get("/notifications/:group_key/accounts", NotificationController, :group_accounts)
|
||||||
|
get("/notifications/:group_key", NotificationController, :show_group)
|
||||||
|
post("/notifications/:group_key/dismiss", NotificationController, :dismiss_group)
|
||||||
|
end
|
||||||
|
|
||||||
scope "/api/v2", Pleroma.Web.MastodonAPI do
|
scope "/api/v2", Pleroma.Web.MastodonAPI do
|
||||||
pipe_through(:api)
|
pipe_through(:api)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
defmodule Pleroma.Repo.Migrations.AddGroupKeyToNotifications do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table(:notifications) do
|
||||||
|
add(:group_key, :string)
|
||||||
|
end
|
||||||
|
|
||||||
|
create_if_not_exists(
|
||||||
|
index(:notifications, [:user_id, :group_key, "id desc nulls last"],
|
||||||
|
where: "group_key IS NOT NULL"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -577,6 +577,298 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do
|
||||||
assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
|
assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "GET /api/v2/notifications" do
|
||||||
|
test "groups favourite notifications for the same status" do
|
||||||
|
%{user: user, conn: conn} = oauth_access(["read:notifications"])
|
||||||
|
other_user1 = insert(:user)
|
||||||
|
other_user2 = insert(:user)
|
||||||
|
|
||||||
|
{:ok, status} = CommonAPI.post(user, %{status: "hello"})
|
||||||
|
{:ok, _} = CommonAPI.favorite(status.id, other_user1)
|
||||||
|
{:ok, _} = CommonAPI.favorite(status.id, other_user2)
|
||||||
|
|
||||||
|
notifications = Repo.all(Notification)
|
||||||
|
notification_ids = Enum.map(notifications, &to_string(&1.id))
|
||||||
|
assert [persisted_group_key] = notifications |> Enum.map(& &1.group_key) |> Enum.uniq()
|
||||||
|
assert is_binary(persisted_group_key)
|
||||||
|
|
||||||
|
result =
|
||||||
|
conn
|
||||||
|
|> get("/api/v2/notifications")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert [%{"id" => account_id1}, %{"id" => account_id2}] = result["accounts"]
|
||||||
|
assert account_id1 in [other_user1.id, other_user2.id]
|
||||||
|
assert account_id2 in [other_user1.id, other_user2.id]
|
||||||
|
assert [%{"id" => status_id}] = result["statuses"]
|
||||||
|
assert status_id == status.id
|
||||||
|
|
||||||
|
assert [group] = result["notification_groups"]
|
||||||
|
assert group["type"] == "favourite"
|
||||||
|
assert group["notifications_count"] == 2
|
||||||
|
assert group["status_id"] == status.id
|
||||||
|
assert group["most_recent_notification_id"] in notification_ids
|
||||||
|
assert group["page_min_id"] in notification_ids
|
||||||
|
assert group["page_max_id"] in notification_ids
|
||||||
|
assert group["latest_page_notification_at"]
|
||||||
|
assert Enum.sort(group["sample_account_ids"]) == Enum.sort([other_user1.id, other_user2.id])
|
||||||
|
group_key = group["group_key"]
|
||||||
|
assert group_key == persisted_group_key
|
||||||
|
|
||||||
|
assert [%{"group_key" => ^group_key}, %{"group_key" => ^group_key}] =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/notifications")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert %{"notification_groups" => [shown_group]} =
|
||||||
|
conn
|
||||||
|
|> get("/api/v2/notifications/#{group_key}")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert shown_group["group_key"] == group_key
|
||||||
|
assert shown_group["notifications_count"] == 2
|
||||||
|
refute Map.has_key?(shown_group, "page_min_id")
|
||||||
|
refute Map.has_key?(shown_group, "page_max_id")
|
||||||
|
refute Map.has_key?(shown_group, "latest_page_notification_at")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "round-trips ungrouped group keys when grouped_types excludes a type" do
|
||||||
|
%{user: user, conn: conn} = oauth_access(["read:notifications"])
|
||||||
|
other_user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, status} = CommonAPI.post(user, %{status: "hello"})
|
||||||
|
{:ok, _} = CommonAPI.favorite(status.id, other_user)
|
||||||
|
|
||||||
|
%{"notification_groups" => [%{"group_key" => "ungrouped-" <> _ = group_key}]} =
|
||||||
|
conn
|
||||||
|
|> get("/api/v2/notifications?grouped_types[]=reblog")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert %{
|
||||||
|
"notification_groups" => [%{"group_key" => ^group_key, "notifications_count" => 1}]
|
||||||
|
} =
|
||||||
|
conn
|
||||||
|
|> get("/api/v2/notifications/#{group_key}")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "keeps legacy notifications without persisted group keys ungrouped" do
|
||||||
|
%{user: user, conn: conn} = oauth_access(["read:notifications"])
|
||||||
|
other_user1 = insert(:user)
|
||||||
|
other_user2 = insert(:user)
|
||||||
|
|
||||||
|
{:ok, status} = CommonAPI.post(user, %{status: "hello"})
|
||||||
|
{:ok, favorite1} = CommonAPI.favorite(status.id, other_user1)
|
||||||
|
{:ok, favorite2} = CommonAPI.favorite(status.id, other_user2)
|
||||||
|
|
||||||
|
notification_ids =
|
||||||
|
[favorite1, favorite2]
|
||||||
|
|> Enum.map(&get_notification_id_by_activity/1)
|
||||||
|
|
||||||
|
Repo.update_all(Notification, set: [group_key: nil])
|
||||||
|
|
||||||
|
expected_group_keys = Enum.map(notification_ids, &"ungrouped-#{&1}")
|
||||||
|
|
||||||
|
assert [%{"group_key" => group_key1}, %{"group_key" => group_key2}] =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/notifications")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert Enum.sort([group_key1, group_key2]) == Enum.sort(expected_group_keys)
|
||||||
|
|
||||||
|
%{"notification_groups" => groups} =
|
||||||
|
conn
|
||||||
|
|> get("/api/v2/notifications")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert Enum.sort(Enum.map(groups, & &1["group_key"])) == Enum.sort(expected_group_keys)
|
||||||
|
assert Enum.all?(groups, &(&1["notifications_count"] == 1))
|
||||||
|
|
||||||
|
group_key = List.first(expected_group_keys)
|
||||||
|
|
||||||
|
assert %{
|
||||||
|
"notification_groups" => [%{"group_key" => ^group_key, "notifications_count" => 1}]
|
||||||
|
} =
|
||||||
|
conn
|
||||||
|
|> get("/api/v2/notifications/#{group_key}")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "paginates notification groups instead of raw notifications" do
|
||||||
|
%{user: user, conn: conn} = oauth_access(["read:notifications"])
|
||||||
|
other_user1 = insert(:user)
|
||||||
|
other_user2 = insert(:user)
|
||||||
|
other_user3 = insert(:user)
|
||||||
|
|
||||||
|
{:ok, older_status} = CommonAPI.post(user, %{status: "older"})
|
||||||
|
{:ok, _} = CommonAPI.favorite(older_status.id, other_user3)
|
||||||
|
|
||||||
|
{:ok, newer_status} = CommonAPI.post(user, %{status: "newer"})
|
||||||
|
{:ok, _} = CommonAPI.favorite(newer_status.id, other_user1)
|
||||||
|
{:ok, _} = CommonAPI.favorite(newer_status.id, other_user2)
|
||||||
|
older_status_id = older_status.id
|
||||||
|
newer_status_id = newer_status.id
|
||||||
|
|
||||||
|
%{"notification_groups" => groups} =
|
||||||
|
conn
|
||||||
|
|> get("/api/v2/notifications?limit=2")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert [
|
||||||
|
%{"status_id" => ^newer_status_id, "notifications_count" => 2},
|
||||||
|
%{"status_id" => ^older_status_id, "notifications_count" => 1}
|
||||||
|
] = groups
|
||||||
|
end
|
||||||
|
|
||||||
|
test "uses group-level cursors when paginating notification groups" do
|
||||||
|
%{user: user, conn: conn} = oauth_access(["read:notifications"])
|
||||||
|
other_user1 = insert(:user)
|
||||||
|
other_user2 = insert(:user)
|
||||||
|
other_user3 = insert(:user)
|
||||||
|
|
||||||
|
{:ok, older_status} = CommonAPI.post(user, %{status: "older"})
|
||||||
|
{:ok, _} = CommonAPI.favorite(older_status.id, other_user3)
|
||||||
|
|
||||||
|
{:ok, newer_status} = CommonAPI.post(user, %{status: "newer"})
|
||||||
|
{:ok, _} = CommonAPI.favorite(newer_status.id, other_user1)
|
||||||
|
{:ok, _} = CommonAPI.favorite(newer_status.id, other_user2)
|
||||||
|
older_status_id = older_status.id
|
||||||
|
newer_status_id = newer_status.id
|
||||||
|
|
||||||
|
assert %{
|
||||||
|
"notification_groups" => [
|
||||||
|
%{
|
||||||
|
"status_id" => ^newer_status_id,
|
||||||
|
"notifications_count" => 2,
|
||||||
|
"most_recent_notification_id" => cursor
|
||||||
|
}
|
||||||
|
]
|
||||||
|
} =
|
||||||
|
conn
|
||||||
|
|> get("/api/v2/notifications?limit=1")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert %{
|
||||||
|
"notification_groups" => [
|
||||||
|
%{"status_id" => ^older_status_id, "notifications_count" => 1}
|
||||||
|
]
|
||||||
|
} =
|
||||||
|
conn
|
||||||
|
|> get("/api/v2/notifications?limit=1&max_id=#{cursor}")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns total notification count for a partially represented group" do
|
||||||
|
%{user: user, conn: conn} = oauth_access(["read:notifications"])
|
||||||
|
other_user1 = insert(:user)
|
||||||
|
other_user2 = insert(:user)
|
||||||
|
other_user3 = insert(:user)
|
||||||
|
|
||||||
|
{:ok, grouped_status} = CommonAPI.post(user, %{status: "grouped"})
|
||||||
|
{:ok, _} = CommonAPI.favorite(grouped_status.id, other_user1)
|
||||||
|
|
||||||
|
{:ok, other_status} = CommonAPI.post(user, %{status: "other"})
|
||||||
|
{:ok, _} = CommonAPI.favorite(other_status.id, other_user3)
|
||||||
|
{:ok, _} = CommonAPI.favorite(grouped_status.id, other_user2)
|
||||||
|
grouped_status_id = grouped_status.id
|
||||||
|
|
||||||
|
assert %{
|
||||||
|
"notification_groups" => [
|
||||||
|
%{"status_id" => ^grouped_status_id, "notifications_count" => 2}
|
||||||
|
]
|
||||||
|
} =
|
||||||
|
conn
|
||||||
|
|> get("/api/v2/notifications?limit=1")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "counts unread notification groups" do
|
||||||
|
%{user: user, conn: conn} = oauth_access(["read:notifications"])
|
||||||
|
other_user1 = insert(:user)
|
||||||
|
other_user2 = insert(:user)
|
||||||
|
mentioner = insert(:user)
|
||||||
|
|
||||||
|
{:ok, status} = CommonAPI.post(user, %{status: "hello"})
|
||||||
|
{:ok, _} = CommonAPI.favorite(status.id, other_user1)
|
||||||
|
{:ok, _} = CommonAPI.favorite(status.id, other_user2)
|
||||||
|
{:ok, _} = CommonAPI.post(mentioner, %{status: "hi @#{user.nickname}"})
|
||||||
|
|
||||||
|
assert %{"count" => 2} =
|
||||||
|
conn
|
||||||
|
|> get("/api/v2/notifications/unread_count")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "counts unread notification groups newer than the notification marker" do
|
||||||
|
%{user: user, conn: conn} = oauth_access(["read:notifications"])
|
||||||
|
other_user = insert(:user)
|
||||||
|
mentioner = insert(:user)
|
||||||
|
|
||||||
|
{:ok, status} = CommonAPI.post(user, %{status: "older"})
|
||||||
|
{:ok, favorite_activity} = CommonAPI.favorite(status.id, other_user)
|
||||||
|
marker_id = get_notification_id_by_activity(favorite_activity)
|
||||||
|
|
||||||
|
{:ok, _} = CommonAPI.post(mentioner, %{status: "newer @#{user.nickname}"})
|
||||||
|
|
||||||
|
{:ok, _} =
|
||||||
|
Pleroma.Marker.upsert(user, %{"notifications" => %{"last_read_id" => marker_id}})
|
||||||
|
|
||||||
|
assert %{"count" => 1} =
|
||||||
|
conn
|
||||||
|
|> get("/api/v2/notifications/unread_count")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not preserve stale cursor params in grouped notification link headers" do
|
||||||
|
%{user: user, conn: conn} = oauth_access(["read:notifications"])
|
||||||
|
other_user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity1} = CommonAPI.post(other_user, %{status: "one @#{user.nickname}"})
|
||||||
|
{:ok, _activity2} = CommonAPI.post(other_user, %{status: "two @#{user.nickname}"})
|
||||||
|
{:ok, activity3} = CommonAPI.post(other_user, %{status: "three @#{user.nickname}"})
|
||||||
|
|
||||||
|
notification1_id = get_notification_id_by_activity(activity1)
|
||||||
|
notification3_id = get_notification_id_by_activity(activity3)
|
||||||
|
|
||||||
|
conn = get(conn, "/api/v2/notifications?since_id=#{notification1_id}&limit=1")
|
||||||
|
|
||||||
|
assert [link_header] = get_resp_header(conn, "link")
|
||||||
|
assert link_header =~ ~r/max_id=#{notification3_id}/
|
||||||
|
refute link_header =~ "since_id="
|
||||||
|
end
|
||||||
|
|
||||||
|
test "lists accounts from a notification group" do
|
||||||
|
%{user: user, conn: conn} = oauth_access(["read:notifications"])
|
||||||
|
|
||||||
|
{other_users, %{"group_key" => group_key, "sample_account_ids" => sample_account_ids}} =
|
||||||
|
create_favourite_notification_group(user, conn)
|
||||||
|
|
||||||
|
assert length(sample_account_ids) == 8
|
||||||
|
|
||||||
|
account_ids =
|
||||||
|
conn
|
||||||
|
|> get("/api/v2/notifications/#{group_key}/accounts")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|> Enum.map(& &1["id"])
|
||||||
|
|
||||||
|
assert Enum.sort(account_ids) == Enum.sort(Enum.map(other_users, & &1.id))
|
||||||
|
end
|
||||||
|
|
||||||
|
test "dismisses a notification group" do
|
||||||
|
%{user: user, conn: conn} = oauth_access(["read:notifications", "write:notifications"])
|
||||||
|
|
||||||
|
{_other_users, %{"group_key" => group_key}} =
|
||||||
|
create_favourite_notification_group(user, conn)
|
||||||
|
|
||||||
|
assert %{} =
|
||||||
|
conn
|
||||||
|
|> post("/api/v2/notifications/#{group_key}/dismiss")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert [] = Notification.for_user(user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
test "doesn't see notifications after muting user with notifications" do
|
test "doesn't see notifications after muting user with notifications" do
|
||||||
%{user: user, conn: conn} = oauth_access(["read:notifications"])
|
%{user: user, conn: conn} = oauth_access(["read:notifications"])
|
||||||
user2 = insert(:user)
|
user2 = insert(:user)
|
||||||
|
|
@ -708,6 +1000,29 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do
|
||||||
|> to_string()
|
|> to_string()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp create_favourite_notification_group(user, conn) do
|
||||||
|
other_users = insert_list(9, :user)
|
||||||
|
|
||||||
|
{:ok, status} = CommonAPI.post(user, %{status: "hello"})
|
||||||
|
|
||||||
|
Enum.each(other_users, fn other_user ->
|
||||||
|
{:ok, _} = CommonAPI.favorite(status.id, other_user)
|
||||||
|
end)
|
||||||
|
|
||||||
|
%{
|
||||||
|
"notification_groups" => [
|
||||||
|
%{
|
||||||
|
"notifications_count" => 9
|
||||||
|
} = group
|
||||||
|
]
|
||||||
|
} =
|
||||||
|
conn
|
||||||
|
|> get("/api/v2/notifications")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
{other_users, group}
|
||||||
|
end
|
||||||
|
|
||||||
defp params_to_query(%{} = params) do
|
defp params_to_query(%{} = params) do
|
||||||
Enum.map_join(params, "&", fn
|
Enum.map_join(params, "&", fn
|
||||||
{k, v} when is_list(v) -> Enum.map_join(v, "&", &"#{k}[]=#{&1}")
|
{k, v} when is_list(v) -> Enum.map_join(v, "&", &"#{k}[]=#{&1}")
|
||||||
|
|
|
||||||
|
|
@ -98,10 +98,11 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
|
||||||
{:ok, favorite_activity} = CommonAPI.favorite(create_activity.id, another_user)
|
{:ok, favorite_activity} = CommonAPI.favorite(create_activity.id, another_user)
|
||||||
{:ok, [notification]} = Notification.create_notifications(favorite_activity)
|
{:ok, [notification]} = Notification.create_notifications(favorite_activity)
|
||||||
create_activity = Activity.get_by_id(create_activity.id)
|
create_activity = Activity.get_by_id(create_activity.id)
|
||||||
|
assert is_binary(notification.group_key)
|
||||||
|
|
||||||
expected = %{
|
expected = %{
|
||||||
id: to_string(notification.id),
|
id: to_string(notification.id),
|
||||||
group_key: "ungrouped-#{to_string(notification.id)}",
|
group_key: Notification.group_key(notification),
|
||||||
pleroma: %{is_seen: false, is_muted: false},
|
pleroma: %{is_seen: false, is_muted: false},
|
||||||
type: "favourite",
|
type: "favourite",
|
||||||
account: AccountView.render("show.json", %{user: another_user, for: user}),
|
account: AccountView.render("show.json", %{user: another_user, for: user}),
|
||||||
|
|
@ -119,10 +120,11 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
|
||||||
{:ok, reblog_activity} = CommonAPI.repeat(create_activity.id, another_user)
|
{:ok, reblog_activity} = CommonAPI.repeat(create_activity.id, another_user)
|
||||||
{:ok, [notification]} = Notification.create_notifications(reblog_activity)
|
{:ok, [notification]} = Notification.create_notifications(reblog_activity)
|
||||||
reblog_activity = Activity.get_by_id(create_activity.id)
|
reblog_activity = Activity.get_by_id(create_activity.id)
|
||||||
|
assert is_binary(notification.group_key)
|
||||||
|
|
||||||
expected = %{
|
expected = %{
|
||||||
id: to_string(notification.id),
|
id: to_string(notification.id),
|
||||||
group_key: "ungrouped-#{to_string(notification.id)}",
|
group_key: Notification.group_key(notification),
|
||||||
pleroma: %{is_seen: false, is_muted: false},
|
pleroma: %{is_seen: false, is_muted: false},
|
||||||
type: "reblog",
|
type: "reblog",
|
||||||
account: AccountView.render("show.json", %{user: another_user, for: user}),
|
account: AccountView.render("show.json", %{user: another_user, for: user}),
|
||||||
|
|
@ -138,10 +140,11 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
|
||||||
followed = insert(:user)
|
followed = insert(:user)
|
||||||
{:ok, followed, follower, _activity} = CommonAPI.follow(followed, follower)
|
{:ok, followed, follower, _activity} = CommonAPI.follow(followed, follower)
|
||||||
notification = Notification |> Repo.one() |> Repo.preload(:activity)
|
notification = Notification |> Repo.one() |> Repo.preload(:activity)
|
||||||
|
assert is_binary(notification.group_key)
|
||||||
|
|
||||||
expected = %{
|
expected = %{
|
||||||
id: to_string(notification.id),
|
id: to_string(notification.id),
|
||||||
group_key: "ungrouped-#{to_string(notification.id)}",
|
group_key: Notification.group_key(notification),
|
||||||
pleroma: %{is_seen: false, is_muted: false},
|
pleroma: %{is_seen: false, is_muted: false},
|
||||||
type: "follow",
|
type: "follow",
|
||||||
account: AccountView.render("show.json", %{user: follower, for: followed}),
|
account: AccountView.render("show.json", %{user: follower, for: followed}),
|
||||||
|
|
@ -333,7 +336,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
|
||||||
|
|
||||||
expected = %{
|
expected = %{
|
||||||
id: to_string(notification.id),
|
id: to_string(notification.id),
|
||||||
group_key: "ungrouped-#{to_string(notification.id)}",
|
group_key: Notification.group_key(notification),
|
||||||
pleroma: %{is_seen: true, is_muted: true},
|
pleroma: %{is_seen: true, is_muted: true},
|
||||||
type: "favourite",
|
type: "favourite",
|
||||||
account: AccountView.render("show.json", %{user: another_user, for: user}),
|
account: AccountView.render("show.json", %{user: another_user, for: user}),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue