Merge pull request #310 from philomena-dev/subscriptions-cleanup

Deduplicate common subscription logic
This commit is contained in:
liamwhite 2024-07-05 08:43:12 -04:00 committed by GitHub
commit 3c0c12602f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 254 additions and 321 deletions

View file

@ -8,7 +8,10 @@ defmodule Philomena.Channels do
alias Philomena.Channels.AutomaticUpdater alias Philomena.Channels.AutomaticUpdater
alias Philomena.Channels.Channel alias Philomena.Channels.Channel
alias Philomena.Notifications
use Philomena.Subscriptions,
actor_types: ~w(Channel LivestreamChannel),
id_name: :channel_id
@doc """ @doc """
Updates all the tracked channels for which an update scheme is known. 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 def change_channel(%Channel{} = channel) do
Channel.changeset(channel, %{}) Channel.changeset(channel, %{})
end 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 end

View file

@ -19,7 +19,6 @@ defmodule Philomena.Comments do
alias Philomena.NotificationWorker alias Philomena.NotificationWorker
alias Philomena.Versions alias Philomena.Versions
alias Philomena.Reports alias Philomena.Reports
alias Philomena.Users.User
@doc """ @doc """
Gets a single comment. Gets a single comment.
@ -58,24 +57,17 @@ defmodule Philomena.Comments do
Image Image
|> where(id: ^image.id) |> where(id: ^image.id)
image_lock_query =
lock(image_query, "FOR UPDATE")
Multi.new() Multi.new()
|> Multi.one(:image, image_lock_query)
|> Multi.insert(:comment, comment) |> Multi.insert(:comment, comment)
|> Multi.update_all(:image, image_query, inc: [comments_count: 1]) |> Multi.update_all(:update_image, image_query, inc: [comments_count: 1])
|> maybe_create_subscription_on_reply(image, attribution[:user]) |> Images.maybe_subscribe_on(:image, attribution[:user], :watch_on_reply)
|> Repo.transaction() |> Repo.transaction()
end 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 def notify_comment(comment) do
Exq.enqueue(Exq, "notifications", NotificationWorker, ["Comments", comment.id]) Exq.enqueue(Exq, "notifications", NotificationWorker, ["Comments", comment.id])
end end

View file

@ -7,8 +7,10 @@ defmodule Philomena.Forums do
alias Philomena.Repo alias Philomena.Repo
alias Philomena.Forums.Forum alias Philomena.Forums.Forum
alias Philomena.Forums.Subscription
alias Philomena.Notifications use Philomena.Subscriptions,
actor_types: ~w(Forum),
id_name: :forum_id
@doc """ @doc """
Returns the list of forums. Returns the list of forums.
@ -103,45 +105,4 @@ defmodule Philomena.Forums do
def change_forum(%Forum{} = forum) do def change_forum(%Forum{} = forum) do
Forum.changeset(forum, %{}) Forum.changeset(forum, %{})
end 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 end

View file

@ -18,6 +18,10 @@ defmodule Philomena.Galleries do
alias Philomena.Notifications.{Notification, UnreadNotification} alias Philomena.Notifications.{Notification, UnreadNotification}
alias Philomena.Images alias Philomena.Images
use Philomena.Subscriptions,
actor_types: ~w(Gallery),
id_name: :gallery_id
@doc """ @doc """
Gets a single gallery. 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(%{order_position_asc: true}), do: [asc: :position]
defp position_order(_gallery), do: [desc: :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 end

View file

@ -37,6 +37,10 @@ defmodule Philomena.Images do
alias Philomena.Galleries.Interaction alias Philomena.Galleries.Interaction
alias Philomena.Users.User alias Philomena.Users.User
use Philomena.Subscriptions,
actor_types: ~w(Image),
id_name: :image_id
@doc """ @doc """
Gets a single image. Gets a single image.
@ -103,7 +107,7 @@ defmodule Philomena.Images do
{:ok, count} {:ok, count}
end) end)
|> maybe_create_subscription_on_upload(attribution[:user]) |> maybe_subscribe_on(:image, attribution[:user], :watch_on_upload)
|> Repo.transaction() |> Repo.transaction()
|> case do |> case do
{:ok, %{image: image}} = result -> {:ok, %{image: image}} = result ->
@ -157,17 +161,6 @@ defmodule Philomena.Images do
Logger.error("Aborting upload of #{image.id} after #{retry_count} retries") Logger.error("Aborting upload of #{image.id} after #{retry_count} retries")
end 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 def approve_image(image) do
image image
|> Repo.preload(:user) |> Repo.preload(:user)
@ -868,53 +861,6 @@ defmodule Philomena.Images do
alias Philomena.Images.Subscription 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 def migrate_subscriptions(source, target) do
subscriptions = subscriptions =
Subscription Subscription
@ -968,10 +914,4 @@ defmodule Philomena.Images do
} }
) )
end end
def clear_notification(_image, nil), do: nil
def clear_notification(image, user) do
Notifications.delete_unread_notification("Image", image.id, user)
end
end end

View file

@ -20,7 +20,6 @@ defmodule Philomena.Posts do
alias Philomena.Versions alias Philomena.Versions
alias Philomena.Reports alias Philomena.Reports
alias Philomena.Reports.Report alias Philomena.Reports.Report
alias Philomena.Users.User
@doc """ @doc """
Gets a single post. Gets a single post.
@ -66,7 +65,7 @@ defmodule Philomena.Posts do
|> where(id: ^topic.forum_id) |> where(id: ^topic.forum_id)
Multi.new() Multi.new()
|> Multi.all(:topic_lock, topic_lock_query) |> Multi.one(:topic, topic_lock_query)
|> Multi.run(:post, fn repo, _ -> |> Multi.run(:post, fn repo, _ ->
last_position = last_position =
Post Post
@ -95,7 +94,7 @@ defmodule Philomena.Posts do
{:ok, count} {:ok, count}
end) end)
|> maybe_create_subscription_on_reply(topic, attributes[:user]) |> Topics.maybe_subscribe_on(:topic, attributes[:user], :watch_on_reply)
|> Repo.transaction() |> Repo.transaction()
|> case do |> case do
{:ok, %{post: post}} = result -> {:ok, %{post: post}} = result ->
@ -108,17 +107,6 @@ defmodule Philomena.Posts do
end end
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 def notify_post(post) do
Exq.enqueue(Exq, "notifications", NotificationWorker, ["Posts", post.id]) Exq.enqueue(Exq, "notifications", NotificationWorker, ["Posts", post.id])
end end

View file

@ -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

View file

@ -12,7 +12,10 @@ defmodule Philomena.Topics do
alias Philomena.Posts alias Philomena.Posts
alias Philomena.Notifications alias Philomena.Notifications
alias Philomena.NotificationWorker alias Philomena.NotificationWorker
alias Philomena.Users.User
use Philomena.Subscriptions,
actor_types: ~w(Topic),
id_name: :topic_id
@doc """ @doc """
Gets a single topic. Gets a single topic.
@ -70,7 +73,7 @@ defmodule Philomena.Topics do
{:ok, count} {:ok, count}
end) end)
|> maybe_create_subscription_on_new_topic(attribution[:user]) |> maybe_subscribe_on(:topic, attribution[:user], :watch_on_new_topic)
|> Repo.transaction() |> Repo.transaction()
|> case do |> case do
{:ok, %{topic: topic}} = result -> {:ok, %{topic: topic}} = result ->
@ -84,17 +87,6 @@ defmodule Philomena.Topics do
end end
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 def notify_topic(topic, post) do
Exq.enqueue(Exq, "notifications", NotificationWorker, ["Topics", [topic.id, post.id]]) Exq.enqueue(Exq, "notifications", NotificationWorker, ["Topics", [topic.id, post.id]])
end end
@ -173,55 +165,6 @@ defmodule Philomena.Topics do
Topic.changeset(topic, %{}) Topic.changeset(topic, %{})
end 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 def stick_topic(topic) do
Topic.stick_changeset(topic) Topic.stick_changeset(topic)
|> Repo.update() |> Repo.update()
@ -299,10 +242,4 @@ defmodule Philomena.Topics do
|> Topic.title_changeset(attrs) |> Topic.title_changeset(attrs)
|> Repo.update() |> Repo.update()
end end
def clear_notification(_topic, nil), do: nil
def clear_notification(topic, user) do
Notifications.delete_unread_notification("Topic", topic.id, user)
end
end end