X-Git-Url: http://git.squeep.com/?a=blobdiff_plain;f=lib%2Fpleroma%2Fnotification.ex;h=682a26912a5a7fe04d5af08ee256de2ec089d0d4;hb=9189b489eef29be723389e1b3642a843bc0d01bc;hp=3084bac3b47f2ac420b85bd8a0cd6bd89fbb6f04;hpb=4b3b1fec4e57bd07ac75700bf34cd188ce43b545;p=akkoma diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 3084bac3b..682a26912 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Notification do alias Ecto.Multi alias Pleroma.Activity + alias Pleroma.FollowingRelationship alias Pleroma.Marker alias Pleroma.Notification alias Pleroma.Object @@ -29,15 +30,40 @@ defmodule Pleroma.Notification do schema "notifications" do field(:seen, :boolean, default: false) + # This is an enum type in the database. If you add a new notification type, + # remembert to add a migration to add it to the `notifications_type` enum + # as well. + field(:type, :string) belongs_to(:user, User, type: FlakeId.Ecto.CompatType) belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType) timestamps() end + def update_notification_type(user, activity) do + with %__MODULE__{} = notification <- + Repo.get_by(__MODULE__, user_id: user.id, activity_id: activity.id) do + type = + activity + |> type_from_activity() + + notification + |> changeset(%{type: type}) + |> Repo.update() + end + end + + @spec unread_notifications_count(User.t()) :: integer() + def unread_notifications_count(%User{id: user_id}) do + from(q in __MODULE__, + where: q.user_id == ^user_id and q.seen == false + ) + |> Repo.aggregate(:count, :id) + end + def changeset(%Notification{} = notification, attrs) do notification - |> cast(attrs, [:seen]) + |> cast(attrs, [:seen, :type]) end @spec last_read_query(User.t()) :: Ecto.Queryable.t() @@ -83,8 +109,9 @@ defmodule Pleroma.Notification do |> join(:left, [n, a], object in Object, on: fragment( - "(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)", + "(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')", object.data, + a.data, a.data ) ) @@ -94,15 +121,13 @@ defmodule Pleroma.Notification do |> exclude_visibility(opts) end + # Excludes blocked users and non-followed domain-blocked users defp exclude_blocked(query, user, opts) do blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user) query |> where([n, a], a.actor not in ^blocked_ap_ids) - |> where( - [n, a], - fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.domain_blocks - ) + |> FollowingRelationship.keep_following_or_not_domain_blocked(user) end defp exclude_notification_muted(query, _, %{@include_muted_option => true}) do @@ -217,18 +242,8 @@ defmodule Pleroma.Notification do |> Marker.multi_set_last_read_id(user, "notifications") |> Repo.transaction() - Notification + for_user_query(user) |> where([n], n.id in ^notification_ids) - |> join(:inner, [n], activity in assoc(n, :activity)) - |> join(:left, [n, a], object in Object, - on: - fragment( - "(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)", - object.data, - a.data - ) - ) - |> preload([n, a, o], activity: {a, object: o}) |> Repo.all() end @@ -280,6 +295,16 @@ defmodule Pleroma.Notification do |> Repo.delete_all() end + def dismiss(%Pleroma.Activity{} = activity) do + Notification + |> where([n], n.activity_id == ^activity.id) + |> Repo.delete_all() + |> case do + {_, notifications} -> {:ok, notifications} + _ -> {:error, "Cannot dismiss notification"} + end + end + def dismiss(%{id: user_id} = _user, id) do notification = Repo.get(Notification, id) @@ -292,42 +317,95 @@ defmodule Pleroma.Notification do end end - def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity) do - object = Object.normalize(activity) + def create_notifications(activity, options \\ []) + + def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity, options) do + object = Object.normalize(activity, false) if object && object.data["type"] == "Answer" do {:ok, []} else - do_create_notifications(activity) + do_create_notifications(activity, options) end end - def create_notifications(%Activity{data: %{"type" => type}} = activity) - when type in ["Like", "Announce", "Follow", "Move", "EmojiReact"] do - do_create_notifications(activity) + def create_notifications(%Activity{data: %{"type" => type}} = activity, options) + when type in ["Follow", "Like", "Announce", "Move", "EmojiReact"] do + do_create_notifications(activity, options) end - def create_notifications(_), do: {:ok, []} + def create_notifications(_, _), do: {:ok, []} + + defp do_create_notifications(%Activity{} = activity, options) do + do_send = Keyword.get(options, :do_send, true) - defp do_create_notifications(%Activity{} = activity) do {enabled_receivers, disabled_receivers} = get_notified_from_activity(activity) potential_receivers = enabled_receivers ++ disabled_receivers notifications = Enum.map(potential_receivers, fn user -> - do_send = user in enabled_receivers + do_send = do_send && user in enabled_receivers create_notification(activity, user, do_send) end) {:ok, notifications} end + defp type_from_activity(%{data: %{"type" => type}} = activity) do + case type do + "Follow" -> + if Activity.follow_accepted?(activity) do + "follow" + else + "follow_request" + end + + "Announce" -> + "reblog" + + "Like" -> + "favourite" + + "Move" -> + "move" + + "EmojiReact" -> + "pleroma:emoji_reaction" + + # Compatibility with old reactions + "EmojiReaction" -> + "pleroma:emoji_reaction" + + "Create" -> + activity + |> type_from_activity_object() + + t -> + raise "No notification type for activity type #{t}" + end + end + + defp type_from_activity_object(%{data: %{"type" => "Create", "object" => %{}}}), do: "mention" + + defp type_from_activity_object(%{data: %{"type" => "Create"}} = activity) do + object = Object.get_by_ap_id(activity.data["object"]) + + case object && object.data["type"] do + "ChatMessage" -> "pleroma:chat_mention" + _ -> "mention" + end + end + # TODO move to sql, too. def create_notification(%Activity{} = activity, %User{} = user, do_send \\ true) do unless skip?(activity, user) do {:ok, %{notification: notification}} = Multi.new() - |> Multi.insert(:notification, %Notification{user_id: user.id, activity: activity}) + |> Multi.insert(:notification, %Notification{ + user_id: user.id, + activity: activity, + type: type_from_activity(activity) + }) |> Marker.multi_set_last_read_id(user, "notifications") |> Repo.transaction() @@ -342,33 +420,26 @@ defmodule Pleroma.Notification do @doc """ Returns a tuple with 2 elements: - {enabled notification receivers, currently disabled receivers (blocking / [thread] muting)} + {notification-enabled receivers, currently disabled receivers (blocking / [thread] muting)} NOTE: might be called for FAKE Activities, see ActivityPub.Utils.get_notified_from_object/1 """ + @spec get_notified_from_activity(Activity.t(), boolean()) :: {list(User.t()), list(User.t())} def get_notified_from_activity(activity, local_only \\ true) def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only) when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact"] do - potential_receiver_ap_ids = - [] - |> Utils.maybe_notify_to_recipients(activity) - |> Utils.maybe_notify_mentioned_recipients(activity) - |> Utils.maybe_notify_subscribers(activity) - |> Utils.maybe_notify_followers(activity) - |> Enum.uniq() + potential_receiver_ap_ids = get_potential_receiver_ap_ids(activity) + + potential_receivers = + User.get_users_from_set(potential_receiver_ap_ids, local_only: local_only) - # Since even subscribers and followers can mute / thread-mute, filtering all above AP IDs notification_enabled_ap_ids = potential_receiver_ap_ids + |> exclude_domain_blocker_ap_ids(activity, potential_receivers) |> exclude_relationship_restricted_ap_ids(activity) |> exclude_thread_muter_ap_ids(activity) - potential_receivers = - potential_receiver_ap_ids - |> Enum.uniq() - |> User.get_users_from_set(local_only) - notification_enabled_users = Enum.filter(potential_receivers, fn u -> u.ap_id in notification_enabled_ap_ids end) @@ -377,6 +448,59 @@ defmodule Pleroma.Notification do def get_notified_from_activity(_, _local_only), do: {[], []} + # For some activities, only notify the author of the object + def get_potential_receiver_ap_ids(%{data: %{"type" => type, "object" => object_id}}) + when type in ~w{Like Announce EmojiReact} do + case Object.get_cached_by_ap_id(object_id) do + %Object{data: %{"actor" => actor}} -> + [actor] + + _ -> + [] + end + end + + def get_potential_receiver_ap_ids(activity) do + [] + |> Utils.maybe_notify_to_recipients(activity) + |> Utils.maybe_notify_mentioned_recipients(activity) + |> Utils.maybe_notify_subscribers(activity) + |> Utils.maybe_notify_followers(activity) + |> Enum.uniq() + end + + @doc "Filters out AP IDs domain-blocking and not following the activity's actor" + def exclude_domain_blocker_ap_ids(ap_ids, activity, preloaded_users \\ []) + + def exclude_domain_blocker_ap_ids([], _activity, _preloaded_users), do: [] + + def exclude_domain_blocker_ap_ids(ap_ids, %Activity{} = activity, preloaded_users) do + activity_actor_domain = activity.actor && URI.parse(activity.actor).host + + users = + ap_ids + |> Enum.map(fn ap_id -> + Enum.find(preloaded_users, &(&1.ap_id == ap_id)) || + User.get_cached_by_ap_id(ap_id) + end) + |> Enum.filter(& &1) + + domain_blocker_ap_ids = for u <- users, activity_actor_domain in u.domain_blocks, do: u.ap_id + + domain_blocker_follower_ap_ids = + if Enum.any?(domain_blocker_ap_ids) do + activity + |> Activity.user_actor() + |> FollowingRelationship.followers_ap_ids(domain_blocker_ap_ids) + else + [] + end + + ap_ids + |> Kernel.--(domain_blocker_ap_ids) + |> Kernel.++(domain_blocker_follower_ap_ids) + end + @doc "Filters out AP IDs of users basing on their relationships with activity actor user" def exclude_relationship_restricted_ap_ids([], _activity), do: [] @@ -473,4 +597,12 @@ defmodule Pleroma.Notification do end def skip?(_, _, _), do: false + + def for_user_and_activity(user, activity) do + from(n in __MODULE__, + where: n.user_id == ^user.id, + where: n.activity_id == ^activity.id + ) + |> Repo.one() + end end