[#1559] Support for "follow_request" notifications (configurable).

(Not currently supported by PleromaFE, thus disabled by default).
This commit is contained in:
Ivan Tashkinov 2020-04-07 21:52:32 +03:00
parent dd4d10b275
commit 1a4875adfa
9 changed files with 117 additions and 36 deletions

View file

@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- NodeInfo: `pleroma_emoji_reactions` to the `features` list. - NodeInfo: `pleroma_emoji_reactions` to the `features` list.
- Configuration: `:restrict_unauthenticated` setting, restrict access for unauthenticated users to timelines (public and federate), user profiles and statuses. - 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 wont start. For hackney OTP update is not required. - New HTTP adapter [gun](https://github.com/ninenines/gun). Gun adapter requires minimum OTP version of 22.2 otherwise Pleroma wont start. For hackney OTP update is not required.
- Notifications: Added `follow_request` notification type (configurable, see `[:notifications, :enable_follow_request_notifications]` setting).
<details> <details>
<summary>API Changes</summary> <summary>API Changes</summary>
- Mastodon API: Support for `include_types` in `/api/v1/notifications`. - Mastodon API: Support for `include_types` in `/api/v1/notifications`.

View file

@ -559,6 +559,8 @@
inactivity_threshold: 7 inactivity_threshold: 7
} }
config :pleroma, :notifications, enable_follow_request_notifications: false
config :pleroma, :oauth2, config :pleroma, :oauth2,
token_expires_in: 600, token_expires_in: 600,
issue_new_refresh_token: true, issue_new_refresh_token: true,

View file

@ -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, group: :pleroma,
key: Pleroma.Emails.UserEmail, key: Pleroma.Emails.UserEmail,

View file

@ -27,17 +27,13 @@ defmodule Pleroma.Activity do
# https://github.com/tootsuite/mastodon/blob/master/app/models/notification.rb#L19 # https://github.com/tootsuite/mastodon/blob/master/app/models/notification.rb#L19
@mastodon_notification_types %{ @mastodon_notification_types %{
"Create" => "mention", "Create" => "mention",
"Follow" => "follow", "Follow" => ["follow", "follow_request"],
"Announce" => "reblog", "Announce" => "reblog",
"Like" => "favourite", "Like" => "favourite",
"Move" => "move", "Move" => "move",
"EmojiReact" => "pleroma:emoji_reaction" "EmojiReact" => "pleroma:emoji_reaction"
} }
@mastodon_to_ap_notification_types for {k, v} <- @mastodon_notification_types,
into: %{},
do: {v, k}
schema "activities" do schema "activities" do
field(:data, :map) field(:data, :map)
field(:local, :boolean, default: true) field(:local, :boolean, default: true)
@ -291,15 +287,41 @@ defp purge_web_resp_cache(%Activity{} = activity) do
defp purge_web_resp_cache(nil), do: nil 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
for {ap_type, type} <- @mastodon_notification_types, not is_list(type) do
def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}), def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}),
do: unquote(type) do: unquote(type)
end 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 def mastodon_notification_type(%Activity{}), do: nil
def from_mastodon_notification_type(type) do 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} ->
v == type or (is_list(v) and type in v)
end) do
k
end
end end
def all_by_actor_and_id(actor, status_ids \\ []) def all_by_actor_and_id(actor, status_ids \\ [])

View file

@ -284,8 +284,17 @@ def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = act
end end
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) 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) do_create_notifications(activity)
end end

View file

@ -699,6 +699,8 @@ def needs_update?(%User{local: false} = user) do
def needs_update?(_), do: true def needs_update?(_), do: true
@spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()} @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 def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do
follow(follower, followed, "pending") follow(follower, followed, "pending")
end end

View file

@ -116,6 +116,9 @@ def render(
"follow" -> "follow" ->
response response
"follow_request" ->
response
"pleroma:emoji_reaction" -> "pleroma:emoji_reaction" ->
response response
|> put_status(parent_activity_fn.(), reading_user, render_opts) |> put_status(parent_activity_fn.(), reading_user, render_opts)

View file

@ -16,6 +16,8 @@ defmodule Pleroma.Web.Push.Impl do
require Logger require Logger
import Ecto.Query import Ecto.Query
defdelegate mastodon_notification_type(activity), to: Activity
@types ["Create", "Follow", "Announce", "Like", "Move"] @types ["Create", "Follow", "Announce", "Like", "Move"]
@doc "Performs sending notifications for user subscriptions" @doc "Performs sending notifications for user subscriptions"
@ -24,32 +26,32 @@ def perform(
%{ %{
activity: %{data: %{"type" => activity_type}} = activity, activity: %{data: %{"type" => activity_type}} = activity,
user: %User{id: user_id} user: %User{id: user_id}
} = notif } = notification
) )
when activity_type in @types do 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) gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key)
avatar_url = User.avatar_url(actor) avatar_url = User.avatar_url(actor)
object = Object.normalize(activity) object = Object.normalize(activity)
user = User.get_cached_by_id(user_id) user = User.get_cached_by_id(user_id)
direct_conversation_id = Activity.direct_conversation_id(activity, user) direct_conversation_id = Activity.direct_conversation_id(activity, user)
for subscription <- fetch_subsriptions(user_id), for subscription <- fetch_subscriptions(user_id),
get_in(subscription.data, ["alerts", type]) do Subscription.enabled?(subscription, mastodon_type) do
%{ %{
access_token: subscription.token.token, access_token: subscription.token.token,
notification_id: notif.id, notification_id: notification.id,
notification_type: type, notification_type: mastodon_type,
icon: avatar_url, icon: avatar_url,
preferred_locale: "en", preferred_locale: "en",
pleroma: %{ pleroma: %{
activity_id: notif.activity.id, activity_id: notification.activity.id,
direct_conversation_id: direct_conversation_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!() |> Jason.encode!()
|> push_message(build_sub(subscription), gcm_api_key, subscription) |> push_message(build_sub(subscription), gcm_api_key, subscription)
end end
@ -82,7 +84,7 @@ def push_message(body, sub, api_key, subscription) do
end end
@doc "Gets user subscriptions" @doc "Gets user subscriptions"
def fetch_subsriptions(user_id) do def fetch_subscriptions(user_id) do
Subscription Subscription
|> where(user_id: ^user_id) |> where(user_id: ^user_id)
|> preload(:token) |> preload(:token)
@ -99,28 +101,36 @@ def build_sub(subscription) do
} }
end end
def build_content(notification, actor, object, mastodon_type \\ nil)
def build_content( def build_content(
%{ %{
activity: %{data: %{"directMessage" => true}}, activity: %{data: %{"directMessage" => true}},
user: %{notification_settings: %{privacy_option: true}} user: %{notification_settings: %{privacy_option: true}}
}, },
actor, actor,
_ _object,
_mastodon_type
) do ) do
%{title: "New Direct Message", body: "@#{actor.nickname}"} %{title: "New Direct Message", body: "@#{actor.nickname}"}
end 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), title: format_title(notification, mastodon_type),
body: format_body(notif, actor, object) body: format_body(notification, actor, object, mastodon_type)
} }
end end
def format_body(activity, actor, object, mastodon_type \\ nil)
def format_body( def format_body(
%{activity: %{data: %{"type" => "Create"}}}, %{activity: %{data: %{"type" => "Create"}}},
actor, actor,
%{data: %{"content" => content}} %{data: %{"content" => content}},
_mastodon_type
) do ) do
"@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}" "@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}"
end end
@ -128,33 +138,43 @@ def format_body(
def format_body( def format_body(
%{activity: %{data: %{"type" => "Announce"}}}, %{activity: %{data: %{"type" => "Announce"}}},
actor, actor,
%{data: %{"content" => content}} %{data: %{"content" => content}},
_mastodon_type
) do ) do
"@#{actor.nickname} repeated: #{Utils.scrub_html_and_truncate(content, 80)}" "@#{actor.nickname} repeated: #{Utils.scrub_html_and_truncate(content, 80)}"
end end
def format_body( def format_body(
%{activity: %{data: %{"type" => type}}}, %{activity: %{data: %{"type" => type}}} = notification,
actor, actor,
_object _object,
mastodon_type
) )
when type in ["Follow", "Like"] do when type in ["Follow", "Like"] do
case type do mastodon_type = mastodon_type || mastodon_notification_type(notification.activity)
"Follow" -> "@#{actor.nickname} has followed you"
"Like" -> "@#{actor.nickname} has favorited your post" case {type, mastodon_type} do
{"Follow", "follow"} -> "@#{actor.nickname} has followed you"
{"Follow", "follow_request"} -> "@#{actor.nickname} has requested to follow you"
{"Like", _} -> "@#{actor.nickname} has favorited your post"
end end
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" "New Direct Message"
end end
def format_title(%{activity: %{data: %{"type" => type}}}) do def format_title(%{activity: %{data: %{"type" => type}}} = notification, mastodon_type) do
case type do mastodon_type = mastodon_type || mastodon_notification_type(notification.activity)
"Create" -> "New Mention"
"Follow" -> "New Follower" case {type, mastodon_type} do
"Announce" -> "New Repeat" {"Create", _} -> "New Mention"
"Like" -> "New Favorite" {"Follow", "follow"} -> "New Follower"
{"Follow", "follow_request"} -> "New Follow Request"
{"Announce", _} -> "New Repeat"
{"Like", _} -> "New Favorite"
end end
end end
end end

View file

@ -32,6 +32,14 @@ defp alerts(%{"data" => %{"alerts" => alerts}}) do
%{"alerts" => alerts} %{"alerts" => alerts}
end 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( def create(
%User{} = user, %User{} = user,
%Token{} = token, %Token{} = token,