# frozen_string_literal: true class Ticket < ApplicationRecord belongs_to_creator user_status_counter :ticket_count belongs_to :claimant, class_name: "User", optional: true belongs_to :handler, class_name: "User", optional: true belongs_to :accused, class_name: "User", optional: true belongs_to :post_report_reason, foreign_key: "report_reason", optional: true before_validation :initialize_fields, on: :create after_initialize :validate_type after_initialize :classify normalizes :reason, with: ->(reason) { reason.gsub("\r\n", "\n") } validates :qtype, presence: true validates :reason, presence: true validates :reason, length: { minimum: 2, maximum: Danbooru.config.ticket_max_size } validates :response, length: { minimum: 2 }, on: :update enum :status, %i[pending partial approved].index_with(&:to_s) after_update :log_update after_update :create_dmail validate :validate_content_exists, on: :create validate :validate_creator_is_not_limited, on: :create scope :for_creator, ->(uid) {where('creator_id = ?', uid)} attr_accessor :record_type, :send_update_dmail # Permissions Table # # | Type | Can Create | Visible | # |:----------:|:-------------------:|:--------------------:| # | Blip | Visible | Janitor+ / Creator | # | Comment | Visible | Janitor+ / Creator | # | Dmail | Visible & Recipient | Moderator+ / Creator | # | Forum Post | Visible | Janitor+ / Creator | # | Pool | Any | Janitor+ / Creator | # | Post | Any | Janitor+ / Creator | # | Post Set | Visible | Janitor+ / Creator | # | User | Any | Moderator+ / Creator | # | Wiki Page | Any | Janitor+ / Creator | # | Other | None | Moderator+ / Creator | module TicketTypes module Blip def can_create_for?(user) content&.visible_to?(user) end def can_view?(user) (content&.visible_to?(user) && user.is_janitor?) || (user.id == creator_id) end end module Comment def can_create_for?(user) content&.visible_to?(user) end def can_view?(user) (content&.visible_to?(user) && user.is_janitor?) || (user.id == creator_id) end end module Dmail def can_create_for?(user) content&.visible_to?(user) && content.to_id == user.id end def can_view?(user) user.is_moderator? || (user.id == creator_id) end def bot_target_name content&.from&.name end end module Forum # FIXME: Remove this by renaming the qtype value to the correct one def model ::ForumPost end def can_create_for?(user) content.visible?(user) end def can_view?(user) (content&.visible?(user) && user.is_janitor?) || (user.id == creator_id) end end module Pool def can_create_for?(user) true end def bot_target_name content&.name end def can_view?(user) user.is_janitor? || (user.id == creator_id) end end module Post def self.extended(m) m.class_eval do validates :report_reason, presence: true end end def subject reason.split("\n")[0] || "Unknown Report Type" end def can_create_for?(user) true end def bot_target_name content&.uploader&.name end def can_view?(user) user.is_janitor? || (user.id == creator_id) end end module Set def model ::PostSet end def can_create_for?(user) content&.can_view?(user) end def can_view?(user) (content&.can_view?(user) && user.is_janitor?) || (user.id == creator_id) end end module User def can_create_for?(user) true end def can_view?(user) user.is_moderator? || user.id == creator_id end def bot_target_name content&.name end end module Wiki def model ::WikiPage end def can_create_for?(user) true end def bot_target_name content&.title end def can_view?(user) user.is_janitor? || (user.id == creator_id) end end end module APIMethods def hidden_attributes hidden = [] hidden += %i[claimant_id] unless CurrentUser.is_moderator? hidden += %i[creator_id] unless can_see_reporter?(CurrentUser) super + hidden end end module ValidationMethods def validate_type valid_types = TicketTypes.constants.map { |v| v.to_s.downcase } errors.add(:qtype, "is not valid") if valid_types.exclude?(qtype) end def validate_creator_is_not_limited return if creator == User.system allowed = creator.can_ticket_with_reason if allowed != true errors.add(:creator, User.throttle_reason(allowed)) return false end true end def validate_content_exists errors.add model.name.underscore.to_sym, "does not exist" if content.nil? end def initialize_fields self.status = "pending" case qtype when "blip" self.accused_id = Blip.find(disp_id).creator_id when "forum" self.accused_id = ForumPost.find(disp_id).creator_id when "comment" self.accused_id = Comment.find(disp_id).creator_id when "dmail" self.accused_id = Dmail.find(disp_id).from_id when "user" self.accused_id = disp_id end end end module SearchMethods def for_accused(user_id) where(accused_id: user_id) end def active where(status: %w[pending partial]) end def visible(user) if user.is_moderator? all elsif user.is_janitor? for_creator(user.id).or(where.not(qtype: %w[Dmail User])) else for_creator(user.id) end end def search(params) q = super.includes(:creator).includes(:claimant) q = q.where_user(:creator_id, :creator, params) q = q.where_user(:claimant_id, :claimant, params) q = q.where_user(:accused_id, :accused, params) if params[:qtype].present? q = q.where('qtype = ?', params[:qtype]) end if params[:reason].present? q = q.attribute_matches(:reason, params[:reason]) end if params[:status].present? case params[:status] when "pending_claimed" q = q.where('status = ? and claimant_id is not null', 'pending') when "pending_unclaimed" q = q.where('status = ? and claimant_id is null', 'pending') else q = q.where('status = ?', params[:status]) end end if params[:order].present? q.apply_basic_order(params) else q.order(Arel.sql("CASE status WHEN 'pending' THEN 0 WHEN 'partial' THEN 1 ELSE 2 END ASC, id DESC")) end end end module ClassifyMethods def classify extend(TicketTypes.const_get(qtype.camelize)) if TicketTypes.constants.map(&:to_s).include?(qtype&.camelize) end end def content=(new_content) @content = new_content self.disp_id = content&.id end def content @content ||= model.find_by(id: disp_id) end def bot_target_name content&.creator&.name end def can_view?(user) user.is_janitor? end def can_see_reporter?(user) user.is_moderator? || (user.id == creator_id) end def can_create_for?(user) false end def model qtype.classify.constantize end def type_title "#{model.name.titlecase} Complaint" end def subject if reason.length > 40 "#{reason[0, 38]}..." else reason end end def open_duplicates Ticket.where('qtype = ? and disp_id = ? and status = ?', qtype, disp_id, 'pending') end def warnable? content.respond_to?(:user_warned!) && !content.was_warned? && pending? end module ClaimMethods def claim!(user = CurrentUser) transaction do ModAction.log(:ticket_claim, {ticket_id: id}) update_attribute(:claimant_id, user.id) push_pubsub('claim') end end def unclaim!(user = CurrentUser) transaction do ModAction.log(:ticket_unclaim, {ticket_id: id}) update_attribute(:claimant_id, nil) push_pubsub('unclaim') end end end module NotificationMethods def create_dmail return if creator == User.system should_send = saved_change_to_status? || (send_update_dmail.to_s.truthy? && saved_change_to_response?) return unless should_send msg = <<~MSG.chomp "Your ticket":#{Rails.application.routes.url_helpers.ticket_path(self)} has been updated by #{handler.pretty_name}. Ticket Status: #{status} Response: #{response} MSG Dmail.create_split( from_id: CurrentUser.id, to_id: creator.id, title: "Your ticket has been updated#{" to #{status}" if saved_change_to_status?}", body: msg, bypass_limits: true, ) end def log_update return unless saved_change_to_response? || saved_change_to_status? ModAction.log(:ticket_update, { ticket_id: id }) end end module PubSubMethods def pubsub_hash(action) { action: action, ticket: { id: id, user_id: creator_id, user: creator_id ? User.id_to_name(creator_id) : nil, claimant: claimant_id ? User.id_to_name(claimant_id) : nil, target: bot_target_name, status: status, category: qtype, reason: reason, } } end def push_pubsub(action) Cache.redis.publish("ticket_updates", pubsub_hash(action).to_json) end end include ClassifyMethods include ValidationMethods include APIMethods include ClaimMethods include NotificationMethods include PubSubMethods extend SearchMethods end