logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma git clone https://hacktivis.me/git/pleroma.git

notification.ex (21943B)


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