tilde.news/app/controllers/comments_controller.rb

385 lines
11 KiB
Ruby

# typed: false
class CommentsController < ApplicationController
COMMENTS_PER_PAGE = 20
caches_page :index, :threads, if: CACHE_PAGE
before_action :require_logged_in_user_or_400,
only: [:create, :reply, :upvote, :flag, :unvote, :update]
before_action :require_logged_in_user, only: [:upvoted]
before_action :flag_warning, only: [:user_threads]
before_action :show_title_h1
# for rss feeds, load the user's tag filters if a token is passed
before_action :find_user_from_rss_token, only: [:index]
def create
if !(story = Story.where(short_id: params[:story_id]).first) ||
story.is_gone?
return render plain: "can't find story", status: 400
end
comment = story.comments.build
comment.comment = params[:comment].to_s
comment.user = @user
if params[:hat_id] && @user.wearable_hats.where(id: params[:hat_id])
comment.hat_id = params[:hat_id]
end
if params[:parent_comment_short_id].present?
# includes parent story_id to ensure this comment's story_id matches
comment.parent_comment =
Comment.find_by(story_id: story.id, short_id: params[:parent_comment_short_id])
if !comment.parent_comment
return render json: {error: "invalid parent comment", status: 400}
end
end
# sometimes on slow connections people resubmit; silently accept it
if (already = Comment.find_by(user: comment.user,
story: comment.story,
parent_comment_id: comment.parent_comment_id,
comment: comment.comment))
render_created_comment(already)
return
end
if params[:preview].blank? && comment.breaks_speed_limit?
return render partial: "commentbox", layout: false,
content_type: "text/html", locals: {comment: comment}
end
if comment.valid? && params[:preview].blank? && comment.save
comment.current_vote = {vote: 1}
render_created_comment(comment)
else
comment.score = 1
comment.current_vote = {vote: 1}
preview comment
end
end
def render_created_comment(comment)
if request.xhr?
render partial: "comments/postedreply", layout: false,
content_type: "text/html", locals: {comment: comment}
else
redirect_to comment.path
end
end
def show
if !((comment = find_comment) && comment.is_editable_by_user?(@user))
return render plain: "can't find comment", status: 400
end
render partial: "comment",
layout: false,
content_type: "text/html",
locals: {
comment: comment,
show_tree_lines: params[:show_tree_lines]
}
end
def show_short_id
if !(comment = find_comment)
return render plain: "can't find comment", status: 400
end
render json: comment.as_json
end
def redirect_from_short_id
if (comment = find_comment)
redirect_to comment.path
else
render plain: "can't find comment", status: 400
end
end
def edit
if !((comment = find_comment) && comment.is_editable_by_user?(@user))
return render plain: "can't find comment", status: 400
end
render partial: "commentbox", layout: false,
content_type: "text/html", locals: {comment: comment}
end
def reply
if !(parent_comment = find_comment)
return render plain: "can't find comment", status: 400
end
@story = parent_comment.story
comment = @story.comments.build
comment.parent_comment = parent_comment
comment.comment = params[:comment].to_s
comment.user = @user
if !parent_comment.depth_permits_reply?
ModNote.tattle_on_max_depth_limit(@user, parent_comment)
if request.xhr?
render partial: "too_deep"
else
render "_too_deep"
end
return
end
if request.xhr?
render partial: "commentbox", locals: {comment: comment}
else
parents = comment.parents.with_thread_attributes.for_presentation
@votes = Vote.comment_votes_by_user_for_comment_ids_hash(@user&.id, parents.map(&:id))
parents.each { |c| c.current_vote = @votes[c.id] }
render "_commentbox", locals: {
comment: comment,
parents: parents
}
end
end
def delete
if !((comment = find_comment) && comment.is_deletable_by_user?(@user))
return render plain: "can't find comment", status: 400
end
comment.delete_for_user(@user, params[:reason])
render partial: "comment", layout: false,
content_type: "text/html", locals: {comment: comment}
end
def undelete
if !((comment = find_comment) && comment.is_undeletable_by_user?(@user))
return render plain: "can't find comment", status: 400
end
comment.undelete_for_user(@user)
render partial: "comment", layout: false,
content_type: "text/html", locals: {comment: comment}
end
def disown
if !((comment = find_comment) && comment.is_disownable_by_user?(@user))
return render plain: "can't find comment", status: 400
end
InactiveUser.disown! comment
comment = find_comment
render partial: "comment", layout: false,
content_type: "text/html", locals: {comment: comment}
end
def update
if !((comment = find_comment) && comment.is_editable_by_user?(@user))
return render plain: "can't find comment", status: 400
end
comment.comment = params[:comment]
comment.hat_id = nil
if params[:hat_id] && @user.wearable_hats.where(id: params[:hat_id])
comment.hat_id = params[:hat_id]
end
if params[:preview].blank? && comment.save
votes = Vote.comment_votes_by_user_for_comment_ids_hash(@user.id, [comment.id])
comment.current_vote = votes[comment.id]
render partial: "comments/comment",
layout: false,
content_type: "text/html",
locals: {comment: comment, show_tree_lines: params[:show_tree_lines]}
else
comment.current_vote = {vote: 1}
preview comment
end
end
def unvote
if !(comment = find_comment) || comment.is_gone?
return render plain: "can't find comment", status: 400
end
Vote.vote_thusly_on_story_or_comment_for_user_because(
0, comment.story_id, comment.id, @user.id, nil
)
render plain: "ok"
end
def upvote
if !(comment = find_comment) || comment.is_gone?
return render plain: "can't find comment", status: 400
end
Vote.vote_thusly_on_story_or_comment_for_user_because(
1, comment.story_id, comment.id, @user.id, params[:reason]
)
render plain: "ok"
end
def flag
if !(comment = find_comment) || comment.is_gone?
return render plain: "can't find comment", status: 400
end
if !Vote::COMMENT_REASONS[params[:reason]]
return render plain: "invalid reason", status: 400
end
if !@user.can_flag?(comment)
return render plain: "not permitted to flag", status: 400
end
Vote.vote_thusly_on_story_or_comment_for_user_because(
-1, comment.story_id, comment.id, @user.id, params[:reason]
)
render plain: "ok"
end
def index
@rss_link ||= {
title: "RSS 2.0 - Newest Comments",
href: "/comments.rss" + (@user ? "?token=#{@user.rss_token}" : "")
}
@title = "Newest Comments"
@page = params[:page].to_i
if @page == 0
@page = 1
elsif @page < 0 || @page > (2**32)
raise ActionController::RoutingError.new("page out of bounds")
end
@comments = Comment.accessible_to_user(@user)
.not_on_story_hidden_by(@user)
.order("id DESC")
.includes(:user, :hat, story: :user)
.joins(:story).where.not(stories: {is_deleted: true})
.limit(COMMENTS_PER_PAGE)
.offset((@page - 1) * COMMENTS_PER_PAGE)
@votes = Vote.comment_votes_by_user_for_comment_ids_hash(@user&.id, @comments.map(&:id))
@comments.each { |c| c.current_vote = @votes[c.id] } unless @votes.empty?
respond_to do |format|
format.html { render action: "index" }
format.rss {
if @user && params[:token].present?
@title = "Private comments feed for #{@user.username}"
end
render action: "index", layout: false
}
end
end
def upvoted
@rss_link ||= {
title: "RSS 2.0 - Newest Comments",
href: upvoted_comments_path(format: :rss) + (@user ? "?token=#{@user.rss_token}" : "")
}
@title = "Upvoted Comments"
@above = "saved/subnav"
@page = params[:page].to_i
if @page == 0
@page = 1
elsif @page < 0 || @page > (2**32)
raise ActionController::RoutingError.new("page out of bounds")
end
@comments = Comment.accessible_to_user(@user)
.where.not(user_id: @user.id)
.order("id DESC")
.includes(:user, :hat, story: :user)
.joins(:votes).where(votes: {user_id: @user.id, vote: 1})
.joins(:story).where.not(stories: {is_deleted: true})
.limit(COMMENTS_PER_PAGE)
.offset((@page - 1) * COMMENTS_PER_PAGE)
# TODO: respect hidden stories
@votes = Vote.comment_votes_by_user_for_comment_ids_hash(@user.id, @comments.map(&:id))
@comments.each { |c| c.current_vote = @votes[c.id] }
respond_to do |format|
format.html { render action: :index }
format.rss {
if @user && params[:token].present?
@title = "Upvoted comments feed for #{@user.username}"
end
render action: "index", layout: false
}
end
end
def user_threads
if params[:user]
@showing_user = User.find_by!(username: params[:user])
@title = "Threads for #{@showing_user.username}"
elsif !@user
return redirect_to active_path
else
@showing_user = @user
@title = "Your Threads"
end
@threads = Comment.recent_threads(@showing_user)
.accessible_to_user(@user)
.for_presentation
.joins(:story)
if @user
@user.clear_unread_replies!
@votes = Vote.comment_votes_by_user_for_story_hash(@user.id, @threads.map(&:story_id).uniq)
@threads.each { |c| c.current_vote = @votes[c.id] }
end
end
private
def preview(comment)
comment.previewing = true
comment.is_deleted = false # show normal preview for deleted comments
render partial: "comments/commentbox",
layout: false,
content_type: "text/html",
locals: {
comment: comment,
show_comment: comment,
show_tree_lines: params[:show_tree_lines]
}
end
def find_comment
comment = Comment.where(short_id: params[:id]).first
# convenience to use PK (from external queries) without generally permitting enumeration:
comment ||= Comment.find(params[:id]) if @user&.is_admin?
if @user && comment
comment.current_vote = Vote.where(user_id: @user.id,
story_id: comment.story_id, comment_id: comment.id).first
end
comment
end
end