Separate Subscription Notifications from regular Notifications

This commit is contained in:
Roman Chvanikov 2019-09-13 18:25:27 +03:00
parent 53a3ad6043
commit 0bd2b85edb
14 changed files with 670 additions and 12 deletions

View File

@ -228,7 +228,6 @@ def get_notified_from_activity(
[] []
|> Utils.maybe_notify_to_recipients(activity) |> Utils.maybe_notify_to_recipients(activity)
|> Utils.maybe_notify_mentioned_recipients(activity) |> Utils.maybe_notify_mentioned_recipients(activity)
|> Utils.maybe_notify_subscribers(activity)
|> Enum.uniq() |> Enum.uniq()
User.get_users_from_set(recipients, local_only) User.get_users_from_set(recipients, local_only)

View File

@ -0,0 +1,266 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.SubscriptionNotification do
use Ecto.Schema
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Pagination
alias Pleroma.Repo
alias Pleroma.SubscriptionNotification
alias Pleroma.User
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.Push
alias Pleroma.Web.Streamer
import Ecto.Query
import Ecto.Changeset
@type t :: %__MODULE__{}
schema "subscription_notifications" do
belongs_to(:user, User, type: Pleroma.FlakeId)
belongs_to(:activity, Activity, type: Pleroma.FlakeId)
timestamps()
end
def changeset(%SubscriptionNotification{} = notification, attrs) do
cast(notification, attrs, [])
end
def for_user_query(user, opts \\ []) do
query =
SubscriptionNotification
|> where(user_id: ^user.id)
|> where(
[n, a],
fragment(
"? not in (SELECT ap_id FROM users WHERE info->'deactivated' @> 'true')",
a.actor
)
)
|> join(:inner, [n], activity in assoc(n, :activity))
|> join(:left, [n, a], object in Object,
on:
fragment(
"(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
object.data,
a.data
)
)
|> preload([n, a, o], activity: {a, object: o})
if opts[:with_muted] do
query
else
where(query, [n, a], a.actor not in ^user.info.muted_notifications)
|> where([n, a], a.actor not in ^user.info.blocks)
|> where(
[n, a],
fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.info.domain_blocks
)
|> join(:left, [n, a], tm in Pleroma.ThreadMute,
on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data)
)
|> where([n, a, o, tm], is_nil(tm.user_id))
end
end
def for_user(user, opts \\ %{}) do
user
|> for_user_query(opts)
|> Pagination.fetch_paginated(opts)
end
@doc """
Returns notifications for user received since given date.
## Examples
iex> Pleroma.SubscriptionNotification.for_user_since(%Pleroma.User{}, ~N[2019-04-13 11:22:33])
[%Pleroma.SubscriptionNotification{}, %Pleroma.SubscriptionNotification{}]
iex> Pleroma.SubscriptionNotification.for_user_since(%Pleroma.User{}, ~N[2019-04-15 11:22:33])
[]
"""
@spec for_user_since(Pleroma.User.t(), NaiveDateTime.t()) :: [t()]
def for_user_since(user, date) do
from(n in for_user_query(user),
where: n.updated_at > ^date
)
|> Repo.all()
end
def clear_up_to(%{id: user_id} = _user, id) do
from(
n in SubscriptionNotification,
where: n.user_id == ^user_id,
where: n.id <= ^id
)
|> Repo.delete_all([])
end
def get(%{id: user_id} = _user, id) do
query =
from(
n in SubscriptionNotification,
where: n.id == ^id,
join: activity in assoc(n, :activity),
preload: [activity: activity]
)
notification = Repo.one(query)
case notification do
%{user_id: ^user_id} ->
{:ok, notification}
_ ->
{:error, "Cannot get notification"}
end
end
def clear(user) do
from(n in SubscriptionNotification, where: n.user_id == ^user.id)
|> Repo.delete_all()
end
def destroy_multiple(%{id: user_id} = _user, ids) do
from(n in SubscriptionNotification,
where: n.id in ^ids,
where: n.user_id == ^user_id
)
|> Repo.delete_all()
end
def dismiss(%{id: user_id} = _user, id) do
notification = Repo.get(SubscriptionNotification, id)
case notification do
%{user_id: ^user_id} ->
Repo.delete(notification)
_ ->
{:error, "Cannot dismiss notification"}
end
end
def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity) do
object = Object.normalize(activity)
unless object && object.data["type"] == "Answer" do
users = get_notified_from_activity(activity)
notifications = Enum.map(users, fn user -> create_notification(activity, user) end)
{:ok, notifications}
else
{:ok, []}
end
end
def create_notifications(%Activity{data: %{"to" => _, "type" => type}} = activity)
when type in ["Like", "Announce", "Follow"] do
users = get_notified_from_activity(activity)
notifications = Enum.map(users, fn user -> create_notification(activity, user) end)
{:ok, notifications}
end
def create_notifications(_), do: {:ok, []}
# TODO move to sql, too.
def create_notification(%Activity{} = activity, %User{} = user) do
unless skip?(activity, user) do
notification = %SubscriptionNotification{user_id: user.id, activity: activity}
{:ok, notification} = Repo.insert(notification)
Streamer.stream("user", notification)
Streamer.stream("user:subscription_notification", notification)
Push.send(notification)
notification
end
end
def get_notified_from_activity(activity, local_only \\ true)
def get_notified_from_activity(
%Activity{data: %{"to" => _, "type" => type} = _data} = activity,
local_only
)
when type in ["Create", "Like", "Announce", "Follow"] do
recipients =
[]
|> Utils.maybe_notify_subscribers(activity)
|> Enum.uniq()
User.get_users_from_set(recipients, local_only)
end
def get_notified_from_activity(_, _local_only), do: []
@spec skip?(Activity.t(), User.t()) :: boolean()
def skip?(activity, user) do
[
:self,
:followers,
:follows,
:non_followers,
:non_follows,
:recently_followed
]
|> Enum.any?(&skip?(&1, activity, user))
end
@spec skip?(atom(), Activity.t(), User.t()) :: boolean()
def skip?(:self, activity, user) do
activity.data["actor"] == user.ap_id
end
def skip?(
:followers,
activity,
%{info: %{notification_settings: %{"followers" => false}}} = user
) do
actor = activity.data["actor"]
follower = User.get_cached_by_ap_id(actor)
User.following?(follower, user)
end
def skip?(
:non_followers,
activity,
%{info: %{notification_settings: %{"non_followers" => false}}} = user
) do
actor = activity.data["actor"]
follower = User.get_cached_by_ap_id(actor)
!User.following?(follower, user)
end
def skip?(:follows, activity, %{info: %{notification_settings: %{"follows" => false}}} = user) do
actor = activity.data["actor"]
followed = User.get_cached_by_ap_id(actor)
User.following?(user, followed)
end
def skip?(
:non_follows,
activity,
%{info: %{notification_settings: %{"non_follows" => false}}} = user
) do
actor = activity.data["actor"]
followed = User.get_cached_by_ap_id(actor)
!User.following?(user, followed)
end
def skip?(:recently_followed, %{data: %{"type" => "Follow"}} = activity, user) do
actor = activity.data["actor"]
SubscriptionNotification.for_user(user)
|> Enum.any?(fn
%{activity: %{data: %{"type" => "Follow", "actor" => ^actor}}} -> true
_ -> false
end)
end
def skip?(_, _, _), do: false
end

View File

@ -12,6 +12,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Object.Fetcher alias Pleroma.Object.Fetcher
alias Pleroma.Pagination alias Pleroma.Pagination
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.SubscriptionNotification
alias Pleroma.Upload alias Pleroma.Upload
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.MRF alias Pleroma.Web.ActivityPub.MRF
@ -148,6 +149,7 @@ def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when
PleromaJobQueue.enqueue(:background, Pleroma.Web.RichMedia.Helpers, [:fetch, activity]) PleromaJobQueue.enqueue(:background, Pleroma.Web.RichMedia.Helpers, [:fetch, activity])
Notification.create_notifications(activity) Notification.create_notifications(activity)
SubscriptionNotification.create_notifications(activity)
participations = participations =
activity activity

View File

@ -23,6 +23,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.ScheduledActivity alias Pleroma.ScheduledActivity
alias Pleroma.Stats alias Pleroma.Stats
alias Pleroma.SubscriptionNotification
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web alias Pleroma.Web
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
@ -39,6 +40,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
alias Pleroma.Web.MastodonAPI.ReportView alias Pleroma.Web.MastodonAPI.ReportView
alias Pleroma.Web.MastodonAPI.ScheduledActivityView alias Pleroma.Web.MastodonAPI.ScheduledActivityView
alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MastodonAPI.SubscriptionNotificationView
alias Pleroma.Web.MediaProxy alias Pleroma.Web.MediaProxy
alias Pleroma.Web.OAuth.App alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Authorization alias Pleroma.Web.OAuth.Authorization
@ -725,6 +727,28 @@ def notifications(%{assigns: %{user: user}} = conn, params) do
|> render("index.json", %{notifications: notifications, for: user}) |> render("index.json", %{notifications: notifications, for: user})
end end
def subscription_notifications(%{assigns: %{user: user}} = conn, params) do
notifications = MastodonAPI.get_subscription_notifications(user, params)
conn
|> add_link_headers(:subscription_notifications, notifications)
|> put_view(SubscriptionNotificationView)
|> render("index.json", %{notifications: notifications, for: user})
end
def get_subscription_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
with {:ok, notification} <- SubscriptionNotification.get(user, id) do
conn
|> put_view(SubscriptionNotificationView)
|> render("show.json", %{subscription_notification: notification, for: user})
else
{:error, reason} ->
conn
|> put_status(:forbidden)
|> json(%{"error" => reason})
end
end
def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
with {:ok, notification} <- Notification.get(user, id) do with {:ok, notification} <- Notification.get(user, id) do
conn conn
@ -743,6 +767,11 @@ def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
json(conn, %{}) json(conn, %{})
end end
def clear_subscription_notifications(%{assigns: %{user: user}} = conn, _params) do
SubscriptionNotification.clear(user)
json(conn, %{})
end
def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
with {:ok, _notif} <- Notification.dismiss(user, id) do with {:ok, _notif} <- Notification.dismiss(user, id) do
json(conn, %{}) json(conn, %{})
@ -754,11 +783,30 @@ def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _para
end end
end end
def dismiss_subscription_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
with {:ok, _notif} <- SubscriptionNotification.dismiss(user, id) do
json(conn, %{})
else
{:error, reason} ->
conn
|> put_status(:forbidden)
|> json(%{"error" => reason})
end
end
def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
Notification.destroy_multiple(user, ids) Notification.destroy_multiple(user, ids)
json(conn, %{}) json(conn, %{})
end end
def destroy_multiple_subscription_notifications(
%{assigns: %{user: user}} = conn,
%{"ids" => ids} = _params
) do
SubscriptionNotification.destroy_multiple(user, ids)
json(conn, %{})
end
def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
id = List.wrap(id) id = List.wrap(id)
q = from(u in User, where: u.id in ^id) q = from(u in User, where: u.id in ^id)

View File

@ -10,6 +10,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Pagination alias Pleroma.Pagination
alias Pleroma.ScheduledActivity alias Pleroma.ScheduledActivity
alias Pleroma.SubscriptionNotification
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
@ -62,6 +63,15 @@ def get_notifications(user, params \\ %{}) do
|> Pagination.fetch_paginated(params) |> Pagination.fetch_paginated(params)
end end
def get_subscription_notifications(user, params \\ %{}) do
options = cast_params(params)
user
|> SubscriptionNotification.for_user_query(options)
|> restrict(:exclude_types, options)
|> Pagination.fetch_paginated(params)
end
def get_scheduled_activities(user, params \\ %{}) do def get_scheduled_activities(user, params \\ %{}) do
user user
|> ScheduledActivity.for_user_query() |> ScheduledActivity.for_user_query()

View File

@ -0,0 +1,61 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.SubscriptionNotificationView do
use Pleroma.Web, :view
alias Pleroma.Activity
# alias Pleroma.SubscriptionNotification
alias Pleroma.User
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.SubscriptionNotificationView
alias Pleroma.Web.MastodonAPI.StatusView
def render("index.json", %{notifications: notifications, for: user}) do
safe_render_many(notifications, SubscriptionNotificationView, "show.json", %{for: user})
end
def render("show.json", %{
subscription_notification: %{activity: activity} = notification,
for: user
}) do
actor = User.get_cached_by_ap_id(activity.data["actor"])
parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
mastodon_type = Activity.mastodon_notification_type(activity)
response = %{
id: to_string(notification.id),
type: mastodon_type,
created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at),
account: AccountView.render("account.json", %{user: actor, for: user})
}
case mastodon_type do
"mention" ->
response
|> Map.merge(%{
status: StatusView.render("status.json", %{activity: activity, for: user})
})
"favourite" ->
response
|> Map.merge(%{
status: StatusView.render("status.json", %{activity: parent_activity, for: user})
})
"reblog" ->
response
|> Map.merge(%{
status: StatusView.render("status.json", %{activity: parent_activity, for: user})
})
"follow" ->
response
_ ->
nil
end
end
end

View File

@ -9,6 +9,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
alias Pleroma.Conversation.Participation alias Pleroma.Conversation.Participation
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.SubscriptionNotification
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.MastodonAPI.ConversationView alias Pleroma.Web.MastodonAPI.ConversationView
alias Pleroma.Web.MastodonAPI.NotificationView alias Pleroma.Web.MastodonAPI.NotificationView
@ -95,4 +96,29 @@ def read_notification(%{assigns: %{user: user}} = conn, %{"max_id" => max_id}) d
|> render("index.json", %{notifications: notifications, for: user}) |> render("index.json", %{notifications: notifications, for: user})
end end
end end
def delete_subscription_notification(%{assigns: %{user: user}} = conn, %{
"id" => notification_id
}) do
with {:ok, notification} <- SubscriptionNotification.dismiss(user, notification_id) do
conn
|> put_view(NotificationView)
|> render("show.json", %{notification: notification, for: user})
else
{:error, message} ->
conn
|> put_status(:bad_request)
|> json(%{"error" => message})
end
end
def read_subscription_notification(%{assigns: %{user: user}} = conn, %{"max_id" => max_id}) do
with notifications <- SubscriptionNotification.clear_up_to(user, max_id) do
notifications = Enum.take(notifications, 80)
conn
|> put_view(NotificationView)
|> render("index.json", %{notifications: notifications, for: user})
end
end
end end

View File

@ -9,6 +9,7 @@ defmodule Pleroma.Web.Push.Impl do
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.SubscriptionNotification
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.Metadata.Utils alias Pleroma.Web.Metadata.Utils
alias Pleroma.Web.Push.Subscription alias Pleroma.Web.Push.Subscription
@ -19,7 +20,7 @@ defmodule Pleroma.Web.Push.Impl do
@types ["Create", "Follow", "Announce", "Like"] @types ["Create", "Follow", "Announce", "Like"]
@doc "Performs sending notifications for user subscriptions" @doc "Performs sending notifications for user subscriptions"
@spec perform(Notification.t()) :: list(any) | :error @spec perform(Notification.t() | SubscriptionNotification.t()) :: list(any) | :error
def perform( def perform(
%{ %{
activity: %{data: %{"type" => activity_type}, id: activity_id} = activity, activity: %{data: %{"type" => activity_type}, id: activity_id} = activity,

View File

@ -300,11 +300,39 @@ defmodule Pleroma.Web.Router do
get("/bookmarks", MastodonAPIController, :bookmarks) get("/bookmarks", MastodonAPIController, :bookmarks)
post("/notifications/clear", MastodonAPIController, :clear_notifications) post("/notifications/clear", MastodonAPIController, :clear_notifications)
post(
"/notifications/subscription/clear",
MastodonAPIController,
:clear_subscription_notifications
)
post("/notifications/dismiss", MastodonAPIController, :dismiss_notification) post("/notifications/dismiss", MastodonAPIController, :dismiss_notification)
post(
"/notifications/subscription/dismiss",
MastodonAPIController,
:dismiss_subscription_notification
)
get("/notifications", MastodonAPIController, :notifications) get("/notifications", MastodonAPIController, :notifications)
get("/notifications/subscription", MastodonAPIController, :subscription_notifications)
get("/notifications/:id", MastodonAPIController, :get_notification) get("/notifications/:id", MastodonAPIController, :get_notification)
get(
"/notifications/subscription/:id",
MastodonAPIController,
:get_subscription_notification
)
delete("/notifications/destroy_multiple", MastodonAPIController, :destroy_multiple) delete("/notifications/destroy_multiple", MastodonAPIController, :destroy_multiple)
delete(
"/notifications/subscription/destroy_multiple",
MastodonAPIController,
:destroy_multiple_subscription_notifications
)
get("/scheduled_statuses", MastodonAPIController, :scheduled_statuses) get("/scheduled_statuses", MastodonAPIController, :scheduled_statuses)
get("/scheduled_statuses/:id", MastodonAPIController, :show_scheduled_status) get("/scheduled_statuses/:id", MastodonAPIController, :show_scheduled_status)

View File

@ -10,6 +10,7 @@ defmodule Pleroma.Web.Streamer do
alias Pleroma.Conversation.Participation alias Pleroma.Conversation.Participation
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.SubscriptionNotification
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.ActivityPub.Visibility
@ -208,10 +209,17 @@ def represent_conversation(%Participation{} = participation) do
|> Jason.encode!() |> Jason.encode!()
end end
@spec represent_notification(User.t(), Notification.t()) :: binary() @spec represent_notification(User.t(), Notification.t() | %SubscriptionNotification{}) ::
defp represent_notification(%User{} = user, %Notification{} = notify) do binary()
defp represent_notification(%User{} = user, notify) do
event =
case notify do
%Notification{} -> "notification"
%SubscriptionNotification{} -> "subscription_norification"
end
%{ %{
event: "notification", event: event,
payload: payload:
NotificationView.render( NotificationView.render(
"show.json", "show.json",

View File

@ -0,0 +1,15 @@
defmodule Pleroma.Repo.Migrations.CreateSubscriptionNotifications do
use Ecto.Migration
def change do
create_if_not_exists table(:subscription_notifications) do
add(:user_id, references(:users, type: :uuid, on_delete: :delete_all))
add(:activity_id, references(:activities, type: :uuid, on_delete: :delete_all))
timestamps()
end
create_if_not_exists(index(:subscription_notifications, [:user_id]))
create_if_not_exists(index(:subscription_notifications, ["id desc nulls last"]))
end
end

View File

@ -32,16 +32,16 @@ test "notifies someone when they are directly addressed" do
assert other_notification.activity_id == activity.id assert other_notification.activity_id == activity.id
end end
test "it creates a notification for subscribed users" do test "it does not create a notification for subscribed users" do
user = insert(:user) user = insert(:user)
subscriber = insert(:user) subscriber = insert(:user)
User.subscribe(subscriber, user) User.subscribe(subscriber, user)
{:ok, status} = CommonAPI.post(user, %{"status" => "Akariiiin"}) {:ok, status} = CommonAPI.post(user, %{"status" => "Akariiiin"})
{:ok, [notification]} = Notification.create_notifications(status) {:ok, notifications} = Notification.create_notifications(status)
assert notification.user_id == subscriber.id assert notifications == []
end end
test "does not create a notification for subscribed users if status is a reply" do test "does not create a notification for subscribed users if status is a reply" do
@ -190,14 +190,16 @@ test "it doesn't create a notification for follow-unfollow-follow chains" do
refute Notification.create_notification(activity_dupe, followed_user) refute Notification.create_notification(activity_dupe, followed_user)
end end
test "it doesn't create duplicate notifications for follow+subscribed users" do test "it doesn't create notifications for follow+subscribed users" do
user = insert(:user) user = insert(:user)
subscriber = insert(:user) subscriber = insert(:user)
{:ok, _, _, _} = CommonAPI.follow(subscriber, user) {:ok, _, _, _} = CommonAPI.follow(subscriber, user)
User.subscribe(subscriber, user) User.subscribe(subscriber, user)
{:ok, status} = CommonAPI.post(user, %{"status" => "Akariiiin"}) {:ok, status} = CommonAPI.post(user, %{"status" => "Akariiiin"})
{:ok, [_notif]} = Notification.create_notifications(status) {:ok, notifications} = Notification.create_notifications(status)
assert notifications == []
end end
test "it doesn't create subscription notifications if the recipient cannot see the status" do test "it doesn't create subscription notifications if the recipient cannot see the status" do

View File

@ -13,6 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.ScheduledActivity alias Pleroma.ScheduledActivity
alias Pleroma.SubscriptionNotification
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
@ -1273,6 +1274,197 @@ test "see notifications after muting user with notifications and with_muted para
end end
end end
describe "subscription_notifications" do
setup do
user = insert(:user)
subscriber = insert(:user)
User.subscribe(subscriber, user)
{:ok, %{user: user, subscriber: subscriber}}
end
test "list of notifications", %{conn: conn, user: user, subscriber: subscriber} do
status_text = "Hello"
{:ok, _activity} = CommonAPI.post(user, %{"status" => status_text})
conn =
conn
|> assign(:user, subscriber)
|> get("/api/v1/notifications/subscription")
assert [%{"status" => %{"content" => response}} | _rest] = json_response(conn, 200)
assert response == status_text
end
test "getting a single notification", %{conn: conn, user: user, subscriber: subscriber} do
status_text = "Hello"
{:ok, _activity} = CommonAPI.post(user, %{"status" => status_text})
[notification] = Repo.all(SubscriptionNotification)
conn =
conn
|> assign(:user, subscriber)
|> get("/api/v1/notifications/subscription/#{notification.id}")
assert %{"status" => %{"content" => response}} = json_response(conn, 200)
assert response == status_text
end
test "dismissing a single notification also deletes it", %{
conn: conn,
user: user,
subscriber: subscriber
} do
status_text = "Hello"
{:ok, _activity} = CommonAPI.post(user, %{"status" => status_text})
[notification] = Repo.all(SubscriptionNotification)
conn =
conn
|> assign(:user, subscriber)
|> post("/api/v1/notifications/subscription/dismiss", %{"id" => notification.id})
assert %{} = json_response(conn, 200)
assert Repo.all(SubscriptionNotification) == []
end
test "clearing all notifications also deletes them", %{
conn: conn,
user: user,
subscriber: subscriber
} do
status_text1 = "Hello"
status_text2 = "Hello again"
{:ok, _activity1} = CommonAPI.post(user, %{"status" => status_text1})
{:ok, _activity2} = CommonAPI.post(user, %{"status" => status_text2})
conn =
conn
|> assign(:user, subscriber)
|> post("/api/v1/notifications/subscription/clear")
assert %{} = json_response(conn, 200)
conn =
build_conn()
|> assign(:user, subscriber)
|> get("/api/v1/notifications/subscription")
assert json_response(conn, 200) == []
assert Repo.all(SubscriptionNotification) == []
end
test "paginates notifications using min_id, since_id, max_id, and limit", %{
conn: conn,
user: user,
subscriber: subscriber
} do
{:ok, activity1} = CommonAPI.post(user, %{"status" => "Hello 1"})
{:ok, activity2} = CommonAPI.post(user, %{"status" => "Hello 2"})
{:ok, activity3} = CommonAPI.post(user, %{"status" => "Hello 3"})
{:ok, activity4} = CommonAPI.post(user, %{"status" => "Hello 4"})
notification1_id =
Repo.get_by(SubscriptionNotification, activity_id: activity1.id).id |> to_string()
notification2_id =
Repo.get_by(SubscriptionNotification, activity_id: activity2.id).id |> to_string()
notification3_id =
Repo.get_by(SubscriptionNotification, activity_id: activity3.id).id |> to_string()
notification4_id =
Repo.get_by(SubscriptionNotification, activity_id: activity4.id).id |> to_string()
conn = assign(conn, :user, subscriber)
# min_id
conn_res =
get(conn, "/api/v1/notifications/subscription?limit=2&min_id=#{notification1_id}")
result = json_response(conn_res, 200)
assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result
# since_id
conn_res =
get(conn, "/api/v1/notifications/subscription?limit=2&since_id=#{notification1_id}")
result = json_response(conn_res, 200)
assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
# max_id
conn_res =
get(conn, "/api/v1/notifications/subscription?limit=2&max_id=#{notification4_id}")
result = json_response(conn_res, 200)
assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result
end
test "destroy multiple", %{conn: conn, user: user1, subscriber: user2} do
# mutual subscription
User.subscribe(user1, user2)
{:ok, activity1} = CommonAPI.post(user1, %{"status" => "Hello 1"})
{:ok, activity2} = CommonAPI.post(user1, %{"status" => "World 1"})
{:ok, activity3} = CommonAPI.post(user2, %{"status" => "Hello 2"})
{:ok, activity4} = CommonAPI.post(user2, %{"status" => "World 2"})
notification1_id =
Repo.get_by(SubscriptionNotification, activity_id: activity1.id).id |> to_string()
notification2_id =
Repo.get_by(SubscriptionNotification, activity_id: activity2.id).id |> to_string()
notification3_id =
Repo.get_by(SubscriptionNotification, activity_id: activity3.id).id |> to_string()
notification4_id =
Repo.get_by(SubscriptionNotification, activity_id: activity4.id).id |> to_string()
conn = assign(conn, :user, user1)
conn_res = get(conn, "/api/v1/notifications/subscription")
result = json_response(conn_res, 200)
Enum.each(result, fn %{"id" => id} ->
assert id in [notification3_id, notification4_id]
end)
conn2 = assign(conn, :user, user2)
conn_res = get(conn2, "/api/v1/notifications/subscription")
result = json_response(conn_res, 200)
Enum.each(result, fn %{"id" => id} ->
assert id in [notification1_id, notification2_id]
end)
conn_destroy =
delete(conn, "/api/v1/notifications/subscription/destroy_multiple", %{
"ids" => [notification3_id, notification4_id]
})
assert json_response(conn_destroy, 200) == %{}
conn_res = get(conn2, "/api/v1/notifications/subscription")
result = json_response(conn_res, 200)
Enum.each(result, fn %{"id" => id} ->
assert id in [notification1_id, notification2_id]
end)
assert length(Repo.all(SubscriptionNotification)) == 2
end
end
describe "reblogging" do describe "reblogging" do
test "reblogs and returns the reblogged status", %{conn: conn} do test "reblogs and returns the reblogged status", %{conn: conn} do
activity = insert(:note_activity) activity = insert(:note_activity)

View File

@ -75,9 +75,9 @@ test "returns notifications for user" do
User.subscribe(subscriber, user) User.subscribe(subscriber, user)
{:ok, status} = CommonAPI.post(user, %{"status" => "Akariiiin"}) {:ok, status} = CommonAPI.post(user, %{"status" => "Akariiiin @#{subscriber.nickname}"})
{:ok, status1} = CommonAPI.post(user, %{"status" => "Magi"}) {:ok, status1} = CommonAPI.post(user, %{"status" => "Magi @#{subscriber.nickname}"})
{:ok, [notification]} = Notification.create_notifications(status) {:ok, [notification]} = Notification.create_notifications(status)
{:ok, [notification1]} = Notification.create_notifications(status1) {:ok, [notification1]} = Notification.create_notifications(status1)
res = MastodonAPI.get_notifications(subscriber) res = MastodonAPI.get_notifications(subscriber)