philomena/lib/philomena/images.ex
2020-01-31 23:50:50 +00:00

603 lines
15 KiB
Elixir

defmodule Philomena.Images do
@moduledoc """
The Images context.
"""
import Ecto.Query, warn: false
alias Ecto.Multi
alias Philomena.Repo
alias Philomena.Elasticsearch
alias Philomena.DuplicateReports.DuplicateReport
alias Philomena.Images.Image
alias Philomena.Images.Hider
alias Philomena.Images.Uploader
alias Philomena.Images.Tagging
alias Philomena.ImageFeatures.ImageFeature
alias Philomena.SourceChanges.SourceChange
alias Philomena.TagChanges.TagChange
alias Philomena.Tags
alias Philomena.Tags.Tag
alias Philomena.Notifications
alias Philomena.Interactions
alias Philomena.Reports.Report
alias Philomena.Comments
@doc """
Gets a single image.
Raises `Ecto.NoResultsError` if the Image does not exist.
## Examples
iex> get_image!(123)
%Image{}
iex> get_image!(456)
** (Ecto.NoResultsError)
"""
def get_image!(id) do
Repo.one!(Image |> where(id: ^id) |> preload(:tags))
end
@doc """
Creates a image.
## Examples
iex> create_image(%{field: value})
{:ok, %Image{}}
iex> create_image(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_image(attribution, attrs \\ %{}) do
tags = Tags.get_or_create_tags(attrs["tag_input"])
image =
%Image{}
|> Image.creation_changeset(attrs, attribution)
|> Image.tag_changeset(attrs, [], tags)
|> Uploader.analyze_upload(attrs)
Multi.new()
|> Multi.insert(:image, image)
|> Multi.run(:name_caches, fn repo, %{image: image} ->
image
|> Image.cache_changeset()
|> repo.update()
end)
|> Multi.run(:source_change, fn repo, %{image: image} ->
%SourceChange{image_id: image.id, initial: true}
|> SourceChange.creation_changeset(attrs, attribution)
|> repo.insert()
end)
|> Multi.run(:added_tag_count, fn repo, %{image: image} ->
tag_ids = image.added_tags |> Enum.map(& &1.id)
tags = Tag |> where([t], t.id in ^tag_ids)
{count, nil} = repo.update_all(tags, inc: [images_count: 1])
{:ok, count}
end)
|> Multi.run(:subscribe, fn _repo, %{image: image} ->
create_subscription(image, attribution[:user])
end)
|> Multi.run(:after, fn _repo, %{image: image} ->
Uploader.persist_upload(image)
Uploader.unpersist_old_upload(image)
{:ok, nil}
end)
|> Repo.isolated_transaction(:serializable)
end
def feature_image(featurer, %Image{} = image) do
image = Repo.preload(image, :tags)
[featured] = Tags.get_or_create_tags("featured image")
feature =
%ImageFeature{user_id: featurer.id, image_id: image.id}
|> ImageFeature.changeset(%{})
image =
image
|> Image.tag_changeset(%{}, image.tags, [featured | image.tags])
|> Image.cache_changeset()
Multi.new()
|> Multi.insert(:feature, feature)
|> Multi.update(:image, image)
|> Multi.run(:added_tag_count, fn repo, %{image: image} ->
tag_ids = image.added_tags |> Enum.map(& &1.id)
tags = Tag |> where([t], t.id in ^tag_ids)
{count, nil} = repo.update_all(tags, inc: [images_count: 1])
{:ok, count}
end)
|> Repo.isolated_transaction(:serializable)
end
def lock_comments(%Image{} = image, locked) do
image
|> Image.lock_comments_changeset(locked)
|> Repo.update()
end
def lock_description(%Image{} = image, locked) do
image
|> Image.lock_description_changeset(locked)
|> Repo.update()
end
def lock_tags(%Image{} = image, locked) do
image
|> Image.lock_tags_changeset(locked)
|> Repo.update()
end
def remove_hash(%Image{} = image) do
image
|> Image.remove_hash_changeset()
|> Repo.update()
end
def update_scratchpad(%Image{} = image, attrs) do
image
|> Image.scratchpad_changeset(attrs)
|> Repo.update()
end
def remove_source_history(%Image{} = image) do
image
|> Repo.preload(:source_changes)
|> Image.remove_source_history_changeset()
|> Repo.update()
end
def repair_image(%Image{} = image) do
Image
|> where(id: ^image.id)
|> Repo.update_all(set: [thumbnails_generated: false, processed: false])
Philomena.Images.Thumbnailer.generate_thumbnails(image.id)
end
def update_file(%Image{} = image, attrs) do
image =
image
|> Image.changeset(attrs)
|> Uploader.analyze_upload(attrs)
Multi.new()
|> Multi.update(:image, image)
|> Multi.run(:after, fn _repo, %{image: image} ->
Uploader.persist_upload(image)
Uploader.unpersist_old_upload(image)
{:ok, nil}
end)
|> Repo.isolated_transaction(:serializable)
end
@doc """
Updates a image.
## Examples
iex> update_image(image, %{field: new_value})
{:ok, %Image{}}
iex> update_image(image, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_image(%Image{} = image, attrs) do
image
|> Image.changeset(attrs)
|> Repo.update()
end
def update_description(%Image{} = image, attrs) do
image
|> Image.description_changeset(attrs)
|> Repo.update()
end
def update_source(%Image{} = image, attribution, attrs) do
image_changes =
image
|> Image.source_changeset(attrs)
source_changes =
Ecto.build_assoc(image, :source_changes)
|> SourceChange.creation_changeset(attrs, attribution)
Multi.new()
|> Multi.update(:image, image_changes)
|> Multi.run(:source_change, fn repo, _changes ->
case image_changes.changes do
%{source_url: _new_source} ->
repo.insert(source_changes)
_ ->
{:ok, nil}
end
end)
|> Repo.isolated_transaction(:serializable)
end
def update_tags(%Image{} = image, attribution, attrs) do
old_tags = Tags.get_or_create_tags(attrs["old_tag_input"])
new_tags = Tags.get_or_create_tags(attrs["tag_input"])
Multi.new()
|> Multi.run(:image, fn repo, _chg ->
image
|> repo.preload(:tags)
|> Image.tag_changeset(%{}, old_tags, new_tags)
|> repo.update()
|> case do
{:ok, image} ->
{:ok, {image, image.added_tags, image.removed_tags}}
error ->
error
end
end)
|> Multi.run(:added_tag_changes, fn repo, %{image: {image, added_tags, _removed}} ->
tag_changes =
added_tags
|> Enum.map(&tag_change_attributes(attribution, image, &1, true, attribution[:user]))
{count, nil} = repo.insert_all(TagChange, tag_changes)
{:ok, count}
end)
|> Multi.run(:removed_tag_changes, fn repo, %{image: {image, _added, removed_tags}} ->
tag_changes =
removed_tags
|> Enum.map(&tag_change_attributes(attribution, image, &1, false, attribution[:user]))
{count, nil} = repo.insert_all(TagChange, tag_changes)
{:ok, count}
end)
|> Multi.run(:added_tag_count, fn
_repo, %{image: {%{hidden_from_users: true}, _added, _removed}} ->
{:ok, 0}
repo, %{image: {_image, added_tags, _removed}} ->
tag_ids = added_tags |> Enum.map(& &1.id)
tags = Tag |> where([t], t.id in ^tag_ids)
{count, nil} = repo.update_all(tags, inc: [images_count: 1])
{:ok, count}
end)
|> Multi.run(:removed_tag_count, fn
_repo, %{image: {%{hidden_from_users: true}, _added, _removed}} ->
{:ok, 0}
repo, %{image: {_image, _added, removed_tags}} ->
tag_ids = removed_tags |> Enum.map(& &1.id)
tags = Tag |> where([t], t.id in ^tag_ids)
{count, nil} = repo.update_all(tags, inc: [images_count: -1])
{:ok, count}
end)
|> Repo.isolated_transaction(:serializable)
end
defp tag_change_attributes(attribution, image, tag, added, user) do
now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
user_id =
case user do
nil -> nil
user -> user.id
end
%{
image_id: image.id,
tag_id: tag.id,
user_id: user_id,
created_at: now,
updated_at: now,
tag_name_cache: tag.name,
ip: attribution[:ip],
fingerprint: attribution[:fingerprint],
user_agent: attribution[:user_agent],
referrer: attribution[:referrer],
added: added
}
end
def update_uploader(%Image{} = image, attrs) do
image
|> Image.uploader_changeset(attrs)
|> Repo.update()
end
def hide_image(%Image{} = image, user, attrs) do
DuplicateReport
|> where(state: "open")
|> where([d], d.image_id == ^image.id or d.duplicate_of_image_id == ^image.id)
|> Repo.update_all(set: [state: "rejected"])
Image.hide_changeset(image, attrs, user)
|> internal_hide_image(image)
end
def update_hide_reason(%Image{} = image, attrs) do
image
|> Image.hide_reason_changeset(attrs)
|> Repo.update()
end
def merge_image(%Image{} = image, duplicate_of_image) do
result =
Image.merge_changeset(image, duplicate_of_image)
|> internal_hide_image(image)
case result do
{:ok, changes} ->
update_first_seen_at(
duplicate_of_image,
image.first_seen_at,
duplicate_of_image.first_seen_at
)
tags = Tags.copy_tags(image, duplicate_of_image)
Comments.migrate_comments(image, duplicate_of_image)
Interactions.migrate_interactions(image, duplicate_of_image)
{:ok, %{changes | tags: changes.tags ++ tags}}
_error ->
result
end
end
defp update_first_seen_at(image, time_1, time_2) do
min_time =
case NaiveDateTime.compare(time_1, time_2) do
:gt -> time_2
_ -> time_1
end
Image
|> where(id: ^image.id)
|> Repo.update_all(set: [first_seen_at: min_time])
end
defp internal_hide_image(changeset, image) do
reports =
Report
|> where(reportable_type: "Image", reportable_id: ^image.id)
|> select([r], r.id)
|> update(set: [open: false, state: "closed"])
Multi.new()
|> Multi.update(:image, changeset)
|> Multi.update_all(:reports, reports, [])
|> Multi.run(:tags, fn repo, %{image: image} ->
image = Repo.preload(image, :tags, force: true)
# I'm not convinced this is a good idea. It leads
# to way too much drift, and the index has to be
# maintained.
tag_ids = Enum.map(image.tags, & &1.id)
query = where(Tag, [t], t.id in ^tag_ids)
repo.update_all(query, inc: [images_count: -1])
{:ok, image.tags}
end)
|> Multi.run(:file, fn _repo, %{image: image} ->
Hider.hide_thumbnails(image, image.hidden_image_key)
{:ok, nil}
end)
|> Repo.isolated_transaction(:serializable)
end
def unhide_image(%Image{hidden_from_users: true} = image) do
key = image.hidden_image_key
Multi.new()
|> Multi.update(:image, Image.unhide_changeset(image))
|> Multi.run(:tags, fn repo, %{image: image} ->
image = Repo.preload(image, :tags, force: true)
tag_ids = Enum.map(image.tags, & &1.id)
query = where(Tag, [t], t.id in ^tag_ids)
repo.update_all(query, inc: [images_count: 1])
{:ok, image.tags}
end)
|> Multi.run(:file, fn _repo, %{image: image} ->
Hider.unhide_thumbnails(image, key)
{:ok, nil}
end)
|> Repo.isolated_transaction(:serializable)
end
def unhide_image(image), do: {:ok, image}
def batch_update(image_ids, added_tags, removed_tags, tag_change_attributes) do
image_ids =
Image
|> where([i], i.id in ^image_ids and i.hidden_from_users == false)
|> select([i], i.id)
|> Repo.all()
added_tags = Enum.map(added_tags, & &1.id)
removed_tags = Enum.map(removed_tags, & &1.id)
# Change everything in one go, ignoring any validation errors
# Note: computing the Cartesian product
insertions =
for tag_id <- added_tags, image_id <- image_ids do
%{tag_id: tag_id, image_id: image_id}
end
deletions =
Tagging
|> where([t], t.image_id in ^image_ids and t.tag_id in ^removed_tags)
|> select([t], [t.image_id, t.tag_id])
now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
tag_change_attributes = Map.merge(tag_change_attributes, %{created_at: now, updated_at: now})
Repo.transaction(fn ->
{added_count, inserted} =
Repo.insert_all(Tagging, insertions,
on_conflict: :nothing,
returning: [:image_id, :tag_id]
)
{removed_count, deleted} = Repo.delete_all(deletions)
inserted = Enum.map(inserted, &[&1.image_id, &1.tag_id])
added_changes =
Enum.map(inserted, fn [image_id, tag_id] ->
Map.merge(tag_change_attributes, %{image_id: image_id, tag_id: tag_id, added: true})
end)
removed_changes =
Enum.map(deleted, fn [image_id, tag_id] ->
Map.merge(tag_change_attributes, %{image_id: image_id, tag_id: tag_id, added: false})
end)
changes = added_changes ++ removed_changes
Repo.insert_all(TagChange, changes)
Repo.update_all(where(Tag, [t], t.id in ^added_tags), inc: [images_count: added_count])
Repo.update_all(where(Tag, [t], t.id in ^removed_tags), inc: [images_count: -removed_count])
end)
end
@doc """
Deletes a Image.
## Examples
iex> delete_image(image)
{:ok, %Image{}}
iex> delete_image(image)
{:error, %Ecto.Changeset{}}
"""
def delete_image(%Image{} = image) do
Repo.delete(image)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking image changes.
## Examples
iex> change_image(image)
%Ecto.Changeset{source: %Image{}}
"""
def change_image(%Image{} = image) do
Image.changeset(image, %{})
end
def reindex_image(%Image{} = image) do
reindex_images([image.id])
image
end
def reindex_images(image_ids) do
spawn(fn ->
Image
|> preload(^indexing_preloads())
|> where([i], i.id in ^image_ids)
|> Elasticsearch.reindex(Image)
end)
image_ids
end
def indexing_preloads do
[
:user,
:favers,
:downvoters,
:upvoters,
:hiders,
:deleter,
:gallery_interactions,
tags: [:aliases, :aliased_tag]
]
end
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 clear_notification(_image, nil), do: nil
def clear_notification(image, user) do
Notifications.delete_unread_notification("Image", image.id, user)
end
end