diff --git a/lib/philomena/channels.ex b/lib/philomena/channels.ex index d1d31bce..ea07f0d5 100644 --- a/lib/philomena/channels.ex +++ b/lib/philomena/channels.ex @@ -8,7 +8,10 @@ defmodule Philomena.Channels do alias Philomena.Channels.AutomaticUpdater alias Philomena.Channels.Channel - alias Philomena.Notifications + + use Philomena.Subscriptions, + actor_types: ~w(Channel LivestreamChannel), + id_name: :channel_id @doc """ Updates all the tracked channels for which an update scheme is known. @@ -115,69 +118,4 @@ defmodule Philomena.Channels do def change_channel(%Channel{} = channel) do Channel.changeset(channel, %{}) end - - alias Philomena.Channels.Subscription - - @doc """ - Creates a subscription. - - ## Examples - - iex> create_subscription(%{field: value}) - {:ok, %Subscription{}} - - iex> create_subscription(%{field: bad_value}) - {:error, %Ecto.Changeset{}} - - """ - def create_subscription(_channel, nil), do: {:ok, nil} - - def create_subscription(channel, user) do - %Subscription{channel_id: channel.id, user_id: user.id} - |> Subscription.changeset(%{}) - |> Repo.insert(on_conflict: :nothing) - end - - @doc """ - Deletes a Subscription. - - ## Examples - - iex> delete_subscription(subscription) - {:ok, %Subscription{}} - - iex> delete_subscription(subscription) - {:error, %Ecto.Changeset{}} - - """ - def delete_subscription(channel, user) do - clear_notification(channel, user) - - %Subscription{channel_id: channel.id, user_id: user.id} - |> Repo.delete() - end - - def subscribed?(_channel, nil), do: false - - def subscribed?(channel, user) do - Subscription - |> where(channel_id: ^channel.id, user_id: ^user.id) - |> Repo.exists?() - end - - def subscriptions(_channels, nil), do: %{} - - def subscriptions(channels, user) do - channel_ids = Enum.map(channels, & &1.id) - - Subscription - |> where([s], s.channel_id in ^channel_ids and s.user_id == ^user.id) - |> Repo.all() - |> Map.new(&{&1.channel_id, true}) - end - - def clear_notification(channel, user) do - Notifications.delete_unread_notification("Channel", channel.id, user) - Notifications.delete_unread_notification("LivestreamChannel", channel.id, user) - end end diff --git a/lib/philomena/comments.ex b/lib/philomena/comments.ex index 1d606ff0..359347d0 100644 --- a/lib/philomena/comments.ex +++ b/lib/philomena/comments.ex @@ -19,7 +19,6 @@ defmodule Philomena.Comments do alias Philomena.NotificationWorker alias Philomena.Versions alias Philomena.Reports - alias Philomena.Users.User @doc """ Gets a single comment. @@ -58,24 +57,17 @@ defmodule Philomena.Comments do Image |> where(id: ^image.id) + image_lock_query = + lock(image_query, "FOR UPDATE") + Multi.new() + |> Multi.one(:image, image_lock_query) |> Multi.insert(:comment, comment) - |> Multi.update_all(:image, image_query, inc: [comments_count: 1]) - |> maybe_create_subscription_on_reply(image, attribution[:user]) + |> Multi.update_all(:update_image, image_query, inc: [comments_count: 1]) + |> Images.maybe_subscribe_on(:image, attribution[:user], :watch_on_reply) |> Repo.transaction() end - defp maybe_create_subscription_on_reply(multi, image, %User{watch_on_reply: true} = user) do - multi - |> Multi.run(:subscribe, fn _repo, _changes -> - Images.create_subscription(image, user) - end) - end - - defp maybe_create_subscription_on_reply(multi, _image, _user) do - multi - end - def notify_comment(comment) do Exq.enqueue(Exq, "notifications", NotificationWorker, ["Comments", comment.id]) end diff --git a/lib/philomena/forums.ex b/lib/philomena/forums.ex index f77df915..ba8006bc 100644 --- a/lib/philomena/forums.ex +++ b/lib/philomena/forums.ex @@ -7,8 +7,10 @@ defmodule Philomena.Forums do alias Philomena.Repo alias Philomena.Forums.Forum - alias Philomena.Forums.Subscription - alias Philomena.Notifications + + use Philomena.Subscriptions, + actor_types: ~w(Forum), + id_name: :forum_id @doc """ Returns the list of forums. @@ -103,45 +105,4 @@ defmodule Philomena.Forums do def change_forum(%Forum{} = forum) do Forum.changeset(forum, %{}) end - - def subscribed?(_forum, nil), do: false - - def subscribed?(forum, user) do - Subscription - |> where(forum_id: ^forum.id, user_id: ^user.id) - |> Repo.exists?() - end - - def create_subscription(_forum, nil), do: {:ok, nil} - - def create_subscription(forum, user) do - %Subscription{forum_id: forum.id, user_id: user.id} - |> Subscription.changeset(%{}) - |> Repo.insert(on_conflict: :nothing) - end - - @doc """ - Deletes a Subscription. - - ## Examples - - iex> delete_subscription(subscription) - {:ok, %Subscription{}} - - iex> delete_subscription(subscription) - {:error, %Ecto.Changeset{}} - - """ - def delete_subscription(forum, user) do - clear_notification(forum, user) - - %Subscription{forum_id: forum.id, user_id: user.id} - |> Repo.delete() - end - - def clear_notification(_forum, nil), do: nil - - def clear_notification(forum, user) do - Notifications.delete_unread_notification("Forum", forum.id, user) - end end diff --git a/lib/philomena/galleries.ex b/lib/philomena/galleries.ex index 1943fc25..b60b8b72 100644 --- a/lib/philomena/galleries.ex +++ b/lib/philomena/galleries.ex @@ -18,6 +18,10 @@ defmodule Philomena.Galleries do alias Philomena.Notifications.{Notification, UnreadNotification} alias Philomena.Images + use Philomena.Subscriptions, + actor_types: ~w(Gallery), + id_name: :gallery_id + @doc """ Gets a single gallery. @@ -356,55 +360,4 @@ defmodule Philomena.Galleries do defp position_order(%{order_position_asc: true}), do: [asc: :position] defp position_order(_gallery), do: [desc: :position] - - alias Philomena.Galleries.Subscription - - def subscribed?(_gallery, nil), do: false - - def subscribed?(gallery, user) do - Subscription - |> where(gallery_id: ^gallery.id, user_id: ^user.id) - |> Repo.exists?() - end - - @doc """ - Creates a subscription. - - ## Examples - - iex> create_subscription(%{field: value}) - {:ok, %Subscription{}} - - iex> create_subscription(%{field: bad_value}) - {:error, %Ecto.Changeset{}} - - """ - def create_subscription(gallery, user) do - %Subscription{gallery_id: gallery.id, user_id: user.id} - |> Subscription.changeset(%{}) - |> Repo.insert(on_conflict: :nothing) - end - - @doc """ - Deletes a Subscription. - - ## Examples - - iex> delete_subscription(subscription) - {:ok, %Subscription{}} - - iex> delete_subscription(subscription) - {:error, %Ecto.Changeset{}} - - """ - def delete_subscription(gallery, user) do - %Subscription{gallery_id: gallery.id, user_id: user.id} - |> Repo.delete() - end - - def clear_notification(_gallery, nil), do: nil - - def clear_notification(gallery, user) do - Notifications.delete_unread_notification("Gallery", gallery.id, user) - end end diff --git a/lib/philomena/images.ex b/lib/philomena/images.ex index d68595fb..af775c4c 100644 --- a/lib/philomena/images.ex +++ b/lib/philomena/images.ex @@ -37,6 +37,10 @@ defmodule Philomena.Images do alias Philomena.Galleries.Interaction alias Philomena.Users.User + use Philomena.Subscriptions, + actor_types: ~w(Image), + id_name: :image_id + @doc """ Gets a single image. @@ -103,7 +107,7 @@ defmodule Philomena.Images do {:ok, count} end) - |> maybe_create_subscription_on_upload(attribution[:user]) + |> maybe_subscribe_on(:image, attribution[:user], :watch_on_upload) |> Repo.transaction() |> case do {:ok, %{image: image}} = result -> @@ -157,17 +161,6 @@ defmodule Philomena.Images do Logger.error("Aborting upload of #{image.id} after #{retry_count} retries") end - defp maybe_create_subscription_on_upload(multi, %User{watch_on_upload: true} = user) do - multi - |> Multi.run(:subscribe, fn _repo, %{image: image} -> - create_subscription(image, user) - end) - end - - defp maybe_create_subscription_on_upload(multi, _user) do - multi - end - def approve_image(image) do image |> Repo.preload(:user) @@ -868,53 +861,6 @@ defmodule Philomena.Images do alias Philomena.Images.Subscription - def subscribed?(_image, nil), do: false - - def subscribed?(image, user) do - Subscription - |> where(image_id: ^image.id, user_id: ^user.id) - |> Repo.exists?() - end - - @doc """ - Creates a subscription. - - ## Examples - - iex> create_subscription(%{field: value}) - {:ok, %Subscription{}} - - iex> create_subscription(%{field: bad_value}) - {:error, %Ecto.Changeset{}} - - """ - def create_subscription(_image, nil), do: {:ok, nil} - - def create_subscription(image, user) do - %Subscription{image_id: image.id, user_id: user.id} - |> Subscription.changeset(%{}) - |> Repo.insert(on_conflict: :nothing) - end - - @doc """ - Deletes a subscription. - - ## Examples - - iex> delete_subscription(subscription) - {:ok, %Subscription{}} - - iex> delete_subscription(subscription) - {:error, %Ecto.Changeset{}} - - """ - def delete_subscription(image, user) do - clear_notification(image, user) - - %Subscription{image_id: image.id, user_id: user.id} - |> Repo.delete() - end - def migrate_subscriptions(source, target) do subscriptions = Subscription @@ -968,10 +914,4 @@ defmodule Philomena.Images do } ) end - - def clear_notification(_image, nil), do: nil - - def clear_notification(image, user) do - Notifications.delete_unread_notification("Image", image.id, user) - end end diff --git a/lib/philomena/posts.ex b/lib/philomena/posts.ex index 16795e6c..b591ce0e 100644 --- a/lib/philomena/posts.ex +++ b/lib/philomena/posts.ex @@ -20,7 +20,6 @@ defmodule Philomena.Posts do alias Philomena.Versions alias Philomena.Reports alias Philomena.Reports.Report - alias Philomena.Users.User @doc """ Gets a single post. @@ -66,7 +65,7 @@ defmodule Philomena.Posts do |> where(id: ^topic.forum_id) Multi.new() - |> Multi.all(:topic_lock, topic_lock_query) + |> Multi.one(:topic, topic_lock_query) |> Multi.run(:post, fn repo, _ -> last_position = Post @@ -95,7 +94,7 @@ defmodule Philomena.Posts do {:ok, count} end) - |> maybe_create_subscription_on_reply(topic, attributes[:user]) + |> Topics.maybe_subscribe_on(:topic, attributes[:user], :watch_on_reply) |> Repo.transaction() |> case do {:ok, %{post: post}} = result -> @@ -108,17 +107,6 @@ defmodule Philomena.Posts do end end - defp maybe_create_subscription_on_reply(multi, topic, %User{watch_on_reply: true} = user) do - multi - |> Multi.run(:subscribe, fn _repo, _changes -> - Topics.create_subscription(topic, user) - end) - end - - defp maybe_create_subscription_on_reply(multi, _topic, _user) do - multi - end - def notify_post(post) do Exq.enqueue(Exq, "notifications", NotificationWorker, ["Posts", post.id]) end diff --git a/lib/philomena/subscriptions.ex b/lib/philomena/subscriptions.ex new file mode 100644 index 00000000..8c0d53f0 --- /dev/null +++ b/lib/philomena/subscriptions.ex @@ -0,0 +1,224 @@ +defmodule Philomena.Subscriptions do + @moduledoc """ + Common subscription logic. + + `use Philomena.Subscriptions` requires the following properties: + + - `:actor_types` + This is the "actor_type" in the notifications table. + For `Philomena.Images`, this would be `["Image"]`. + + - `:id_name` + This is the name of the object field in the subscription table. + For `Philomena.Images`, this would be `:image_id`. + + The following functions and documentation are produced in the calling module: + - `subscribed?/2` + - `subscriptions/2` + - `create_subscription/2` + - `delete_subscription/2` + - `clear_notification/2` + - `maybe_subscribe_on/4` + """ + + import Ecto.Query, warn: false + alias Ecto.Multi + + alias Philomena.Notifications + alias Philomena.Repo + + defmacro __using__(opts) do + # For Philomena.Images, this yields ["Image"] + actor_types = Keyword.fetch!(opts, :actor_types) + + # For Philomena.Images, this yields :image_id + field_name = Keyword.fetch!(opts, :id_name) + + # For Philomena.Images, this yields Philomena.Images.Subscription + subscription_module = Module.concat(__CALLER__.module, Subscription) + + quote do + @doc """ + Returns whether the user is currently subscribed to this object. + + ## Examples + + iex> subscribed?(object, user) + false + + """ + def subscribed?(object, user) do + Philomena.Subscriptions.subscribed?( + unquote(subscription_module), + unquote(field_name), + object, + user + ) + end + + @doc """ + Returns a map containing whether the user is currently subscribed to any of + the provided objects. + + ## Examples + + iex> subscriptions([%{id: 1}, %{id: 2}], user) + %{2 => true} + + """ + def subscriptions(objects, user) do + Philomena.Subscriptions.subscriptions( + unquote(subscription_module), + unquote(field_name), + objects, + user + ) + end + + @doc """ + Creates a subscription. + + ## Examples + + iex> create_subscription(object, user) + {:ok, %Subscription{}} + + iex> create_subscription(object, user) + {:error, %Ecto.Changeset{}} + + """ + def create_subscription(object, user) do + Philomena.Subscriptions.create_subscription( + unquote(subscription_module), + unquote(field_name), + object, + user + ) + end + + @doc """ + Deletes a subscription and removes notifications for it. + + ## Examples + + iex> delete_subscription(object, user) + {:ok, %Subscription{}} + + iex> delete_subscription(object, user) + {:error, %Ecto.Changeset{}} + + """ + def delete_subscription(object, user) do + clear_notification(object, user) + + Philomena.Subscriptions.delete_subscription( + unquote(subscription_module), + unquote(field_name), + object, + user + ) + end + + @doc """ + Deletes any active notifications for a subscription. + + ## Examples + + iex> clear_notification(object, user) + :ok + + """ + def clear_notification(object, user) do + for type <- unquote(actor_types) do + Philomena.Subscriptions.clear_notification(type, object, user) + end + + :ok + end + + @doc """ + Creates a subscription inside the `m:Ecto.Multi` flow if `user` is not nil + and `field` in `user` is `true`. + + Valid values for field are `:watch_on_reply`, `:watch_on_upload`, `:watch_on_new_topic`. + + ## Examples + + iex> maybe_subscribe_on(multi, :image, user, :watch_on_reply) + %Ecto.Multi{} + + iex> maybe_subscribe_on(multi, :topic, nil, :watch_on_reply) + %Ecto.Multi{} + + """ + def maybe_subscribe_on(multi, change_name, user, field) do + Philomena.Subscriptions.maybe_subscribe_on(multi, __MODULE__, change_name, user, field) + end + end + end + + @doc false + def subscribed?(subscription_module, field_name, object, user) do + case user do + nil -> + false + + _ -> + subscription_module + |> where([s], field(s, ^field_name) == ^object.id and s.user_id == ^user.id) + |> Repo.exists?() + end + end + + @doc false + def subscriptions(subscription_module, field_name, objects, user) do + case user do + nil -> + %{} + + _ -> + object_ids = Enum.map(objects, & &1.id) + + subscription_module + |> where([s], field(s, ^field_name) in ^object_ids and s.user_id == ^user.id) + |> Repo.all() + |> Map.new(&{Map.fetch!(&1, field_name), true}) + end + end + + @doc false + def create_subscription(subscription_module, field_name, object, user) do + struct!(subscription_module, [{field_name, object.id}, {:user_id, user.id}]) + |> subscription_module.changeset(%{}) + |> Repo.insert(on_conflict: :nothing) + end + + @doc false + def delete_subscription(subscription_module, field_name, object, user) do + struct!(subscription_module, [{field_name, object.id}, {:user_id, user.id}]) + |> Repo.delete() + end + + @doc false + def clear_notification(type, object, user) do + case user do + nil -> nil + _ -> Notifications.delete_unread_notification(type, object.id, user) + end + end + + @doc false + def maybe_subscribe_on(multi, module, change_name, user, field) + when field in [:watch_on_reply, :watch_on_upload, :watch_on_new_topic] do + case user do + %{^field => true} -> + Multi.run(multi, :subscribe, fn _repo, changes -> + object = Map.fetch!(changes, change_name) + module.create_subscription(object, user) + end) + + _ -> + multi + end + end +end diff --git a/lib/philomena/topics.ex b/lib/philomena/topics.ex index 4892c06d..a2356f92 100644 --- a/lib/philomena/topics.ex +++ b/lib/philomena/topics.ex @@ -12,7 +12,10 @@ defmodule Philomena.Topics do alias Philomena.Posts alias Philomena.Notifications alias Philomena.NotificationWorker - alias Philomena.Users.User + + use Philomena.Subscriptions, + actor_types: ~w(Topic), + id_name: :topic_id @doc """ Gets a single topic. @@ -70,7 +73,7 @@ defmodule Philomena.Topics do {:ok, count} end) - |> maybe_create_subscription_on_new_topic(attribution[:user]) + |> maybe_subscribe_on(:topic, attribution[:user], :watch_on_new_topic) |> Repo.transaction() |> case do {:ok, %{topic: topic}} = result -> @@ -84,17 +87,6 @@ defmodule Philomena.Topics do end end - defp maybe_create_subscription_on_new_topic(multi, %User{watch_on_new_topic: true} = user) do - multi - |> Multi.run(:subscribe, fn _repo, %{topic: topic} -> - create_subscription(topic, user) - end) - end - - defp maybe_create_subscription_on_new_topic(multi, _user) do - multi - end - def notify_topic(topic, post) do Exq.enqueue(Exq, "notifications", NotificationWorker, ["Topics", [topic.id, post.id]]) end @@ -173,55 +165,6 @@ defmodule Philomena.Topics do Topic.changeset(topic, %{}) end - alias Philomena.Topics.Subscription - - def subscribed?(_topic, nil), do: false - - def subscribed?(topic, user) do - Subscription - |> where(topic_id: ^topic.id, user_id: ^user.id) - |> Repo.exists?() - end - - @doc """ - Creates a subscription. - - ## Examples - - iex> create_subscription(%{field: value}) - {:ok, %Subscription{}} - - iex> create_subscription(%{field: bad_value}) - {:error, %Ecto.Changeset{}} - - """ - def create_subscription(_topic, nil), do: {:ok, nil} - - def create_subscription(topic, user) do - %Subscription{topic_id: topic.id, user_id: user.id} - |> Subscription.changeset(%{}) - |> Repo.insert(on_conflict: :nothing) - end - - @doc """ - Deletes a Subscription. - - ## Examples - - iex> delete_subscription(subscription) - {:ok, %Subscription{}} - - iex> delete_subscription(subscription) - {:error, %Ecto.Changeset{}} - - """ - def delete_subscription(topic, user) do - clear_notification(topic, user) - - %Subscription{topic_id: topic.id, user_id: user.id} - |> Repo.delete() - end - def stick_topic(topic) do Topic.stick_changeset(topic) |> Repo.update() @@ -299,10 +242,4 @@ defmodule Philomena.Topics do |> Topic.title_changeset(attrs) |> Repo.update() end - - def clear_notification(_topic, nil), do: nil - - def clear_notification(topic, user) do - Notifications.delete_unread_notification("Topic", topic.id, user) - end end