diff --git a/docs/api/pleroma_api.md b/docs/api/pleroma_api.md index 2e8fb04d2..05bcea1b6 100644 --- a/docs/api/pleroma_api.md +++ b/docs/api/pleroma_api.md @@ -116,3 +116,13 @@ See [Admin-API](Admin-API.md) * Params: * `id`: notifications's id * Response: JSON. Returns `{"status": "success"}` if the reading was successful, otherwise returns `{"error": "error_msg"}` +## `/api/pleroma/notification_settings` +### Updates user notification settings +* Method `PUT` +* Authentication: required +* Params: + * `followers`: BOOLEAN field, receives notifications from followers + * `follows`: BOOLEAN field, receives notifications from people the user follows + * `remote`: BOOLEAN field, receives notifications from people on remote instances + * `local`: BOOLEAN field, receives notifications from people on the local instance +* Response: JSON. Returns `{"status": "success"}` if the update was successful, otherwise returns `{"error": "error_msg"}` diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index cac10f24a..14de1a097 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -122,13 +122,7 @@ def create_notifications(_), do: {:ok, []} # TODO move to sql, too. def create_notification(%Activity{} = activity, %User{} = user) do - unless User.blocks?(user, %{ap_id: activity.data["actor"]}) or - CommonAPI.thread_muted?(user, activity) or user.ap_id == activity.data["actor"] or - (activity.data["type"] == "Follow" and - Enum.any?(Notification.for_user(user), fn notif -> - notif.activity.data["type"] == "Follow" and - notif.activity.data["actor"] == activity.data["actor"] - end)) do + unless skip?(activity, user) do notification = %Notification{user_id: user.id, activity: activity} {:ok, notification} = Repo.insert(notification) Pleroma.Web.Streamer.stream("user", notification) @@ -154,4 +148,59 @@ def get_notified_from_activity( end def get_notified_from_activity(_, _local_only), do: [] + + def skip?(activity, user) do + [:self, :blocked, :local, :muted, :followers, :follows, :recently_followed] + |> Enum.any?(&skip?(&1, activity, user)) + end + + def skip?(:self, activity, user) do + activity.data["actor"] == user.ap_id + end + + def skip?(:blocked, activity, user) do + actor = activity.data["actor"] + User.blocks?(user, %{ap_id: actor}) + end + + def skip?(:local, %{local: true}, %{info: %{notification_settings: %{"local" => false}}}), + do: true + + def skip?(:local, %{local: false}, %{info: %{notification_settings: %{"remote" => false}}}), + do: true + + def skip?(:muted, activity, user) do + actor = activity.data["actor"] + + User.mutes?(user, %{ap_id: actor}) or + CommonAPI.thread_muted?(user, activity) + end + + def skip?( + :followers, + activity, + %{info: %{notification_settings: %{"followers" => false}}} = user + ) do + actor = activity.data["actor"] + follower = User.get_cached_by_ap_id(actor) + User.following?(follower, user) + end + + def skip?(:follows, activity, %{info: %{notification_settings: %{"follows" => false}}} = user) do + actor = activity.data["actor"] + followed = User.get_by_ap_id(actor) + User.following?(user, followed) + end + + def skip?(:recently_followed, %{data: %{"type" => "Follow"}} = activity, user) do + actor = activity.data["actor"] + + Notification.for_user(user) + |> Enum.any?(fn + %{activity: %{data: %{"type" => "Follow", "actor" => ^actor}}} -> true + _ -> false + end) + end + + def skip?(_, _, _), do: false end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 4f579b597..ed23b8ef0 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1092,6 +1092,14 @@ def deactivate(%User{} = user, status \\ true) do update_and_set_cache(cng) end + def update_notification_settings(%User{} = user, settings \\ %{}) do + info_changeset = User.Info.update_notification_settings(user.info, settings) + + change(user) + |> put_embed(:info, info_changeset) + |> update_and_set_cache() + end + def delete(%User{} = user) do {:ok, user} = User.deactivate(user) diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex index 740a46727..33fd77b02 100644 --- a/lib/pleroma/user/info.ex +++ b/lib/pleroma/user/info.ex @@ -40,6 +40,10 @@ defmodule Pleroma.User.Info do field(:pinned_activities, {:array, :string}, default: []) field(:flavour, :string, default: nil) + field(:notification_settings, :map, + default: %{"remote" => true, "local" => true, "followers" => true, "follows" => true} + ) + # Found in the wild # ap_id -> Where is this used? # bio -> Where is this used? @@ -57,6 +61,19 @@ def set_activation_status(info, deactivated) do |> validate_required([:deactivated]) end + def update_notification_settings(info, settings) do + notification_settings = + info.notification_settings + |> Map.merge(settings) + |> Map.take(["remote", "local", "followers", "follows"]) + + params = %{notification_settings: notification_settings} + + info + |> cast(params, [:notification_settings]) + |> validate_required([:notification_settings]) + end + def add_to_note_count(info, number) do set_note_count(info, info.note_count + number) end diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index b5f3bbb9d..25899e491 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -117,13 +117,15 @@ defp do_render("account.json", %{user: user} = opts) do }, # Pleroma extension - pleroma: %{ - confirmation_pending: user_info.confirmation_pending, - tags: user.tags, - is_moderator: user.info.is_moderator, - is_admin: user.info.is_admin, - relationship: relationship - } + pleroma: + %{ + confirmation_pending: user_info.confirmation_pending, + tags: user.tags, + is_moderator: user.info.is_moderator, + is_admin: user.info.is_admin, + relationship: relationship + } + |> with_notification_settings(user, opts[:for]) } end @@ -132,4 +134,10 @@ defp username_from_nickname(string) when is_binary(string) do end defp username_from_nickname(_), do: nil + + defp with_notification_settings(data, %User{id: user_id} = user, %User{id: user_id}) do + Map.put(data, :notification_settings, user.info.notification_settings) + end + + defp with_notification_settings(data, _, _), do: data end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index ef38fc34d..66db5e628 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -193,6 +193,7 @@ defmodule Pleroma.Web.Router do post("/change_password", UtilController, :change_password) post("/delete_account", UtilController, :delete_account) + put("/notification_settings", UtilController, :update_notificaton_settings) end scope [] do diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index 26407aebd..bb71742ec 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -292,6 +292,12 @@ def emoji(conn, _params) do json(conn, emoji) end + def update_notificaton_settings(%{assigns: %{user: user}} = conn, params) do + with {:ok, _} <- User.update_notification_settings(user, params) do + json(conn, %{status: "success"}) + end + end + def follow_import(conn, %{"list" => %Plug.Upload{} = listfile}) do follow_import(conn, %{"list" => File.read!(listfile.path)}) end diff --git a/test/notification_test.exs b/test/notification_test.exs index 12b4292aa..89d06b3a2 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -41,6 +41,75 @@ test "it doesn't create a notification for user if the user blocks the activity assert nil == Notification.create_notification(activity, user) end + test "it doesn't create 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 nil == Notification.create_notification(activity, muter) + end + + test "it doesn't create 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 nil == Notification.create_notification(activity, muter) + end + + test "it disables notifications from people on remote instances" do + user = insert(:user, info: %{notification_settings: %{"remote" => false}}) + other_user = insert(:user) + + create_activity = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "actor" => other_user.ap_id, + "object" => %{ + "type" => "Note", + "content" => "Hi @#{user.nickname}", + "attributedTo" => other_user.ap_id + } + } + + {:ok, %{local: false} = activity} = Transmogrifier.handle_incoming(create_activity) + assert nil == Notification.create_notification(activity, user) + end + + test "it disables notifications from people on the local instance" do + user = insert(:user, info: %{notification_settings: %{"local" => false}}) + other_user = insert(:user) + {:ok, activity} = CommonAPI.post(other_user, %{"status" => "hey @#{user.nickname}"}) + assert nil == Notification.create_notification(activity, user) + end + + test "it disables notifications from followers" do + follower = insert(:user) + followed = insert(:user, info: %{notification_settings: %{"followers" => false}}) + User.follow(follower, followed) + {:ok, activity} = CommonAPI.post(follower, %{"status" => "hey @#{followed.nickname}"}) + assert nil == Notification.create_notification(activity, followed) + end + + test "it disables notifications from people the user follows" do + follower = insert(:user, info: %{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}"}) + assert nil == 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_by_ap_id(activity.data["actor"]) diff --git a/test/web/mastodon_api/account_view_test.exs b/test/web/mastodon_api/account_view_test.exs index 6dc60afe9..aa6a1e960 100644 --- a/test/web/mastodon_api/account_view_test.exs +++ b/test/web/mastodon_api/account_view_test.exs @@ -71,6 +71,20 @@ test "Represent a user account" do assert expected == AccountView.render("account.json", %{user: user}) end + test "Represent the user account for the account owner" do + user = insert(:user) + + notification_settings = %{ + "remote" => true, + "local" => true, + "followers" => true, + "follows" => true + } + + assert %{pleroma: %{notification_settings: ^notification_settings}} = + AccountView.render("account.json", %{user: user, for: user}) + end + test "Represent a Service(bot) account" do user = insert(:user, %{ diff --git a/test/web/twitter_api/util_controller_test.exs b/test/web/twitter_api/util_controller_test.exs index 410f20f87..01af8c1df 100644 --- a/test/web/twitter_api/util_controller_test.exs +++ b/test/web/twitter_api/util_controller_test.exs @@ -3,6 +3,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do alias Pleroma.Notification alias Pleroma.Repo + alias Pleroma.User alias Pleroma.Web.CommonAPI import Pleroma.Factory @@ -79,6 +80,26 @@ test "it marks a single notification as read", %{conn: conn} do end end + describe "PUT /api/pleroma/notification_settings" do + test "it updates notification settings", %{conn: conn} do + user = insert(:user) + + conn + |> assign(:user, user) + |> put("/api/pleroma/notification_settings", %{ + "remote" => false, + "followers" => false, + "bar" => 1 + }) + |> json_response(:ok) + + user = Repo.get(User, user.id) + + assert %{"remote" => false, "local" => true, "followers" => false, "follows" => true} == + user.info.notification_settings + end + end + describe "GET /api/statusnet/config.json" do test "returns the state of safe_dm_mentions flag", %{conn: conn} do option = Pleroma.Config.get([:instance, :safe_dm_mentions])