notification.ex 15.6 KB
Newer Older
1
# Pleroma: A lightweight social networking server
2
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 4
# SPDX-License-Identifier: AGPL-3.0-only

5 6
defmodule Pleroma.Notification do
  use Ecto.Schema
Haelwenn's avatar
Haelwenn committed
7

8
  alias Ecto.Multi
Haelwenn's avatar
Haelwenn committed
9
  alias Pleroma.Activity
10
  alias Pleroma.FollowingRelationship
11
  alias Pleroma.Marker
Haelwenn's avatar
Haelwenn committed
12
  alias Pleroma.Notification
13
  alias Pleroma.Object
14
  alias Pleroma.Pagination
Haelwenn's avatar
Haelwenn committed
15
  alias Pleroma.Repo
16
  alias Pleroma.ThreadMute
17 18
  alias Pleroma.User
  alias Pleroma.Web.CommonAPI.Utils
Maksim's avatar
Maksim committed
19 20
  alias Pleroma.Web.Push
  alias Pleroma.Web.Streamer
Haelwenn's avatar
Haelwenn committed
21

22
  import Ecto.Query
23
  import Ecto.Changeset
24

25
  require Logger
26

27 28
  @type t :: %__MODULE__{}

29 30
  @include_muted_option :with_muted

31
  schema "notifications" do
lain's avatar
lain committed
32
    field(:seen, :boolean, default: false)
33 34
    belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
    belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
35 36 37 38

    timestamps()
  end

39 40 41 42 43 44 45 46
  @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

47 48 49 50 51
  def changeset(%Notification{} = notification, attrs) do
    notification
    |> cast(attrs, [:seen])
  end

52 53 54 55 56 57 58 59 60 61 62
  @spec last_read_query(User.t()) :: Ecto.Queryable.t()
  def last_read_query(user) do
    from(q in Pleroma.Notification,
      where: q.user_id == ^user.id,
      where: q.seen == true,
      select: type(q.id, :string),
      limit: 1,
      order_by: [desc: :id]
    )
  end

63
  defp for_user_query_ap_id_opts(user, opts) do
64
    ap_id_relationships =
65 66 67
      [:block] ++
        if opts[@include_muted_option], do: [], else: [:notification_mute]

68
    preloaded_ap_ids = User.outgoing_relationships_ap_ids(user, ap_id_relationships)
69 70 71 72 73 74 75 76 77 78 79 80 81

    exclude_blocked_opts = Map.merge(%{blocked_users_ap_ids: preloaded_ap_ids[:block]}, opts)

    exclude_notification_muted_opts =
      Map.merge(%{notification_muted_users_ap_ids: preloaded_ap_ids[:notification_mute]}, opts)

    {exclude_blocked_opts, exclude_notification_muted_opts}
  end

  def for_user_query(user, opts \\ %{}) do
    {exclude_blocked_opts, exclude_notification_muted_opts} =
      for_user_query_ap_id_opts(user, opts)

82 83 84 85 86
    Notification
    |> where(user_id: ^user.id)
    |> where(
      [n, a],
      fragment(
87
        "? not in (SELECT ap_id FROM users WHERE deactivated = 'true')",
88 89 90 91 92 93
        a.actor
      )
    )
    |> join(:inner, [n], activity in assoc(n, :activity))
    |> join(:left, [n, a], object in Object,
      on:
William Pitcock's avatar
William Pitcock committed
94
        fragment(
lain's avatar
lain committed
95
          "(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')",
96
          object.data,
lain's avatar
lain committed
97
          a.data,
98
          a.data
William Pitcock's avatar
William Pitcock committed
99
        )
100 101
    )
    |> preload([n, a, o], activity: {a, object: o})
102 103
    |> exclude_notification_muted(user, exclude_notification_muted_opts)
    |> exclude_blocked(user, exclude_blocked_opts)
104 105 106
    |> exclude_visibility(opts)
  end

107
  # Excludes blocked users and non-followed domain-blocked users
108 109
  defp exclude_blocked(query, user, opts) do
    blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user)
110

111
    query
112
    |> where([n, a], a.actor not in ^blocked_ap_ids)
113
    |> FollowingRelationship.keep_following_or_not_domain_blocked(user)
114 115
  end

116
  defp exclude_notification_muted(query, _, %{@include_muted_option => true}) do
117 118 119
    query
  end

120 121 122
  defp exclude_notification_muted(query, user, opts) do
    notification_muted_ap_ids =
      opts[:notification_muted_users_ap_ids] || User.notification_muted_users_ap_ids(user)
123

124
    query
125
    |> where([n, a], a.actor not in ^notification_muted_ap_ids)
126
    |> join(:left, [n, a], tm in ThreadMute,
127 128 129 130 131 132
      on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data)
    )
    |> where([n, a, o, tm], is_nil(tm.user_id))
  end

  @valid_visibilities ~w[direct unlisted public private]
133

134 135 136
  defp exclude_visibility(query, %{exclude_visibilities: visibility})
       when is_list(visibility) do
    if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
137
      query
138 139 140 141 142 143 144 145
      |> join(:left, [n, a], mutated_activity in Pleroma.Activity,
        on:
          fragment("?->>'context'", a.data) ==
            fragment("?->>'context'", mutated_activity.data) and
            fragment("(?->>'type' = 'Like' or ?->>'type' = 'Announce')", a.data, a.data) and
            fragment("?->>'type'", mutated_activity.data) == "Create",
        as: :mutated_activity
      )
146
      |> where(
147
        [n, a, mutated_activity: mutated_activity],
148
        not fragment(
149 150 151 152 153 154 155 156 157 158 159
          """
          CASE WHEN (?->>'type') = 'Like' or (?->>'type') = 'Announce'
            THEN (activity_visibility(?, ?, ?) = ANY (?))
            ELSE (activity_visibility(?, ?, ?) = ANY (?)) END
          """,
          a.data,
          a.data,
          mutated_activity.actor,
          mutated_activity.recipients,
          mutated_activity.data,
          ^visibility,
160 161 162 163 164
          a.actor,
          a.recipients,
          a.data,
          ^visibility
        )
165
      )
166 167 168
    else
      Logger.error("Could not exclude visibility to #{visibility}")
      query
169
    end
lain's avatar
lain committed
170
  end
lain's avatar
lain committed
171

172 173
  defp exclude_visibility(query, %{exclude_visibilities: visibility})
       when visibility in @valid_visibilities do
174
    exclude_visibility(query, [visibility])
175 176 177 178 179 180 181 182 183 184
  end

  defp exclude_visibility(query, %{exclude_visibilities: visibility})
       when visibility not in @valid_visibilities do
    Logger.error("Could not exclude visibility to #{visibility}")
    query
  end

  defp exclude_visibility(query, _visibility), do: query

185
  def for_user(user, opts \\ %{}) do
186
    user
187
    |> for_user_query(opts)
188
    |> Pagination.fetch_paginated(opts)
189 190
  end

191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209
  @doc """
  Returns notifications for user received since given date.

  ## Examples

      iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-13 11:22:33])
      [%Pleroma.Notification{}, %Pleroma.Notification{}]

      iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-15 11:22:33])
      []
  """
  @spec for_user_since(Pleroma.User.t(), NaiveDateTime.t()) :: [t()]
  def for_user_since(user, date) do
    from(n in for_user_query(user),
      where: n.updated_at > ^date
    )
    |> Repo.all()
  end

210
  def set_read_up_to(%{id: user_id} = user, id) do
211 212 213 214 215
    query =
      from(
        n in Notification,
        where: n.user_id == ^user_id,
        where: n.id <= ^id,
216 217 218 219
        where: n.seen == false,
        # Ideally we would preload object and activities here
        # but Ecto does not support preloads in update_all
        select: n.id
220 221
      )

222 223 224 225 226
    {:ok, %{ids: {_, notification_ids}}} =
      Multi.new()
      |> Multi.update_all(:ids, query, set: [seen: true, updated_at: NaiveDateTime.utc_now()])
      |> Marker.multi_set_last_read_id(user, "notifications")
      |> Repo.transaction()
227

lain's avatar
lain committed
228
    for_user_query(user)
229
    |> where([n], n.id in ^notification_ids)
230
    |> Repo.all()
231 232
  end

233 234
  @spec read_one(User.t(), String.t()) ::
          {:ok, Notification.t()} | {:error, Ecto.Changeset.t()} | nil
235 236
  def read_one(%User{} = user, notification_id) do
    with {:ok, %Notification{} = notification} <- get(user, notification_id) do
237 238 239 240 241 242 243 244
      Multi.new()
      |> Multi.update(:update, changeset(notification, %{seen: true}))
      |> Marker.multi_set_last_read_id(user, "notifications")
      |> Repo.transaction()
      |> case do
        {:ok, %{update: notification}} -> {:ok, notification}
        {:error, :update, changeset, _} -> {:error, changeset}
      end
245 246 247
    end
  end

248
  def get(%{id: user_id} = _user, id) do
lain's avatar
lain committed
249 250 251 252
    query =
      from(
        n in Notification,
        where: n.id == ^id,
href's avatar
href committed
253 254
        join: activity in assoc(n, :activity),
        preload: [activity: activity]
lain's avatar
lain committed
255
      )
256 257

    notification = Repo.one(query)
lain's avatar
lain committed
258

259 260 261
    case notification do
      %{user_id: ^user_id} ->
        {:ok, notification}
lain's avatar
lain committed
262

263 264 265 266 267 268
      _ ->
        {:error, "Cannot get notification"}
    end
  end

  def clear(user) do
Maxim Filippov's avatar
Maxim Filippov committed
269 270
    from(n in Notification, where: n.user_id == ^user.id)
    |> Repo.delete_all()
271 272
  end

273 274 275 276 277 278 279 280
  def destroy_multiple(%{id: user_id} = _user, ids) do
    from(n in Notification,
      where: n.id in ^ids,
      where: n.user_id == ^user_id
    )
    |> Repo.delete_all()
  end

281 282 283 284 285 286 287 288 289 290
  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

291 292
  def dismiss(%{id: user_id} = _user, id) do
    notification = Repo.get(Notification, id)
lain's avatar
lain committed
293

294 295 296
    case notification do
      %{user_id: ^user_id} ->
        Repo.delete(notification)
lain's avatar
lain committed
297

298 299 300 301 302
      _ ->
        {:error, "Cannot dismiss notification"}
    end
  end

303
  def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity) do
rinpatch's avatar
rinpatch committed
304 305
    object = Object.normalize(activity)

306
    if object && object.data["type"] == "Answer" do
rinpatch's avatar
rinpatch committed
307
      {:ok, []}
308 309
    else
      do_create_notifications(activity)
rinpatch's avatar
rinpatch committed
310
    end
311
  end
lain's avatar
lain committed
312

313
  def create_notifications(%Activity{data: %{"type" => type}} = activity)
314
      when type in ["Follow", "Like", "Announce", "Move", "EmojiReact"] do
315 316 317 318 319 320 321 322 323
    do_create_notifications(activity)
  end

  def create_notifications(_), do: {:ok, []}

  defp do_create_notifications(%Activity{} = activity) do
    {enabled_receivers, disabled_receivers} = get_notified_from_activity(activity)
    potential_receivers = enabled_receivers ++ disabled_receivers

324
    notifications =
325 326 327 328
      Enum.map(potential_receivers, fn user ->
        do_send = user in enabled_receivers
        create_notification(activity, user, do_send)
      end)
329

330 331 332
    {:ok, notifications}
  end

333
  # TODO move to sql, too.
334
  def create_notification(%Activity{} = activity, %User{} = user, do_send \\ true) do
Eugenij's avatar
Eugenij committed
335
    unless skip?(activity, user) do
336 337 338 339 340
      {:ok, %{notification: notification}} =
        Multi.new()
        |> Multi.insert(:notification, %Notification{user_id: user.id, activity: activity})
        |> Marker.multi_set_last_read_id(user, "notifications")
        |> Repo.transaction()
341

342 343 344 345
      if do_send do
        Streamer.stream(["user", "user:notification"], notification)
        Push.send(notification)
      end
346

347 348
      notification
    end
349
  end
350

351 352
  @doc """
  Returns a tuple with 2 elements:
353
    {notification-enabled receivers, currently disabled receivers (blocking / [thread] muting)}
354 355

  NOTE: might be called for FAKE Activities, see ActivityPub.Utils.get_notified_from_object/1
356
  """
357
  @spec get_notified_from_activity(Activity.t(), boolean()) :: {list(User.t()), list(User.t())}
358 359
  def get_notified_from_activity(activity, local_only \\ true)

360
  def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
lain's avatar
lain committed
361
      when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact"] do
362
    potential_receiver_ap_ids = get_potential_receiver_ap_ids(activity)
363

364 365
    potential_receivers =
      User.get_users_from_set(potential_receiver_ap_ids, local_only: local_only)
366

367 368
    notification_enabled_ap_ids =
      potential_receiver_ap_ids
369
      |> exclude_domain_blocker_ap_ids(activity, potential_receivers)
370
      |> exclude_relationship_restricted_ap_ids(activity)
371 372 373 374 375 376
      |> exclude_thread_muter_ap_ids(activity)

    notification_enabled_users =
      Enum.filter(potential_receivers, fn u -> u.ap_id in notification_enabled_ap_ids end)

    {notification_enabled_users, potential_receivers -- notification_enabled_users}
377 378
  end

379 380
  def get_notified_from_activity(_, _local_only), do: {[], []}

381
  # For some activities, only notify the author of the object
382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401
  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

402
  @doc "Filters out AP IDs domain-blocking and not following the activity's actor"
403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433
  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

434
  @doc "Filters out AP IDs of users basing on their relationships with activity actor user"
435
  def exclude_relationship_restricted_ap_ids([], _activity), do: []
436

437 438
  def exclude_relationship_restricted_ap_ids(ap_ids, %Activity{} = activity) do
    relationship_restricted_ap_ids =
439 440
      activity
      |> Activity.user_actor()
441
      |> User.incoming_relationships_ungrouped_ap_ids([
442 443 444 445
        :block,
        :notification_mute
      ])

446
    Enum.uniq(ap_ids) -- relationship_restricted_ap_ids
447 448
  end

449 450 451 452 453 454 455 456
  @doc "Filters out AP IDs of users who mute activity thread"
  def exclude_thread_muter_ap_ids([], _activity), do: []

  def exclude_thread_muter_ap_ids(ap_ids, %Activity{} = activity) do
    thread_muter_ap_ids = ThreadMute.muter_ap_ids(activity.data["context"])

    Enum.uniq(ap_ids) -- thread_muter_ap_ids
  end
Eugenij's avatar
Eugenij committed
457

458
  @spec skip?(Activity.t(), User.t()) :: boolean()
459
  def skip?(%Activity{} = activity, %User{} = user) do
460 461 462 463 464 465 466 467
    [
      :self,
      :followers,
      :follows,
      :non_followers,
      :non_follows,
      :recently_followed
    ]
468
    |> Enum.find(&skip?(&1, activity, user))
Eugenij's avatar
Eugenij committed
469 470
  end

471 472
  def skip?(_, _), do: false

473
  @spec skip?(atom(), Activity.t(), User.t()) :: boolean()
474
  def skip?(:self, %Activity{} = activity, %User{} = user) do
Eugenij's avatar
Eugenij committed
475 476 477 478 479
    activity.data["actor"] == user.ap_id
  end

  def skip?(
        :followers,
480 481
        %Activity{} = activity,
        %User{notification_settings: %{followers: false}} = user
Eugenij's avatar
Eugenij committed
482 483 484 485 486 487
      ) do
    actor = activity.data["actor"]
    follower = User.get_cached_by_ap_id(actor)
    User.following?(follower, user)
  end

488 489
  def skip?(
        :non_followers,
490 491
        %Activity{} = activity,
        %User{notification_settings: %{non_followers: false}} = user
492 493 494 495 496 497
      ) do
    actor = activity.data["actor"]
    follower = User.get_cached_by_ap_id(actor)
    !User.following?(follower, user)
  end

498 499 500 501 502
  def skip?(
        :follows,
        %Activity{} = activity,
        %User{notification_settings: %{follows: false}} = user
      ) do
Eugenij's avatar
Eugenij committed
503
    actor = activity.data["actor"]
minibikini's avatar
minibikini committed
504
    followed = User.get_cached_by_ap_id(actor)
Eugenij's avatar
Eugenij committed
505 506 507
    User.following?(user, followed)
  end

508 509
  def skip?(
        :non_follows,
510 511
        %Activity{} = activity,
        %User{notification_settings: %{non_follows: false}} = user
512 513 514 515 516 517
      ) do
    actor = activity.data["actor"]
    followed = User.get_cached_by_ap_id(actor)
    !User.following?(user, followed)
  end

518 519
  # To do: consider defining recency in hours and checking FollowingRelationship with a single SQL
  def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity, %User{} = user) do
Eugenij's avatar
Eugenij committed
520 521 522 523 524 525 526 527 528 529
    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
530
end