Page MenuHomePhorge

No OneTemporary

Size
55 KB
Referenced Files
None
Subscribers
None
diff --git a/docs/API/differences_in_mastoapi_responses.md b/docs/API/differences_in_mastoapi_responses.md
index e649bdf24..006d17c1b 100644
--- a/docs/API/differences_in_mastoapi_responses.md
+++ b/docs/API/differences_in_mastoapi_responses.md
@@ -1,165 +1,171 @@
# Differences in Mastodon API responses from vanilla Mastodon
A Pleroma instance can be identified by "<Mastodon version> (compatible; Pleroma <version>)" present in `version` field in response from `/api/v1/instance`
## Flake IDs
Pleroma uses 128-bit ids as opposed to Mastodon's 64 bits. However just like Mastodon's ids they are sortable strings
## Attachment cap
Some apps operate under the assumption that no more than 4 attachments can be returned or uploaded. Pleroma however does not enforce any limits on attachment count neither when returning the status object nor when posting.
## Timelines
Adding the parameter `with_muted=true` to the timeline queries will also return activities by muted (not by blocked!) users.
Adding the parameter `exclude_visibilities` to the timeline queries will exclude the statuses with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`), e.g., `exclude_visibilities[]=direct&exclude_visibilities[]=private`.
## Statuses
- `visibility`: has an additional possible value `list`
Has these additional fields under the `pleroma` object:
- `local`: true if the post was made on the local instance
- `conversation_id`: the ID of the AP context the status is associated with (if any)
- `direct_conversation_id`: the ID of the Mastodon direct message conversation the status is associated with (if any)
- `in_reply_to_account_acct`: the `acct` property of User entity for replied user (if any)
- `content`: a map consisting of alternate representations of the `content` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`
- `spoiler_text`: a map consisting of alternate representations of the `spoiler_text` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`
- `expires_at`: a datetime (iso8601) that states when the post will expire (be deleted automatically), or empty if the post won't expire
- `thread_muted`: true if the thread the post belongs to is muted
## Attachments
Has these additional fields under the `pleroma` object:
- `mime_type`: mime type of the attachment.
## Accounts
The `id` parameter can also be the `nickname` of the user. This only works in these endpoints, not the deeper nested ones for following etc.
- `/api/v1/accounts/:id`
- `/api/v1/accounts/:id/statuses`
Has these additional fields under the `pleroma` object:
- `tags`: Lists an array of tags for the user
- `relationship{}`: Includes fields as documented for Mastodon API https://docs.joinmastodon.org/api/entities/#relationship
- `is_moderator`: boolean, nullable, true if user is a moderator
- `is_admin`: boolean, nullable, true if user is an admin
- `confirmation_pending`: boolean, true if a new user account is waiting on email confirmation to be activated
- `hide_followers`: boolean, true when the user has follower hiding enabled
- `hide_follows`: boolean, true when the user has follow hiding enabled
- `hide_followers_count`: boolean, true when the user has follower stat hiding enabled
- `hide_follows_count`: boolean, true when the user has follow stat hiding enabled
- `settings_store`: A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials`
- `chat_token`: The token needed for Pleroma chat. Only returned in `verify_credentials`
- `deactivated`: boolean, true when the user is deactivated
- `allow_following_move`: boolean, true when the user allows automatically follow moved following accounts
- `unread_conversation_count`: The count of unread conversations. Only returned to the account owner.
### Source
Has these additional fields under the `pleroma` object:
- `show_role`: boolean, nullable, true when the user wants his role (e.g admin, moderator) to be shown
- `no_rich_text` - boolean, nullable, true when html tags are stripped from all statuses requested from the API
## Conversations
Has an additional field under the `pleroma` object:
- `recipients`: The list of the recipients of this Conversation. These will be addressed when replying to this conversation.
## GET `/api/v1/conversations`
Accepts additional parameters:
- `recipients`: Only return conversations with the given recipients (a list of user ids). Usage example: `GET /api/v1/conversations?recipients[]=1&recipients[]=2`
## Account Search
Behavior has changed:
- `/api/v1/accounts/search`: Does not require authentication
## Notifications
Has these additional fields under the `pleroma` object:
- `is_seen`: true if the notification was read by the user
+### Move Notification
+
+The `type` value is `move`. Has an additional field:
+
+- `target`: new account
+
## GET `/api/v1/notifications`
Accepts additional parameters:
- `exclude_visibilities`: will exclude the notifications for activities with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`). Usage example: `GET /api/v1/notifications?exclude_visibilities[]=direct&exclude_visibilities[]=private`.
## POST `/api/v1/statuses`
Additional parameters can be added to the JSON body/Form data:
- `preview`: boolean, if set to `true` the post won't be actually posted, but the status entitiy would still be rendered back. This could be useful for previewing rich text/custom emoji, for example.
- `content_type`: string, contain the MIME type of the status, it is transformed into HTML by the backend. You can get the list of the supported MIME types with the nodeinfo endpoint.
- `to`: A list of nicknames (like `lain@soykaf.club` or `lain` on the local server) that will be used to determine who is going to be addressed by this post. Using this will disable the implicit addressing by mentioned names in the `status` body, only the people in the `to` list will be addressed. The normal rules for for post visibility are not affected by this and will still apply.
- `visibility`: string, besides standard MastoAPI values (`direct`, `private`, `unlisted` or `public`) it can be used to address a List by setting it to `list:LIST_ID`.
- `expires_in`: The number of seconds the posted activity should expire in. When a posted activity expires it will be deleted from the server, and a delete request for it will be federated. This needs to be longer than an hour.
- `in_reply_to_conversation_id`: Will reply to a given conversation, addressing only the people who are part of the recipient set of that conversation. Sets the visibility to `direct`.
## GET `/api/v1/statuses`
An endpoint to get multiple statuses by IDs.
Required parameters:
- `ids`: array of activity ids
Usage example: `GET /api/v1/statuses/?ids[]=1&ids[]=2`.
Returns: array of Status.
The maximum number of statuses is limited to 100 per request.
## PATCH `/api/v1/update_credentials`
Additional parameters can be added to the JSON body/Form data:
- `no_rich_text` - if true, html tags are stripped from all statuses requested from the API
- `hide_followers` - if true, user's followers will be hidden
- `hide_follows` - if true, user's follows will be hidden
- `hide_followers_count` - if true, user's follower count will be hidden
- `hide_follows_count` - if true, user's follow count will be hidden
- `hide_favorites` - if true, user's favorites timeline will be hidden
- `show_role` - if true, user's role (e.g admin, moderator) will be exposed to anyone in the API
- `default_scope` - the scope returned under `privacy` key in Source subentity
- `pleroma_settings_store` - Opaque user settings to be saved on the backend.
- `skip_thread_containment` - if true, skip filtering out broken threads
- `allow_following_move` - if true, allows automatically follow moved following accounts
- `pleroma_background_image` - sets the background image of the user.
### Pleroma Settings Store
Pleroma has mechanism that allows frontends to save blobs of json for each user on the backend. This can be used to save frontend-specific settings for a user that the backend does not need to know about.
The parameter should have a form of `{frontend_name: {...}}`, with `frontend_name` identifying your type of client, e.g. `pleroma_fe`. It will overwrite everything under this property, but will not overwrite other frontend's settings.
This information is returned in the `verify_credentials` endpoint.
## Authentication
*Pleroma supports refreshing tokens.
`POST /oauth/token`
Post here request with grant_type=refresh_token to obtain new access token. Returns an access token.
## Account Registration
`POST /api/v1/accounts`
Has theses additionnal parameters (which are the same as in Pleroma-API):
* `fullname`: optional
* `bio`: optional
* `captcha_solution`: optional, contains provider-specific captcha solution,
* `captcha_token`: optional, contains provider-specific captcha token
* `token`: invite token required when the registerations aren't public.
diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex
index c1065611b..992298c2d 100644
--- a/lib/pleroma/activity.ex
+++ b/lib/pleroma/activity.ex
@@ -1,289 +1,290 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Activity do
use Ecto.Schema
alias Pleroma.Activity
alias Pleroma.Activity.Queries
alias Pleroma.ActivityExpiration
alias Pleroma.Bookmark
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.ThreadMute
alias Pleroma.User
import Ecto.Changeset
import Ecto.Query
@type t :: %__MODULE__{}
@type actor :: String.t()
@primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
# https://github.com/tootsuite/mastodon/blob/master/app/models/notification.rb#L19
@mastodon_notification_types %{
"Create" => "mention",
"Follow" => "follow",
"Announce" => "reblog",
- "Like" => "favourite"
+ "Like" => "favourite",
+ "Move" => "move"
}
@mastodon_to_ap_notification_types for {k, v} <- @mastodon_notification_types,
into: %{},
do: {v, k}
schema "activities" do
field(:data, :map)
field(:local, :boolean, default: true)
field(:actor, :string)
field(:recipients, {:array, :string}, default: [])
field(:thread_muted?, :boolean, virtual: true)
# This is a fake relation, do not use outside of with_preloaded_bookmark/get_bookmark
has_one(:bookmark, Bookmark)
has_many(:notifications, Notification, on_delete: :delete_all)
# Attention: this is a fake relation, don't try to preload it blindly and expect it to work!
# The foreign key is embedded in a jsonb field.
#
# To use it, you probably want to do an inner join and a preload:
#
# ```
# |> join(:inner, [activity], o in Object,
# on: fragment("(?->>'id') = COALESCE((?)->'object'->> 'id', (?)->>'object')",
# o.data, activity.data, activity.data))
# |> preload([activity, object], [object: object])
# ```
#
# As a convenience, Activity.with_preloaded_object() sets up an inner join and preload for the
# typical case.
has_one(:object, Object, on_delete: :nothing, foreign_key: :id)
has_one(:expiration, ActivityExpiration, on_delete: :delete_all)
timestamps()
end
def with_joined_object(query, join_type \\ :inner) do
join(query, join_type, [activity], o in Object,
on:
fragment(
"(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')",
o.data,
activity.data,
activity.data
),
as: :object
)
end
def with_preloaded_object(query, join_type \\ :inner) do
query
|> has_named_binding?(:object)
|> if(do: query, else: with_joined_object(query, join_type))
|> preload([activity, object: object], object: object)
end
def with_preloaded_bookmark(query, %User{} = user) do
from([a] in query,
left_join: b in Bookmark,
on: b.user_id == ^user.id and b.activity_id == a.id,
preload: [bookmark: b]
)
end
def with_preloaded_bookmark(query, _), do: query
def with_set_thread_muted_field(query, %User{} = user) do
from([a] in query,
left_join: tm in ThreadMute,
on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data),
as: :thread_mute,
select: %Activity{a | thread_muted?: not is_nil(tm.id)}
)
end
def with_set_thread_muted_field(query, _), do: query
def get_by_ap_id(ap_id) do
ap_id
|> Queries.by_ap_id()
|> Repo.one()
end
def get_bookmark(%Activity{} = activity, %User{} = user) do
if Ecto.assoc_loaded?(activity.bookmark) do
activity.bookmark
else
Bookmark.get(user.id, activity.id)
end
end
def get_bookmark(_, _), do: nil
def change(struct, params \\ %{}) do
struct
|> cast(params, [:data, :recipients])
|> validate_required([:data])
|> unique_constraint(:ap_id, name: :activities_unique_apid_index)
end
def get_by_ap_id_with_object(ap_id) do
ap_id
|> Queries.by_ap_id()
|> with_preloaded_object(:left)
|> Repo.one()
end
@spec get_by_id(String.t()) :: Activity.t() | nil
def get_by_id(id) do
case FlakeId.flake_id?(id) do
true ->
Activity
|> where([a], a.id == ^id)
|> restrict_deactivated_users()
|> Repo.one()
_ ->
nil
end
end
def get_by_id_with_object(id) do
Activity
|> where(id: ^id)
|> with_preloaded_object()
|> Repo.one()
end
def all_by_ids_with_object(ids) do
Activity
|> where([a], a.id in ^ids)
|> with_preloaded_object()
|> Repo.all()
end
@doc """
Accepts `ap_id` or list of `ap_id`.
Returns a query.
"""
@spec create_by_object_ap_id(String.t() | [String.t()]) :: Ecto.Queryable.t()
def create_by_object_ap_id(ap_id) do
ap_id
|> Queries.by_object_id()
|> Queries.by_type("Create")
end
def get_all_create_by_object_ap_id(ap_id) do
ap_id
|> create_by_object_ap_id()
|> Repo.all()
end
def get_create_by_object_ap_id(ap_id) when is_binary(ap_id) do
create_by_object_ap_id(ap_id)
|> restrict_deactivated_users()
|> Repo.one()
end
def get_create_by_object_ap_id(_), do: nil
@doc """
Accepts `ap_id` or list of `ap_id`.
Returns a query.
"""
@spec create_by_object_ap_id_with_object(String.t() | [String.t()]) :: Ecto.Queryable.t()
def create_by_object_ap_id_with_object(ap_id) do
ap_id
|> create_by_object_ap_id()
|> with_preloaded_object()
end
def get_create_by_object_ap_id_with_object(ap_id) when is_binary(ap_id) do
ap_id
|> create_by_object_ap_id_with_object()
|> Repo.one()
end
def get_create_by_object_ap_id_with_object(_), do: nil
defp get_in_reply_to_activity_from_object(%Object{data: %{"inReplyTo" => ap_id}}) do
get_create_by_object_ap_id_with_object(ap_id)
end
defp get_in_reply_to_activity_from_object(_), do: nil
def get_in_reply_to_activity(%Activity{} = activity) do
get_in_reply_to_activity_from_object(Object.normalize(activity))
end
def normalize(obj) when is_map(obj), do: get_by_ap_id_with_object(obj["id"])
def normalize(ap_id) when is_binary(ap_id), do: get_by_ap_id_with_object(ap_id)
def normalize(_), do: nil
def delete_by_ap_id(id) when is_binary(id) do
id
|> Queries.by_object_id()
|> select([u], u)
|> Repo.delete_all()
|> elem(1)
|> Enum.find(fn
%{data: %{"type" => "Create", "object" => ap_id}} when is_binary(ap_id) -> ap_id == id
%{data: %{"type" => "Create", "object" => %{"id" => ap_id}}} -> ap_id == id
_ -> nil
end)
|> purge_web_resp_cache()
end
def delete_by_ap_id(_), do: nil
defp purge_web_resp_cache(%Activity{} = activity) do
%{path: path} = URI.parse(activity.data["id"])
Cachex.del(:web_resp_cache, path)
activity
end
defp purge_web_resp_cache(nil), do: nil
for {ap_type, type} <- @mastodon_notification_types do
def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}),
do: unquote(type)
end
def mastodon_notification_type(%Activity{}), do: nil
def from_mastodon_notification_type(type) do
Map.get(@mastodon_to_ap_notification_types, type)
end
def all_by_actor_and_id(actor, status_ids \\ [])
def all_by_actor_and_id(_actor, []), do: []
def all_by_actor_and_id(actor, status_ids) do
Activity
|> where([s], s.id in ^status_ids)
|> where([s], s.actor == ^actor)
|> Repo.all()
end
def follow_requests_for_actor(%Pleroma.User{ap_id: ap_id}) do
ap_id
|> Queries.by_object_id()
|> Queries.by_type("Follow")
|> where([a], fragment("? ->> 'state' = 'pending'", a.data))
end
def restrict_deactivated_users(query) do
deactivated_users =
from(u in User.Query.build(deactivated: true), select: u.ap_id)
|> Repo.all()
from(activity in query,
where: activity.actor not in ^deactivated_users
)
end
defdelegate search(user, query, options \\ []), to: Pleroma.Activity.Search
end
diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex
index 5e3dbe728..ddd7f5318 100644
--- a/lib/pleroma/web/mastodon_api/views/notification_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex
@@ -1,68 +1,60 @@
# 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.NotificationView do
use Pleroma.Web, :view
alias Pleroma.Activity
alias Pleroma.Notification
alias Pleroma.User
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.NotificationView
alias Pleroma.Web.MastodonAPI.StatusView
def render("index.json", %{notifications: notifications, for: user}) do
safe_render_many(notifications, NotificationView, "show.json", %{for: user})
end
def render("show.json", %{
notification: %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)
with %{id: _} = account <- AccountView.render("show.json", %{user: actor, for: user}) do
response = %{
id: to_string(notification.id),
type: mastodon_type,
created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at),
account: account,
pleroma: %{
is_seen: notification.seen
}
}
case mastodon_type do
- "mention" ->
- response
- |> Map.merge(%{
- status: StatusView.render("show.json", %{activity: activity, for: user})
- })
-
- "favourite" ->
- response
- |> Map.merge(%{
- status: StatusView.render("show.json", %{activity: parent_activity, for: user})
- })
-
- "reblog" ->
- response
- |> Map.merge(%{
- status: StatusView.render("show.json", %{activity: parent_activity, for: user})
- })
-
- "follow" ->
- response
-
- _ ->
- nil
+ "mention" -> put_status(response, activity, user)
+ "favourite" -> put_status(response, parent_activity, user)
+ "reblog" -> put_status(response, parent_activity, user)
+ "move" -> put_target(response, activity, user)
+ "follow" -> response
+ _ -> nil
end
else
_ -> nil
end
end
+
+ defp put_status(response, activity, user) do
+ Map.put(response, :status, StatusView.render("show.json", %{activity: activity, for: user}))
+ end
+
+ defp put_target(response, activity, user) do
+ target = User.get_cached_by_ap_id(activity.data["target"])
+ Map.put(response, :target, AccountView.render("show.json", %{user: target, for: user}))
+ end
end
diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex
index dd445e8bf..8c131409d 100644
--- a/lib/pleroma/web/push/impl.ex
+++ b/lib/pleroma/web/push/impl.ex
@@ -1,140 +1,140 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Push.Impl do
@moduledoc "The module represents implementation push web notification"
alias Pleroma.Activity
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.Metadata.Utils
alias Pleroma.Web.Push.Subscription
require Logger
import Ecto.Query
- @types ["Create", "Follow", "Announce", "Like"]
+ @types ["Create", "Follow", "Announce", "Like", "Move"]
@doc "Performs sending notifications for user subscriptions"
@spec perform(Notification.t()) :: list(any) | :error
def perform(
%{
activity: %{data: %{"type" => activity_type}, id: activity_id} = activity,
user_id: user_id
} = notif
)
when activity_type in @types do
actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
type = Activity.mastodon_notification_type(notif.activity)
gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key)
avatar_url = User.avatar_url(actor)
object = Object.normalize(activity)
for subscription <- fetch_subsriptions(user_id),
get_in(subscription.data, ["alerts", type]) do
%{
title: format_title(notif),
access_token: subscription.token.token,
body: format_body(notif, actor, object),
notification_id: notif.id,
notification_type: type,
icon: avatar_url,
preferred_locale: "en",
pleroma: %{
activity_id: activity_id
}
}
|> Jason.encode!()
|> push_message(build_sub(subscription), gcm_api_key, subscription)
end
end
def perform(_) do
Logger.warn("Unknown notification type")
:error
end
@doc "Push message to web"
def push_message(body, sub, api_key, subscription) do
case WebPushEncryption.send_web_push(body, sub, api_key) do
{:ok, %{status_code: code}} when 400 <= code and code < 500 ->
Logger.debug("Removing subscription record")
Repo.delete!(subscription)
:ok
{:ok, %{status_code: code}} when 200 <= code and code < 300 ->
:ok
{:ok, %{status_code: code}} ->
Logger.error("Web Push Notification failed with code: #{code}")
:error
_ ->
Logger.error("Web Push Notification failed with unknown error")
:error
end
end
@doc "Gets user subscriptions"
def fetch_subsriptions(user_id) do
Subscription
|> where(user_id: ^user_id)
|> preload(:token)
|> Repo.all()
end
def build_sub(subscription) do
%{
keys: %{
p256dh: subscription.key_p256dh,
auth: subscription.key_auth
},
endpoint: subscription.endpoint
}
end
def format_body(
%{activity: %{data: %{"type" => "Create"}}},
actor,
%{data: %{"content" => content}}
) do
"@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}"
end
def format_body(
%{activity: %{data: %{"type" => "Announce"}}},
actor,
%{data: %{"content" => content}}
) do
"@#{actor.nickname} repeated: #{Utils.scrub_html_and_truncate(content, 80)}"
end
def format_body(
%{activity: %{data: %{"type" => type}}},
actor,
_object
)
when type in ["Follow", "Like"] do
case type do
"Follow" -> "@#{actor.nickname} has followed you"
"Like" -> "@#{actor.nickname} has favorited your post"
end
end
def format_title(%{activity: %{data: %{"directMessage" => true}}}) do
"New Direct Message"
end
def format_title(%{activity: %{data: %{"type" => type}}}) do
case type do
"Create" -> "New Mention"
"Follow" -> "New Follower"
"Announce" -> "New Repeat"
"Like" -> "New Favorite"
end
end
end
diff --git a/test/notification_test.exs b/test/notification_test.exs
index f8d429223..dcbffeafe 100644
--- a/test/notification_test.exs
+++ b/test/notification_test.exs
@@ -1,726 +1,755 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.NotificationTest do
use Pleroma.DataCase
import Pleroma.Factory
alias Pleroma.Notification
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Streamer
describe "create_notifications" do
test "notifies someone when they are directly addressed" do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "hey @#{other_user.nickname} and @#{third_user.nickname}"
})
{:ok, [notification, other_notification]} = Notification.create_notifications(activity)
notified_ids = Enum.sort([notification.user_id, other_notification.user_id])
assert notified_ids == [other_user.id, third_user.id]
assert notification.activity_id == activity.id
assert other_notification.activity_id == activity.id
end
test "it creates a notification for subscribed users" do
user = insert(:user)
subscriber = insert(:user)
User.subscribe(subscriber, user)
{:ok, status} = CommonAPI.post(user, %{"status" => "Akariiiin"})
{:ok, [notification]} = Notification.create_notifications(status)
assert notification.user_id == subscriber.id
end
test "does not create a notification for subscribed users if status is a reply" do
user = insert(:user)
other_user = insert(:user)
subscriber = insert(:user)
User.subscribe(subscriber, other_user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "test post"})
{:ok, _reply_activity} =
CommonAPI.post(other_user, %{
"status" => "test reply",
"in_reply_to_status_id" => activity.id
})
user_notifications = Notification.for_user(user)
assert length(user_notifications) == 1
subscriber_notifications = Notification.for_user(subscriber)
assert Enum.empty?(subscriber_notifications)
end
end
describe "create_notification" do
@tag needs_streamer: true
test "it creates a notification for user and send to the 'user' and the 'user:notification' stream" do
user = insert(:user)
task = Task.async(fn -> assert_receive {:text, _}, 4_000 end)
task_user_notification = Task.async(fn -> assert_receive {:text, _}, 4_000 end)
Streamer.add_socket("user", %{transport_pid: task.pid, assigns: %{user: user}})
Streamer.add_socket(
"user:notification",
%{transport_pid: task_user_notification.pid, assigns: %{user: user}}
)
activity = insert(:note_activity)
notify = Notification.create_notification(activity, user)
assert notify.user_id == user.id
Task.await(task)
Task.await(task_user_notification)
end
test "it creates a notification for user if the user blocks the activity author" do
activity = insert(:note_activity)
author = User.get_cached_by_ap_id(activity.data["actor"])
user = insert(:user)
{:ok, user} = User.block(user, author)
assert Notification.create_notification(activity, user)
end
test "it creates a notificatin for the user if the user mutes the activity author" do
muter = insert(:user)
muted = insert(:user)
{:ok, _} = User.mute(muter, muted)
muter = Repo.get(User, muter.id)
{:ok, activity} = CommonAPI.post(muted, %{"status" => "Hi @#{muter.nickname}"})
assert Notification.create_notification(activity, muter)
end
test "notification created if user is muted without notifications" do
muter = insert(:user)
muted = insert(:user)
{:ok, muter} = User.mute(muter, muted, false)
{:ok, activity} = CommonAPI.post(muted, %{"status" => "Hi @#{muter.nickname}"})
assert Notification.create_notification(activity, muter)
end
test "it creates a notification for an activity from a muted thread" do
muter = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(muter, %{"status" => "hey"})
CommonAPI.add_mute(muter, activity)
{:ok, activity} =
CommonAPI.post(other_user, %{
"status" => "Hi @#{muter.nickname}",
"in_reply_to_status_id" => activity.id
})
assert Notification.create_notification(activity, muter)
end
test "it disables notifications from followers" do
follower = insert(:user)
followed = insert(:user, notification_settings: %{"followers" => false})
User.follow(follower, followed)
{:ok, activity} = CommonAPI.post(follower, %{"status" => "hey @#{followed.nickname}"})
refute Notification.create_notification(activity, followed)
end
test "it disables notifications from non-followers" do
follower = insert(:user)
followed = insert(:user, notification_settings: %{"non_followers" => false})
{:ok, activity} = CommonAPI.post(follower, %{"status" => "hey @#{followed.nickname}"})
refute Notification.create_notification(activity, followed)
end
test "it disables notifications from people the user follows" do
follower = insert(:user, notification_settings: %{"follows" => false})
followed = insert(:user)
User.follow(follower, followed)
follower = Repo.get(User, follower.id)
{:ok, activity} = CommonAPI.post(followed, %{"status" => "hey @#{follower.nickname}"})
refute Notification.create_notification(activity, follower)
end
test "it disables notifications from people the user does not follow" do
follower = insert(:user, notification_settings: %{"non_follows" => false})
followed = insert(:user)
{:ok, activity} = CommonAPI.post(followed, %{"status" => "hey @#{follower.nickname}"})
refute Notification.create_notification(activity, follower)
end
test "it doesn't create a notification for user if he is the activity author" do
activity = insert(:note_activity)
author = User.get_cached_by_ap_id(activity.data["actor"])
refute Notification.create_notification(activity, author)
end
test "it doesn't create a notification for follow-unfollow-follow chains" do
user = insert(:user)
followed_user = insert(:user)
{:ok, _, _, activity} = CommonAPI.follow(user, followed_user)
Notification.create_notification(activity, followed_user)
CommonAPI.unfollow(user, followed_user)
{:ok, _, _, activity_dupe} = CommonAPI.follow(user, followed_user)
refute Notification.create_notification(activity_dupe, followed_user)
end
test "it doesn't create duplicate notifications for follow+subscribed users" do
user = insert(:user)
subscriber = insert(:user)
{:ok, _, _, _} = CommonAPI.follow(subscriber, user)
User.subscribe(subscriber, user)
{:ok, status} = CommonAPI.post(user, %{"status" => "Akariiiin"})
{:ok, [_notif]} = Notification.create_notifications(status)
end
test "it doesn't create subscription notifications if the recipient cannot see the status" do
user = insert(:user)
subscriber = insert(:user)
User.subscribe(subscriber, user)
{:ok, status} = CommonAPI.post(user, %{"status" => "inwisible", "visibility" => "direct"})
assert {:ok, []} == Notification.create_notifications(status)
end
end
describe "get notification" do
test "it gets a notification that belongs to the user" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{other_user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity)
{:ok, notification} = Notification.get(other_user, notification.id)
assert notification.user_id == other_user.id
end
test "it returns error if the notification doesn't belong to the user" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{other_user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity)
{:error, _notification} = Notification.get(user, notification.id)
end
end
describe "dismiss notification" do
test "it dismisses a notification that belongs to the user" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{other_user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity)
{:ok, notification} = Notification.dismiss(other_user, notification.id)
assert notification.user_id == other_user.id
end
test "it returns error if the notification doesn't belong to the user" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{other_user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity)
{:error, _notification} = Notification.dismiss(user, notification.id)
end
end
describe "clear notification" do
test "it clears all notifications belonging to the user" do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "hey @#{other_user.nickname} and @#{third_user.nickname} !"
})
{:ok, _notifs} = Notification.create_notifications(activity)
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "hey again @#{other_user.nickname} and @#{third_user.nickname} !"
})
{:ok, _notifs} = Notification.create_notifications(activity)
Notification.clear(other_user)
assert Notification.for_user(other_user) == []
assert Notification.for_user(third_user) != []
end
end
describe "set_read_up_to()" do
test "it sets all notifications as read up to a specified notification ID" do
user = insert(:user)
other_user = insert(:user)
{:ok, _activity} =
CommonAPI.post(user, %{
"status" => "hey @#{other_user.nickname}!"
})
{:ok, _activity} =
CommonAPI.post(user, %{
"status" => "hey again @#{other_user.nickname}!"
})
[n2, n1] = notifs = Notification.for_user(other_user)
assert length(notifs) == 2
assert n2.id > n1.id
{:ok, _activity} =
CommonAPI.post(user, %{
"status" => "hey yet again @#{other_user.nickname}!"
})
Notification.set_read_up_to(other_user, n2.id)
[n3, n2, n1] = Notification.for_user(other_user)
assert n1.seen == true
assert n2.seen == true
assert n3.seen == false
end
end
describe "for_user_since/2" do
defp days_ago(days) do
NaiveDateTime.add(
NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second),
-days * 60 * 60 * 24,
:second
)
end
test "Returns recent notifications" do
user1 = insert(:user)
user2 = insert(:user)
Enum.each(0..10, fn i ->
{:ok, _activity} =
CommonAPI.post(user1, %{
"status" => "hey ##{i} @#{user2.nickname}!"
})
end)
{old, new} = Enum.split(Notification.for_user(user2), 5)
Enum.each(old, fn notification ->
notification
|> cast(%{updated_at: days_ago(10)}, [:updated_at])
|> Pleroma.Repo.update!()
end)
recent_notifications_ids =
user2
|> Notification.for_user_since(
NaiveDateTime.add(NaiveDateTime.utc_now(), -5 * 86_400, :second)
)
|> Enum.map(& &1.id)
Enum.each(old, fn %{id: id} ->
refute id in recent_notifications_ids
end)
Enum.each(new, fn %{id: id} ->
assert id in recent_notifications_ids
end)
end
end
describe "notification target determination" do
test "it sends notifications to addressed users in new messages" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "hey @#{other_user.nickname}!"
})
assert other_user in Notification.get_notified_from_activity(activity)
end
test "it sends notifications to mentioned users in new messages" do
user = insert(:user)
other_user = insert(:user)
create_activity = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Create",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"actor" => user.ap_id,
"object" => %{
"type" => "Note",
"content" => "message with a Mention tag, but no explicit tagging",
"tag" => [
%{
"type" => "Mention",
"href" => other_user.ap_id,
"name" => other_user.nickname
}
],
"attributedTo" => user.ap_id
}
}
{:ok, activity} = Transmogrifier.handle_incoming(create_activity)
assert other_user in Notification.get_notified_from_activity(activity)
end
test "it does not send notifications to users who are only cc in new messages" do
user = insert(:user)
other_user = insert(:user)
create_activity = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Create",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [other_user.ap_id],
"actor" => user.ap_id,
"object" => %{
"type" => "Note",
"content" => "hi everyone",
"attributedTo" => user.ap_id
}
}
{:ok, activity} = Transmogrifier.handle_incoming(create_activity)
assert other_user not in Notification.get_notified_from_activity(activity)
end
test "it does not send notification to mentioned users in likes" do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
{:ok, activity_one} =
CommonAPI.post(user, %{
"status" => "hey @#{other_user.nickname}!"
})
{:ok, activity_two, _} = CommonAPI.favorite(activity_one.id, third_user)
assert other_user not in Notification.get_notified_from_activity(activity_two)
end
test "it does not send notification to mentioned users in announces" do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
{:ok, activity_one} =
CommonAPI.post(user, %{
"status" => "hey @#{other_user.nickname}!"
})
{:ok, activity_two, _} = CommonAPI.repeat(activity_one.id, third_user)
assert other_user not in Notification.get_notified_from_activity(activity_two)
end
end
describe "notification lifecycle" do
test "liking an activity results in 1 notification, then 0 if the activity is deleted" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "test post"})
assert Enum.empty?(Notification.for_user(user))
{:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
assert length(Notification.for_user(user)) == 1
{:ok, _} = CommonAPI.delete(activity.id, user)
assert Enum.empty?(Notification.for_user(user))
end
test "liking an activity results in 1 notification, then 0 if the activity is unliked" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "test post"})
assert Enum.empty?(Notification.for_user(user))
{:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
assert length(Notification.for_user(user)) == 1
{:ok, _, _, _} = CommonAPI.unfavorite(activity.id, other_user)
assert Enum.empty?(Notification.for_user(user))
end
test "repeating an activity results in 1 notification, then 0 if the activity is deleted" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "test post"})
assert Enum.empty?(Notification.for_user(user))
{:ok, _, _} = CommonAPI.repeat(activity.id, other_user)
assert length(Notification.for_user(user)) == 1
{:ok, _} = CommonAPI.delete(activity.id, user)
assert Enum.empty?(Notification.for_user(user))
end
test "repeating an activity results in 1 notification, then 0 if the activity is unrepeated" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "test post"})
assert Enum.empty?(Notification.for_user(user))
{:ok, _, _} = CommonAPI.repeat(activity.id, other_user)
assert length(Notification.for_user(user)) == 1
{:ok, _, _} = CommonAPI.unrepeat(activity.id, other_user)
assert Enum.empty?(Notification.for_user(user))
end
test "liking an activity which is already deleted does not generate a notification" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "test post"})
assert Enum.empty?(Notification.for_user(user))
{:ok, _deletion_activity} = CommonAPI.delete(activity.id, user)
assert Enum.empty?(Notification.for_user(user))
{:error, _} = CommonAPI.favorite(activity.id, other_user)
assert Enum.empty?(Notification.for_user(user))
end
test "repeating an activity which is already deleted does not generate a notification" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "test post"})
assert Enum.empty?(Notification.for_user(user))
{:ok, _deletion_activity} = CommonAPI.delete(activity.id, user)
assert Enum.empty?(Notification.for_user(user))
{:error, _} = CommonAPI.repeat(activity.id, other_user)
assert Enum.empty?(Notification.for_user(user))
end
test "replying to a deleted post without tagging does not generate a notification" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "test post"})
{:ok, _deletion_activity} = CommonAPI.delete(activity.id, user)
{:ok, _reply_activity} =
CommonAPI.post(other_user, %{
"status" => "test reply",
"in_reply_to_status_id" => activity.id
})
assert Enum.empty?(Notification.for_user(user))
end
test "notifications are deleted if a local user is deleted" do
user = insert(:user)
other_user = insert(:user)
{:ok, _activity} =
CommonAPI.post(user, %{"status" => "hi @#{other_user.nickname}", "visibility" => "direct"})
refute Enum.empty?(Notification.for_user(other_user))
{:ok, job} = User.delete(user)
ObanHelpers.perform(job)
assert Enum.empty?(Notification.for_user(other_user))
end
test "notifications are deleted if a remote user is deleted" do
remote_user = insert(:user)
local_user = insert(:user)
dm_message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Create",
"actor" => remote_user.ap_id,
"id" => remote_user.ap_id <> "/activities/test",
"to" => [local_user.ap_id],
"cc" => [],
"object" => %{
"type" => "Note",
"content" => "Hello!",
"tag" => [
%{
"type" => "Mention",
"href" => local_user.ap_id,
"name" => "@#{local_user.nickname}"
}
],
"to" => [local_user.ap_id],
"cc" => [],
"attributedTo" => remote_user.ap_id
}
}
{:ok, _dm_activity} = Transmogrifier.handle_incoming(dm_message)
refute Enum.empty?(Notification.for_user(local_user))
delete_user_message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => remote_user.ap_id <> "/activities/delete",
"actor" => remote_user.ap_id,
"type" => "Delete",
"object" => remote_user.ap_id
}
{:ok, _delete_activity} = Transmogrifier.handle_incoming(delete_user_message)
ObanHelpers.perform_all()
assert Enum.empty?(Notification.for_user(local_user))
end
+
+ test "move activity generates a notification" do
+ %{ap_id: old_ap_id} = old_user = insert(:user)
+ %{ap_id: new_ap_id} = new_user = insert(:user, also_known_as: [old_ap_id])
+ follower = insert(:user)
+ other_follower = insert(:user, %{allow_following_move: false})
+
+ User.follow(follower, old_user)
+ User.follow(other_follower, old_user)
+
+ Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user)
+ ObanHelpers.perform_all()
+
+ assert [
+ %{
+ activity: %{
+ data: %{"type" => "Move", "actor" => ^old_ap_id, "target" => ^new_ap_id}
+ }
+ }
+ ] = Notification.for_user(follower)
+
+ assert [
+ %{
+ activity: %{
+ data: %{"type" => "Move", "actor" => ^old_ap_id, "target" => ^new_ap_id}
+ }
+ }
+ ] = Notification.for_user(other_follower)
+ end
end
describe "for_user" do
test "it returns notifications for muted user without notifications" do
user = insert(:user)
muted = insert(:user)
{:ok, user} = User.mute(user, muted, false)
{:ok, _activity} = CommonAPI.post(muted, %{"status" => "hey @#{user.nickname}"})
assert length(Notification.for_user(user)) == 1
end
test "it doesn't return notifications for muted user with notifications" do
user = insert(:user)
muted = insert(:user)
{:ok, user} = User.mute(user, muted)
{:ok, _activity} = CommonAPI.post(muted, %{"status" => "hey @#{user.nickname}"})
assert Notification.for_user(user) == []
end
test "it doesn't return notifications for blocked user" do
user = insert(:user)
blocked = insert(:user)
{:ok, user} = User.block(user, blocked)
{:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"})
assert Notification.for_user(user) == []
end
test "it doesn't return notificatitons for blocked domain" do
user = insert(:user)
blocked = insert(:user, ap_id: "http://some-domain.com")
{:ok, user} = User.block_domain(user, "some-domain.com")
{:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"})
assert Notification.for_user(user) == []
end
test "it doesn't return notifications for muted thread" do
user = insert(:user)
another_user = insert(:user)
{:ok, activity} = CommonAPI.post(another_user, %{"status" => "hey @#{user.nickname}"})
{:ok, _} = Pleroma.ThreadMute.add_mute(user.id, activity.data["context"])
assert Notification.for_user(user) == []
end
test "it returns notifications from a muted user when with_muted is set" do
user = insert(:user)
muted = insert(:user)
{:ok, user} = User.mute(user, muted)
{:ok, _activity} = CommonAPI.post(muted, %{"status" => "hey @#{user.nickname}"})
assert length(Notification.for_user(user, %{with_muted: true})) == 1
end
test "it doesn't return notifications from a blocked user when with_muted is set" do
user = insert(:user)
blocked = insert(:user)
{:ok, user} = User.block(user, blocked)
{:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"})
assert length(Notification.for_user(user, %{with_muted: true})) == 0
end
test "it doesn't return notifications from a domain-blocked user when with_muted is set" do
user = insert(:user)
blocked = insert(:user, ap_id: "http://some-domain.com")
{:ok, user} = User.block_domain(user, "some-domain.com")
{:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"})
assert length(Notification.for_user(user, %{with_muted: true})) == 0
end
test "it returns notifications from muted threads when with_muted is set" do
user = insert(:user)
another_user = insert(:user)
{:ok, activity} = CommonAPI.post(another_user, %{"status" => "hey @#{user.nickname}"})
{:ok, _} = Pleroma.ThreadMute.add_mute(user.id, activity.data["context"])
assert length(Notification.for_user(user, %{with_muted: true})) == 1
end
end
end
diff --git a/test/web/mastodon_api/views/notification_view_test.exs b/test/web/mastodon_api/views/notification_view_test.exs
index c9043a69a..80b6d414c 100644
--- a/test/web/mastodon_api/views/notification_view_test.exs
+++ b/test/web/mastodon_api/views/notification_view_test.exs
@@ -1,110 +1,134 @@
# 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.NotificationViewTest do
use Pleroma.DataCase
alias Pleroma.Activity
alias Pleroma.Notification
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.NotificationView
alias Pleroma.Web.MastodonAPI.StatusView
import Pleroma.Factory
test "Mention notification" do
user = insert(:user)
mentioned_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{mentioned_user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity)
user = User.get_cached_by_id(user.id)
expected = %{
id: to_string(notification.id),
pleroma: %{is_seen: false},
type: "mention",
account: AccountView.render("show.json", %{user: user, for: mentioned_user}),
status: StatusView.render("show.json", %{activity: activity, for: mentioned_user}),
created_at: Utils.to_masto_date(notification.inserted_at)
}
result =
NotificationView.render("index.json", %{notifications: [notification], for: mentioned_user})
assert [expected] == result
end
test "Favourite notification" do
user = insert(:user)
another_user = insert(:user)
{:ok, create_activity} = CommonAPI.post(user, %{"status" => "hey"})
{:ok, favorite_activity, _object} = CommonAPI.favorite(create_activity.id, another_user)
{:ok, [notification]} = Notification.create_notifications(favorite_activity)
create_activity = Activity.get_by_id(create_activity.id)
expected = %{
id: to_string(notification.id),
pleroma: %{is_seen: false},
type: "favourite",
account: AccountView.render("show.json", %{user: another_user, for: user}),
status: StatusView.render("show.json", %{activity: create_activity, for: user}),
created_at: Utils.to_masto_date(notification.inserted_at)
}
result = NotificationView.render("index.json", %{notifications: [notification], for: user})
assert [expected] == result
end
test "Reblog notification" do
user = insert(:user)
another_user = insert(:user)
{:ok, create_activity} = CommonAPI.post(user, %{"status" => "hey"})
{:ok, reblog_activity, _object} = CommonAPI.repeat(create_activity.id, another_user)
{:ok, [notification]} = Notification.create_notifications(reblog_activity)
reblog_activity = Activity.get_by_id(create_activity.id)
expected = %{
id: to_string(notification.id),
pleroma: %{is_seen: false},
type: "reblog",
account: AccountView.render("show.json", %{user: another_user, for: user}),
status: StatusView.render("show.json", %{activity: reblog_activity, for: user}),
created_at: Utils.to_masto_date(notification.inserted_at)
}
result = NotificationView.render("index.json", %{notifications: [notification], for: user})
assert [expected] == result
end
test "Follow notification" do
follower = insert(:user)
followed = insert(:user)
{:ok, follower, followed, _activity} = CommonAPI.follow(follower, followed)
notification = Notification |> Repo.one() |> Repo.preload(:activity)
expected = %{
id: to_string(notification.id),
pleroma: %{is_seen: false},
type: "follow",
account: AccountView.render("show.json", %{user: follower, for: followed}),
created_at: Utils.to_masto_date(notification.inserted_at)
}
result =
NotificationView.render("index.json", %{notifications: [notification], for: followed})
assert [expected] == result
User.perform(:delete, follower)
notification = Notification |> Repo.one() |> Repo.preload(:activity)
assert [] ==
NotificationView.render("index.json", %{notifications: [notification], for: followed})
end
+
+ test "Move notification" do
+ %{ap_id: old_ap_id} = old_user = insert(:user)
+ %{ap_id: _new_ap_id} = new_user = insert(:user, also_known_as: [old_ap_id])
+ follower = insert(:user)
+
+ User.follow(follower, old_user)
+ Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user)
+ Pleroma.Tests.ObanHelpers.perform_all()
+
+ [notification] = Notification.for_user(follower)
+
+ expected = %{
+ id: to_string(notification.id),
+ pleroma: %{is_seen: false},
+ type: "move",
+ account: AccountView.render("show.json", %{user: old_user, for: follower}),
+ target: AccountView.render("show.json", %{user: new_user, for: follower}),
+ created_at: Utils.to_masto_date(notification.inserted_at)
+ }
+
+ assert [expected] ==
+ NotificationView.render("index.json", %{notifications: [notification], for: follower})
+ end
end

File Metadata

Mime Type
text/x-diff
Expires
Sat, Aug 29, 12:48 PM (1 d, 20 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1737078
Default Alt Text
(55 KB)

Event Timeline