# frozen_string_literal: true module Booru class VotingEngine VALID_DIRECTIONS = [:up, :down].freeze def initialize(user, post) @user = user @post = post end def toggle_vote!(direction) raise 'bad direction' unless VALID_DIRECTIONS.include? direction @post.with_lock do user_votes = @post.votes.where(user: @user) has_up = user_votes.exists?(up: true) has_down = user_votes.exists?(up: false) want_up = direction == :up && !has_up want_down = direction == :down && !has_down unvote_internal! if (has_up || has_down) vote_internal!(:up) if want_up vote_internal!(:down) if want_down end end def unvote! @post.with_lock do unvote_internal! end end def vote!(direction) @post.with_lock do vote_internal!(direction) end end def fave!(state) @post.with_lock do fave_or_hide_internal!(@post.faves, state) vote_internal!(:up) if state end end def hide!(state) @post.with_lock do fave_or_hide_internal!(@post.hides, state) end end def toggle_fave! @post.with_lock do toggle_fave_internal! end end def toggle_hide! @post.with_lock do toggle_hide_internal! end end private def unvote_internal! user_votes = @post.votes.where(user: @user) has_up = user_votes.exists?(up: true) has_down = user_votes.exists?(up: false) if has_up || has_down upvote_change = has_up ? -1 : 0 downvote_change = has_down ? -1 : 0 user_votes.destroy_all @post.update!( upvotes_count: @post.upvotes_count + upvote_change, downvotes_count: @post.downvotes_count + downvote_change, score: @post.score + upvote_change - downvote_change, updated_at: Time.zone.now ) end end def vote_internal!(direction) raise 'bad direction' unless VALID_DIRECTIONS.include? direction user_votes = @post.votes.where(user: @user) has_up = user_votes.exists?(up: true) has_down = user_votes.exists?(up: false) want_up = direction == :up want_down = direction == :down return if want_up && has_up return if want_down && has_down # At this point we know that either we have an upvote and don't want it, # have a downvote and don't want it, or have one type of vote and want the other. # We know that if we have a vote at this point, we're going to be destroying it. downvote_change = 0 downvote_change = -1 if has_down downvote_change = 1 if want_down upvote_change = 0 upvote_change = -1 if has_up upvote_change = 1 if want_up @post.votes.where(user: @user).destroy_all @post.votes.create!(user: @user, up: (direction == :up)) @post.update!( upvotes_count: @post.upvotes_count + upvote_change, downvotes_count: @post.downvotes_count + downvote_change, score: @post.score + upvote_change - downvote_change, updated_at: Time.zone.now ) end def toggle_fave_internal! if fave_or_hide_internal!(@post.faves, :toggle) vote_internal!(:up) end end def toggle_hide_internal! fave_or_hide_internal!(@post.hides, :toggle) end def fave_or_hide_internal!(relation, desired_state) current_state = relation.exists?(user: @user) if desired_state == :toggle desired_state = !current_state end if desired_state && !current_state relation.create!(user: @user) elsif !desired_state && current_state relation.where(user: @user).destroy_all end # Otherwise, we either have a fave and want it, or have no fave and don't want it. No-op. desired_state end end end