image hiding

This commit is contained in:
byte[] 2019-12-07 23:53:18 -05:00
parent 756d2c91a6
commit f9f6518d97
11 changed files with 270 additions and 12 deletions

View file

@ -9,12 +9,14 @@ defmodule Philomena.Images do
alias Philomena.Repo
alias Philomena.Images.Image
alias Philomena.Images.Hider
alias Philomena.Images.Uploader
alias Philomena.SourceChanges.SourceChange
alias Philomena.TagChanges.TagChange
alias Philomena.Tags
alias Philomena.Tags.Tag
alias Philomena.Notifications
alias Philomena.Interactions
@doc """
Gets a single image.
@ -199,6 +201,73 @@ defmodule Philomena.Images do
}
end
def hide_image(%Image{} = image, user, attrs) do
Image.hide_changeset(image, attrs, user)
|> internal_hide_image()
end
def merge_image(%Image{} = image, duplicate_of_image) do
result =
Image.merge_changeset(image, duplicate_of_image)
|> internal_hide_image()
case result do
{:ok, _changes} ->
Interactions.migrate_interactions(image, duplicate_of_image)
result
_error ->
result
end
end
defp internal_hide_image(changeset) do
Multi.new
|> Multi.update(:image, changeset)
|> 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{} = 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
@doc """
Deletes a Image.

View file

@ -0,0 +1,34 @@
defmodule Philomena.Images.Hider do
@moduledoc """
Hiding logic for images.
"""
alias Philomena.Images.Image
def hide_thumbnails(image, key) do
source = image_thumb_dir(image)
target = image_thumb_dir(image, key)
File.rename!(source, target)
end
def unhide_thumbnails(image, key) do
source = image_thumb_dir(image, key)
target = image_thumb_dir(image)
File.rename!(source, target)
end
# fixme: these are copied from the thumbnailer
defp image_thumb_dir(%Image{created_at: created_at, id: id}),
do: Path.join([image_thumbnail_root(), time_identifier(created_at), to_string(id)])
defp image_thumb_dir(%Image{created_at: created_at, id: id}, key),
do: Path.join([image_thumbnail_root(), time_identifier(created_at), to_string(id) <> "-" <> key])
defp time_identifier(time),
do: Enum.join([time.year, time.month, time.day], "/")
defp image_thumbnail_root,
do: Application.get_env(:philomena, :image_file_root) <> "/thumbs"
end

View file

@ -179,6 +179,30 @@ defmodule Philomena.Images.Image do
|> validate_length(:description, max: 50_000, count: :bytes)
end
def hide_changeset(image, attrs, user) do
image
|> cast(attrs, [:deletion_reason])
|> put_change(:deleter_id, user.id)
|> put_change(:hidden_image_key, create_key())
|> put_change(:hidden_from_users, true)
|> validate_required([:deletion_reason, :deleter_id])
end
def merge_changeset(image, duplicate_of_image) do
change(image)
|> put_change(:duplicate_id, duplicate_of_image.id)
|> put_change(:hidden_image_key, create_key())
|> put_change(:hidden_from_users, true)
end
def unhide_changeset(image) do
change(image)
|> put_change(:deleter_id, nil)
|> put_change(:hidden_image_key, nil)
|> put_change(:hidden_from_users, false)
|> put_change(:deletion_reason, nil)
end
def cache_changeset(image) do
changeset = change(image)
image = apply_changes(changeset)
@ -225,4 +249,8 @@ defmodule Philomena.Images.Image do
{tag_list_cache, tag_list_plus_alias_cache, file_name_cache}
end
defp create_key do
Base.encode16(:crypto.strong_rand_bytes(6), case: :lower)
end
end

View file

@ -4,7 +4,9 @@ defmodule Philomena.Interactions do
alias Philomena.ImageHides.ImageHide
alias Philomena.ImageFaves.ImageFave
alias Philomena.ImageVotes.ImageVote
alias Philomena.Images.Image
alias Philomena.Repo
alias Ecto.Multi
def user_interactions(_images, nil),
do: []
@ -53,6 +55,46 @@ defmodule Philomena.Interactions do
|> Repo.all()
end
def migrate_interactions(source, target) do
now = DateTime.utc_now()
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: hides, faves: faves, upvotes: upvotes, downvotes: downvotes, score: upvotes - downvotes])
{:ok, nil}
end)
|> Repo.isolated_transaction(:serializable)
end
defp union_all_queries([query]),
do: query
defp union_all_queries([query | rest]),

View file

@ -0,0 +1,43 @@
defmodule PhilomenaWeb.Image.DeleteController do
use PhilomenaWeb, :controller
# N.B.: this would be Image.Hide, because it hides the image, but that is
# taken by the user action
alias Philomena.Images.Image
alias Philomena.Images
alias Philomena.Tags
plug PhilomenaWeb.CanaryMapPlug, create: :hide, delete: :hide
plug :load_and_authorize_resource, model: Image, id_name: "image_id", persisted: true
def create(conn, %{"image" => image_params}) do
image = conn.assigns.image
user = conn.assigns.current_user
case Images.hide_image(image, user, image_params) do
{:ok, %{image: image, tags: tags}} ->
Images.reindex_image(image)
Tags.reindex_tags(tags)
conn
|> put_flash(:info, "Image successfully hidden.")
|> redirect(to: Routes.image_path(conn, :show, image))
{:error, :image, changeset, _changes} ->
render(conn, "new.html", image: image, changeset: changeset)
end
end
def delete(conn, _params) do
image = conn.assigns.image
{:ok, %{image: image, tags: tags}} = Images.unhide_image(image)
Images.reindex_image(image)
Tags.reindex_tags(tags)
conn
|> put_flash(:info, "Image successfully unhidden.")
|> redirect(to: Routes.image_path(conn, :show, image))
end
end

View file

@ -73,9 +73,7 @@ defmodule PhilomenaWeb.ImageController do
{user_galleries, image_galleries} = image_and_user_galleries(image, conn.assigns.current_user)
render(
conn,
"show.html",
assigns = [
image: image,
comments: comments,
image_changeset: image_changeset,
@ -86,7 +84,13 @@ defmodule PhilomenaWeb.ImageController do
interactions: interactions,
watching: watching,
layout_class: "layout--wide"
)
]
if image.hidden_from_users do
render(conn, "deleted.html", assigns)
else
render(conn, "show.html", assigns)
end
end
def new(conn, _params) do
@ -157,9 +161,6 @@ defmodule PhilomenaWeb.ImageController do
|> put_flash(:info, "The image you were looking for has been marked a duplicate of the image below")
|> redirect(to: Routes.image_path(conn, :show, image.duplicate_id))
image.hidden_from_users ->
render(conn, "deleted.html", image: image)
true ->
conn
|> assign(:image, image)

View file

@ -105,6 +105,7 @@ defmodule PhilomenaWeb.Router do
resources "/subscription", Image.SubscriptionController, only: [:create, :delete], singleton: true
resources "/read", Image.ReadController, only: [:create], singleton: true
resources "/comments", Image.CommentController, only: [:edit, :update]
resources "/delete", Image.DeleteController, only: [:create, :delete], singleton: true
end
resources "/forums", ForumController, only: [] do

View file

@ -8,7 +8,7 @@
=< link("your current filter", to: Routes.filter_path(@conn, :show, @conn.assigns.current_filter), class: "filter-link")
' .
#image_target.hidden.image-show data-scaled="true" data-uris=Jason.encode!(thumb_urls(@image, false)) data-width=@image.image_width data-height=@image.image_height
#image_target.hidden.image-show data-scaled="true" data-uris=Jason.encode!(thumb_urls(@image, can?(@conn, :hide, @image))) data-width=@image.image_width data-height=@image.image_height
= if @image.image_mime_type == "video/webm" do
video controls=true
- else

View file

@ -1,3 +1,5 @@
- display_mod_tools? = can?(@conn, :hide, @image)
#image_options_area
.block__header.block__header--js-tabbed
a href="#" data-click-tab="reporting" data-load-tab=Routes.image_reporting_path(@conn, :show, @image)
@ -9,6 +11,16 @@
a href="#" data-click-tab="favoriters" data-load-tab=Routes.image_favorites_path(@conn, :index, @image)
i.fa.fa-star>
| List favoriters
= if display_mod_tools? do
a href="#" data-click-tab="replace"
i.fa.fa-upload>
| Replace
a href="#" data-click-tab="administration"
i.fa.fa-toolbox>
| Administrate
= if present?(@image.scratchpad) do
i.fa.fa-sticky-note.fa--important<
i.fa.fa-exclamation.fa--important
.block__tab.hidden data-tab="favoriters"
p Loading...
@ -52,4 +64,29 @@
| Copy
br
textarea.input.input--wide.input--separate-top#bbcode_embed_thumbnail_tag rows="2" cols="100" readonly="readonly"
= "[img]#{thumb_url(@image, false, :medium)}[/img]\n[url=#{Routes.image_url(@conn, :show, @image)}]View on Derpibooru[/url]#{source_link}"
= "[img]#{thumb_url(@image, false, :medium)}[/img]\n[url=#{Routes.image_url(@conn, :show, @image)}]View on Derpibooru[/url]#{source_link}"
= if display_mod_tools? do
.block__tab.hidden data-tab="replace"
/= form_tag image_file_path(@image), method: :put, multipart: true do
= render partial: 'layouts/image_upload', locals: { form: nil, field: :image }
= submit_tag 'Save changes', class: 'button', autocomplete: 'off', data: { disable_with: 'Replacing...' }
.block__tab.hidden data-tab="administration"
.block.block--danger
a.button.button--link> href="#"
i.far.fa-edit
= if present?(@image.scratchpad) do
strong> Mod notes:
= escape_nl2br @image.scratchpad
- else
em No mod notes present
= if not @image.hidden_from_users do
= form_for @changeset, Routes.image_delete_path(@conn, :create, @image), [method: "post"], fn f ->
= label f, :deletion_reason, "Deletion reason (cannot be empty)"
.field.field--inline
= text_input f, :deletion_reason, class: "input input--wide", placeholder: "Rule violation", required: true
= submit "Delete", class: "button button--state-danger button--separate-left"
- else
= button_to "Restore", Routes.image_delete_path(@conn, :delete, @image), method: "delete", class: "button button--state-success"

View file

@ -6,7 +6,7 @@
strong
= @image.deletion_reason || "Unknown (likely deleted in error). Please contact a moderator."
= if can?(@conn, :undelete, @image) do
= if can?(@conn, :hide, @image) do
p
strong> Spoilers!
' Done by:
@ -20,4 +20,7 @@
=> link "tagging guidelines", to: "/pages/tags"
' and
= link "rules of the site", to: "/pages/rules"
' . Other useful links can be found at the bottom of the page.
' . Other useful links can be found at the bottom of the page.
= if can?(@conn, :hide, @image) do
= render PhilomenaWeb.ImageView, "show.html", assigns

View file

@ -11,7 +11,7 @@
= render PhilomenaWeb.ImageView, "_tags.html", image: @image, tag_change_count: @tag_change_count, changeset: @image_changeset, conn: @conn
= render PhilomenaWeb.ImageView, "_source.html", image: @image, source_change_count: @source_change_count, changeset: @image_changeset, conn: @conn
= render PhilomenaWeb.ImageView, "_options.html", image: @image, conn: @conn
= render PhilomenaWeb.ImageView, "_options.html", image: @image, changeset: @image_changeset, conn: @conn
h4 Comments
= cond do