eBooru/app/models/user.rb
2025-02-26 05:34:43 -08:00

969 lines
30 KiB
Ruby

# frozen_string_literal: true
require "zxcvbn"
class User < ApplicationRecord
class Error < Exception ; end
class PrivilegeError < Exception
attr_accessor :message
def initialize(msg = nil)
@message = "Access Denied: #{msg}" if msg
end
end
module Levels
Danbooru.config.levels.each do |name, level|
const_set(name.upcase.tr(' ', '_'), level)
end
end
# Used for `before_action :<role>_only`. Must have a corresponding `is_<role>?` method.
Roles = Levels.constants.map(&:downcase) + [
:approver,
]
BOOLEAN_ATTRIBUTES = %w[
_show_avatars
_blacklist_avatars
blacklist_users
description_collapsed_initially
hide_comments
show_hidden_comments
show_post_statistics
is_banned
_has_mail
receive_email_notifications
enable_keyboard_navigation
enable_privacy_mode
style_usernames
enable_auto_complete
_has_saved_searches
can_approve_posts
can_upload_free
disable_cropped_thumbnails
_disable_mobile_gestures
enable_safe_mode
disable_responsive_mode
_disable_post_tooltips
no_flagging
_no_feedback
disable_user_dmails
enable_compact_uploader
replacements_beta
is_bd_staff
].freeze
include Danbooru::HasBitFlags
has_bit_flags BOOLEAN_ATTRIBUTES, :field => "bit_prefs"
attr_accessor :password, :old_password, :validate_email_format, :is_admin_edit
after_initialize :initialize_attributes, if: :new_record?
validates :email, presence: { if: :enable_email_verification? }
validates :email, uniqueness: { case_sensitive: false, if: :enable_email_verification? }
validates :email, format: { with: /\A.+@[^ ,;@]+\.[^ ,;@]+\z/, if: :enable_email_verification? }
validates :email, length: { maximum: 100 }
validate :validate_email_address_allowed, on: [:create, :update], if: ->(rec) { (rec.new_record? && rec.email.present?) || (rec.email.present? && rec.email_changed?) }
normalizes :profile_about, :profile_artinfo, with: ->(value) { value.gsub("\r\n", "\n") }
validates :name, user_name: true, on: :create
validates :default_image_size, inclusion: { :in => %w(large fit fitv original) }
validates :per_page, inclusion: { :in => 1..320 }
validates :comment_threshold, presence: true
validates :comment_threshold, numericality: { only_integer: true, less_than: 50_000, greater_than: -50_000 }
validates :password, length: { minimum: 8, if: ->(rec) { rec.new_record? || rec.password.present? || rec.old_password.present? } }
validate :password_is_secure, if: ->(rec) { rec.new_record? || rec.password.present? || rec.old_password.present? }
validates :password, confirmation: true
validates :password_confirmation, presence: { if: ->(rec) { rec.new_record? || rec.old_password.present? } }
validate :validate_ip_addr_is_not_banned, :on => :create
validate :validate_sock_puppets, :on => :create, :if => -> { Danbooru.config.enable_sock_puppet_validation? }
before_validation :normalize_blacklisted_tags, if: ->(rec) { rec.blacklisted_tags_changed? }
before_validation :staff_cant_disable_dmail
before_validation :blank_out_nonexistent_avatars
validates :blacklisted_tags, length: { maximum: 150_000 }
validates :custom_style, length: { maximum: 500_000 }
validates :profile_about, length: { maximum: Danbooru.config.user_about_max_size }
validates :profile_artinfo, length: { maximum: Danbooru.config.user_about_max_size }
validates :time_zone, inclusion: { in: ActiveSupport::TimeZone.all.map(&:name) }
before_create :encrypt_password_on_create
before_update :encrypt_password_on_update
after_save :update_cache
#after_create :notify_sock_puppets
after_create :create_user_status
has_one :api_key
has_one :dmail_filter
has_one :user_status
has_one :recent_ban, -> { order("bans.id desc") }, class_name: "Ban"
has_many :bans, -> { order("bans.id desc") }
has_many :dmails, -> { order("dmails.id desc") }, foreign_key: "owner_id"
has_many :favorites, -> { order(id: :desc) }
has_many :feedback, class_name: "UserFeedback", dependent: :destroy
has_many :forum_posts, -> { order("forum_posts.created_at, forum_posts.id") }, foreign_key: "creator_id"
has_many :forum_topic_visits
has_many :note_versions, foreign_key: "updater_id"
has_many :posts, foreign_key: "uploader_id"
has_many :post_approvals, dependent: :destroy
has_many :post_disapprovals, dependent: :destroy
has_many :post_replacements, foreign_key: :creator_id
has_many :post_sets, -> { order(name: :asc) }, foreign_key: :creator_id
has_many :post_versions
has_many :post_votes
has_many :staff_notes, -> { active.order("staff_notes.id desc") }
has_many :user_name_change_requests, -> { order(id: :asc) }
belongs_to :avatar, class_name: 'Post', optional: true
accepts_nested_attributes_for :dmail_filter
module BanMethods
def validate_ip_addr_is_not_banned
if IpBan.is_banned?(CurrentUser.ip_addr)
self.errors.add(:base, "IP address is banned")
return false
end
end
def unban!
self.is_banned = false
self.level = 20
save
end
def ban_expired?
is_banned? && recent_ban.try(:expired?)
end
end
module NameMethods
extend ActiveSupport::Concern
module ClassMethods
def name_to_id(name)
normalized_name = normalize_name(name)
Cache.fetch("uni:#{normalized_name}", expires_in: 4.hours) do
User.where("lower(name) = ?", normalized_name).pick(:id)
end
end
def name_or_id_to_id(name)
if name =~ /\A!\d+\z/
return name[1..-1].to_i
end
User.name_to_id(name)
end
def name_or_id_to_id_forced(name)
if name =~ /\A\d+\z/
return name.to_i
end
User.name_to_id(name)
end
def id_to_name(user_id)
RequestStore[:id_name_cache] ||= {}
if RequestStore[:id_name_cache].key?(user_id)
return RequestStore[:id_name_cache][user_id]
end
name = Cache.fetch("uin:#{user_id}", expires_in: 4.hours) do
User.where(id: user_id).pick(:name) || Danbooru.config.default_guest_name
end
RequestStore[:id_name_cache][user_id] = name
name
end
def find_by_name(name)
where("lower(name) = ?", normalize_name(name)).first
end
def find_by_name_or_id(name)
if name =~ /\A!\d+\z/
where('id = ?', name[1..-1].to_i).first
else
find_by_name(name)
end
end
def normalize_name(name)
name.to_s.downcase.strip.tr(" ", "_").to_s
end
end
def pretty_name
name.gsub(/([^_])_+(?=[^_])/, "\\1 \\2")
end
def update_cache
Cache.write("uin:#{id}", name, expires_in: 4.hours)
Cache.write("uni:#{User.normalize_name(name)}", id, expires_in: 4.hours)
end
end
module PasswordMethods
def password_token
Zlib::crc32(bcrypt_password_hash)
end
def bcrypt_password
BCrypt::Password.new(bcrypt_password_hash)
end
def encrypt_password_on_create
self.password_hash = ""
self.bcrypt_password_hash = User.bcrypt(password)
end
def encrypt_password_on_update
return if password.blank?
return if old_password.blank?
if bcrypt_password == old_password
self.bcrypt_password_hash = User.bcrypt(password)
return true
else
errors.add(:old_password, "is incorrect")
return false
end
end
def upgrade_password(pass)
self.update_columns(password_hash: '', bcrypt_password_hash: User.bcrypt(pass))
end
def password_is_secure
analysis = Zxcvbn.test(password, [name, email])
return unless analysis.score < 2
if analysis.feedback.warning
errors.add(:password, "is insecure: #{analysis.feedback.warning}")
else
errors.add(:password, "is insecure")
end
end
end
module AuthenticationMethods
extend ActiveSupport::Concern
module ClassMethods
def authenticate(name, pass)
user = find_by_name(name)
if user && user.password_hash.present? && Pbkdf2.validate_password(pass, user.password_hash)
user.upgrade_password(pass)
user
elsif user && user.bcrypt_password_hash && user.bcrypt_password == pass
user
else
nil
end
end
def authenticate_api_key(name, api_key)
key = ApiKey.where(:key => api_key).first
return nil if key.nil?
user = find_by_name(name)
return nil if user.nil?
return user if key.user_id == user.id
nil
end
def bcrypt(pass)
BCrypt::Password.create(pass)
end
end
end
module LevelMethods
extend ActiveSupport::Concern
module ClassMethods
def system
User.find_by!(name: Danbooru.config.system_user)
end
def anonymous
user = User.new(name: "Anonymous", created_at: Time.now)
user.level = Levels::ANONYMOUS
user.freeze.readonly!
user
end
def level_hash
Danbooru.config.levels
end
def level_string(value)
Danbooru.config.levels.invert[value] || ""
end
end
def promote_to!(new_level, options = {})
UserPromotion.new(self, CurrentUser.user, new_level, options).promote!
end
def level_string_was
level_string(level_was)
end
def level_string(value = nil)
User.level_string(value || level)
end
def is_anonymous?
level == Levels::ANONYMOUS
end
def is_blocked?
is_banned? || level == Levels::BLOCKED
end
# Defines various convenience methods for finding out the user's level
Danbooru.config.levels.each do |name, value|
# TODO: HACK: Remove this and make the below logic better to work with the new setup.
next if [0, 10].include?(value)
normalized_name = name.downcase.tr(' ', '_')
# Changed from e6 to match new Danbooru semantics.
define_method("is_#{normalized_name}?") do
is_verified? && self.level >= value && self.id.present?
end
end
def is_bd_staff?
is_bd_staff
end
def is_staff?
is_janitor?
end
def is_approver?
can_approve_posts?
end
def blank_out_nonexistent_avatars
if avatar_id.present? && avatar.nil?
self.avatar_id = nil
end
end
def staff_cant_disable_dmail
self.disable_user_dmails = false if self.is_janitor?
end
def level_css_class
"user-#{level_string.parameterize}"
end
def create_user_status
UserStatus.create!(user_id: id)
end
end
module EmailMethods
def is_verified?
id.present? && email_verification_key.nil?
end
def mark_unverified!
update_attribute(:email_verification_key, '1')
end
def mark_verified!
update_attribute(:email_verification_key, nil)
end
def enable_email_verification?
# Allow admins to edit users with blank/duplicate emails
return false if is_admin_edit && !email_changed?
Danbooru.config.enable_email_verification? && validate_email_format
end
def validate_email_address_allowed
if EmailBlacklist.is_banned?(self.email)
self.errors.add(:base, "Email address may not be used")
return false
end
end
end
module BlacklistMethods
def normalize_blacklisted_tags
self.blacklisted_tags = TagAlias.to_aliased_query(blacklisted_tags, comments: true) if blacklisted_tags.present?
end
def is_blacklisting_user?(user)
return false if blacklisted_tags.blank?
bltags = blacklisted_tags.split("\n").map(&:downcase)
strings = %W[user:#{user.name.downcase} user:!#{user.id} userid:#{user.id}]
strings.any? { |str| bltags.include?(str) }
end
end
module ForumMethods
def has_forum_been_updated?
return false unless is_member?
max_updated_at = ForumTopic.visible(self).order(updated_at: :desc).first&.updated_at
return false if max_updated_at.nil?
return true if last_forum_read_at.nil?
return max_updated_at > last_forum_read_at
end
def has_viewed_thread?(id, last_updated)
@topic_views ||= forum_topic_visits.pluck(:forum_topic_id, :last_read_at).to_h
@topic_views.key?(id) && @topic_views[id] >= last_updated
end
end
module ThrottleMethods
def throttle_reason(reason, timeframe = "hourly")
reasons = {
REJ_NEWBIE: "can not yet perform this action. Account is too new",
REJ_LIMITED: "have reached the #{timeframe} limit for this action",
}
reasons.fetch(reason, "unknown throttle reason, please report this as a bug")
end
def upload_reason_string(reason)
reasons = {
REJ_UPLOAD_HOURLY: "have reached your hourly upload limit",
REJ_UPLOAD_EDIT: "have no remaining tag edits available",
REJ_UPLOAD_LIMIT: "have reached your upload limit",
REJ_UPLOAD_NEWBIE: "cannot upload during your first week"
}
reasons.fetch(reason, "unknown upload rejection reason")
end
end
module LimitMethods
def younger_than(duration)
return false if Danbooru.config.disable_age_checks?
created_at > duration.ago
end
def older_than(duration)
return true if Danbooru.config.disable_age_checks?
created_at < duration.ago
end
def self.create_user_throttle(name, limiter, checker, newbie_duration)
define_method(:"#{name}_limit", limiter)
define_method(:"can_#{name}_with_reason") do
return true if Danbooru.config.disable_throttles?
return send(checker) if checker && send(checker)
return :REJ_NEWBIE if newbie_duration && younger_than(newbie_duration)
return :REJ_LIMITED if send("#{name}_limit") <= 0
true
end
end
def token_bucket
@token_bucket ||= UserThrottle.new({prefix: "thtl:", duration: 1.minute}, self)
end
def general_bypass_throttle?
is_privileged?
end
create_user_throttle(:artist_edit, ->{ Danbooru.config.artist_edit_limit - ArtistVersion.for_user(id).where('updated_at > ?', 1.hour.ago).count },
:general_bypass_throttle?, 7.days)
create_user_throttle(:post_edit, ->{ Danbooru.config.post_edit_limit - PostVersion.for_user(id).where('updated_at > ?', 1.hour.ago).count },
:general_bypass_throttle?, 7.days)
create_user_throttle(:wiki_edit, ->{ Danbooru.config.wiki_edit_limit - WikiPageVersion.for_user(id).where('updated_at > ?', 1.hour.ago).count },
:general_bypass_throttle?, 7.days)
create_user_throttle(:pool, ->{ Danbooru.config.pool_limit - Pool.for_user(id).where('created_at > ?', 1.hour.ago).count },
:is_janitor?, 7.days)
create_user_throttle(:pool_edit, ->{ Danbooru.config.pool_edit_limit - PoolVersion.for_user(id).where('updated_at > ?', 1.hour.ago).count },
:is_janitor?, 3.days)
create_user_throttle(:pool_post_edit, -> { Danbooru.config.pool_post_edit_limit - PoolVersion.for_user(id).where('updated_at > ?', 1.hour.ago).group(:pool_id).count(:pool_id).length },
:general_bypass_throttle?, 7.days)
create_user_throttle(:note_edit, ->{ Danbooru.config.note_edit_limit - NoteVersion.for_user(id).where('updated_at > ?', 1.hour.ago).count },
:general_bypass_throttle?, 3.days)
create_user_throttle(:comment, ->{ Danbooru.config.member_comment_limit - Comment.for_creator(id).where('created_at > ?', 1.hour.ago).count },
:general_bypass_throttle?, 7.days)
create_user_throttle(:forum_post, ->{ Danbooru.config.member_comment_limit - ForumPost.for_user(id).where('created_at > ?', 1.hour.ago).count },
nil, 3.days)
create_user_throttle(:blip, ->{ Danbooru.config.blip_limit - Blip.for_creator(id).where('created_at > ?', 1.hour.ago).count },
:general_bypass_throttle?, 3.days)
create_user_throttle(:dmail_minute, ->{ Danbooru.config.dmail_minute_limit - Dmail.sent_by_id(id).where('created_at > ?', 1.minute.ago).count },
nil, 7.days)
create_user_throttle(:dmail, ->{ Danbooru.config.dmail_limit - Dmail.sent_by_id(id).where('created_at > ?', 1.hour.ago).count },
nil, 7.days)
create_user_throttle(:dmail_day, ->{ Danbooru.config.dmail_day_limit - Dmail.sent_by_id(id).where('created_at > ?', 1.day.ago).count },
nil, 7.days)
create_user_throttle(:comment_vote, ->{ Danbooru.config.comment_vote_limit - CommentVote.for_user(id).where("created_at > ?", 1.hour.ago).count },
:general_bypass_throttle?, 3.days)
create_user_throttle(:post_vote, ->{ Danbooru.config.post_vote_limit - PostVote.for_user(id).where("created_at > ?", 1.hour.ago).count },
:general_bypass_throttle?, nil)
create_user_throttle(:post_flag, ->{ Danbooru.config.post_flag_limit - PostFlag.for_creator(id).where("created_at > ?", 1.hour.ago).count },
:can_approve_posts?, 3.days)
create_user_throttle(:ticket, ->{ Danbooru.config.ticket_limit - Ticket.for_creator(id).where("created_at > ?", 1.hour.ago).count },
:general_bypass_throttle?, 3.days)
create_user_throttle(:suggest_tag, -> { Danbooru.config.tag_suggestion_limit - (TagAlias.for_creator(id).where("created_at > ?", 1.hour.ago).count + TagImplication.for_creator(id).where("created_at > ?", 1.hour.ago).count + BulkUpdateRequest.for_creator(id).where("created_at > ?", 1.hour.ago).count) },
:is_janitor?, 7.days)
create_user_throttle(:forum_vote, -> { Danbooru.config.forum_vote_limit - ForumPostVote.by(id).where("created_at > ?", 1.hour.ago).count },
:is_janitor?, 3.days)
def can_remove_from_pools?
is_member? && older_than(7.days)
end
def can_discord?
is_member? && older_than(7.days)
end
def can_view_flagger?(flagger_id)
is_janitor? || flagger_id == id
end
def can_view_flagger_on_post?(flag)
is_janitor? || flag.creator_id == id || flag.is_deletion
end
def can_replace?
is_janitor? || replacements_beta?
end
def can_view_staff_notes?
is_staff?
end
def can_handle_takedowns?
is_bd_staff?
end
def can_edit_avoid_posting_entries?
is_bd_staff?
end
def can_undo_post_versions?
is_member?
end
def can_revert_post_versions?
is_member?
end
def can_upload_with_reason
if hourly_upload_limit <= 0 && !Danbooru.config.disable_throttles?
:REJ_UPLOAD_HOURLY
elsif can_upload_free? || is_admin?
true
elsif younger_than(7.days)
:REJ_UPLOAD_NEWBIE
elsif !is_privileged? && post_edit_limit <= 0 && !Danbooru.config.disable_throttles?
:REJ_UPLOAD_EDIT
elsif upload_limit <= 0 && !Danbooru.config.disable_throttles?
:REJ_UPLOAD_LIMIT
else
true
end
end
def hourly_upload_limit
@hourly_upload_limit ||= begin
post_count = posts.where("created_at >= ?", 1.hour.ago).count
replacement_count = can_approve_posts? ? 0 : post_replacements.where("created_at >= ? and status != ?", 1.hour.ago, "original").count
Danbooru.config.hourly_upload_limit - post_count - replacement_count
end
end
def upload_limit
pieces = upload_limit_pieces
base_upload_limit + (pieces[:approved] / 10) - (pieces[:deleted] / 4) - pieces[:pending]
end
def upload_limit_max
pieces = upload_limit_pieces
base_upload_limit + (pieces[:approved] / 10) - (pieces[:deleted] / 4)
end
def upload_limit_pieces
@upload_limit_pieces ||= begin
deleted_count = Post.deleted.for_user(id).count
rejected_replacement_count = post_replacement_rejected_count
replaced_penalize_count = own_post_replaced_penalize_count
unapproved_count = Post.pending_or_flagged.for_user(id).count
unapproved_replacements_count = post_replacements.pending.count
approved_count = Post.for_user(id).where(is_flagged: false, is_deleted: false, is_pending: false).count
{
deleted: deleted_count + replaced_penalize_count + rejected_replacement_count,
deleted_ignore: own_post_replaced_count - replaced_penalize_count,
approved: approved_count,
pending: unapproved_count + unapproved_replacements_count,
}
end
end
def post_upload_throttle
@post_upload_throttle ||= is_privileged? ? hourly_upload_limit : [hourly_upload_limit, post_edit_limit].min
end
def tag_query_limit
Danbooru.config.tag_query_limit
end
def favorite_limit
Danbooru.config.legacy_favorite_limit.fetch(id, 80_000)
end
def api_regen_multiplier
1
end
def api_burst_limit
# can make this many api calls at once before being bound by
# api_regen_multiplier refilling your pool
if is_former_staff?
120
elsif is_privileged?
90
else
60
end
end
def remaining_api_limit
token_bucket.uncached_count
end
def statement_timeout
if is_former_staff?
9_000
elsif is_privileged?
6_000
else
3_000
end
end
end
module ApiMethods
# blacklist all attributes by default. whitelist only safe attributes.
def hidden_attributes
super + attributes.keys.map(&:to_sym)
end
def method_attributes
list = super + [
:id, :created_at, :name, :level, :base_upload_limit,
:post_upload_count, :post_update_count, :note_update_count,
:is_banned, :can_approve_posts, :can_upload_free,
:level_string, :avatar_id
]
if id == CurrentUser.user.id
boolean_attributes = %i[
blacklist_users description_collapsed_initially
hide_comments show_hidden_comments show_post_statistics
is_banned receive_email_notifications
enable_keyboard_navigation enable_privacy_mode
style_usernames enable_auto_complete
can_approve_posts can_upload_free
disable_cropped_thumbnails enable_safe_mode
disable_responsive_mode no_flagging disable_user_dmails
enable_compact_uploader replacements_beta
]
list += boolean_attributes + [
:updated_at, :email, :last_logged_in_at, :last_forum_read_at,
:recent_tags, :comment_threshold, :default_image_size,
:favorite_tags, :blacklisted_tags, :time_zone, :per_page,
:custom_style, :favorite_count,
:api_regen_multiplier, :api_burst_limit, :remaining_api_limit,
:statement_timeout, :favorite_limit,
:tag_query_limit, :has_mail?
]
end
list
end
# extra attributes returned for /users/:id.json but not for /users.json.
def full_attributes
%i[
wiki_page_version_count artist_version_count pool_version_count
forum_post_count comment_count flag_count favorite_count
positive_feedback_count neutral_feedback_count negative_feedback_count
upload_limit profile_about profile_artinfo
]
end
end
module CountMethods
def wiki_page_version_count
user_status.wiki_edit_count
end
def post_update_count
user_status.post_update_count
end
def post_upload_count
user_status.post_count
end
def post_deleted_count
user_status.post_deleted_count
end
def note_version_count
user_status.note_count
end
def note_update_count
note_version_count
end
def artist_version_count
user_status.artist_edit_count
end
def pool_version_count
user_status.pool_edit_count
end
def forum_post_count
user_status.forum_post_count
end
def favorite_count
user_status.favorite_count
end
def comment_count
user_status.comment_count
end
def flag_count
user_status.post_flag_count
end
def ticket_count
user_status.ticket_count
end
def feedback_pieces
@feedback_pieces ||= begin
count = {
deleted: 0,
negative: 0,
neutral: 0,
positive: 0,
}
feedback.each do |one|
if one.is_deleted
count[:deleted] += 1
next
end
count[one.category.to_sym] += 1
end
count
end
end
def positive_feedback_count
feedback.active.positive.count
end
def neutral_feedback_count
feedback.active.neutral.count
end
def negative_feedback_count
feedback.active.negative.count
end
def deleted_feedback_count
feedback.deleted.count
end
def post_replacement_rejected_count
user_status.post_replacement_rejected_count
end
def own_post_replaced_count
user_status.own_post_replaced_count
end
def own_post_replaced_penalize_count
user_status.own_post_replaced_penalize_count
end
def refresh_counts!
self.class.without_timeout do
UserStatus.where(user_id: id).update_all(
post_count: Post.for_user(id).count,
post_deleted_count: Post.for_user(id).deleted.count,
post_update_count: PostVersion.for_user(id).count,
favorite_count: Favorite.for_user(id).count,
note_count: NoteVersion.for_user(id).count,
own_post_replaced_count: PostReplacement.for_uploader_on_approve(id).count,
own_post_replaced_penalize_count: PostReplacement.penalized.for_uploader_on_approve(id).count,
post_replacement_rejected_count: post_replacements.rejected.count,
)
end
end
end
module SearchMethods
def admins
where("level = ?", Levels::ADMIN)
end
def with_email(email)
if email.blank?
where("FALSE")
else
where("lower(email) = lower(?)", email)
end
end
def search(params)
q = super
q = q.joins(:user_status)
q = q.attribute_matches(:level, params[:level])
if params[:about_me].present?
q = q.attribute_matches(:profile_about, params[:about_me]).or(attribute_matches(:profile_artinfo, params[:about_me]))
end
if params[:avatar_id].present?
q = q.where(avatar_id: params[:avatar_id])
end
if params[:email_matches].present?
q = q.where_ilike(:email, params[:email_matches])
end
if params[:name_matches].present?
q = q.where_ilike(:name, normalize_name(params[:name_matches]))
end
if params[:min_level].present?
q = q.where("level >= ?", params[:min_level].to_i)
end
if params[:max_level].present?
q = q.where("level <= ?", params[:max_level].to_i)
end
bitprefs_length = BOOLEAN_ATTRIBUTES.length
bitprefs_include = nil
bitprefs_exclude = nil
[:can_approve_posts, :can_upload_free].each do |x|
if params[x].present?
attr_idx = BOOLEAN_ATTRIBUTES.index(x.to_s)
if params[x].to_s.truthy?
bitprefs_include ||= "0"*bitprefs_length
bitprefs_include[attr_idx] = '1'
elsif params[x].to_s.falsy?
bitprefs_exclude ||= "0"*bitprefs_length
bitprefs_exclude[attr_idx] = '1'
end
end
end
if bitprefs_include
bitprefs_include.reverse!
q = q.where("bit_prefs::bit(:len) & :bits::bit(:len) = :bits::bit(:len)",
{:len => bitprefs_length, :bits => bitprefs_include})
end
if bitprefs_exclude
bitprefs_exclude.reverse!
q = q.where("bit_prefs::bit(:len) & :bits::bit(:len) = 0::bit(:len)",
{:len => bitprefs_length, :bits => bitprefs_exclude})
end
if params[:ip_addr].present?
q = q.where("last_ip_addr <<= ?", params[:ip_addr])
end
case params[:order]
when "name"
q = q.order("name")
when "post_upload_count"
q = q.order("user_statuses.post_count desc")
when "note_count"
q = q.order("user_statuses.note_count desc")
when "post_update_count"
q = q.order("user_statuses.post_update_count desc")
else
q = q.apply_basic_order(params)
end
q
end
end
concerning :SockPuppetMethods do
def validate_sock_puppets
if User.where(last_ip_addr: CurrentUser.ip_addr).where("created_at > ?", 1.day.ago).exists?
errors.add(:last_ip_addr, "was used recently for another account and cannot be reused for another day")
end
end
end
include BanMethods
include NameMethods
include PasswordMethods
include AuthenticationMethods
include LevelMethods
include EmailMethods
include BlacklistMethods
include ForumMethods
include LimitMethods
include ApiMethods
include CountMethods
extend SearchMethods
extend ThrottleMethods
def has_mail?
unread_dmail_count > 0
end
def hide_favorites?
return false if CurrentUser.is_moderator?
return true if is_blocked?
enable_privacy_mode? && CurrentUser.user.id != id
end
def compact_uploader?
post_upload_count >= 10 && enable_compact_uploader?
end
def initialize_attributes
return if Rails.env.test?
Danbooru.config.customize_new_user(self)
end
def presenter
@presenter ||= UserPresenter.new(self)
end
# Copied from UserNameValidator. Check back later how effective this was.
# Users with invalid names may be automatically renamed in the future.
def name_error
if name.length > 20
"must be 2 to 20 characters long"
elsif name !~ /\A[a-zA-Z0-9\-_~']+\z/
"must contain only alphanumeric characters, hypens, apostrophes, tildes and underscores"
elsif name =~ /\A[_\-~']/
"must not begin with a special character"
elsif name =~ /_{2}|-{2}|~{2}|'{2}/
"must not contain consecutive special characters"
elsif name =~ /\A_|_\z/
"cannot begin or end with an underscore"
elsif name =~ /\A[0-9]+\z/
"cannot consist of numbers only"
end
end
end