notification.ex 19.2 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
    # This is an enum type in the database. If you add a new notification type,
34
    # remember to add a migration to add it to the `notifications_type` enum
35
    # as well.
36
    field(:type, :string)
37 38
    belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
    belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
39 40 41 42

    timestamps()
  end

43 44 45 46 47 48 49 50 51 52 53 54 55
  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

56 57 58 59 60 61 62 63
  @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

64 65 66 67 68 69 70 71 72 73 74
  @notification_types ~w{
    favourite
    follow
    follow_request
    mention
    move
    pleroma:chat_mention
    pleroma:emoji_reaction
    reblog
  }

75 76
  def changeset(%Notification{} = notification, attrs) do
    notification
77
    |> cast(attrs, [:seen, :type])
78
    |> validate_inclusion(:type, @notification_types)
79 80
  end

81 82 83 84 85 86 87 88 89 90 91
  @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

92
  defp for_user_query_ap_id_opts(user, opts) do
93
    ap_id_relationships =
94 95 96
      [:block] ++
        if opts[@include_muted_option], do: [], else: [:notification_mute]

97
    preloaded_ap_ids = User.outgoing_relationships_ap_ids(user, ap_id_relationships)
98 99 100 101 102 103 104 105 106 107 108 109 110

    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)

111 112 113 114 115
    Notification
    |> where(user_id: ^user.id)
    |> where(
      [n, a],
      fragment(
116
        "? not in (SELECT ap_id FROM users WHERE deactivated = 'true')",
117 118 119 120 121 122
        a.actor
      )
    )
    |> join(:inner, [n], activity in assoc(n, :activity))
    |> join(:left, [n, a], object in Object,
      on:
kaniini's avatar
kaniini committed
123
        fragment(
124
          "(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')",
125
          object.data,
126
          a.data,
127
          a.data
kaniini's avatar
kaniini committed
128
        )
129 130
    )
    |> preload([n, a, o], activity: {a, object: o})
131 132
    |> exclude_notification_muted(user, exclude_notification_muted_opts)
    |> exclude_blocked(user, exclude_blocked_opts)
133
    |> exclude_filtered(user)
134 135 136
    |> exclude_visibility(opts)
  end

137
  # Excludes blocked users and non-followed domain-blocked users
138 139
  defp exclude_blocked(query, user, opts) do
    blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user)
140

141
    query
142
    |> where([n, a], a.actor not in ^blocked_ap_ids)
143
    |> FollowingRelationship.keep_following_or_not_domain_blocked(user)
144 145
  end

146
  defp exclude_notification_muted(query, _, %{@include_muted_option => true}) do
147 148 149
    query
  end

150 151 152
  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)
153

154
    query
155
    |> where([n, a], a.actor not in ^notification_muted_ap_ids)
156
    |> join(:left, [n, a], tm in ThreadMute,
157 158 159 160 161
      on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data)
    )
    |> where([n, a, o, tm], is_nil(tm.user_id))
  end

162 163 164 165 166 167 168 169 170 171 172 173 174 175
  defp exclude_filtered(query, user) do
    case Pleroma.Filter.compose_regex(user) do
      nil ->
        query

      regex ->
        from([_n, a, o] in query,
          where:
            fragment("not(?->>'content' ~* ?)", o.data, ^regex) or
              fragment("?->>'actor' = ?", o.data, ^user.ap_id)
        )
    end
  end

176
  @valid_visibilities ~w[direct unlisted public private]
177

178 179 180
  defp exclude_visibility(query, %{exclude_visibilities: visibility})
       when is_list(visibility) do
    if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
181
      query
182 183
      |> join(:left, [n, a], mutated_activity in Pleroma.Activity,
        on:
184 185 186 187 188 189 190 191 192 193
          fragment(
            "COALESCE((?->'object')->>'id', ?->>'object')",
            a.data,
            a.data
          ) ==
            fragment(
              "COALESCE((?->'object')->>'id', ?->>'object')",
              mutated_activity.data,
              mutated_activity.data
            ) and
194 195 196 197
            fragment("(?->>'type' = 'Like' or ?->>'type' = 'Announce')", a.data, a.data) and
            fragment("?->>'type'", mutated_activity.data) == "Create",
        as: :mutated_activity
      )
198
      |> where(
199
        [n, a, mutated_activity: mutated_activity],
200
        not fragment(
201 202 203 204 205 206 207 208 209 210 211
          """
          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,
212 213 214 215 216
          a.actor,
          a.recipients,
          a.data,
          ^visibility
        )
217
      )
218 219 220
    else
      Logger.error("Could not exclude visibility to #{visibility}")
      query
221
    end
lain's avatar
lain committed
222
  end
lain's avatar
lain committed
223

224 225
  defp exclude_visibility(query, %{exclude_visibilities: visibility})
       when visibility in @valid_visibilities do
226
    exclude_visibility(query, [visibility])
227 228 229 230 231 232 233 234 235 236
  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

237
  def for_user(user, opts \\ %{}) do
238
    user
239
    |> for_user_query(opts)
240
    |> Pagination.fetch_paginated(opts)
241 242
  end

243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261
  @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

262
  def set_read_up_to(%{id: user_id} = user, id) do
263 264 265 266 267
    query =
      from(
        n in Notification,
        where: n.user_id == ^user_id,
        where: n.id <= ^id,
268 269 270 271
        where: n.seen == false,
        # Ideally we would preload object and activities here
        # but Ecto does not support preloads in update_all
        select: n.id
272 273
      )

274 275 276 277 278
    {: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()
279

280
    for_user_query(user)
281
    |> where([n], n.id in ^notification_ids)
282
    |> Repo.all()
283 284
  end

285 286
  @spec read_one(User.t(), String.t()) ::
          {:ok, Notification.t()} | {:error, Ecto.Changeset.t()} | nil
287 288
  def read_one(%User{} = user, notification_id) do
    with {:ok, %Notification{} = notification} <- get(user, notification_id) do
289 290 291 292 293 294 295 296
      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
297 298 299
    end
  end

300
  def get(%{id: user_id} = _user, id) do
lain's avatar
lain committed
301 302 303 304
    query =
      from(
        n in Notification,
        where: n.id == ^id,
href's avatar
href committed
305 306
        join: activity in assoc(n, :activity),
        preload: [activity: activity]
lain's avatar
lain committed
307
      )
308 309

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

311 312 313
    case notification do
      %{user_id: ^user_id} ->
        {:ok, notification}
lain's avatar
lain committed
314

315 316 317 318 319 320
      _ ->
        {:error, "Cannot get notification"}
    end
  end

  def clear(user) do
Maxim Filippov's avatar
Maxim Filippov committed
321 322
    from(n in Notification, where: n.user_id == ^user.id)
    |> Repo.delete_all()
323 324
  end

325 326 327 328 329 330 331 332
  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

333 334 335 336 337 338 339 340 341 342
  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

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

346 347 348
    case notification do
      %{user_id: ^user_id} ->
        Repo.delete(notification)
lain's avatar
lain committed
349

350 351 352 353 354
      _ ->
        {:error, "Cannot dismiss notification"}
    end
  end

355
  @spec create_notifications(Activity.t(), keyword()) :: {:ok, [Notification.t()] | []}
356 357 358
  def create_notifications(activity, options \\ [])

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

361
    if object && object.data["type"] == "Answer" do
rinpatch's avatar
rinpatch committed
362
      {:ok, []}
363
    else
364
      do_create_notifications(activity, options)
rinpatch's avatar
rinpatch committed
365
    end
366
  end
lain's avatar
lain committed
367

368
  def create_notifications(%Activity{data: %{"type" => type}} = activity, options)
369
      when type in ["Follow", "Like", "Announce", "Move", "EmojiReact"] do
370
    do_create_notifications(activity, options)
371 372
  end

373 374 375 376
  def create_notifications(_, _), do: {:ok, []}

  defp do_create_notifications(%Activity{} = activity, options) do
    do_send = Keyword.get(options, :do_send, true)
377 378 379 380

    {enabled_receivers, disabled_receivers} = get_notified_from_activity(activity)
    potential_receivers = enabled_receivers ++ disabled_receivers

381
    notifications =
382
      Enum.map(potential_receivers, fn user ->
383
        do_send = do_send && user in enabled_receivers
384 385
        create_notification(activity, user, do_send)
      end)
386
      |> Enum.reject(&is_nil/1)
387

388 389 390
    {:ok, notifications}
  end

391
  defp type_from_activity(%{data: %{"type" => type}} = activity) do
392 393
    case type do
      "Follow" ->
394
        if Activity.follow_accepted?(activity) do
395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411
          "follow"
        else
          "follow_request"
        end

      "Announce" ->
        "reblog"

      "Like" ->
        "favourite"

      "Move" ->
        "move"

      "EmojiReact" ->
        "pleroma:emoji_reaction"

412 413 414 415
      # Compatibility with old reactions
      "EmojiReaction" ->
        "pleroma:emoji_reaction"

416 417 418 419 420 421 422 423 424
      "Create" ->
        activity
        |> type_from_activity_object()

      t ->
        raise "No notification type for activity type #{t}"
    end
  end

425 426
  defp type_from_activity_object(%{data: %{"type" => "Create", "object" => %{}}}), do: "mention"

427
  defp type_from_activity_object(%{data: %{"type" => "Create"}} = activity) do
428
    object = Object.get_by_ap_id(activity.data["object"])
429

430
    case object && object.data["type"] do
431 432 433 434 435
      "ChatMessage" -> "pleroma:chat_mention"
      _ -> "mention"
    end
  end

436
  # TODO move to sql, too.
437
  def create_notification(%Activity{} = activity, %User{} = user, do_send \\ true) do
Eugenij's avatar
Eugenij committed
438
    unless skip?(activity, user) do
439 440
      {:ok, %{notification: notification}} =
        Multi.new()
441 442 443 444 445
        |> Multi.insert(:notification, %Notification{
          user_id: user.id,
          activity: activity,
          type: type_from_activity(activity)
        })
446 447
        |> Marker.multi_set_last_read_id(user, "notifications")
        |> Repo.transaction()
448

449 450 451 452
      if do_send do
        Streamer.stream(["user", "user:notification"], notification)
        Push.send(notification)
      end
453

454 455
      notification
    end
456
  end
457

458 459
  @doc """
  Returns a tuple with 2 elements:
460
    {notification-enabled receivers, currently disabled receivers (blocking / [thread] muting)}
461 462

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

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

471 472
    potential_receivers =
      User.get_users_from_set(potential_receiver_ap_ids, local_only: local_only)
473

474 475
    notification_enabled_ap_ids =
      potential_receiver_ap_ids
476
      |> exclude_domain_blocker_ap_ids(activity, potential_receivers)
477
      |> exclude_relationship_restricted_ap_ids(activity)
478 479 480 481 482 483 484 485 486 487
      |> 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}
  end

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

488
  # For some activities, only notify the author of the object
489 490 491 492 493 494 495 496 497 498 499
  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

500 501 502 503
  def get_potential_receiver_ap_ids(%{data: %{"type" => "Follow", "object" => object_id}}) do
    [object_id]
  end

504 505 506 507 508 509 510 511 512
  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

513
  @doc "Filters out AP IDs domain-blocking and not following the activity's actor"
514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544
  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

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

548 549
  def exclude_relationship_restricted_ap_ids(ap_ids, %Activity{} = activity) do
    relationship_restricted_ap_ids =
550 551
      activity
      |> Activity.user_actor()
552
      |> User.incoming_relationships_ungrouped_ap_ids([
553 554 555 556
        :block,
        :notification_mute
      ])

557
    Enum.uniq(ap_ids) -- relationship_restricted_ap_ids
558 559
  end

560 561 562 563 564 565 566 567
  @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
568

569
  @spec skip?(Activity.t(), User.t()) :: boolean()
570
  def skip?(%Activity{} = activity, %User{} = user) do
571 572
    [
      :self,
573
      :invisible,
574 575 576 577
      :followers,
      :follows,
      :non_followers,
      :non_follows,
578 579
      :recently_followed,
      :filtered
580
    ]
581
    |> Enum.find(&skip?(&1, activity, user))
Eugenij's avatar
Eugenij committed
582 583
  end

584 585
  def skip?(_, _), do: false

586
  @spec skip?(atom(), Activity.t(), User.t()) :: boolean()
587
  def skip?(:self, %Activity{} = activity, %User{} = user) do
Eugenij's avatar
Eugenij committed
588 589 590
    activity.data["actor"] == user.ap_id
  end

591 592 593 594 595 596
  def skip?(:invisible, %Activity{} = activity, _) do
    actor = activity.data["actor"]
    user = User.get_cached_by_ap_id(actor)
    User.invisible?(user)
  end

Eugenij's avatar
Eugenij committed
597 598
  def skip?(
        :followers,
599 600
        %Activity{} = activity,
        %User{notification_settings: %{followers: false}} = user
Eugenij's avatar
Eugenij committed
601 602 603 604 605 606
      ) do
    actor = activity.data["actor"]
    follower = User.get_cached_by_ap_id(actor)
    User.following?(follower, user)
  end

607 608
  def skip?(
        :non_followers,
609 610
        %Activity{} = activity,
        %User{notification_settings: %{non_followers: false}} = user
611 612 613 614 615 616
      ) do
    actor = activity.data["actor"]
    follower = User.get_cached_by_ap_id(actor)
    !User.following?(follower, user)
  end

617 618 619 620 621
  def skip?(
        :follows,
        %Activity{} = activity,
        %User{notification_settings: %{follows: false}} = user
      ) do
Eugenij's avatar
Eugenij committed
622
    actor = activity.data["actor"]
623
    followed = User.get_cached_by_ap_id(actor)
Eugenij's avatar
Eugenij committed
624 625 626
    User.following?(user, followed)
  end

627 628
  def skip?(
        :non_follows,
629 630
        %Activity{} = activity,
        %User{notification_settings: %{non_follows: false}} = user
631 632 633 634 635 636
      ) do
    actor = activity.data["actor"]
    followed = User.get_cached_by_ap_id(actor)
    !User.following?(user, followed)
  end

637 638
  # 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
639 640 641 642 643 644 645 646 647
    actor = activity.data["actor"]

    Notification.for_user(user)
    |> Enum.any?(fn
      %{activity: %{data: %{"type" => "Follow", "actor" => ^actor}}} -> true
      _ -> false
    end)
  end

648 649
  def skip?(:filtered, %{data: %{"type" => type}}, _) when type in ["Follow", "Move"], do: false

650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667
  def skip?(:filtered, activity, user) do
    object = Object.normalize(activity)

    cond do
      is_nil(object) ->
        false

      object.data["actor"] == user.ap_id ->
        false

      not is_nil(regex = Pleroma.Filter.compose_regex(user, :re)) ->
        Regex.match?(regex, object.data["content"])

      true ->
        false
    end
  end

Eugenij's avatar
Eugenij committed
668
  def skip?(_, _, _), do: false
669 670 671 672 673 674 675 676

  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
677
end