1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Notification do
11 alias Pleroma.Notification
13 alias Pleroma.Pagination
16 alias Pleroma.Web.CommonAPI.Utils
17 alias Pleroma.Web.Push
18 alias Pleroma.Web.Streamer
24 @type t :: %__MODULE__{}
26 schema "notifications" do
27 field(:seen, :boolean, default: false)
28 belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
29 belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
34 def changeset(%Notification{} = notification, attrs) do
36 |> cast(attrs, [:seen])
39 @spec unread_count_query(User.t()) :: Ecto.Queryable.t()
40 def unread_count_query(user) do
41 from(q in Pleroma.Notification,
42 where: q.user_id == ^user.id,
43 where: q.seen == false
47 @spec last_read_query(User.t()) :: Ecto.Queryable.t()
48 def last_read_query(user) do
49 from(q in Pleroma.Notification,
50 where: q.user_id == ^user.id,
51 where: q.seen == true,
52 select: type(q.id, :string),
58 def for_user_query(user, opts \\ []) do
60 |> where(user_id: ^user.id)
64 "? not in (SELECT ap_id FROM users WHERE deactivated = 'true')",
68 |> join(:inner, [n], activity in assoc(n, :activity))
69 |> join(:left, [n, a], object in Object,
72 "(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
77 |> preload([n, a, o], activity: {a, object: o})
78 |> exclude_muted(user, opts)
79 |> exclude_blocked(user)
80 |> exclude_visibility(opts)
83 defp exclude_blocked(query, user) do
85 |> where([n, a], a.actor not in ^user.blocks)
88 fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.domain_blocks
92 defp exclude_muted(query, _, %{with_muted: true}) do
96 defp exclude_muted(query, user, _opts) do
98 |> where([n, a], a.actor not in ^user.muted_notifications)
99 |> join(:left, [n, a], tm in Pleroma.ThreadMute,
100 on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data)
102 |> where([n, a, o, tm], is_nil(tm.user_id))
105 @valid_visibilities ~w[direct unlisted public private]
107 defp exclude_visibility(query, %{exclude_visibilities: visibility})
108 when is_list(visibility) do
109 if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
114 "activity_visibility(?, ?, ?) = ANY (?)",
122 Logger.error("Could not exclude visibility to #{visibility}")
127 defp exclude_visibility(query, %{exclude_visibilities: visibility})
128 when visibility in @valid_visibilities do
133 "activity_visibility(?, ?, ?) = (?)",
142 defp exclude_visibility(query, %{exclude_visibilities: visibility})
143 when visibility not in @valid_visibilities do
144 Logger.error("Could not exclude visibility to #{visibility}")
148 defp exclude_visibility(query, _visibility), do: query
150 def for_user(user, opts \\ %{}) do
152 |> for_user_query(opts)
153 |> Pagination.fetch_paginated(opts)
157 Returns notifications for user received since given date.
161 iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-13 11:22:33])
162 [%Pleroma.Notification{}, %Pleroma.Notification{}]
164 iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-15 11:22:33])
167 @spec for_user_since(Pleroma.User.t(), NaiveDateTime.t()) :: [t()]
168 def for_user_since(user, date) do
169 from(n in for_user_query(user),
170 where: n.updated_at > ^date
175 def set_read_up_to(%{id: user_id} = user, id) do
179 where: n.user_id == ^user_id,
181 where: n.seen == false,
182 # Ideally we would preload object and activities here
183 # but Ecto does not support preloads in update_all
187 {:ok, %{ids: {_, notification_ids}}} =
189 |> Multi.update_all(:ids, query, set: [seen: true, updated_at: NaiveDateTime.utc_now()])
190 |> Marker.multi_set_unread_count(user, "notifications")
191 |> Repo.transaction()
194 |> where([n], n.id in ^notification_ids)
195 |> join(:inner, [n], activity in assoc(n, :activity))
196 |> join(:left, [n, a], object in Object,
199 "(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
204 |> preload([n, a, o], activity: {a, object: o})
208 @spec read_one(User.t(), String.t()) ::
209 {:ok, Notification.t()} | {:error, Ecto.Changeset.t()} | nil
210 def read_one(%User{} = user, notification_id) do
211 with {:ok, %Notification{} = notification} <- get(user, notification_id) do
213 |> Multi.update(:update, changeset(notification, %{seen: true}))
214 |> Marker.multi_set_unread_count(user, "notifications")
215 |> Repo.transaction()
217 {:ok, %{update: notification}} -> {:ok, notification}
218 {:error, :update, changeset, _} -> {:error, changeset}
223 def get(%{id: user_id} = _user, id) do
228 join: activity in assoc(n, :activity),
229 preload: [activity: activity]
232 notification = Repo.one(query)
235 %{user_id: ^user_id} ->
239 {:error, "Cannot get notification"}
244 from(n in Notification, where: n.user_id == ^user.id)
248 def destroy_multiple(%{id: user_id} = _user, ids) do
249 from(n in Notification,
251 where: n.user_id == ^user_id
256 def dismiss(%{id: user_id} = _user, id) do
257 notification = Repo.get(Notification, id)
260 %{user_id: ^user_id} ->
261 Repo.delete(notification)
264 {:error, "Cannot dismiss notification"}
268 def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity) do
269 object = Object.normalize(activity)
271 unless object && object.data["type"] == "Answer" do
274 |> get_notified_from_activity()
275 |> Enum.map(&create_notification(activity, &1))
283 def create_notifications(%Activity{data: %{"type" => type}} = activity)
284 when type in ["Like", "Announce", "Follow", "Move"] do
287 |> get_notified_from_activity()
288 |> Enum.map(&create_notification(activity, &1))
293 def create_notifications(_), do: {:ok, []}
295 # TODO move to sql, too.
296 def create_notification(%Activity{} = activity, %User{} = user) do
297 unless skip?(activity, user) do
298 {:ok, %{notification: notification}} =
300 |> Multi.insert(:notification, %Notification{user_id: user.id, activity: activity})
301 |> Marker.multi_set_unread_count(user, "notifications")
302 |> Repo.transaction()
304 ["user", "user:notification"]
305 |> Streamer.stream(notification)
307 Push.send(notification)
312 def get_notified_from_activity(activity, local_only \\ true)
314 def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
315 when type in ["Create", "Like", "Announce", "Follow", "Move"] do
317 |> Utils.maybe_notify_to_recipients(activity)
318 |> Utils.maybe_notify_mentioned_recipients(activity)
319 |> Utils.maybe_notify_subscribers(activity)
320 |> Utils.maybe_notify_followers(activity)
322 |> User.get_users_from_set(local_only)
325 def get_notified_from_activity(_, _local_only), do: []
327 @spec skip?(Activity.t(), User.t()) :: boolean()
328 def skip?(activity, user) do
337 |> Enum.any?(&skip?(&1, activity, user))
340 @spec skip?(atom(), Activity.t(), User.t()) :: boolean()
341 def skip?(:self, activity, user) do
342 activity.data["actor"] == user.ap_id
348 %{notification_settings: %{"followers" => false}} = user
350 actor = activity.data["actor"]
351 follower = User.get_cached_by_ap_id(actor)
352 User.following?(follower, user)
358 %{notification_settings: %{"non_followers" => false}} = user
360 actor = activity.data["actor"]
361 follower = User.get_cached_by_ap_id(actor)
362 !User.following?(follower, user)
365 def skip?(:follows, activity, %{notification_settings: %{"follows" => false}} = user) do
366 actor = activity.data["actor"]
367 followed = User.get_cached_by_ap_id(actor)
368 User.following?(user, followed)
374 %{notification_settings: %{"non_follows" => false}} = user
376 actor = activity.data["actor"]
377 followed = User.get_cached_by_ap_id(actor)
378 !User.following?(user, followed)
381 def skip?(:recently_followed, %{data: %{"type" => "Follow"}} = activity, user) do
382 actor = activity.data["actor"]
384 Notification.for_user(user)
386 %{activity: %{data: %{"type" => "Follow", "actor" => ^actor}}} -> true
391 def skip?(_, _, _), do: false