mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-27 13:47:58 +01:00
644 lines
16 KiB
Elixir
644 lines
16 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.ThumbnailWorker
|
|
alias Philomena.DuplicateReports.DuplicateReport
|
|
alias Philomena.Images.Image
|
|
alias Philomena.Images.Hider
|
|
alias Philomena.Images.Uploader
|
|
alias Philomena.Images.Tagging
|
|
alias Philomena.Images.ElasticsearchIndex, as: ImageIndex
|
|
alias Philomena.ImageFeatures.ImageFeature
|
|
alias Philomena.SourceChanges.SourceChange
|
|
alias Philomena.TagChanges.TagChange
|
|
alias Philomena.Tags
|
|
alias Philomena.UserStatistics
|
|
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)
|
|
|> Image.dnp_changeset(attribution[:user])
|
|
|> 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)
|
|
|> case do
|
|
{:ok, %{image: image}} = result ->
|
|
repair_image(image)
|
|
reindex_image(image)
|
|
Tags.reindex_tags(image.added_tags)
|
|
UserStatistics.inc_stat(attribution[:user], :uploads)
|
|
|
|
result
|
|
|
|
result ->
|
|
result
|
|
end
|
|
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])
|
|
|
|
Exq.enqueue(Exq, queue(image.image_mime_type), ThumbnailWorker, [image.id])
|
|
end
|
|
|
|
defp queue("video/webm"), do: "videos"
|
|
defp queue(_mime_type), do: "images"
|
|
|
|
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 update_anonymous(%Image{} = image, attrs) do
|
|
image
|
|
|> Image.anonymous_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)
|
|
|> case do
|
|
{:ok, _} = result ->
|
|
reindex_images(image_ids)
|
|
Tags.reindex_tags(Enum.map(added_tags ++ removed_tags, &%{id: &1}))
|
|
|
|
result
|
|
|
|
result ->
|
|
result
|
|
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 user_name_reindex(old_name, new_name) do
|
|
data = ImageIndex.user_name_update_by_query(old_name, new_name)
|
|
|
|
Elasticsearch.update_by_query(Image, data.query, data.set_replacements, data.replacements)
|
|
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
|