notification.ex 8.02 KB
Newer Older
1
# Pleroma: A lightweight social networking server
kaniini's avatar
kaniini committed
2
# Copyright © 2017-2019 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
14
  alias Pleroma.User
  alias Pleroma.Web.CommonAPI.Utils
Maksim's avatar
Maksim committed
15
16
  alias Pleroma.Web.Push
  alias Pleroma.Web.Streamer
Haelwenn's avatar
Haelwenn committed
17

18
  import Ecto.Query
19
  import Ecto.Changeset
20

21
22
  @type t :: %__MODULE__{}

23
  schema "notifications" do
lain's avatar
lain committed
24
    field(:seen, :boolean, default: false)
25
26
    belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
    belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
27
28
29
30

    timestamps()
  end

31
32
33
34
35
  def changeset(%Notification{} = notification, attrs) do
    notification
    |> cast(attrs, [:seen])
  end

36
  def for_user_query(user, opts \\ []) do
37
38
39
40
41
    query =
      Notification
      |> where(user_id: ^user.id)
      |> where(
        [n, a],
kaniini's avatar
kaniini committed
42
        fragment(
43
          "? not in (SELECT ap_id FROM users WHERE deactivated = 'true')",
44
          a.actor
kaniini's avatar
kaniini committed
45
        )
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
      )
      |> 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})

    if opts[:with_muted] do
      query
    else
      where(query, [n, a], a.actor not in ^user.info.muted_notifications)
      |> where([n, a], a.actor not in ^user.info.blocks)
      |> where(
        [n, a],
        fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.info.domain_blocks
      )
      |> join(:left, [n, a], tm in Pleroma.ThreadMute,
        on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data)
      )
Alexander Strizhakov's avatar
Alexander Strizhakov committed
70
      |> where([n, a, o, tm], is_nil(tm.user_id))
71
    end
lain's avatar
lain committed
72
  end
lain's avatar
lain committed
73

74
  def for_user(user, opts \\ %{}) do
75
    user
76
    |> for_user_query(opts)
77
    |> Pagination.fetch_paginated(opts)
78
79
  end

80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
  @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

99
100
101
102
103
104
  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,
105
        where: n.seen == false,
106
        update: [
107
108
109
110
          set: [
            seen: true,
            updated_at: ^NaiveDateTime.utc_now()
          ]
111
112
113
114
        ],
        # Ideally we would preload object and activities here
        # but Ecto does not support preloads in update_all
        select: n.id
115
116
      )

117
118
    {_, notification_ids} = Repo.update_all(query, [])

119
120
    Notification
    |> where([n], n.id in ^notification_ids)
121
122
123
124
125
126
127
128
129
130
131
    |> 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()
132
133
  end

134
135
136
137
138
139
140
141
  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

142
  def get(%{id: user_id} = _user, id) do
lain's avatar
lain committed
143
144
145
146
    query =
      from(
        n in Notification,
        where: n.id == ^id,
href's avatar
href committed
147
148
        join: activity in assoc(n, :activity),
        preload: [activity: activity]
lain's avatar
lain committed
149
      )
150
151

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

153
154
155
    case notification do
      %{user_id: ^user_id} ->
        {:ok, notification}
lain's avatar
lain committed
156

157
158
159
160
161
162
      _ ->
        {:error, "Cannot get notification"}
    end
  end

  def clear(user) do
Maxim Filippov's avatar
Maxim Filippov committed
163
164
    from(n in Notification, where: n.user_id == ^user.id)
    |> Repo.delete_all()
165
166
  end

167
168
169
170
171
172
173
174
  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

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

178
179
180
    case notification do
      %{user_id: ^user_id} ->
        Repo.delete(notification)
lain's avatar
lain committed
181

182
183
184
185
186
      _ ->
        {:error, "Cannot dismiss notification"}
    end
  end

187
  def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity) do
rinpatch's avatar
rinpatch committed
188
189
190
191
192
193
194
195
196
    object = Object.normalize(activity)

    unless object && object.data["type"] == "Answer" do
      users = get_notified_from_activity(activity)
      notifications = Enum.map(users, fn user -> create_notification(activity, user) end)
      {:ok, notifications}
    else
      {:ok, []}
    end
197
  end
lain's avatar
lain committed
198

199
200
201
202
203
204
205
  def create_notifications(%Activity{data: %{"to" => _, "type" => type}} = activity)
      when type in ["Like", "Announce", "Follow"] do
    users = get_notified_from_activity(activity)
    notifications = Enum.map(users, fn user -> create_notification(activity, user) end)
    {:ok, notifications}
  end

206
207
208
209
  def create_notifications(_), do: {:ok, []}

  # TODO move to sql, too.
  def create_notification(%Activity{} = activity, %User{} = user) do
Eugenij's avatar
Eugenij committed
210
    unless skip?(activity, user) do
211
      notification = %Notification{user_id: user.id, activity: activity}
212
      {:ok, notification} = Repo.insert(notification)
213
214
215
216

      ["user", "user:notification"]
      |> Streamer.stream(notification)

Maksim's avatar
Maksim committed
217
      Push.send(notification)
218
219
      notification
    end
220
  end
221

222
223
  def get_notified_from_activity(activity, local_only \\ true)

224
  def get_notified_from_activity(
Maksim's avatar
Maksim committed
225
        %Activity{data: %{"to" => _, "type" => type} = _data} = activity,
226
        local_only
227
228
229
230
      )
      when type in ["Create", "Like", "Announce", "Follow"] do
    recipients =
      []
231
232
      |> Utils.maybe_notify_to_recipients(activity)
      |> Utils.maybe_notify_mentioned_recipients(activity)
rinpatch's avatar
rinpatch committed
233
      |> Utils.maybe_notify_subscribers(activity)
234
235
236
237
238
      |> Enum.uniq()

    User.get_users_from_set(recipients, local_only)
  end

Maksim's avatar
Maksim committed
239
  def get_notified_from_activity(_, _local_only), do: []
Eugenij's avatar
Eugenij committed
240

241
  @spec skip?(Activity.t(), User.t()) :: boolean()
Eugenij's avatar
Eugenij committed
242
  def skip?(activity, user) do
243
244
245
246
247
248
249
250
    [
      :self,
      :followers,
      :follows,
      :non_followers,
      :non_follows,
      :recently_followed
    ]
Eugenij's avatar
Eugenij committed
251
252
253
    |> Enum.any?(&skip?(&1, activity, user))
  end

254
  @spec skip?(atom(), Activity.t(), User.t()) :: boolean()
Eugenij's avatar
Eugenij committed
255
256
257
258
259
260
261
  def skip?(:self, activity, user) do
    activity.data["actor"] == user.ap_id
  end

  def skip?(
        :followers,
        activity,
262
        %{notification_settings: %{"followers" => false}} = user
Eugenij's avatar
Eugenij committed
263
264
265
266
267
268
      ) do
    actor = activity.data["actor"]
    follower = User.get_cached_by_ap_id(actor)
    User.following?(follower, user)
  end

269
270
271
  def skip?(
        :non_followers,
        activity,
272
        %{notification_settings: %{"non_followers" => false}} = user
273
274
275
276
277
278
      ) do
    actor = activity.data["actor"]
    follower = User.get_cached_by_ap_id(actor)
    !User.following?(follower, user)
  end

279
  def skip?(:follows, activity, %{notification_settings: %{"follows" => false}} = user) do
Eugenij's avatar
Eugenij committed
280
    actor = activity.data["actor"]
minibikini's avatar
minibikini committed
281
    followed = User.get_cached_by_ap_id(actor)
Eugenij's avatar
Eugenij committed
282
283
284
    User.following?(user, followed)
  end

285
286
287
  def skip?(
        :non_follows,
        activity,
288
        %{notification_settings: %{"non_follows" => false}} = user
289
290
291
292
293
294
      ) do
    actor = activity.data["actor"]
    followed = User.get_cached_by_ap_id(actor)
    !User.following?(user, followed)
  end

295
  def skip?(:recently_followed, %{data: %{"type" => "Follow"}} = activity, user) do
Eugenij's avatar
Eugenij committed
296
297
298
299
300
301
302
303
304
305
    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
306
end