philomena/lib/philomena/interactions.ex
2025-02-06 21:56:12 -05:00

173 lines
4.6 KiB
Elixir

defmodule Philomena.Interactions do
import Ecto.Query
alias Philomena.ImageHides.ImageHide
alias Philomena.ImageFaves.ImageFave
alias Philomena.ImageVotes.ImageVote
alias Philomena.Images.Image
alias Philomena.Repo
alias Ecto.Multi
@doc """
Gets all interactions for a list of images for a given user.
Returns an empty list if no user is provided. Otherwise returns a list of maps containing:
- image_id: The ID of the image
- user_id: The ID of the user
- interaction_type: One of "hidden", "faved", or "voted"
- value: For votes, either "up" or "down". Empty string for other interaction types.
## Parameters
* images - List of images or image IDs to get interactions for
* user - The user to get interactions for, or nil
"""
def user_interactions(_images, nil),
do: []
def user_interactions(images, user) do
ids =
images
|> flatten_images()
|> Enum.uniq()
hide_interactions =
ImageHide
|> select([h], %{
image_id: h.image_id,
user_id: h.user_id,
interaction_type: ^"hidden",
value: ^""
})
|> where([h], h.image_id in ^ids)
|> where(user_id: ^user.id)
fave_interactions =
ImageFave
|> select([f], %{
image_id: f.image_id,
user_id: f.user_id,
interaction_type: ^"faved",
value: ^""
})
|> where([f], f.image_id in ^ids)
|> where(user_id: ^user.id)
upvote_interactions =
ImageVote
|> select([v], %{
image_id: v.image_id,
user_id: v.user_id,
interaction_type: ^"voted",
value: ^"up"
})
|> where([v], v.image_id in ^ids)
|> where(user_id: ^user.id, up: true)
downvote_interactions =
ImageVote
|> select([v], %{
image_id: v.image_id,
user_id: v.user_id,
interaction_type: ^"voted",
value: ^"down"
})
|> where([v], v.image_id in ^ids)
|> where(user_id: ^user.id, up: false)
[
hide_interactions,
fave_interactions,
upvote_interactions,
downvote_interactions
]
|> union_all_queries()
|> Repo.all()
end
@doc """
Migrates all interactions from one image to another.
Copies all hides, faves, and votes from the source image to the target image.
Updates the target image's counters to reflect the new interactions.
All operations are performed in a single transaction.
## Parameters
* source - The source Image struct to copy interactions from
* target - The target Image struct to copy interactions to
"""
def migrate_interactions(source, target) do
now = DateTime.utc_now(:second)
source = Repo.preload(source, [:hiders, :favers, :upvoters, :downvoters])
new_hides = Enum.map(source.hiders, &%{image_id: target.id, user_id: &1.id, created_at: now})
new_faves = Enum.map(source.favers, &%{image_id: target.id, user_id: &1.id, created_at: now})
new_upvotes =
Enum.map(
source.upvoters,
&%{image_id: target.id, user_id: &1.id, created_at: now, up: true}
)
new_downvotes =
Enum.map(
source.downvoters,
&%{image_id: target.id, user_id: &1.id, created_at: now, up: false}
)
Multi.new()
|> Multi.run(:hides, fn repo, %{} ->
{count, nil} = repo.insert_all(ImageHide, new_hides, on_conflict: :nothing)
{:ok, count}
end)
|> Multi.run(:faves, fn repo, %{} ->
{count, nil} = repo.insert_all(ImageFave, new_faves, on_conflict: :nothing)
{:ok, count}
end)
|> Multi.run(:upvotes, fn repo, %{} ->
{count, nil} = repo.insert_all(ImageVote, new_upvotes, on_conflict: :nothing)
{:ok, count}
end)
|> Multi.run(:downvotes, fn repo, %{} ->
{count, nil} = repo.insert_all(ImageVote, new_downvotes, on_conflict: :nothing)
{:ok, count}
end)
|> Multi.run(:image, fn repo,
%{hides: hides, faves: faves, upvotes: upvotes, downvotes: downvotes} ->
image_query = where(Image, id: ^target.id)
repo.update_all(
image_query,
inc: [
hides_count: hides,
faves_count: faves,
upvotes_count: upvotes,
downvotes_count: downvotes,
score: upvotes - downvotes
]
)
{:ok, nil}
end)
|> Repo.transaction()
end
defp union_all_queries([query]),
do: query
defp union_all_queries([query | rest]),
do: query |> union_all(^union_all_queries(rest))
defp flatten_images(images) do
Enum.flat_map(images, fn
nil -> []
%{id: id} -> [id]
{%{id: id}, _hit} -> [id]
enum -> flatten_images(enum)
end)
end
end