Merge branch '1559-follow-request-notifications' into 'develop'
[#1559] Support for "follow_request" notifications Closes #1559 See merge request pleroma/pleroma!2354
This commit is contained in:
commit
918a8094fc
|
@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- Configuration: `:restrict_unauthenticated` setting, restrict access for unauthenticated users to timelines (public and federate), user profiles and statuses.
|
||||
- New HTTP adapter [gun](https://github.com/ninenines/gun). Gun adapter requires minimum OTP version of 22.2 otherwise Pleroma won’t start. For hackney OTP update is not required.
|
||||
- Mix task to create trusted OAuth App.
|
||||
- Notifications: Added `follow_request` notification type (configurable, see `[:notifications, :enable_follow_request_notifications]` setting).
|
||||
<details>
|
||||
<summary>API Changes</summary>
|
||||
- Mastodon API: Support for `include_types` in `/api/v1/notifications`.
|
||||
|
|
|
@ -561,6 +561,8 @@
|
|||
inactivity_threshold: 7
|
||||
}
|
||||
|
||||
config :pleroma, :notifications, enable_follow_request_notifications: false
|
||||
|
||||
config :pleroma, :oauth2,
|
||||
token_expires_in: 600,
|
||||
issue_new_refresh_token: true,
|
||||
|
|
|
@ -2267,6 +2267,20 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
%{
|
||||
group: :pleroma,
|
||||
key: :notifications,
|
||||
type: :group,
|
||||
description: "Notification settings",
|
||||
children: [
|
||||
%{
|
||||
key: :enable_follow_request_notifications,
|
||||
type: :boolean,
|
||||
description:
|
||||
"Enables notifications on new follow requests (causes issues with older PleromaFE versions)."
|
||||
}
|
||||
]
|
||||
},
|
||||
%{
|
||||
group: :pleroma,
|
||||
key: Pleroma.Emails.UserEmail,
|
||||
|
|
|
@ -27,17 +27,13 @@ defmodule Pleroma.Activity do
|
|||
# https://github.com/tootsuite/mastodon/blob/master/app/models/notification.rb#L19
|
||||
@mastodon_notification_types %{
|
||||
"Create" => "mention",
|
||||
"Follow" => "follow",
|
||||
"Follow" => ["follow", "follow_request"],
|
||||
"Announce" => "reblog",
|
||||
"Like" => "favourite",
|
||||
"Move" => "move",
|
||||
"EmojiReact" => "pleroma:emoji_reaction"
|
||||
}
|
||||
|
||||
@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)
|
||||
|
@ -291,15 +287,43 @@ defp purge_web_resp_cache(%Activity{} = activity) do
|
|||
|
||||
defp purge_web_resp_cache(nil), do: nil
|
||||
|
||||
for {ap_type, type} <- @mastodon_notification_types do
|
||||
def follow_accepted?(
|
||||
%Activity{data: %{"type" => "Follow", "object" => followed_ap_id}} = activity
|
||||
) do
|
||||
with %User{} = follower <- Activity.user_actor(activity),
|
||||
%User{} = followed <- User.get_cached_by_ap_id(followed_ap_id) do
|
||||
Pleroma.FollowingRelationship.following?(follower, followed)
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
def follow_accepted?(_), do: false
|
||||
|
||||
@spec mastodon_notification_type(Activity.t()) :: String.t() | nil
|
||||
|
||||
for {ap_type, type} <- @mastodon_notification_types, not is_list(type) do
|
||||
def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}),
|
||||
do: unquote(type)
|
||||
end
|
||||
|
||||
def mastodon_notification_type(%Activity{data: %{"type" => "Follow"}} = activity) do
|
||||
if follow_accepted?(activity) do
|
||||
"follow"
|
||||
else
|
||||
"follow_request"
|
||||
end
|
||||
end
|
||||
|
||||
def mastodon_notification_type(%Activity{}), do: nil
|
||||
|
||||
@spec from_mastodon_notification_type(String.t()) :: String.t() | nil
|
||||
@doc "Converts Mastodon notification type to AR activity type"
|
||||
def from_mastodon_notification_type(type) do
|
||||
Map.get(@mastodon_to_ap_notification_types, type)
|
||||
with {k, _v} <-
|
||||
Enum.find(@mastodon_notification_types, fn {_k, v} -> type in List.wrap(v) end) do
|
||||
k
|
||||
end
|
||||
end
|
||||
|
||||
def all_by_actor_and_id(actor, status_ids \\ [])
|
||||
|
|
|
@ -284,8 +284,17 @@ def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = act
|
|||
end
|
||||
end
|
||||
|
||||
def create_notifications(%Activity{data: %{"type" => "Follow"}} = activity) do
|
||||
if Pleroma.Config.get([:notifications, :enable_follow_request_notifications]) ||
|
||||
Activity.follow_accepted?(activity) do
|
||||
do_create_notifications(activity)
|
||||
else
|
||||
{:ok, []}
|
||||
end
|
||||
end
|
||||
|
||||
def create_notifications(%Activity{data: %{"type" => type}} = activity)
|
||||
when type in ["Like", "Announce", "Follow", "Move", "EmojiReact"] do
|
||||
when type in ["Like", "Announce", "Move", "EmojiReact"] do
|
||||
do_create_notifications(activity)
|
||||
end
|
||||
|
||||
|
|
|
@ -688,6 +688,8 @@ def needs_update?(%User{local: false} = user) do
|
|||
def needs_update?(_), do: true
|
||||
|
||||
@spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
|
||||
|
||||
# "Locked" (self-locked) users demand explicit authorization of follow requests
|
||||
def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do
|
||||
follow(follower, followed, :follow_pending)
|
||||
end
|
||||
|
|
|
@ -117,14 +117,14 @@ def render(
|
|||
# Note: :skip_relationships option being applied to _account_ rendering (here)
|
||||
put_target(response, activity, reading_user, render_opts)
|
||||
|
||||
"follow" ->
|
||||
response
|
||||
|
||||
"pleroma:emoji_reaction" ->
|
||||
response
|
||||
|> put_status(parent_activity_fn.(), reading_user, render_opts)
|
||||
|> put_emoji(activity)
|
||||
|
||||
type when type in ["follow", "follow_request"] ->
|
||||
response
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
|
|
|
@ -16,6 +16,8 @@ defmodule Pleroma.Web.Push.Impl do
|
|||
require Logger
|
||||
import Ecto.Query
|
||||
|
||||
defdelegate mastodon_notification_type(activity), to: Activity
|
||||
|
||||
@types ["Create", "Follow", "Announce", "Like", "Move"]
|
||||
|
||||
@doc "Performs sending notifications for user subscriptions"
|
||||
|
@ -24,32 +26,32 @@ def perform(
|
|||
%{
|
||||
activity: %{data: %{"type" => activity_type}} = activity,
|
||||
user: %User{id: user_id}
|
||||
} = notif
|
||||
} = notification
|
||||
)
|
||||
when activity_type in @types do
|
||||
actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
|
||||
actor = User.get_cached_by_ap_id(notification.activity.data["actor"])
|
||||
|
||||
type = Activity.mastodon_notification_type(notif.activity)
|
||||
mastodon_type = mastodon_notification_type(notification.activity)
|
||||
gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key)
|
||||
avatar_url = User.avatar_url(actor)
|
||||
object = Object.normalize(activity)
|
||||
user = User.get_cached_by_id(user_id)
|
||||
direct_conversation_id = Activity.direct_conversation_id(activity, user)
|
||||
|
||||
for subscription <- fetch_subsriptions(user_id),
|
||||
get_in(subscription.data, ["alerts", type]) do
|
||||
for subscription <- fetch_subscriptions(user_id),
|
||||
Subscription.enabled?(subscription, mastodon_type) do
|
||||
%{
|
||||
access_token: subscription.token.token,
|
||||
notification_id: notif.id,
|
||||
notification_type: type,
|
||||
notification_id: notification.id,
|
||||
notification_type: mastodon_type,
|
||||
icon: avatar_url,
|
||||
preferred_locale: "en",
|
||||
pleroma: %{
|
||||
activity_id: notif.activity.id,
|
||||
activity_id: notification.activity.id,
|
||||
direct_conversation_id: direct_conversation_id
|
||||
}
|
||||
}
|
||||
|> Map.merge(build_content(notif, actor, object))
|
||||
|> Map.merge(build_content(notification, actor, object, mastodon_type))
|
||||
|> Jason.encode!()
|
||||
|> push_message(build_sub(subscription), gcm_api_key, subscription)
|
||||
end
|
||||
|
@ -82,7 +84,7 @@ def push_message(body, sub, api_key, subscription) do
|
|||
end
|
||||
|
||||
@doc "Gets user subscriptions"
|
||||
def fetch_subsriptions(user_id) do
|
||||
def fetch_subscriptions(user_id) do
|
||||
Subscription
|
||||
|> where(user_id: ^user_id)
|
||||
|> preload(:token)
|
||||
|
@ -99,28 +101,36 @@ def build_sub(subscription) do
|
|||
}
|
||||
end
|
||||
|
||||
def build_content(notification, actor, object, mastodon_type \\ nil)
|
||||
|
||||
def build_content(
|
||||
%{
|
||||
activity: %{data: %{"directMessage" => true}},
|
||||
user: %{notification_settings: %{privacy_option: true}}
|
||||
},
|
||||
actor,
|
||||
_
|
||||
_object,
|
||||
_mastodon_type
|
||||
) do
|
||||
%{title: "New Direct Message", body: "@#{actor.nickname}"}
|
||||
end
|
||||
|
||||
def build_content(notif, actor, object) do
|
||||
def build_content(notification, actor, object, mastodon_type) do
|
||||
mastodon_type = mastodon_type || mastodon_notification_type(notification.activity)
|
||||
|
||||
%{
|
||||
title: format_title(notif),
|
||||
body: format_body(notif, actor, object)
|
||||
title: format_title(notification, mastodon_type),
|
||||
body: format_body(notification, actor, object, mastodon_type)
|
||||
}
|
||||
end
|
||||
|
||||
def format_body(activity, actor, object, mastodon_type \\ nil)
|
||||
|
||||
def format_body(
|
||||
%{activity: %{data: %{"type" => "Create"}}},
|
||||
actor,
|
||||
%{data: %{"content" => content}}
|
||||
%{data: %{"content" => content}},
|
||||
_mastodon_type
|
||||
) do
|
||||
"@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}"
|
||||
end
|
||||
|
@ -128,33 +138,44 @@ def format_body(
|
|||
def format_body(
|
||||
%{activity: %{data: %{"type" => "Announce"}}},
|
||||
actor,
|
||||
%{data: %{"content" => content}}
|
||||
%{data: %{"content" => content}},
|
||||
_mastodon_type
|
||||
) do
|
||||
"@#{actor.nickname} repeated: #{Utils.scrub_html_and_truncate(content, 80)}"
|
||||
end
|
||||
|
||||
def format_body(
|
||||
%{activity: %{data: %{"type" => type}}},
|
||||
%{activity: %{data: %{"type" => type}}} = notification,
|
||||
actor,
|
||||
_object
|
||||
_object,
|
||||
mastodon_type
|
||||
)
|
||||
when type in ["Follow", "Like"] do
|
||||
case type do
|
||||
"Follow" -> "@#{actor.nickname} has followed you"
|
||||
"Like" -> "@#{actor.nickname} has favorited your post"
|
||||
mastodon_type = mastodon_type || mastodon_notification_type(notification.activity)
|
||||
|
||||
case mastodon_type do
|
||||
"follow" -> "@#{actor.nickname} has followed you"
|
||||
"follow_request" -> "@#{actor.nickname} has requested to follow you"
|
||||
"favourite" -> "@#{actor.nickname} has favorited your post"
|
||||
end
|
||||
end
|
||||
|
||||
def format_title(%{activity: %{data: %{"directMessage" => true}}}) do
|
||||
def format_title(activity, mastodon_type \\ nil)
|
||||
|
||||
def format_title(%{activity: %{data: %{"directMessage" => true}}}, _mastodon_type) 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"
|
||||
def format_title(%{activity: activity}, mastodon_type) do
|
||||
mastodon_type = mastodon_type || mastodon_notification_type(activity)
|
||||
|
||||
case mastodon_type do
|
||||
"mention" -> "New Mention"
|
||||
"follow" -> "New Follower"
|
||||
"follow_request" -> "New Follow Request"
|
||||
"reblog" -> "New Repeat"
|
||||
"favourite" -> "New Favorite"
|
||||
type -> "New #{String.capitalize(type || "event")}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -32,6 +32,14 @@ defp alerts(%{"data" => %{"alerts" => alerts}}) do
|
|||
%{"alerts" => alerts}
|
||||
end
|
||||
|
||||
def enabled?(subscription, "follow_request") do
|
||||
enabled?(subscription, "follow")
|
||||
end
|
||||
|
||||
def enabled?(subscription, alert_type) do
|
||||
get_in(subscription.data, ["alerts", alert_type])
|
||||
end
|
||||
|
||||
def create(
|
||||
%User{} = user,
|
||||
%Token{} = token,
|
||||
|
|
|
@ -8,11 +8,13 @@ defmodule Pleroma.NotificationTest do
|
|||
import Pleroma.Factory
|
||||
import Mock
|
||||
|
||||
alias Pleroma.FollowingRelationship
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Tests.ObanHelpers
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.MastodonAPI.NotificationView
|
||||
alias Pleroma.Web.Push
|
||||
alias Pleroma.Web.Streamer
|
||||
|
||||
|
@ -272,16 +274,6 @@ test "it doesn't create a notification for user if he is the activity author" do
|
|||
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)
|
||||
|
@ -304,6 +296,74 @@ test "it doesn't create subscription notifications if the recipient cannot see t
|
|||
end
|
||||
end
|
||||
|
||||
describe "follow / follow_request notifications" do
|
||||
test "it creates `follow` notification for approved Follow activity" do
|
||||
user = insert(:user)
|
||||
followed_user = insert(:user, locked: false)
|
||||
|
||||
{:ok, _, _, _activity} = CommonAPI.follow(user, followed_user)
|
||||
assert FollowingRelationship.following?(user, followed_user)
|
||||
assert [notification] = Notification.for_user(followed_user)
|
||||
|
||||
assert %{type: "follow"} =
|
||||
NotificationView.render("show.json", %{
|
||||
notification: notification,
|
||||
for: followed_user
|
||||
})
|
||||
end
|
||||
|
||||
test "if `follow_request` notifications are enabled, " <>
|
||||
"it creates `follow_request` notification for pending Follow activity" do
|
||||
clear_config([:notifications, :enable_follow_request_notifications], true)
|
||||
user = insert(:user)
|
||||
followed_user = insert(:user, locked: true)
|
||||
|
||||
{:ok, _, _, _activity} = CommonAPI.follow(user, followed_user)
|
||||
refute FollowingRelationship.following?(user, followed_user)
|
||||
assert [notification] = Notification.for_user(followed_user)
|
||||
|
||||
render_opts = %{notification: notification, for: followed_user}
|
||||
assert %{type: "follow_request"} = NotificationView.render("show.json", render_opts)
|
||||
|
||||
# After request is accepted, the same notification is rendered with type "follow":
|
||||
assert {:ok, _} = CommonAPI.accept_follow_request(user, followed_user)
|
||||
|
||||
notification_id = notification.id
|
||||
assert [%{id: ^notification_id}] = Notification.for_user(followed_user)
|
||||
assert %{type: "follow"} = NotificationView.render("show.json", render_opts)
|
||||
end
|
||||
|
||||
test "if `follow_request` notifications are disabled, " <>
|
||||
"it does NOT create `follow*` notification for pending Follow activity" do
|
||||
clear_config([:notifications, :enable_follow_request_notifications], false)
|
||||
user = insert(:user)
|
||||
followed_user = insert(:user, locked: true)
|
||||
|
||||
{:ok, _, _, _activity} = CommonAPI.follow(user, followed_user)
|
||||
refute FollowingRelationship.following?(user, followed_user)
|
||||
assert [] = Notification.for_user(followed_user)
|
||||
|
||||
# After request is accepted, no new notifications are generated:
|
||||
assert {:ok, _} = CommonAPI.accept_follow_request(user, followed_user)
|
||||
assert [] = Notification.for_user(followed_user)
|
||||
end
|
||||
|
||||
test "it doesn't create a notification for follow-unfollow-follow chains" do
|
||||
user = insert(:user)
|
||||
followed_user = insert(:user, locked: false)
|
||||
|
||||
{:ok, _, _, _activity} = CommonAPI.follow(user, followed_user)
|
||||
assert FollowingRelationship.following?(user, followed_user)
|
||||
assert [notification] = Notification.for_user(followed_user)
|
||||
|
||||
CommonAPI.unfollow(user, followed_user)
|
||||
{:ok, _, _, _activity_dupe} = CommonAPI.follow(user, followed_user)
|
||||
|
||||
notification_id = notification.id
|
||||
assert [%{id: ^notification_id}] = Notification.for_user(followed_user)
|
||||
end
|
||||
end
|
||||
|
||||
describe "get notification" do
|
||||
test "it gets a notification that belongs to the user" do
|
||||
user = insert(:user)
|
||||
|
|
Loading…
Reference in a new issue