Takedowns

This commit is contained in:
Kira 2019-02-23 08:45:10 -08:00
parent d0deccf402
commit 118459af1b
14 changed files with 1229 additions and 2 deletions

View File

@ -0,0 +1,184 @@
class TakedownsController < ApplicationController
respond_to :html, :xml, :json
before_action :admin_only, only: [:update, :destroy, :add_by_ids, :add_by_tags, :count_matching_posts, :remove_by_id]
def index
@takedowns = Takedown.search(search_params).paginate(params[:page], limit: params[:limit])
respond_with(@takedowns)
end
def destroy
@takedown = Takedown.find(params[:id])
@takedown.destroy
end
def show
@takedown = Takedown.find(params[:id])
@show_instructions = (CurrentUser.ip_addr == @takedown.creator_ip_addr) || (@takedown.vericode == params[:code])
respond_with(@takedown, @show_instructions)
end
def new
@takedown = Takedown.new
respond_with(@takedown)
end
def create
@takedown = Takedown.create(takedown_params)
flash[:notice] = @takedown.valid? ? "Takedown created" : @takedown.errors.full_messages.join(". ")
if @takedown.valid?
redirect_to(takedown_url(@takedown, code: @takedown.vericode))
else
respond_with(@takedown)
end
end
def update
takedown = Takedown.find(params[:id])
takedown.notes = params[:takedown][:notes]
takedown.reason_hidden = params[:takedown][:reason_hidden]
takedown.approver = current_user.id
# If the takedown is pending or inactive, and the new status is pending or inactive
if ["pending", "inactive"].include?(takedown.status) && ["pending", "inactive"].include?(params[:takedown][:status])
takedown.status = params[:takedown][:status]
end
if params[:process_takedown]
# Handle posts, delete ones marked for deletion
if params[:takedown_posts]
params[:takedown_posts].each do |post_id, value|
takedown_post = TakedownPost.find_by_takedown_id_and_post_id(takedown.id, post_id)
takedown_post.status = status = (value == "1" ? "deleted" : "kept")
takedown_post.save
if takedown_post.status == "deleted"
takedown_post.post.undelete!(current_user) if takedown_post.post.is_deleted?
delete_reason = params[:delete_reason].presence || "Artist requested removal"
Resque.enqueue(
DeletePost,
post_id,
"takedown ##{takedown.id}: #{delete_reason}",
current_user.id,
false) #Do not transfer favorites on takedowns.
end
if takedown_post.post.status == "deleted" && takedown_post.status == "kept"
takedown_post.post.undelete!(current_user)
end
end
end
# Calculate and update the status (approved, partial, denied) based on number of kept/deleted posts
takedown.status = takedown.calculated_status
ModAction.create(user_id: current_user.id, action: "completed_takedown", values: {takedown_id: takedown.id})
end
if takedown.save
respond_to_success("Request updated, status set to #{takedown.status}", {action: "show", id: takedown.id})
if params[:takedown][:process_takedown] && takedown.email.include?("@")
begin
UserMailer::deliver_takedown_updated(takedown, current_user)
rescue Net::SMTPAuthenticationError, Net::SMTPServerBusy, Net::SMTPSyntaxError, Net::SMTPFatalError, Net::SMTPUnknownError => e
flash[:error] = 'Error emailing: ' + e.message
end
end
else
respond_to_error(takedown, action: "show", id: takedown.id)
end
end
def add_by_ids
begin
takedown = Takedown.find(params[:id])
rescue ActiveRecord::RecordNotFound => x
respond_to_error("Takedown ##{params[:id]} not found", action: "index")
return
end
added_post_ids = takedown.add_post_ids(params[:post_ids])
api_return = {added_count: added_post_ids.length, added_post_ids: added_post_ids}
respond_to do |fmt|
fmt.html {respond_to_success("#{added_post_ids.length} posts added to takedown ##{params[:id]}", action: "show", id: params[:id])}
fmt.xml {render xml: api_return.to_xml}
fmt.json {render json: api_return.to_json, callback: params[:callback]}
end
end
def add_by_tags
begin
takedown = Takedown.find(params[:id])
rescue ActiveRecord::RecordNotFound => x
respond_to_error("Takedown ##{params[:id]} not found", action: "index")
return
end
posts = Post.find_by_sql(Post.generate_sql(
QueryParser.parse(params[:tags].to_s + " status:any order:id_asc").join(" "),
user: current_user,
select: "posts.id"
))
# Collect all post ids into an array
post_ids = posts.map(&:id)
added_post_ids = takedown.add_post_ids(post_ids)
api_return = {added_count: added_post_ids.length, added_post_ids: added_post_ids}
respond_to do |fmt|
fmt.html {respond_to_success("#{added_count} posts with tags '#{}' added to takedown ##{params[:id]}", action: "show", id: params[:id])}
fmt.xml {render xml: api_return.to_xml}
fmt.json {render json: api_return.to_json, callback: params[:callback]}
end
end
def count_matching_posts
posts = Post.find_by_sql(Post.generate_sql(
QueryParser.parse(params[:tags].to_s + " status:any").join(" "),
user: current_user,
select: "posts.id"
))
api_return = {matched_post_count: posts.length}
respond_to do |fmt|
fmt.xml {render xml: api_return.to_xml}
fmt.json {render json: api_return.to_json, callback: params[:callback]}
end
end
def remove_by_id
begin
takedown_post = TakedownPost.find_by_takedown_id_and_post_id(params[:id], params[:post_id])
rescue ActiveRecord::RecordNotFound => x
respond_to_error("Post ##{params[:post_id]} not found in takedown ##{params[:id]}", action: "show", id: params[:id])
return
end
takedown_post.destroy
respond_to_success("Post ##{params[:post_id]} removed from takedown ##{params[:id]}", action: "show", id: params[:id])
end
private
def search_params
permitted_params = %i[status]
if CurrentUser.is_admin?
permitted_params << %i[source reason ip_addr creator_id creator_name email vericode status order]
end
params.fetch(:search, {}).permit(*permitted_params)
end
def takedown_params
permitted_params = %i[email source instructions reason post_ids reason_hidden]
if CurrentUser.is_admin?
permitted_params << %i[notes del_post_ids status]
end
params.require(:takedown).permit(*permitted_params, post_ids: [])
end
end

View File

@ -0,0 +1,10 @@
module TakedownsHelper
def pretty_status(takedown)
status = takedown.pretty_status
classes = {'inactive': 'sect_grey',
'denied': 'sect_red',
'partial': 'sect_green',
'approved': 'sect_green'}
tag.td(status, class: classes[takedown.status])
end
end

View File

@ -1377,7 +1377,7 @@ class Post < ApplicationRecord
end
def delete!(reason, options = {})
if is_status_locked?
if is_status_locked? && !options.fetch(:force, false)
self.errors.add(:is_status_locked, "; cannot delete post")
return false
end

202
app/models/takedown.rb Normal file
View File

@ -0,0 +1,202 @@
class Takedown < ApplicationRecord
belongs_to_creator
belongs_to :approver
before_validation :initialize_fields, on: :create
before_validation :normalize_post_ids
validates_presence_of :email
validates_presence_of :reason
validates_format_of :email, with: /\A([\s*A-Z0-9._%+-]+@[\s*A-Z0-9.-]+\.\s*[A-Z\s*]{2,15}\s*)\z/i, on: :create
validate :can_create_takedown
validate :valid_posts_or_instructions
validate :validate_number_of_posts
validate :validate_post_ids
after_validation :normalize_deleted_post_ids
before_save :update_post_count
PRETTY_STATUS = {
'partial': 'Partially Approved'
}
def pretty_status
PRETTY_STATUS.fetch(status, status.capitalize)
end
def initialize_fields
self.status = "pending"
self.vericode = Takedown.create_vericode
self.del_post_ids = ''
end
def self.create_vericode
consonants = "bcdfghjklmnpqrstvqxyz"
vowels = "aeiou"
pass = ""
4.times do
pass << consonants[rand(21), 1]
pass << vowels[rand(5), 1]
end
pass << rand(100).to_s
pass
end
module ValidationMethods
def valid_posts_or_instructions
errors[:base] << "You must provide post ids or instructions." if post_array.size <= 0 && instructions.blank?
end
def can_create_takedown
return if creator.is_mod?
errors[:base] << "You have created a takedown too recently" if self.where('creator_id = ? AND created_at > ?', creator_id, 5.minutes.ago).count > 0
errors[:base] << "You have created a takedown too recently" if self.where('creator_ip_addr = ? AND created_at > ?', creator_ip_addr, 5.minutes.ago).count > 0
end
def validate_number_of_posts
if post_array.size > 5_000
self.errors.add(:base, "You can only have 5000 posts in a takedown.")
return false
end
true
end
end
module AddPostMethods
def add_posts_by_ids!(ids)
with_lock do
self.post_ids = (post_array + ids.scan(/\d+/).uniq).join(' ')
save!
end
end
def add_posts_by_tags!(tag_string)
new_ids = Post.tag_match(tag_string).limit(1000).map(&:id)
add_posts_by_ids!(new_ids)
end
end
module PostMethods
def normalize_post_ids
self.post_ids = post_ids.scan(/\d+/).uniq.join(' ')
end
def normalize_deleted_post_ids
posts = post_ids.scan(/\d+/).uniq
del_posts = del_post_ids.scan(/\d+/).uniq
del_posts = del_posts & posts # ensure that all deleted posts are also posts
self.del_post_ids = del_posts.join(' ')
end
def validate_post_ids
temp_post_ids = Post.select(:id).where(id: post_array).map {|x| x.id.to_s}
self.post_ids = temp_post_ids.join(' ')
end
def self.validated_posts(ids)
Post.select(:id).where(id: ids).map {|x| x.id}.to_set
end
def del_post_array
@del_post_array ||= del_post_ids.scan(/\d+/).map(&:to_i).to_set
end
def actual_deleted_posts
Post.where(id: del_post_array)
end
def post_array
@post_array ||= post_ids.scan(/\d+/).map(&:to_i).to_set
end
def actual_kept_posts
Post.where(id: kept_post_array)
end
def kept_post_array
@kept_post_array ||= post_array - del_post_array
end
def clear_cached_arrays
@post_array = nil
@del_post_array = nil
@kept_post_array = nil
end
def update_post_count
normalize_post_ids
normalize_deleted_post_ids
clear_cached_arrays
self.post_count = del_post_array.size
end
end
module ProcessMethods
end
module SearchMethods
def search(params)
q = super
if params[:source].present?
q = q.where('source ILIKE ?', params[:source].to_escaped_for_sql_like)
end
if params[:reason].present?
q = q.where('reason ILIKE ?', params[:reason].to_escaped_for_sql_like)
end
if params[:ip_addr].present?
q = q.where('creator_ip_addr <<= ?', params[:ip_addr])
end
if params[:creator_id].present?
q = q.where('creator_id = ?', params[:creator_id])
end
if params[:creator_name].present?
q = q.where('takedowns.creator_id = (select _.id from users _ WHERE lower(_.name) ? ?)', params[:creator_name].tr(' ', '_').downcase)
end
if params[:email].present?
q = q.where('email ILIKE ?', params[:email].to_escaped_for_sql_like)
end
if params[:vericode].present?
q = q.where('vericode = ?', params[:vericode])
end
if params[:status].present?
q = q.where('status = ?', params[:status])
end
params[:order] ||= params.delete(:sort)
case params[:order]
when 'status'
q = q.order('status ASC')
when 'post_count'
q = q.order('post_count DESC')
else
q = q.order('id DESC')
end
q
end
end
module StatusMethods
def completed?
["approved", "denied", "partial"].include?(status)
end
def calculated_status
kept_count = kept_posts_array.size
deleted_count = del_posts_array.size
if kept_count == 0 # All were deleted, so it was approved
"approved"
elsif deleted_count == 0 # All were kept, so it was denied
"denied"
else # Some were kept and some were deleted, so it was partially approved
"partial"
end
end
end
include PostMethods
include ValidationMethods
include StatusMethods
include ProcessMethods
include SearchMethods
end

View File

@ -0,0 +1,55 @@
<% unless params[:show] %><div id='searchform_hide'><%= link_to_function "Show Search Options", "$j('#searchform').fadeIn('fast'); $('searchform_hide').hide();" %></div><% end %>
<div class='section' style='width:400px;<% unless params[:show] %>display:none;<% end %>' id='searchform'>
<% unless params[:show] %><%= link_to_function "Hide Search Options", "$j('#searchform').fadeOut('fast'); $('searchform_hide').show();" %><% end %>
<% form_tag({action: "index"}, method: :get) do %>
<table class='nomargin'>
<tr><td><label for="source">Source</label></td><td><%= text_field_tag "source", params[:source], style: "width:195px"%></td></tr>
<tr><td><label for="reason">Reason</label></td><td><%= text_field_tag "reason", params[:reason], style: "width:195px"%></td></tr>
<tr><td><label for="notes">Admin Response</label></td><td><%= text_field_tag "notes", params[:notes], style: "width:195px"%></td></tr>
<% if current_user.is_admin? %>
<tr><td><label for="reason_hidden">Reason hidden?</label></td><td>
<%= select_tag "reason_hidden", options_for_select([
["Any", "any"],
["Yes", "true"],
["No", "false"],
], params[:reason_hidden]), style: "width:200px;" %>
</td></tr>
<tr><td><label for="instructions">Instructions</label></td><td><%= text_field_tag "instructions", params[:instructions], style: "width:195px"%></td></tr>
<tr><td><label for="post_id">Post ID</label></td><td><%= text_field_tag "post_id", params[:post_id], style: "width:195px"%></td></tr>
<tr><td><label for="email">Email</label></td><td><%= text_field_tag "email", params[:email], style: "width:195px"%></td></tr>
<tr><td><label for="ip_addr">IP Address</label></td><td><%= text_field_tag "ip_addr", params[:ip_addr], style: "width:195px"%></td></tr>
<tr><td><label for="vericode">Vericode</label></td><td><%= text_field_tag "vericode", params[:vericode], style: "width:195px"%></td></tr>
<% end %>
<tr><td><label for="status">Status</label></td><td>
<%= select_tag "status", options_for_select([
["Any", "any"],
["Pending", "pending"],
["Inactive", "inactive"],
["Denied", "denied"],
["Partially Approved", "partial"],
["Approved", "approved"]
], params[:status]), style: "width:200px;" %>
</td></tr>
<tr><td><label for="order">Order</label></td><td>
<%= select_tag "order", options_for_select([
["Date", "date"],
["Source", "source"],
["Email", "email"],
["IP Address", "ip_addr"],
["Status", "status"],
["Post count", "post_count"]
], params[:order]), style: "width:200px;" %>
</td></tr>
<tr><td colspan="2"><%= submit_tag "Search", name: nil %></td></tr>
</table>
<% if params[:show] %>
<input type='hidden' name='show' value='1'/>
<% end %>
<% end %>
</div>
<% if params[:source] || params[:reason] || params[:notes] || params[:reason_hidden] || params[:instructions] || params[:post_id] || params[:email] || params[:ip_addr] || params[:vericode] || params[:status] || params[:order] %>
<script type='text/javascript'>$('searchform_hide').hide(); $('searchform').show();</script>
<% end %>

View File

@ -0,0 +1,9 @@
<% content_for(:secondary_links) do %>
<menu>
<%= subnav_link_to "Listing", takedowns_path %>
<%= subnav_link_to "New", new_takedown_path %>
<% if CurrentUser.is_admin? && params[:action] == 'show' %>
<%= subnav_link_to 'Delete', {action: 'destroy', id: @takedown.id}, confirm: 'Are you sure you want to dele this takedown?' %>
<% end %>
</menu>
<% end %>

View File

@ -0,0 +1,372 @@
<div id="c-takedowns">
<div id="c-edit">
<h2>Takedown #<%= @takedown.id %></h2>
<div class='section'>
<table style="margin-bottom:0px;">
<tr>
<td><span class='title'>Source</span></td>
<td>
<% if !@takedown.reason_hidden || current_user.is_admin? || @show_instructions %>
<% if @takedown.source.match(/^https?:\/\//i) %>
<a href='<%= h @takedown.source %>'><%= h @takedown.source %></a>
<% else %>
<a href='<%= h "http://" + @takedown.source %>'><%= h @takedown.source %></a>
<% end %>
<% else %>
<span class="redtext">[Source hidden by submitter]</span>
<% end %>
</td>
</tr>
<tr>
<td><span class='title'>Reason</span></td>
<% if !@takedown.reason_hidden || current_user.is_admin? || @show_instructions %>
<td><%= h @takedown.reason %>
<% if @takedown.reason_hidden %><span class="redtext">(HIDDEN)</span>
<% end %></td>
<% else %>
<td><span class="redtext">[Reason hidden by submitter]</span></td>
<% end %>
</tr>
<tr>
<td colspan="2">&nbsp;</td>
</tr>
<% if current_user.is_admin? %>
<tr>
<td><span class='title'>Vericode</span></td>
<td><%= @takedown.vericode %></td>
</tr>
<tr>
<td><span class='title'>Email</span></td>
<td><a href="mailto:<%= h @takedown.email %>"><%= h @takedown.email %></a></td>
</tr>
<tr>
<td><span class='title'>IP Addr</span></td>
<td><%= ip_to_link(@takedown.ip_addr) %></td>
</tr>
<tr>
<td><span class='title'>User</span></td>
<td><%= link_to_if(@takedown.user, @takedown.user_name, controller: :user, action: :show, id: @takedown.user ? @takedown.user.id : 0) %></td>
</tr>
<tr>
<td colspan="2">&nbsp;</td>
</tr>
<% end %>
<tr>
<td><span class='title'>Created</span></td>
<td style="cursor:help;" title="<%= @takedown.created_at.strftime("%b %d, %Y %I:%M %p") %>"><%= time_ago_in_words(@takedown.created_at) %>
ago
</td>
</tr>
<% if @takedown.created_at != @takedown.updated_at %>
<tr>
<td><span class='title'>Handled</span></td>
<td style="cursor:help;" title="<%= @takedown.updated_at.strftime("%b %d, %Y %I:%M %p") %>"><%= time_ago_in_words(@takedown.updated_at) %>
ago
</td>
</tr>
<% end %>
<tr>
<td><span class='title'>Status</span></td>
<td>
<% if @takedown.status == "pending" %>
Pending
<% elsif @takedown.status == "inactive" %>
Inactive
<% elsif @takedown.status == "partial" %>
<span class='yellowtext'>Partially Approved</span>
<% elsif @takedown.status == "denied" %>
<span class='redtext'>Denied</span>
<% elsif @takedown.status == "approved" %>
<span class='greentext'>Approved</span>
<% end %>
</td>
</tr>
<% if @takedown.status != "pending" %>
<tr>
<td><span class='title'>Approver</span></td>
<td><%= link_to User.find(@takedown.approver).name, {controller: "user", action: "show", id: @takedown.approver} %></td>
</tr>
<% end %>
</table>
</div>
<% if !@takedown.notes.blank? && @takedown.notes.downcase != "none" %>
<h3>Admin notes</h3>
<div class="section">
<% if !@takedown.reason_hidden || current_user.is_admin? %>
<%= format_text(@takedown.notes) %>
<% if @takedown.reason_hidden %><span class="redtext">(HIDDEN)</span>
<% end %>
<% else %>
<span class="redtext">[Admin notes hidden]</span>
<% end %>
</div>
<% end %>
<% if @show_instructions || current_user.is_admin? %>
<% if !@takedown.instructions.blank? %>
<h3>Special instructions</h3>
<div class="section">
<%= h @takedown.instructions %>
</div>
<% end %>
<% if @takedown.status == "pending" && !current_user.is_admin? && !@takedown.takedown_posts.blank? %>
<div class='section'>
<p>The following posts are up for dispute:</p>
<% @takedown.kept_posts.each do |post| %>
<%= link_to("post ##{post.post_id}", {controller: "post", action: "show", id: post.post_id}) %><br>
<% end %>
</div>
<% elsif @takedown.status == "inactive" && !@takedown.takedown_posts.blank? %>
<div class='section sect_grey'>
<p style="margin-bottom:0px;">This takedown request has been marked as inactive as the submitter has not
responded in a reasonable time frame. It will be handled once the submitter responds.</p>
<% if !current_user.is_admin? %>
<br>
<p>The following posts are up for dispute:</p>
<% @takedown.kept_posts.each do |post| %>
<%= link_to("post ##{post.post_id}", {controller: "post", action: "show", id: post.post_id}) %><br>
<% end %>
<% end %>
</div>
<% elsif @takedown.status == "denied" %>
<div class='section sect_red'>
<p>The request has been denied. The following posts were not removed:</p>
<% @takedown.kept_posts.each do |post| %>
<%= link_to("post ##{post.post_id}", {controller: "post", action: "show", id: post.post_id}) %><br>
<% end %>
</div>
<% elsif @takedown.status == "partial" %>
<div class='section sect_green'>
<p>The request has been partially approved. The following posts were removed:</p>
<% @takedown.deleted_posts.each do |post| %>
<%= link_to("post ##{post.post_id}", {controller: "post", action: "show", id: post.post_id}, class: "takedown_post_deleted") %>
<br>
<% end %>
</div>
<div class='section sect_red'>
<p>The following posts were kept:</p>
<% @takedown.kept_posts.each do |post| %>
<%= link_to("post ##{post.post_id}", {controller: "post", action: "show", id: post.post_id}, class: "takedown_post_kept") %>
<br>
<% end %>
</div>
<% elsif @takedown.status == "approved" %>
<div class='section sect_green'>
<p>The request has been approved. The following posts were removed:</p>
<% @takedown.deleted_posts.each do |post| %>
<%= link_to("post ##{post.post_id}", {controller: "post", action: "show", id: post.post_id}) %><br>
<% end %>
</div>
<% end %>
<% elsif !current_user.is_admin? %>
<div class="section">
Post lists and special instructions are not visible to users.
</div>
<% end %>
<% if current_user.is_admin? %>
<% form_tag(controller: "takedown", action: "update") do %>
<%= hidden_field_tag "id", @takedown.id %>
<h2>Post list</h2>
<div id="takedown-posts" class="section">
<%= check_box_tag "process_takedown", true, true %>
<label for="process_takedown">Process takedown and delete posts</label>
<br><br>
<label for="takedown_post_delete_reason">Post deletion reason</label><br>
<%= text_field_tag "delete_reason", "Artist requested removal", {size: 80} %>
<br><br>
<div id="takedown-keepall" class="takedown-post-label takedown-postall-label takedown-post-keep">Keep all
</div>
<div id="takedown-deleteall" class="takedown-post-label takedown-postall-label takedown-post-delete">Delete
all
</div>
<br><br>
<div id="takedown-post-buttons">
<% @takedown.takedown_posts.each do |post| %>
<% if post.post %>
<div id="takedown-post-<%= post.post_id %>" data-post-id="<%= post.post_id %>" class="takedown-post">
<div class="takedown-post-label takedown-post-remove" title="Remove this post from the takedown">X</div>
<label for="takedown_posts_<%= post.post_id %>" class="takedown-post-label"><%= check_box "takedown_posts", post.post_id, checked: post.status == "deleted" %>
<span>Keep</span></label>
<span class="<%= post.post.status == 'deleted' ? 'redtext' : 'greentext' %>">
<%= link_to("post ##{post.post_id}", {controller: "post", action: "show", id: post.post_id}) %>
</span>
</div>
<% else %>
<div id="takedown-post-<%= post.post_id %>" data-post-id="<%= post.post_id %>" class="takedown-post">
<div class="takedown-post-label takedown-post-remove" title="Remove this post from the takedown">X</div>
Post <%= post.post_id %> was destroyed or not found.
</div>
<% end %>
<% end %>
</div>
<label for="takedown-add-posts-tags">Add posts with tags:</label><br>
<%= text_field_tag "takedown-add-posts-tags", "", size: 40 %>
<%= button_to_function "Add", "Takedown.add_posts_by_tags_preview(#{@takedown.id}, jQuery('#takedown-add-posts-tags').val());", id: "takedown-add-posts-tags-preview", disabled: true %>
<%= button_to_function "Confirm", "Takedown.add_posts_by_tags(#{@takedown.id}, jQuery('#takedown-add-posts-tags').val());", id: "takedown-add-posts-tags-confirm" %>
<%= button_to_function "Cancel", "Takedown.add_posts_by_tags_cancel();", id: "takedown-add-posts-tags-cancel" %>
<div id="takedown-add-posts-tags-warning"></div>
<br><br>
<label for="takedown-add-posts-ids">Add post IDs to takedown (space-separated):</label><br>
<%= text_field_tag "takedown-add-posts-ids", "", size: 40 %>
<%= button_to_function "Add", "Takedown.add_posts_by_ids(#{@takedown.id}, jQuery('#takedown-add-posts-ids').val())", id: "takedown-add-posts-ids-submit", disabled: true %>
</div>
<h2>Update</h2>
<div class='section'>
<table style="margin-bottom:0px;">
<% if @takedown.status == "pending" || @takedown.status == "inactive" %>
<tr>
<td><label for="status">Status</label></td>
<td><%= select_tag "takedown[status]", options_for_select([["Pending", "pending"], ["Inactive", "inactive"]], @takedown.status) %>
<span id="post-status-select"></span></td>
</tr>
<% end %>
<tr>
<td><label for="takedown_notes">Admin notes</label></td>
<td>
<textarea id="takedown_notes" rows="6" name="takedown[notes]" cols="62"><%= @takedown.notes %></textarea>
</td>
</tr>
<tr>
<td colspan='2'><%= check_box "takedown", "reason_hidden", checked: @takedown.reason_hidden %>
<label for="takedown_reason_hidden">Hide Reason?
(Currently
<% if !@takedown.reason_hidden %><span class='greentext'>not hidden</span>
<% else %><span class='redtext'>hidden</span>
<% end %>)
</label></td>
</tr>
</table>
</div>
<%= submit_tag "Submit", value: "Update" %>
<% end %>
<% end %>
</div>
</div>
<%= render partial: "secondary_links" %>
<script type="text/javascript">
jQuery(function () {
jQuery("body").on("change", "[id^='takedown_posts_']", function () {
// Update labels when checkboxes are changed
var label = jQuery("label[for=" + jQuery(this).attr("id") + "]");
if (jQuery(this).prop("checked"))
label.removeClass("takedown-post-keep").addClass("takedown-post-delete").find("> span").html("Delete");
else
label.removeClass("takedown-post-delete").addClass("takedown-post-keep").find("> span").html("Keep");
// Update text next to status dropdown based on kept/deleted post count
var kept_count = jQuery("label.takedown-post-keep").length;
var deleted_count = jQuery("label.takedown-post-delete").length;
if (kept_count == 0) { // None are kept, so takedown status will be 'approved'
jQuery("#post-status-select").html("Status will be set to Approved");
} else if (deleted_count == 0) { // None are deleted, so takedown status will be 'denied'
jQuery("#post-status-select").html("Status will be set to Denied");
} else { // Some kept, some deleted, so takedown status will be 'partial'
jQuery("#post-status-select").html("Status will be set to Partially Approved");
}
});
var originalHeight = jQuery("#takedown-posts").height();
// Toggle post list div based on 'process takedown' checkbox state
jQuery("#process_takedown").change(function () {
if (jQuery(this).prop("checked")) {
// Animate to the original height since jquery can't animate to 'auto', then set height back to auto after the animation finishes
jQuery("#takedown-posts").animate({
height: originalHeight,
}, 500, function () {
jQuery(this).css("height", "auto");
});
jQuery("#takedown_status").attr("disabled", true);
jQuery("#post-status-select").fadeIn();
} else {
originalHeight = jQuery("#takedown-posts").height();
jQuery("#takedown-posts").animate({
height: 20,
}, 500);
jQuery("#takedown_status").attr("disabled", false);
jQuery("#post-status-select").fadeOut();
}
});
// Delete all
jQuery("#takedown-deleteall").click(function () {
jQuery("[id^='takedown_posts_']").each(function () {
jQuery(this).prop("checked", true).trigger("change");
});
});
// Keep all
jQuery("#takedown-keepall").click(function () {
jQuery("[id^='takedown_posts_']").each(function () {
jQuery(this).prop("checked", false).trigger("change");
});
});
// Remove post
jQuery("body").on("click", ".takedown-post-remove", function () {
Takedown.remove_post(<%= @takedown.id %>, jQuery(this).parent().data("post-id"));
});
// Enable/disable add-post input submit buttons if input is filled/blank
jQuery("#takedown-add-posts-tags").keyup(function (e) {
jQuery("#takedown-add-posts-tags-preview").prop("disabled", jQuery(this).val().length == 0)
});
jQuery("#takedown-add-posts-ids").keyup(function (e) {
jQuery("#takedown-add-posts-ids-submit").prop("disabled", jQuery(this).val().length == 0)
});
// Handle enter keys in add-post inputs
jQuery("#takedown-add-posts-tags").keydown(function (e) {
if (e.keyCode == 13) {
jQuery("#takedown-add-posts-tags-preview:enabled").click();
e.preventDefault();
return false;
}
});
jQuery("#takedown-add-posts-ids").keydown(function (e) {
if (e.keyCode == 13) {
jQuery("#takedown-add-posts-ids-submit:enabled").click();
e.preventDefault();
return false;
}
});
// Trigger change event for elements to run their update handlers
jQuery("#process_takedown").trigger("change");
jQuery("[id^='takedown_posts_']").trigger("change");
});
</script>

View File

@ -0,0 +1,62 @@
<div id="c-takedowns">
<div id="a-index">
<%#= render 'search', path: takedowns_path %>
<table class="striped" width="100%">
<thead>
<tr>
<th>ID</th>
<th>Source</th>
<% if CurrentUser.is_admin? %>
<th>Email</th>
<th>IP Address</th>
<% end %>
<th>Status</th>
<th>>Post count</th>
<th>Date</th>
<th width="5%"></th>
</tr>
</thead>
<tbody>
<% @takedowns.each do |takedown| %>
<%= content_tag(:tr, id: "takedown-#{takedown.id}") do %>
<td><%= link_to takedown.id, takedown_path(takedown) %></td>
<td>
<% if !takedown.reason_hidden || CurrentUser.is_admin? %>
<% if takedown.source.match(/\Ahttps?:\/\//i) %>
<%= link_to takedown.source, takedown.source %>
<% else %>
<%= link_to takedown.source, "https://#{takedown.source}" %>
<% end %>
<% else %>
<span class="redtext">(Source hidden)</span>
<% end %>
</td>
<% if CurrentUser.is_admin? %>
<td><%= takedown.email %></td>
<td><%= takedown.ip_addr %></td>
<% end %>
<%= pretty_status(takedown.status) %>
<td><%= takedown.post_count %></td>
<td><%= time_ago_in_words_tagged takedown.created_at %></td>
<% if Currentuser.is_admin? %>
<!-- <td><%#= link_to_function "Delete", "if (confirm('Do you really want to delete this takedown?')) {Takedown.destroy(#{takedown.id})}" %></td>-->
<% end %>
</tr>
<% end %>
<% end %>
</tbody>
</table>
<%= numbered_paginator(@takedowns) %>
</div>
</div>
<%= render "takedowns/secondary_links" %>
<% content_for(:page_title) do %>
Takedowns - <%= Danbooru.config.app_name %>
<% end %>

View File

@ -0,0 +1,57 @@
<div id="c-takedowns">
<div id="c-new">
<h4>You may request a takedown by submitting the following form.</h4>
<p>Please read our <%#= link_to "takedown policy page", {controller: "static", action: "takedown"} %> before submitting a takedown request form. This form is for use ONLY by people who have a legitimate copyright/trademark infringement claim.</p>
<p>If you feel you need to handle a takedown request with an admin personally, you can email <%= Danbooru.config.takedown_email %></p>
<br>
<%= simple_form_for(@takedown) do |f| %>
<div class='section' style='width:600px;'>
<label for="takedown_source">Your Gallery URL (FurAffinity, deviantArt, personal site, etc.)</label>
<p class='nomargin'>Example:</p>
<ul>
<li>http://www.furaffinity.net/user/your_name</li>
<li>http://your_name.deviantart.com</li>
<li>http://mywebsite.com</li>
</ul>
<p class='nomargin'>In the next step, you will send a private message from the gallery account you specify, or an e-mail from your website's address/domain.</p>
<%= f.input :source, as: :string, required: true %>
</div>
<div class='section' style='width:600px;'>
<label for="takedown_email">Your Email Address</label>
<p class='nomargin'>Used to contact you if we need further information, and to inform you when the request is completed. It is only visible to admins and will not be used for any other purpose than to contact you regarding your takedown request.</p>
<%= f.input :email, as: :string, required: true %>
</div>
<div class='section' style='width:600px;'>
<label for="takedown_post_list">IDs or URLs of the offending images</label>
<p class='nomargin'>A list of either image IDs (like 75512) or URLs (like http://<%= Danbooru.config.server_host %>/post/show/75512), one per line.</p>
<%= f.input :post_ids, as: :text, label: 'Posts' %>
<div class='takedown-instructions-header'>OR</div>
<label for="takedown_instructions">Special instructions</label>
<p class="nomargin">You can supply instructions instead of a list of posts to delete. Example: "Delete everything with the tag john_doe"</p>
<%= f.input :instructions, as: :text, label: 'Special Instructions' %>
</div>
<div class='section' style='width:600px;'>
<label for="takedown_reason">Reason for takedown request</label><br>
<p class='nomargin'>Why do you want the artwork to be removed?</p>
<%= f.input :reason, as: :text, required: true %>
<br>
<%= f.input :reason_hidden, label: 'Hide Reason From Public' %><label for="takedown_reason_hidden">Hide the reason from the public?</label><br>Note: This is only to be used if your takedown request contains sensitive or private information. It is NOT for hiding reasons such as "No one asked me for permission". The site administration may remove this checkmark from your takedown request if it's been added unnecessarily.</label>
</div>
<p style='font-weight:bold;'>For the quickest processing of your takedown request, please follow the additional instructions that are displayed on the next page after your request has been submitted.</p>
<%= f.button :submit, "Submit" %>
<% end %>
</div>
</div>
<%= render partial: 'secondary_links' %>

View File

@ -0,0 +1,180 @@
<div id="c-takedowns">
<div id="c-show">
<% if @show_instructions && (!@takedown.completed?) %>
<div class='section sect_red' style='padding-top:15px; padding-bottom:1px;'>
<div style="font-size:200%;margin-top:-10px;margin-bottom:10px;">Wait! You're not done yet!</div>
<p>Your verification code is <span class='takedown-vericode'><%= @takedown.vericode %></span></p>
<p>Your takedown request has been successfully created. Using the gallery account that you specified below as the "source", <span style="font-weight:bold;">please send your verification code via PM/note to</span>:</p>
<ul>
<%# Danbooru.config.takedown_links.each do |link| %>
<!-- <li><%#= link %></li>-->
<%# end %>
<li>Or via email from your personal website (e.g. from me@mywebsite.com to <%= Danbooru.config.takedown_email %>)</li>
</ul>
<p>Once you send the verification code, we can process your takedown. This step is necessary in order to confirm that you are who you claim to be.</p>
<p>Bookmark this page to be able to access your verification code later, as it will not appear when viewing the takedown otherwise.</p>
</div>
<% end %>
<h2>Takedown #<%= @takedown.id %></h2>
<div class='section'>
<table style="margin-bottom:0px;">
<tr>
<td><span class='title'>Source</span></td>
<td>
<% if !@takedown.reason_hidden || CurrentUser.is_admin? || @show_instructions %>
<% if @takedown.source.match(/^https?:\/\//i) %>
<a href='<%= h @takedown.source %>'><%= h @takedown.source %></a>
<% else %>
<a href='<%= h "http://" + @takedown.source %>'><%= h @takedown.source %></a>
<% end %>
<% else %>
<span class="redtext">[Source hidden by submitter]</span>
<% end %>
</td>
</tr>
<tr>
<td><span class='title'>Reason</span></td>
<% if !@takedown.reason_hidden || CurrentUser.is_admin? || @show_instructions %>
<td><%= h @takedown.reason %>
<% if @takedown.reason_hidden %><span class="redtext">(HIDDEN)</span><% end %></td>
<% else %>
<td><span class="redtext">[Reason hidden by submitter]</span></td>
<% end %>
</tr>
<tr><td colspan="2">&nbsp;</td></tr>
<% if CurrentUser.is_admin? %>
<tr>
<td><span class='title'>Vericode</span></td>
<td><%= @takedown.vericode %></td>
</tr>
<tr>
<td><span class='title'>Email</span></td>
<td><%= link_to @takedown.email, "mailto:#{@takedown.email}" %></td>
</tr>
<tr>
<td><span class='title'>IP Addr</span></td>
<td><%= link_to_ip(@takedown.creator_ip_addr) %></td>
</tr>
<tr>
<td><span class='title'>User</span></td>
<td><%= link_to_user @takedown.creator %></td>
</tr>
<tr><td colspan="2">&nbsp;</td></tr>
<% end %>
<tr>
<td><span class='title'>Created</span></td>
<td style="cursor:help;" title="<%= @takedown.created_at.strftime("%b %d, %Y %I:%M %p") %>"><%= time_ago_in_words(@takedown.created_at) %> ago</td>
</tr>
<% if @takedown.created_at != @takedown.updated_at %>
<tr>
<td><span class='title'>Handled</span></td>
<td style="cursor:help;" title="<%= @takedown.updated_at.strftime("%b %d, %Y %I:%M %p") %>"><%= time_ago_in_words(@takedown.updated_at) %> ago</td>
</tr>
<% end %>
<tr>
<td><span class='title'>Status</span></td>
<%= pretty_status(@takedown) %>
</tr>
<% if @takedown.status != "pending" %>
<tr>
<td><span class='title'>Approver</span></td>
<td><%= link_to_user @takedown.approver %></td>
</tr>
<% end %>
</table>
</div>
<% if !@takedown.notes.blank? && @takedown.notes.downcase != "none" %>
<h3>Admin notes</h3>
<div class="section">
<% if !@takedown.reason_hidden || CurrentUser.is_admin? %>
<%= format_text(@takedown.notes) %>
<% if @takedown.reason_hidden %><span class="redtext">(HIDDEN)</span><% end %>
<% else %>
<span class="redtext">[Admin notes hidden]</span>
<% end %>
</div>
<% end %>
<% if @show_instructions || CurrentUser.is_admin? %>
<% if !@takedown.instructions.blank? %>
<h3>Special instructions</h3>
<div class="section">
<%= h @takedown.instructions %>
</div>
<% end %>
<% if @takedown.status == "pending" && !@takedown.kept_post_array.blank? %>
<div class='section'>
<p>The following posts are up for dispute:</p>
<% @takedown.actual_kept_posts.each do |post| %>
<%= link_to("post ##{post.id}", post_path(post)) %><br>
<% end %>
</div>
<% elsif @takedown.status == "inactive" && !@takedown.takedown_posts.blank? %>
<div class='section sect_grey'>
<p style="margin-bottom:0px;">This takedown request has been marked as inactive as the submitter has not responded in a reasonable time frame. It will be handled once the submitter responds.</p>
<br>
<p>The following posts are up for dispute:</p>
<% @takedown.actual_kept_posts.each do |post| %>
<%= link_to("post ##{post.id}", post_path(post)) %><br>
<% end %>
</div>
<% elsif @takedown.status == "denied" %>
<div class='section sect_red'>
<p>The request has been denied. The following posts were not removed:</p>
<% @takedown.actual_kept_posts.each do |post| %>
<%= link_to("post ##{post.id}", post_path(post)) %><br>
<% end %>
</div>
<% elsif @takedown.status == "partial" %>
<div class='section sect_green'>
<p>The request has been partially approved. The following posts were removed:</p>
<% @takedown.actual_deleted_posts.each do |post| %>
<%= link_to("post ##{post.id}", post_path(post), class: "takedown_post_deleted") %><br>
<% end %>
</div>
<div class='section sect_red'>
<p>The following posts were kept:</p>
<% @takedown.actual_kept_posts.each do |post| %>
<%= link_to("post ##{post.id}", post_path(post), class: "takedown_post_kept") %><br>
<% end %>
</div>
<% elsif @takedown.status == "approved" %>
<div class='section sect_green'>
<p>The request has been approved. The following posts were removed:</p>
<% @takedown.actual_deleted_posts.each do |post| %>
<%= link_to("post ##{post.id}", post_path(post)) %><br>
<% end %>
</div>
<% end %>
<% elsif !CurrentUser.is_admin? %>
<div class="section">
Post lists and special instructions are not visible to users.
</div>
<% end %>
</div>
</div>
<%= render partial: "secondary_links" %>

View File

@ -40,6 +40,10 @@ module Danbooru
"webmaster@#{server_host}"
end
def takedown_email
"management@#{server_host}"
end
# System actions, such as sending automated dmails, will be performed with
# this account. This account must have Moderator privileges.
#

View File

@ -74,6 +74,17 @@ Rails.application.routes.draw do
end
end
resources :takedowns do
collection do
post :count_matching_posts
end
member do
post :add_by_ids
post :add_by_tags
post :remove_by_id
end
end
resources :artists do
member do
put :revert

View File

@ -0,0 +1,21 @@
class CreateTakedowns < ActiveRecord::Migration[5.2]
def change
create_table :takedowns do |t|
t.timestamps
t.integer :creator_id, null: true
t.column :creator_ip_addr, :inet, null: false
t.integer :approver_id
t.string :status, default: 'pending'
t.string :vericode, null: false
t.string :source
t.string :email
t.text :reason
t.boolean :reason_hidden, null: false, default: false
t.text :notes, default: 'none', null: false
t.text :instructions
t.text :post_ids, default: ''
t.text :del_post_ids, default: ''
t.integer :post_count, default: 0, null: false
end
end
end

View File

@ -1801,6 +1801,50 @@ CREATE SEQUENCE public.tags_id_seq
ALTER SEQUENCE public.tags_id_seq OWNED BY public.tags.id;
--
-- Name: takedowns; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.takedowns (
id bigint NOT NULL,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL,
creator_id integer,
creator_ip_addr inet NOT NULL,
approver_id integer,
status character varying DEFAULT 'pending'::character varying,
vericode character varying NOT NULL,
source character varying,
email character varying,
reason text,
reason_hidden boolean DEFAULT false NOT NULL,
notes text DEFAULT 'none'::text NOT NULL,
instructions text,
post_ids text DEFAULT ''::text,
del_post_ids text DEFAULT ''::text,
post_count integer DEFAULT 0 NOT NULL
);
--
-- Name: takedowns_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.takedowns_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: takedowns_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.takedowns_id_seq OWNED BY public.takedowns.id;
--
-- Name: token_buckets; Type: TABLE; Schema: public; Owner: -
--
@ -2460,6 +2504,13 @@ ALTER TABLE ONLY public.tag_subscriptions ALTER COLUMN id SET DEFAULT nextval('p
ALTER TABLE ONLY public.tags ALTER COLUMN id SET DEFAULT nextval('public.tags_id_seq'::regclass);
--
-- Name: takedowns id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.takedowns ALTER COLUMN id SET DEFAULT nextval('public.takedowns_id_seq'::regclass);
--
-- Name: upload_whitelists id; Type: DEFAULT; Schema: public; Owner: -
--
@ -2892,6 +2943,14 @@ ALTER TABLE ONLY public.tags
ADD CONSTRAINT tags_pkey PRIMARY KEY (id);
--
-- Name: takedowns takedowns_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.takedowns
ADD CONSTRAINT takedowns_pkey PRIMARY KEY (id);
--
-- Name: upload_whitelists upload_whitelists_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@ -4242,6 +4301,7 @@ INSERT INTO "schema_migrations" (version) VALUES
('20190214040324'),
('20190214090126'),
('20190220025517'),
('20190220041928');
('20190220041928'),
('20190222082952');