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.Repo
alias Philomena.Images.Image alias Philomena.Images.Image
alias Philomena.Images.Hider
alias Philomena.Images.Uploader alias Philomena.Images.Uploader
alias Philomena.SourceChanges.SourceChange alias Philomena.SourceChanges.SourceChange
alias Philomena.TagChanges.TagChange alias Philomena.TagChanges.TagChange
alias Philomena.Tags alias Philomena.Tags
alias Philomena.Tags.Tag alias Philomena.Tags.Tag
alias Philomena.Notifications alias Philomena.Notifications
alias Philomena.Interactions
@doc """ @doc """
Gets a single image. Gets a single image.
@ -199,6 +201,73 @@ defmodule Philomena.Images do
} }
end 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 """ @doc """
Deletes a Image. 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) |> validate_length(:description, max: 50_000, count: :bytes)
end 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 def cache_changeset(image) do
changeset = change(image) changeset = change(image)
image = apply_changes(changeset) image = apply_changes(changeset)
@ -225,4 +249,8 @@ defmodule Philomena.Images.Image do
{tag_list_cache, tag_list_plus_alias_cache, file_name_cache} {tag_list_cache, tag_list_plus_alias_cache, file_name_cache}
end end
defp create_key do
Base.encode16(:crypto.strong_rand_bytes(6), case: :lower)
end
end end

View file

@ -4,7 +4,9 @@ defmodule Philomena.Interactions do
alias Philomena.ImageHides.ImageHide alias Philomena.ImageHides.ImageHide
alias Philomena.ImageFaves.ImageFave alias Philomena.ImageFaves.ImageFave
alias Philomena.ImageVotes.ImageVote alias Philomena.ImageVotes.ImageVote
alias Philomena.Images.Image
alias Philomena.Repo alias Philomena.Repo
alias Ecto.Multi
def user_interactions(_images, nil), def user_interactions(_images, nil),
do: [] do: []
@ -53,6 +55,46 @@ defmodule Philomena.Interactions do
|> Repo.all() |> Repo.all()
end 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]), defp union_all_queries([query]),
do: query do: query
defp union_all_queries([query | rest]), 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) {user_galleries, image_galleries} = image_and_user_galleries(image, conn.assigns.current_user)
render( assigns = [
conn,
"show.html",
image: image, image: image,
comments: comments, comments: comments,
image_changeset: image_changeset, image_changeset: image_changeset,
@ -86,7 +84,13 @@ defmodule PhilomenaWeb.ImageController do
interactions: interactions, interactions: interactions,
watching: watching, watching: watching,
layout_class: "layout--wide" layout_class: "layout--wide"
) ]
if image.hidden_from_users do
render(conn, "deleted.html", assigns)
else
render(conn, "show.html", assigns)
end
end end
def new(conn, _params) do 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") |> 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)) |> redirect(to: Routes.image_path(conn, :show, image.duplicate_id))
image.hidden_from_users ->
render(conn, "deleted.html", image: image)
true -> true ->
conn conn
|> assign(:image, image) |> assign(:image, image)

View file

@ -105,6 +105,7 @@ defmodule PhilomenaWeb.Router do
resources "/subscription", Image.SubscriptionController, only: [:create, :delete], singleton: true resources "/subscription", Image.SubscriptionController, only: [:create, :delete], singleton: true
resources "/read", Image.ReadController, only: [:create], singleton: true resources "/read", Image.ReadController, only: [:create], singleton: true
resources "/comments", Image.CommentController, only: [:edit, :update] resources "/comments", Image.CommentController, only: [:edit, :update]
resources "/delete", Image.DeleteController, only: [:create, :delete], singleton: true
end end
resources "/forums", ForumController, only: [] do 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") =< 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 = if @image.image_mime_type == "video/webm" do
video controls=true video controls=true
- else - else

View file

@ -1,3 +1,5 @@
- display_mod_tools? = can?(@conn, :hide, @image)
#image_options_area #image_options_area
.block__header.block__header--js-tabbed .block__header.block__header--js-tabbed
a href="#" data-click-tab="reporting" data-load-tab=Routes.image_reporting_path(@conn, :show, @image) 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) a href="#" data-click-tab="favoriters" data-load-tab=Routes.image_favorites_path(@conn, :index, @image)
i.fa.fa-star> i.fa.fa-star>
| List favoriters | 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" .block__tab.hidden data-tab="favoriters"
p Loading... p Loading...
@ -53,3 +65,28 @@
br br
textarea.input.input--wide.input--separate-top#bbcode_embed_thumbnail_tag rows="2" cols="100" readonly="readonly" 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 strong
= @image.deletion_reason || "Unknown (likely deleted in error). Please contact a moderator." = @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 p
strong> Spoilers! strong> Spoilers!
' Done by: ' Done by:
@ -21,3 +21,6 @@
' and ' and
= link "rules of the site", to: "/pages/rules" = 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, "_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, "_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 h4 Comments
= cond do = cond do