notification.ex 13.3 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
9

  alias Pleroma.Activity
  alias Pleroma.Notification
kaniini's avatar
kaniini committed
10
  alias Pleroma.Object
11
  alias Pleroma.Pagination
Haelwenn's avatar
Haelwenn committed
12
  alias Pleroma.Repo
13
  alias Pleroma.ThreadMute
14
15
  alias Pleroma.User
  alias Pleroma.Web.CommonAPI.Utils
Maksim's avatar
Maksim committed
16
17
  alias Pleroma.Web.Push
  alias Pleroma.Web.Streamer
Haelwenn's avatar
Haelwenn committed
18

19
  import Ecto.Query
20
  import Ecto.Changeset
21

22
  require Logger
23

24
25
  @type t :: %__MODULE__{}

26
27
  @include_muted_option :with_muted

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

    timestamps()
  end

36
37
38
39
40
  def changeset(%Notification{} = notification, attrs) do
    notification
    |> cast(attrs, [:seen])
  end

41
  defp for_user_query_ap_id_opts(user, opts) do
42
    ap_id_relationships =
43
44
45
      [:block] ++
        if opts[@include_muted_option], do: [], else: [:notification_mute]

46
    preloaded_ap_ids = User.outgoing_relationships_ap_ids(user, ap_id_relationships)
47
48
49
50
51
52
53
54
55
56
57
58
59

    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)

60
61
62
63
64
    Notification
    |> where(user_id: ^user.id)
    |> where(
      [n, a],
      fragment(
65
        "? not in (SELECT ap_id FROM users WHERE deactivated = 'true')",
66
67
68
69
70
71
        a.actor
      )
    )
    |> join(:inner, [n], activity in assoc(n, :activity))
    |> join(:left, [n, a], object in Object,
      on:
kaniini's avatar
kaniini committed
72
        fragment(
73
74
75
          "(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
          object.data,
          a.data
kaniini's avatar
kaniini committed
76
        )
77
78
    )
    |> preload([n, a, o], activity: {a, object: o})
79
80
    |> exclude_notification_muted(user, exclude_notification_muted_opts)
    |> exclude_blocked(user, exclude_blocked_opts)
81
82
83
    |> exclude_visibility(opts)
  end

84
85
  defp exclude_blocked(query, user, opts) do
    blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user)
86

87
    query
88
    |> where([n, a], a.actor not in ^blocked_ap_ids)
89
90
    |> where(
      [n, a],
91
      fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.domain_blocks
92
93
94
    )
  end

95
  defp exclude_notification_muted(query, _, %{@include_muted_option => true}) do
96
97
98
    query
  end

99
100
101
  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)
102

103
    query
104
    |> where([n, a], a.actor not in ^notification_muted_ap_ids)
105
    |> join(:left, [n, a], tm in ThreadMute,
106
107
108
109
110
111
      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]
112

113
114
115
  defp exclude_visibility(query, %{exclude_visibilities: visibility})
       when is_list(visibility) do
    if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
116
      query
117
118
119
120
121
122
123
124
      |> 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
      )
125
      |> where(
126
        [n, a, mutated_activity: mutated_activity],
127
        not fragment(
128
129
130
131
132
133
134
135
136
137
138
          """
          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,
139
140
141
142
143
          a.actor,
          a.recipients,
          a.data,
          ^visibility
        )
144
      )
145
146
147
    else
      Logger.error("Could not exclude visibility to #{visibility}")
      query
148
    end
lain's avatar
lain committed
149
  end
lain's avatar
lain committed
150

151
152
  defp exclude_visibility(query, %{exclude_visibilities: visibility})
       when visibility in @valid_visibilities do
153
    exclude_visibility(query, [visibility])
154
155
156
157
158
159
160
161
162
163
  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

164
  def for_user(user, opts \\ %{}) do
165
    user
166
    |> for_user_query(opts)
167
    |> Pagination.fetch_paginated(opts)
168
169
  end

170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
  @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

189
190
191
192
193
194
  def set_read_up_to(%{id: user_id} = _user, id) do
    query =
      from(
        n in Notification,
        where: n.user_id == ^user_id,
        where: n.id <= ^id,
195
        where: n.seen == false,
196
        update: [
197
198
199
200
          set: [
            seen: true,
            updated_at: ^NaiveDateTime.utc_now()
          ]
201
202
203
204
        ],
        # Ideally we would preload object and activities here
        # but Ecto does not support preloads in update_all
        select: n.id
205
206
      )

207
208
    {_, notification_ids} = Repo.update_all(query, [])

209
210
    Notification
    |> where([n], n.id in ^notification_ids)
211
212
213
214
215
216
217
218
219
220
221
    |> 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()
222
223
  end

224
225
226
227
228
229
230
231
  def read_one(%User{} = user, notification_id) do
    with {:ok, %Notification{} = notification} <- get(user, notification_id) do
      notification
      |> changeset(%{seen: true})
      |> Repo.update()
    end
  end

232
  def get(%{id: user_id} = _user, id) do
lain's avatar
lain committed
233
234
235
236
    query =
      from(
        n in Notification,
        where: n.id == ^id,
href's avatar
href committed
237
238
        join: activity in assoc(n, :activity),
        preload: [activity: activity]
lain's avatar
lain committed
239
      )
240
241

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

243
244
245
    case notification do
      %{user_id: ^user_id} ->
        {:ok, notification}
lain's avatar
lain committed
246

247
248
249
250
251
252
      _ ->
        {:error, "Cannot get notification"}
    end
  end

  def clear(user) do
Maxim Filippov's avatar
Maxim Filippov committed
253
254
    from(n in Notification, where: n.user_id == ^user.id)
    |> Repo.delete_all()
255
256
  end

257
258
259
260
261
262
263
264
  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

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

268
269
270
    case notification do
      %{user_id: ^user_id} ->
        Repo.delete(notification)
lain's avatar
lain committed
271

272
273
274
275
276
      _ ->
        {:error, "Cannot dismiss notification"}
    end
  end

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

280
    if object && object.data["type"] == "Answer" do
rinpatch's avatar
rinpatch committed
281
      {:ok, []}
282
283
    else
      do_create_notifications(activity)
rinpatch's avatar
rinpatch committed
284
    end
285
  end
lain's avatar
lain committed
286

287
288
289
290
291
292
293
294
295
  def create_notifications(%Activity{data: %{"type" => "Follow"}} = activity) do
    if Pleroma.Config.get([:notifications, :enable_follow_request_notifications]) ||
         Activity.follow_accepted?(activity) do
      do_create_notifications(activity)
    else
      {:ok, []}
    end
  end

296
  def create_notifications(%Activity{data: %{"type" => type}} = activity)
297
      when type in ["Like", "Announce", "Move", "EmojiReact"] do
298
299
300
301
302
303
304
305
306
    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

307
    notifications =
308
309
310
311
      Enum.map(potential_receivers, fn user ->
        do_send = user in enabled_receivers
        create_notification(activity, user, do_send)
      end)
312

313
314
315
    {:ok, notifications}
  end

316
  # TODO move to sql, too.
317
  def create_notification(%Activity{} = activity, %User{} = user, do_send \\ true) do
Eugenij's avatar
Eugenij committed
318
    unless skip?(activity, user) do
319
      notification = %Notification{user_id: user.id, activity: activity}
320
      {:ok, notification} = Repo.insert(notification)
321

322
323
324
325
      if do_send do
        Streamer.stream(["user", "user:notification"], notification)
        Push.send(notification)
      end
326

327
328
      notification
    end
329
  end
330

331
332
333
  @doc """
  Returns a tuple with 2 elements:
    {enabled notification receivers, currently disabled receivers (blocking / [thread] muting)}
334
335

  NOTE: might be called for FAKE Activities, see ActivityPub.Utils.get_notified_from_object/1
336
  """
337
338
  def get_notified_from_activity(activity, local_only \\ true)

339
  def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
lain's avatar
lain committed
340
      when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact"] do
341
342
343
344
345
346
347
348
    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()

349
    # Since even subscribers and followers can mute / thread-mute, filtering all above AP IDs
350
351
    notification_enabled_ap_ids =
      potential_receiver_ap_ids
352
      |> exclude_relationship_restricted_ap_ids(activity)
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
      |> 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)

    {notification_enabled_users, potential_receivers -- notification_enabled_users}
  end

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

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

371
372
  def exclude_relationship_restricted_ap_ids(ap_ids, %Activity{} = activity) do
    relationship_restricted_ap_ids =
373
374
      activity
      |> Activity.user_actor()
375
      |> User.incoming_relationships_ungrouped_ap_ids([
376
377
378
379
        :block,
        :notification_mute
      ])

380
    Enum.uniq(ap_ids) -- relationship_restricted_ap_ids
381
382
  end

383
384
385
386
387
388
389
390
  @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
391

392
  @spec skip?(Activity.t(), User.t()) :: boolean()
393
  def skip?(%Activity{} = activity, %User{} = user) do
394
395
396
397
398
399
400
401
    [
      :self,
      :followers,
      :follows,
      :non_followers,
      :non_follows,
      :recently_followed
    ]
402
    |> Enum.find(&skip?(&1, activity, user))
Eugenij's avatar
Eugenij committed
403
404
  end

405
406
  def skip?(_, _), do: false

407
  @spec skip?(atom(), Activity.t(), User.t()) :: boolean()
408
  def skip?(:self, %Activity{} = activity, %User{} = user) do
Eugenij's avatar
Eugenij committed
409
410
411
412
413
    activity.data["actor"] == user.ap_id
  end

  def skip?(
        :followers,
414
415
        %Activity{} = activity,
        %User{notification_settings: %{followers: false}} = user
Eugenij's avatar
Eugenij committed
416
417
418
419
420
421
      ) do
    actor = activity.data["actor"]
    follower = User.get_cached_by_ap_id(actor)
    User.following?(follower, user)
  end

422
423
  def skip?(
        :non_followers,
424
425
        %Activity{} = activity,
        %User{notification_settings: %{non_followers: false}} = user
426
427
428
429
430
431
      ) do
    actor = activity.data["actor"]
    follower = User.get_cached_by_ap_id(actor)
    !User.following?(follower, user)
  end

432
433
434
435
436
  def skip?(
        :follows,
        %Activity{} = activity,
        %User{notification_settings: %{follows: false}} = user
      ) do
Eugenij's avatar
Eugenij committed
437
    actor = activity.data["actor"]
minibikini's avatar
minibikini committed
438
    followed = User.get_cached_by_ap_id(actor)
Eugenij's avatar
Eugenij committed
439
440
441
    User.following?(user, followed)
  end

442
443
  def skip?(
        :non_follows,
444
445
        %Activity{} = activity,
        %User{notification_settings: %{non_follows: false}} = user
446
447
448
449
450
451
      ) do
    actor = activity.data["actor"]
    followed = User.get_cached_by_ap_id(actor)
    !User.following?(user, followed)
  end

452
453
  # 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
454
455
456
457
458
459
460
461
462
463
    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
464
end