Merge branch 'issue/1342' into 'develop'

[#1342] Added privacy option to push notifications

See merge request pleroma/pleroma!1920
This commit is contained in:
lain 2019-12-09 14:30:35 +00:00
commit e4ea0e20b3
16 changed files with 273 additions and 48 deletions

View file

@ -48,6 +48,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mix task to list all users (`mix pleroma.user list`) - Mix task to list all users (`mix pleroma.user list`)
- Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache). - Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache).
- MRF: New module which handles incoming posts based on their age. By default, all incoming posts that are older than 2 days will be unlisted and not shown to their followers. - MRF: New module which handles incoming posts based on their age. By default, all incoming posts that are older than 2 days will be unlisted and not shown to their followers.
- User notification settings: Add `privacy_option` option.
<details> <details>
<summary>API Changes</summary> <summary>API Changes</summary>

View file

@ -302,6 +302,7 @@ See [Admin-API](admin_api.md)
* `follows`: BOOLEAN field, receives notifications from people the user follows * `follows`: BOOLEAN field, receives notifications from people the user follows
* `remote`: BOOLEAN field, receives notifications from people on remote instances * `remote`: BOOLEAN field, receives notifications from people on remote instances
* `local`: BOOLEAN field, receives notifications from people on the local instance * `local`: BOOLEAN field, receives notifications from people on the local instance
* `privacy_option`: BOOLEAN field. When set to true, it removes the contents of a message from the push notification.
* Response: JSON. Returns `{"status": "success"}` if the update was successful, otherwise returns `{"error": "error_msg"}` * Response: JSON. Returns `{"status": "success"}` if the update was successful, otherwise returns `{"error": "error_msg"}`
## `/api/pleroma/healthcheck` ## `/api/pleroma/healthcheck`

View file

@ -0,0 +1,83 @@
defmodule Mix.Tasks.Pleroma.NotificationSettings do
@shortdoc "Enable&Disable privacy option for push notifications"
@moduledoc """
Example:
> mix pleroma.notification_settings --privacy-option=false --nickname-users="parallel588" # set false only for parallel588 user
> mix pleroma.notification_settings --privacy-option=true # set true for all users
"""
use Mix.Task
import Mix.Pleroma
import Ecto.Query
def run(args) do
start_pleroma()
{options, _, _} =
OptionParser.parse(
args,
strict: [
privacy_option: :boolean,
email_users: :string,
nickname_users: :string
]
)
privacy_option = Keyword.get(options, :privacy_option)
if not is_nil(privacy_option) do
privacy_option
|> build_query(options)
|> Pleroma.Repo.update_all([])
end
shell_info("Done")
end
defp build_query(privacy_option, options) do
query =
from(u in Pleroma.User,
update: [
set: [
notification_settings:
fragment(
"jsonb_set(notification_settings, '{privacy_option}', ?)",
^privacy_option
)
]
]
)
user_emails =
options
|> Keyword.get(:email_users, "")
|> String.split(",")
|> Enum.map(&String.trim(&1))
|> Enum.reject(&(&1 == ""))
query =
if length(user_emails) > 0 do
where(query, [u], u.email in ^user_emails)
else
query
end
user_nicknames =
options
|> Keyword.get(:nickname_users, "")
|> String.split(",")
|> Enum.map(&String.trim(&1))
|> Enum.reject(&(&1 == ""))
query =
if length(user_nicknames) > 0 do
where(query, [u], u.nickname in ^user_nicknames)
else
query
end
query
end
end

View file

@ -347,7 +347,7 @@ def skip?(:self, activity, user) do
def skip?( def skip?(
:followers, :followers,
activity, activity,
%{notification_settings: %{"followers" => false}} = user %{notification_settings: %{followers: false}} = user
) do ) do
actor = activity.data["actor"] actor = activity.data["actor"]
follower = User.get_cached_by_ap_id(actor) follower = User.get_cached_by_ap_id(actor)
@ -357,14 +357,14 @@ def skip?(
def skip?( def skip?(
:non_followers, :non_followers,
activity, activity,
%{notification_settings: %{"non_followers" => false}} = user %{notification_settings: %{non_followers: false}} = user
) do ) do
actor = activity.data["actor"] actor = activity.data["actor"]
follower = User.get_cached_by_ap_id(actor) follower = User.get_cached_by_ap_id(actor)
!User.following?(follower, user) !User.following?(follower, user)
end end
def skip?(:follows, activity, %{notification_settings: %{"follows" => false}} = user) do def skip?(:follows, activity, %{notification_settings: %{follows: false}} = user) do
actor = activity.data["actor"] actor = activity.data["actor"]
followed = User.get_cached_by_ap_id(actor) followed = User.get_cached_by_ap_id(actor)
User.following?(user, followed) User.following?(user, followed)
@ -373,7 +373,7 @@ def skip?(:follows, activity, %{notification_settings: %{"follows" => false}} =
def skip?( def skip?(
:non_follows, :non_follows,
activity, activity,
%{notification_settings: %{"non_follows" => false}} = user %{notification_settings: %{non_follows: false}} = user
) do ) do
actor = activity.data["actor"] actor = activity.data["actor"]
followed = User.get_cached_by_ap_id(actor) followed = User.get_cached_by_ap_id(actor)

View file

@ -129,13 +129,10 @@ defmodule Pleroma.User do
field(:skip_thread_containment, :boolean, default: false) field(:skip_thread_containment, :boolean, default: false)
field(:also_known_as, {:array, :string}, default: []) field(:also_known_as, {:array, :string}, default: [])
field(:notification_settings, :map, embeds_one(
default: %{ :notification_settings,
"followers" => true, Pleroma.User.NotificationSetting,
"follows" => true, on_replace: :update
"non_follows" => true,
"non_followers" => true
}
) )
has_many(:notifications, Notification) has_many(:notifications, Notification)
@ -1221,20 +1218,9 @@ def deactivate(%User{} = user, status) do
end end
def update_notification_settings(%User{} = user, settings) do def update_notification_settings(%User{} = user, settings) do
settings =
settings
|> Enum.map(fn {k, v} -> {k, v in [true, "true", "True", "1"]} end)
|> Map.new()
notification_settings =
user.notification_settings
|> Map.merge(settings)
|> Map.take(["followers", "follows", "non_follows", "non_followers"])
params = %{notification_settings: notification_settings}
user user
|> cast(params, [:notification_settings]) |> cast(%{notification_settings: settings}, [])
|> cast_embed(:notification_settings)
|> validate_required([:notification_settings]) |> validate_required([:notification_settings])
|> update_and_set_cache() |> update_and_set_cache()
end end

View file

@ -0,0 +1,40 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User.NotificationSetting do
use Ecto.Schema
import Ecto.Changeset
@derive Jason.Encoder
@primary_key false
embedded_schema do
field(:followers, :boolean, default: true)
field(:follows, :boolean, default: true)
field(:non_follows, :boolean, default: true)
field(:non_followers, :boolean, default: true)
field(:privacy_option, :boolean, default: false)
end
def changeset(schema, params) do
schema
|> cast(prepare_attrs(params), [
:followers,
:follows,
:non_follows,
:non_followers,
:privacy_option
])
end
defp prepare_attrs(params) do
Enum.reduce(params, %{}, fn
{k, v}, acc when is_binary(v) ->
Map.put(acc, k, String.downcase(v))
{k, v}, acc ->
Map.put(acc, k, v)
end)
end
end

View file

@ -22,8 +22,8 @@ defmodule Pleroma.Web.Push.Impl do
@spec perform(Notification.t()) :: list(any) | :error @spec perform(Notification.t()) :: list(any) | :error
def perform( def perform(
%{ %{
activity: %{data: %{"type" => activity_type}, id: activity_id} = activity, activity: %{data: %{"type" => activity_type}} = activity,
user_id: user_id user: %User{id: user_id}
} = notif } = notif
) )
when activity_type in @types do when activity_type in @types do
@ -39,18 +39,17 @@ def perform(
for subscription <- fetch_subsriptions(user_id), for subscription <- fetch_subsriptions(user_id),
get_in(subscription.data, ["alerts", type]) do get_in(subscription.data, ["alerts", type]) do
%{ %{
title: format_title(notif),
access_token: subscription.token.token, access_token: subscription.token.token,
body: format_body(notif, actor, object),
notification_id: notif.id, notification_id: notif.id,
notification_type: type, notification_type: type,
icon: avatar_url, icon: avatar_url,
preferred_locale: "en", preferred_locale: "en",
pleroma: %{ pleroma: %{
activity_id: activity_id, activity_id: notif.activity.id,
direct_conversation_id: direct_conversation_id direct_conversation_id: direct_conversation_id
} }
} }
|> Map.merge(build_content(notif, actor, object))
|> Jason.encode!() |> Jason.encode!()
|> push_message(build_sub(subscription), gcm_api_key, subscription) |> push_message(build_sub(subscription), gcm_api_key, subscription)
end end
@ -100,6 +99,24 @@ def build_sub(subscription) do
} }
end end
def build_content(
%{
activity: %{data: %{"directMessage" => true}},
user: %{notification_settings: %{privacy_option: true}}
},
actor,
_
) do
%{title: "New Direct Message", body: "@#{actor.nickname}"}
end
def build_content(notif, actor, object) do
%{
title: format_title(notif),
body: format_body(notif, actor, object)
}
end
def format_body( def format_body(
%{activity: %{data: %{"type" => "Create"}}}, %{activity: %{data: %{"type" => "Create"}}},
actor, actor,

View file

@ -13,7 +13,7 @@ def perform(%{"op" => "web_push", "notification_id" => notification_id}, _job) d
notification = notification =
Notification Notification
|> Repo.get(notification_id) |> Repo.get(notification_id)
|> Repo.preload([:activity]) |> Repo.preload([:activity, :user])
Pleroma.Web.Push.Impl.perform(notification) Pleroma.Web.Push.Impl.perform(notification)
end end

View file

@ -136,7 +136,10 @@ test "it creates a notification for an activity from a muted thread" do
test "it disables notifications from followers" do test "it disables notifications from followers" do
follower = insert(:user) follower = insert(:user)
followed = insert(:user, notification_settings: %{"followers" => false})
followed =
insert(:user, notification_settings: %Pleroma.User.NotificationSetting{followers: false})
User.follow(follower, followed) User.follow(follower, followed)
{:ok, activity} = CommonAPI.post(follower, %{"status" => "hey @#{followed.nickname}"}) {:ok, activity} = CommonAPI.post(follower, %{"status" => "hey @#{followed.nickname}"})
refute Notification.create_notification(activity, followed) refute Notification.create_notification(activity, followed)
@ -144,13 +147,20 @@ test "it disables notifications from followers" do
test "it disables notifications from non-followers" do test "it disables notifications from non-followers" do
follower = insert(:user) follower = insert(:user)
followed = insert(:user, notification_settings: %{"non_followers" => false})
followed =
insert(:user,
notification_settings: %Pleroma.User.NotificationSetting{non_followers: false}
)
{:ok, activity} = CommonAPI.post(follower, %{"status" => "hey @#{followed.nickname}"}) {:ok, activity} = CommonAPI.post(follower, %{"status" => "hey @#{followed.nickname}"})
refute Notification.create_notification(activity, followed) refute Notification.create_notification(activity, followed)
end end
test "it disables notifications from people the user follows" do test "it disables notifications from people the user follows" do
follower = insert(:user, notification_settings: %{"follows" => false}) follower =
insert(:user, notification_settings: %Pleroma.User.NotificationSetting{follows: false})
followed = insert(:user) followed = insert(:user)
User.follow(follower, followed) User.follow(follower, followed)
follower = Repo.get(User, follower.id) follower = Repo.get(User, follower.id)
@ -159,7 +169,9 @@ test "it disables notifications from people the user follows" do
end end
test "it disables notifications from people the user does not follow" do test "it disables notifications from people the user does not follow" do
follower = insert(:user, notification_settings: %{"non_follows" => false}) follower =
insert(:user, notification_settings: %Pleroma.User.NotificationSetting{non_follows: false})
followed = insert(:user) followed = insert(:user)
{:ok, activity} = CommonAPI.post(followed, %{"status" => "hey @#{follower.nickname}"}) {:ok, activity} = CommonAPI.post(followed, %{"status" => "hey @#{follower.nickname}"})
refute Notification.create_notification(activity, follower) refute Notification.create_notification(activity, follower)

View file

@ -10,7 +10,8 @@ def build(data \\ %{}) do
password_hash: Comeonin.Pbkdf2.hashpwsalt("test"), password_hash: Comeonin.Pbkdf2.hashpwsalt("test"),
bio: "A tester.", bio: "A tester.",
ap_id: "some id", ap_id: "some id",
last_digest_emailed_at: NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) last_digest_emailed_at: NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second),
notification_settings: %Pleroma.User.NotificationSetting{}
} }
Map.merge(user, data) Map.merge(user, data)

View file

@ -31,7 +31,8 @@ def user_factory do
nickname: sequence(:nickname, &"nick#{&1}"), nickname: sequence(:nickname, &"nick#{&1}"),
password_hash: Comeonin.Pbkdf2.hashpwsalt("test"), password_hash: Comeonin.Pbkdf2.hashpwsalt("test"),
bio: sequence(:bio, &"Tester Number #{&1}"), bio: sequence(:bio, &"Tester Number #{&1}"),
last_digest_emailed_at: NaiveDateTime.utc_now() last_digest_emailed_at: NaiveDateTime.utc_now(),
notification_settings: %Pleroma.User.NotificationSetting{}
} }
%{ %{

View file

@ -0,0 +1,21 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User.NotificationSettingTest do
use Pleroma.DataCase
alias Pleroma.User.NotificationSetting
describe "changeset/2" do
test "sets valid privacy option" do
changeset =
NotificationSetting.changeset(
%NotificationSetting{},
%{"privacy_option" => true}
)
assert %Ecto.Changeset{valid?: true} = changeset
end
end
end

View file

@ -174,6 +174,7 @@ test "works with URIs" do
|> Map.put(:search_rank, nil) |> Map.put(:search_rank, nil)
|> Map.put(:search_type, nil) |> Map.put(:search_type, nil)
|> Map.put(:last_digest_emailed_at, nil) |> Map.put(:last_digest_emailed_at, nil)
|> Map.put(:notification_settings, nil)
assert user == expected assert user == expected
end end

View file

@ -92,13 +92,7 @@ test "Represent a user account" do
test "Represent the user account for the account owner" do test "Represent the user account for the account owner" do
user = insert(:user) user = insert(:user)
notification_settings = %{ notification_settings = %Pleroma.User.NotificationSetting{}
"followers" => true,
"follows" => true,
"non_follows" => true,
"non_followers" => true
}
privacy = user.default_scope privacy = user.default_scope
assert %{ assert %{

View file

@ -6,6 +6,7 @@ defmodule Pleroma.Web.Push.ImplTest do
use Pleroma.DataCase use Pleroma.DataCase
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Push.Impl alias Pleroma.Web.Push.Impl
alias Pleroma.Web.Push.Subscription alias Pleroma.Web.Push.Subscription
@ -182,4 +183,50 @@ test "renders title for create activity with direct visibility" do
assert Impl.format_title(%{activity: activity}) == assert Impl.format_title(%{activity: activity}) ==
"New Direct Message" "New Direct Message"
end end
describe "build_content/3" do
test "returns info content for direct message with enabled privacy option" do
user = insert(:user, nickname: "Bob")
user2 = insert(:user, nickname: "Rob", notification_settings: %{privacy_option: true})
{:ok, activity} =
CommonAPI.post(user, %{
"visibility" => "direct",
"status" => "<Lorem ipsum dolor sit amet."
})
notif = insert(:notification, user: user2, activity: activity)
actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
object = Object.normalize(activity)
assert Impl.build_content(notif, actor, object) == %{
body: "@Bob",
title: "New Direct Message"
}
end
test "returns regular content for direct message with disabled privacy option" do
user = insert(:user, nickname: "Bob")
user2 = insert(:user, nickname: "Rob", notification_settings: %{privacy_option: false})
{:ok, activity} =
CommonAPI.post(user, %{
"visibility" => "direct",
"status" =>
"<span>Lorem ipsum dolor sit amet</span>, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis."
})
notif = insert(:notification, user: user2, activity: activity)
actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
object = Object.normalize(activity)
assert Impl.build_content(notif, actor, object) == %{
body:
"@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini...",
title: "New Direct Message"
}
end
end
end end

View file

@ -159,11 +159,31 @@ test "it updates notification settings", %{conn: conn} do
user = Repo.get(User, user.id) user = Repo.get(User, user.id)
assert %{ assert %Pleroma.User.NotificationSetting{
"followers" => false, followers: false,
"follows" => true, follows: true,
"non_follows" => true, non_follows: true,
"non_followers" => true non_followers: true,
privacy_option: false
} == user.notification_settings
end
test "it update notificatin privacy option", %{conn: conn} do
user = insert(:user)
conn
|> assign(:user, user)
|> put("/api/pleroma/notification_settings", %{"privacy_option" => "1"})
|> json_response(:ok)
user = refresh_record(user)
assert %Pleroma.User.NotificationSetting{
followers: true,
follows: true,
non_follows: true,
non_followers: true,
privacy_option: true
} == user.notification_settings } == user.notification_settings
end end
end end