Fork of Pleroma with site-specific changes and feature branches https://git.pleroma.social/pleroma/pleroma
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

668 lines
19KB

  1. # Pleroma: A lightweight social networking server
  2. # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
  3. # SPDX-License-Identifier: AGPL-3.0-only
  4. defmodule Pleroma.Notification do
  5. use Ecto.Schema
  6. alias Ecto.Multi
  7. alias Pleroma.Activity
  8. alias Pleroma.FollowingRelationship
  9. alias Pleroma.Marker
  10. alias Pleroma.Notification
  11. alias Pleroma.Object
  12. alias Pleroma.Pagination
  13. alias Pleroma.Repo
  14. alias Pleroma.ThreadMute
  15. alias Pleroma.User
  16. alias Pleroma.Web.CommonAPI
  17. alias Pleroma.Web.CommonAPI.Utils
  18. alias Pleroma.Web.Push
  19. alias Pleroma.Web.Streamer
  20. import Ecto.Query
  21. import Ecto.Changeset
  22. require Logger
  23. @type t :: %__MODULE__{}
  24. @include_muted_option :with_muted
  25. schema "notifications" do
  26. field(:seen, :boolean, default: false)
  27. # This is an enum type in the database. If you add a new notification type,
  28. # remember to add a migration to add it to the `notifications_type` enum
  29. # as well.
  30. field(:type, :string)
  31. belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
  32. belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
  33. timestamps()
  34. end
  35. def update_notification_type(user, activity) do
  36. with %__MODULE__{} = notification <-
  37. Repo.get_by(__MODULE__, user_id: user.id, activity_id: activity.id) do
  38. type =
  39. activity
  40. |> type_from_activity()
  41. notification
  42. |> changeset(%{type: type})
  43. |> Repo.update()
  44. end
  45. end
  46. @spec unread_notifications_count(User.t()) :: integer()
  47. def unread_notifications_count(%User{id: user_id}) do
  48. from(q in __MODULE__,
  49. where: q.user_id == ^user_id and q.seen == false
  50. )
  51. |> Repo.aggregate(:count, :id)
  52. end
  53. @notification_types ~w{
  54. favourite
  55. follow
  56. follow_request
  57. mention
  58. move
  59. pleroma:chat_mention
  60. pleroma:emoji_reaction
  61. pleroma:report
  62. reblog
  63. }
  64. def changeset(%Notification{} = notification, attrs) do
  65. notification
  66. |> cast(attrs, [:seen, :type])
  67. |> validate_inclusion(:type, @notification_types)
  68. end
  69. @spec last_read_query(User.t()) :: Ecto.Queryable.t()
  70. def last_read_query(user) do
  71. from(q in Pleroma.Notification,
  72. where: q.user_id == ^user.id,
  73. where: q.seen == true,
  74. select: type(q.id, :string),
  75. limit: 1,
  76. order_by: [desc: :id]
  77. )
  78. end
  79. defp for_user_query_ap_id_opts(user, opts) do
  80. ap_id_relationships =
  81. [:block] ++
  82. if opts[@include_muted_option], do: [], else: [:notification_mute]
  83. preloaded_ap_ids = User.outgoing_relationships_ap_ids(user, ap_id_relationships)
  84. exclude_blocked_opts = Map.merge(%{blocked_users_ap_ids: preloaded_ap_ids[:block]}, opts)
  85. exclude_notification_muted_opts =
  86. Map.merge(%{notification_muted_users_ap_ids: preloaded_ap_ids[:notification_mute]}, opts)
  87. {exclude_blocked_opts, exclude_notification_muted_opts}
  88. end
  89. def for_user_query(user, opts \\ %{}) do
  90. {exclude_blocked_opts, exclude_notification_muted_opts} =
  91. for_user_query_ap_id_opts(user, opts)
  92. Notification
  93. |> where(user_id: ^user.id)
  94. |> join(:inner, [n], activity in assoc(n, :activity))
  95. |> join(:left, [n, a], object in Object,
  96. on:
  97. fragment(
  98. "(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')",
  99. object.data,
  100. a.data,
  101. a.data
  102. )
  103. )
  104. |> join(:inner, [_n, a], u in User, on: u.ap_id == a.actor, as: :user_actor)
  105. |> preload([n, a, o], activity: {a, object: o})
  106. |> where([user_actor: user_actor], user_actor.is_active)
  107. |> exclude_notification_muted(user, exclude_notification_muted_opts)
  108. |> exclude_blocked(user, exclude_blocked_opts)
  109. |> exclude_filtered(user)
  110. |> exclude_visibility(opts)
  111. end
  112. # Excludes blocked users and non-followed domain-blocked users
  113. defp exclude_blocked(query, user, opts) do
  114. blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user)
  115. query
  116. |> where([n, a], a.actor not in ^blocked_ap_ids)
  117. |> FollowingRelationship.keep_following_or_not_domain_blocked(user)
  118. end
  119. defp exclude_notification_muted(query, _, %{@include_muted_option => true}) do
  120. query
  121. end
  122. defp exclude_notification_muted(query, user, opts) do
  123. notification_muted_ap_ids =
  124. opts[:notification_muted_users_ap_ids] || User.notification_muted_users_ap_ids(user)
  125. query
  126. |> where([n, a], a.actor not in ^notification_muted_ap_ids)
  127. |> join(:left, [n, a], tm in ThreadMute,
  128. on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data),
  129. as: :thread_mute
  130. )
  131. |> where([thread_mute: thread_mute], is_nil(thread_mute.user_id))
  132. end
  133. defp exclude_filtered(query, user) do
  134. case Pleroma.Filter.compose_regex(user) do
  135. nil ->
  136. query
  137. regex ->
  138. from([_n, a, o] in query,
  139. where:
  140. fragment("not(?->>'content' ~* ?)", o.data, ^regex) or
  141. fragment("?->>'actor' = ?", o.data, ^user.ap_id)
  142. )
  143. end
  144. end
  145. @valid_visibilities ~w[direct unlisted public private]
  146. defp exclude_visibility(query, %{exclude_visibilities: visibility})
  147. when is_list(visibility) do
  148. if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
  149. query
  150. |> join(:left, [n, a], mutated_activity in Pleroma.Activity,
  151. on:
  152. fragment(
  153. "COALESCE((?->'object')->>'id', ?->>'object')",
  154. a.data,
  155. a.data
  156. ) ==
  157. fragment(
  158. "COALESCE((?->'object')->>'id', ?->>'object')",
  159. mutated_activity.data,
  160. mutated_activity.data
  161. ) and
  162. fragment("(?->>'type' = 'Like' or ?->>'type' = 'Announce')", a.data, a.data) and
  163. fragment("?->>'type'", mutated_activity.data) == "Create",
  164. as: :mutated_activity
  165. )
  166. |> where(
  167. [n, a, mutated_activity: mutated_activity],
  168. not fragment(
  169. """
  170. CASE WHEN (?->>'type') = 'Like' or (?->>'type') = 'Announce'
  171. THEN (activity_visibility(?, ?, ?) = ANY (?))
  172. ELSE (activity_visibility(?, ?, ?) = ANY (?)) END
  173. """,
  174. a.data,
  175. a.data,
  176. mutated_activity.actor,
  177. mutated_activity.recipients,
  178. mutated_activity.data,
  179. ^visibility,
  180. a.actor,
  181. a.recipients,
  182. a.data,
  183. ^visibility
  184. )
  185. )
  186. else
  187. Logger.error("Could not exclude visibility to #{visibility}")
  188. query
  189. end
  190. end
  191. defp exclude_visibility(query, %{exclude_visibilities: visibility})
  192. when visibility in @valid_visibilities do
  193. exclude_visibility(query, [visibility])
  194. end
  195. defp exclude_visibility(query, %{exclude_visibilities: visibility})
  196. when visibility not in @valid_visibilities do
  197. Logger.error("Could not exclude visibility to #{visibility}")
  198. query
  199. end
  200. defp exclude_visibility(query, _visibility), do: query
  201. def for_user(user, opts \\ %{}) do
  202. user
  203. |> for_user_query(opts)
  204. |> Pagination.fetch_paginated(opts)
  205. end
  206. @doc """
  207. Returns notifications for user received since given date.
  208. ## Examples
  209. iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-13 11:22:33])
  210. [%Pleroma.Notification{}, %Pleroma.Notification{}]
  211. iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-15 11:22:33])
  212. []
  213. """
  214. @spec for_user_since(Pleroma.User.t(), NaiveDateTime.t()) :: [t()]
  215. def for_user_since(user, date) do
  216. from(n in for_user_query(user),
  217. where: n.updated_at > ^date
  218. )
  219. |> Repo.all()
  220. end
  221. def set_read_up_to(%{id: user_id} = user, id) do
  222. query =
  223. from(
  224. n in Notification,
  225. where: n.user_id == ^user_id,
  226. where: n.id <= ^id,
  227. where: n.seen == false,
  228. # Ideally we would preload object and activities here
  229. # but Ecto does not support preloads in update_all
  230. select: n.id
  231. )
  232. {:ok, %{ids: {_, notification_ids}}} =
  233. Multi.new()
  234. |> Multi.update_all(:ids, query, set: [seen: true, updated_at: NaiveDateTime.utc_now()])
  235. |> Marker.multi_set_last_read_id(user, "notifications")
  236. |> Repo.transaction()
  237. for_user_query(user)
  238. |> where([n], n.id in ^notification_ids)
  239. |> Repo.all()
  240. end
  241. @spec read_one(User.t(), String.t()) ::
  242. {:ok, Notification.t()} | {:error, Ecto.Changeset.t()} | nil
  243. def read_one(%User{} = user, notification_id) do
  244. with {:ok, %Notification{} = notification} <- get(user, notification_id) do
  245. Multi.new()
  246. |> Multi.update(:update, changeset(notification, %{seen: true}))
  247. |> Marker.multi_set_last_read_id(user, "notifications")
  248. |> Repo.transaction()
  249. |> case do
  250. {:ok, %{update: notification}} -> {:ok, notification}
  251. {:error, :update, changeset, _} -> {:error, changeset}
  252. end
  253. end
  254. end
  255. def get(%{id: user_id} = _user, id) do
  256. query =
  257. from(
  258. n in Notification,
  259. where: n.id == ^id,
  260. join: activity in assoc(n, :activity),
  261. preload: [activity: activity]
  262. )
  263. notification = Repo.one(query)
  264. case notification do
  265. %{user_id: ^user_id} ->
  266. {:ok, notification}
  267. _ ->
  268. {:error, "Cannot get notification"}
  269. end
  270. end
  271. def clear(user) do
  272. from(n in Notification, where: n.user_id == ^user.id)
  273. |> Repo.delete_all()
  274. end
  275. def destroy_multiple(%{id: user_id} = _user, ids) do
  276. from(n in Notification,
  277. where: n.id in ^ids,
  278. where: n.user_id == ^user_id
  279. )
  280. |> Repo.delete_all()
  281. end
  282. def dismiss(%Pleroma.Activity{} = activity) do
  283. Notification
  284. |> where([n], n.activity_id == ^activity.id)
  285. |> Repo.delete_all()
  286. |> case do
  287. {_, notifications} -> {:ok, notifications}
  288. _ -> {:error, "Cannot dismiss notification"}
  289. end
  290. end
  291. def dismiss(%{id: user_id} = _user, id) do
  292. notification = Repo.get(Notification, id)
  293. case notification do
  294. %{user_id: ^user_id} ->
  295. Repo.delete(notification)
  296. _ ->
  297. {:error, "Cannot dismiss notification"}
  298. end
  299. end
  300. @spec create_notifications(Activity.t(), keyword()) :: {:ok, [Notification.t()] | []}
  301. def create_notifications(activity, options \\ [])
  302. def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity, options) do
  303. object = Object.normalize(activity, fetch: false)
  304. if object && object.data["type"] == "Answer" do
  305. {:ok, []}
  306. else
  307. do_create_notifications(activity, options)
  308. end
  309. end
  310. def create_notifications(%Activity{data: %{"type" => type}} = activity, options)
  311. when type in ["Follow", "Like", "Announce", "Move", "EmojiReact", "Flag"] do
  312. do_create_notifications(activity, options)
  313. end
  314. def create_notifications(_, _), do: {:ok, []}
  315. defp do_create_notifications(%Activity{} = activity, options) do
  316. do_send = Keyword.get(options, :do_send, true)
  317. {enabled_receivers, disabled_receivers} = get_notified_from_activity(activity)
  318. potential_receivers = enabled_receivers ++ disabled_receivers
  319. notifications =
  320. Enum.map(potential_receivers, fn user ->
  321. do_send = do_send && user in enabled_receivers
  322. create_notification(activity, user, do_send)
  323. end)
  324. |> Enum.reject(&is_nil/1)
  325. {:ok, notifications}
  326. end
  327. defp type_from_activity(%{data: %{"type" => type}} = activity) do
  328. case type do
  329. "Follow" ->
  330. if Activity.follow_accepted?(activity) do
  331. "follow"
  332. else
  333. "follow_request"
  334. end
  335. "Announce" ->
  336. "reblog"
  337. "Like" ->
  338. "favourite"
  339. "Move" ->
  340. "move"
  341. "EmojiReact" ->
  342. "pleroma:emoji_reaction"
  343. "Flag" ->
  344. "pleroma:report"
  345. # Compatibility with old reactions
  346. "EmojiReaction" ->
  347. "pleroma:emoji_reaction"
  348. "Create" ->
  349. activity
  350. |> type_from_activity_object()
  351. t ->
  352. raise "No notification type for activity type #{t}"
  353. end
  354. end
  355. defp type_from_activity_object(%{data: %{"type" => "Create", "object" => %{}}}), do: "mention"
  356. defp type_from_activity_object(%{data: %{"type" => "Create"}} = activity) do
  357. object = Object.get_by_ap_id(activity.data["object"])
  358. case object && object.data["type"] do
  359. "ChatMessage" -> "pleroma:chat_mention"
  360. _ -> "mention"
  361. end
  362. end
  363. # TODO move to sql, too.
  364. def create_notification(%Activity{} = activity, %User{} = user, do_send \\ true) do
  365. unless skip?(activity, user) do
  366. {:ok, %{notification: notification}} =
  367. Multi.new()
  368. |> Multi.insert(:notification, %Notification{
  369. user_id: user.id,
  370. activity: activity,
  371. seen: mark_as_read?(activity, user),
  372. type: type_from_activity(activity)
  373. })
  374. |> Marker.multi_set_last_read_id(user, "notifications")
  375. |> Repo.transaction()
  376. if do_send do
  377. Streamer.stream(["user", "user:notification"], notification)
  378. Push.send(notification)
  379. end
  380. notification
  381. end
  382. end
  383. @doc """
  384. Returns a tuple with 2 elements:
  385. {notification-enabled receivers, currently disabled receivers (blocking / [thread] muting)}
  386. NOTE: might be called for FAKE Activities, see ActivityPub.Utils.get_notified_from_object/1
  387. """
  388. @spec get_notified_from_activity(Activity.t(), boolean()) :: {list(User.t()), list(User.t())}
  389. def get_notified_from_activity(activity, local_only \\ true)
  390. def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
  391. when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact", "Flag"] do
  392. potential_receiver_ap_ids = get_potential_receiver_ap_ids(activity)
  393. potential_receivers =
  394. User.get_users_from_set(potential_receiver_ap_ids, local_only: local_only)
  395. notification_enabled_ap_ids =
  396. potential_receiver_ap_ids
  397. |> exclude_domain_blocker_ap_ids(activity, potential_receivers)
  398. |> exclude_relationship_restricted_ap_ids(activity)
  399. |> exclude_thread_muter_ap_ids(activity)
  400. notification_enabled_users =
  401. Enum.filter(potential_receivers, fn u -> u.ap_id in notification_enabled_ap_ids end)
  402. {notification_enabled_users, potential_receivers -- notification_enabled_users}
  403. end
  404. def get_notified_from_activity(_, _local_only), do: {[], []}
  405. # For some activities, only notify the author of the object
  406. def get_potential_receiver_ap_ids(%{data: %{"type" => type, "object" => object_id}})
  407. when type in ~w{Like Announce EmojiReact} do
  408. case Object.get_cached_by_ap_id(object_id) do
  409. %Object{data: %{"actor" => actor}} ->
  410. [actor]
  411. _ ->
  412. []
  413. end
  414. end
  415. def get_potential_receiver_ap_ids(%{data: %{"type" => "Follow", "object" => object_id}}) do
  416. [object_id]
  417. end
  418. def get_potential_receiver_ap_ids(%{data: %{"type" => "Flag", "actor" => actor}}) do
  419. (User.all_superusers() |> Enum.map(fn user -> user.ap_id end)) -- [actor]
  420. end
  421. def get_potential_receiver_ap_ids(activity) do
  422. []
  423. |> Utils.maybe_notify_to_recipients(activity)
  424. |> Utils.maybe_notify_mentioned_recipients(activity)
  425. |> Utils.maybe_notify_subscribers(activity)
  426. |> Utils.maybe_notify_followers(activity)
  427. |> Enum.uniq()
  428. end
  429. @doc "Filters out AP IDs domain-blocking and not following the activity's actor"
  430. def exclude_domain_blocker_ap_ids(ap_ids, activity, preloaded_users \\ [])
  431. def exclude_domain_blocker_ap_ids([], _activity, _preloaded_users), do: []
  432. def exclude_domain_blocker_ap_ids(ap_ids, %Activity{} = activity, preloaded_users) do
  433. activity_actor_domain = activity.actor && URI.parse(activity.actor).host
  434. users =
  435. ap_ids
  436. |> Enum.map(fn ap_id ->
  437. Enum.find(preloaded_users, &(&1.ap_id == ap_id)) ||
  438. User.get_cached_by_ap_id(ap_id)
  439. end)
  440. |> Enum.filter(& &1)
  441. domain_blocker_ap_ids = for u <- users, activity_actor_domain in u.domain_blocks, do: u.ap_id
  442. domain_blocker_follower_ap_ids =
  443. if Enum.any?(domain_blocker_ap_ids) do
  444. activity
  445. |> Activity.user_actor()
  446. |> FollowingRelationship.followers_ap_ids(domain_blocker_ap_ids)
  447. else
  448. []
  449. end
  450. ap_ids
  451. |> Kernel.--(domain_blocker_ap_ids)
  452. |> Kernel.++(domain_blocker_follower_ap_ids)
  453. end
  454. @doc "Filters out AP IDs of users basing on their relationships with activity actor user"
  455. def exclude_relationship_restricted_ap_ids([], _activity), do: []
  456. def exclude_relationship_restricted_ap_ids(ap_ids, %Activity{} = activity) do
  457. relationship_restricted_ap_ids =
  458. activity
  459. |> Activity.user_actor()
  460. |> User.incoming_relationships_ungrouped_ap_ids([
  461. :block,
  462. :notification_mute
  463. ])
  464. Enum.uniq(ap_ids) -- relationship_restricted_ap_ids
  465. end
  466. @doc "Filters out AP IDs of users who mute activity thread"
  467. def exclude_thread_muter_ap_ids([], _activity), do: []
  468. def exclude_thread_muter_ap_ids(ap_ids, %Activity{} = activity) do
  469. thread_muter_ap_ids = ThreadMute.muter_ap_ids(activity.data["context"])
  470. Enum.uniq(ap_ids) -- thread_muter_ap_ids
  471. end
  472. @spec skip?(Activity.t(), User.t()) :: boolean()
  473. def skip?(%Activity{} = activity, %User{} = user) do
  474. [
  475. :self,
  476. :invisible,
  477. :block_from_strangers,
  478. :recently_followed,
  479. :filtered
  480. ]
  481. |> Enum.find(&skip?(&1, activity, user))
  482. end
  483. def skip?(_, _), do: false
  484. @spec skip?(atom(), Activity.t(), User.t()) :: boolean()
  485. def skip?(:self, %Activity{} = activity, %User{} = user) do
  486. activity.data["actor"] == user.ap_id
  487. end
  488. def skip?(:invisible, %Activity{} = activity, _) do
  489. actor = activity.data["actor"]
  490. user = User.get_cached_by_ap_id(actor)
  491. User.invisible?(user)
  492. end
  493. def skip?(
  494. :block_from_strangers,
  495. %Activity{} = activity,
  496. %User{notification_settings: %{block_from_strangers: true}} = user
  497. ) do
  498. actor = activity.data["actor"]
  499. follower = User.get_cached_by_ap_id(actor)
  500. !User.following?(follower, user)
  501. end
  502. # To do: consider defining recency in hours and checking FollowingRelationship with a single SQL
  503. def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity, %User{} = user) do
  504. actor = activity.data["actor"]
  505. Notification.for_user(user)
  506. |> Enum.any?(fn
  507. %{activity: %{data: %{"type" => "Follow", "actor" => ^actor}}} -> true
  508. _ -> false
  509. end)
  510. end
  511. def skip?(:filtered, %{data: %{"type" => type}}, _) when type in ["Follow", "Move"], do: false
  512. def skip?(:filtered, activity, user) do
  513. object = Object.normalize(activity, fetch: false)
  514. cond do
  515. is_nil(object) ->
  516. false
  517. object.data["actor"] == user.ap_id ->
  518. false
  519. not is_nil(regex = Pleroma.Filter.compose_regex(user, :re)) ->
  520. Regex.match?(regex, object.data["content"])
  521. true ->
  522. false
  523. end
  524. end
  525. def skip?(_, _, _), do: false
  526. def mark_as_read?(activity, target_user) do
  527. user = Activity.user_actor(activity)
  528. User.mutes_user?(target_user, user) || CommonAPI.thread_muted?(target_user, activity)
  529. end
  530. def for_user_and_activity(user, activity) do
  531. from(n in __MODULE__,
  532. where: n.user_id == ^user.id,
  533. where: n.activity_id == ^activity.id
  534. )
  535. |> Repo.one()
  536. end
  537. @spec mark_context_as_read(User.t(), String.t()) :: {integer(), nil | [term()]}
  538. def mark_context_as_read(%User{id: id}, context) do
  539. from(
  540. n in Notification,
  541. join: a in assoc(n, :activity),
  542. where: n.user_id == ^id,
  543. where: n.seen == false,
  544. where: fragment("?->>'context'", a.data) == ^context
  545. )
  546. |> Repo.update_all(set: [seen: true])
  547. end
  548. end