eBooru/app/models/user.rb

969 lines
30 KiB
Ruby
Raw Normal View History

# 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
2013-03-19 08:10:10 -04:00
2011-06-25 19:31:39 -04:00
module Levels
Danbooru.config.levels.each do |name, level|
const_set(name.upcase.tr(' ', '_'), level)
end
2011-06-25 19:31:39 -04:00
end
2013-03-19 08:10:10 -04:00
Raise error on unpermitted params. Fail loudly if we forget to whitelist a param instead of silently ignoring it. misc models: convert to strong params. artist commentaries: convert to strong params. * Disallow changing or setting post_id to a nonexistent post. artists: convert to strong params. * Disallow setting `is_banned` in create/update actions. Changing it this way instead of with the ban/unban actions would leave the artist in a partially banned state. bans: convert to strong params. * Disallow changing the user_id after the ban has been created. comments: convert to strong params. favorite groups: convert to strong params. news updates: convert to strong params. post appeals: convert to strong params. post flags: convert to strong params. * Disallow users from setting the `is_deleted` / `is_resolved` flags. ip bans: convert to strong params. user feedbacks: convert to strong params. * Disallow users from setting `disable_dmail_notification` when creating feedbacks. * Disallow changing the user_id after the feedback has been created. notes: convert to strong params. wiki pages: convert to strong params. * Also fix non-Builders being able to delete wiki pages. saved searches: convert to strong params. pools: convert to strong params. * Disallow setting `post_count` or `is_deleted` in create/update actions. janitor trials: convert to strong params. post disapprovals: convert to strong params. * Factor out quick-mod bar to shared partial. * Fix quick-mod bar to use `Post#is_approvable?` to determine visibility of Approve button. dmail filters: convert to strong params. password resets: convert to strong params. user name change requests: convert to strong params. posts: convert to strong params. users: convert to strong params. * Disallow setting password_hash, last_logged_in_at, last_forum_read_at, has_mail, and dmail_filter_attributes[user_id]. * Remove initialize_default_image_size (dead code). uploads: convert to strong params. * Remove `initialize_status` because status already defaults to pending in the database. tag aliases/implications: convert to strong params. tags: convert to strong params. forum posts: convert to strong params. * Disallow changing the topic_id after creating the post. * Disallow setting is_deleted (destroy/undelete actions should be used instead). * Remove is_sticky / is_locked (nonexistent attributes). forum topics: convert to strong params. * merges https://github.com/evazion/danbooru/tree/wip-rails-5.1 * lock pg gem to 0.21 (1.0.0 is incompatible with rails 5.1.4) * switch to factorybot and change all references Co-authored-by: r888888888 <r888888888@gmail.com> Co-authored-by: evazion <noizave@gmail.com> add diffs
2018-04-02 13:51:26 -04:00
# 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
2015-07-31 19:20:36 -04:00
is_banned
2024-02-23 11:18:22 -05:00
_has_mail
2015-07-31 19:20:36 -04:00
receive_email_notifications
enable_keyboard_navigation
2015-07-31 19:20:36 -04:00
enable_privacy_mode
style_usernames
enable_auto_complete
_has_saved_searches
2015-07-31 19:20:36 -04:00
can_approve_posts
2015-10-15 18:24:24 -04:00
can_upload_free
2017-09-08 18:01:12 -04:00
disable_cropped_thumbnails
_disable_mobile_gestures
enable_safe_mode
2017-12-15 16:52:25 -05:00
disable_responsive_mode
_disable_post_tooltips
no_flagging
_no_feedback
2019-10-03 10:29:49 -04:00
disable_user_dmails
enable_compact_uploader
replacements_beta
is_bd_staff
].freeze
2015-07-31 19:20:36 -04:00
include Danbooru::HasBitFlags
has_bit_flags BOOLEAN_ATTRIBUTES, :field => "bit_prefs"
attr_accessor :password, :old_password, :validate_email_format, :is_admin_edit
Raise error on unpermitted params. Fail loudly if we forget to whitelist a param instead of silently ignoring it. misc models: convert to strong params. artist commentaries: convert to strong params. * Disallow changing or setting post_id to a nonexistent post. artists: convert to strong params. * Disallow setting `is_banned` in create/update actions. Changing it this way instead of with the ban/unban actions would leave the artist in a partially banned state. bans: convert to strong params. * Disallow changing the user_id after the ban has been created. comments: convert to strong params. favorite groups: convert to strong params. news updates: convert to strong params. post appeals: convert to strong params. post flags: convert to strong params. * Disallow users from setting the `is_deleted` / `is_resolved` flags. ip bans: convert to strong params. user feedbacks: convert to strong params. * Disallow users from setting `disable_dmail_notification` when creating feedbacks. * Disallow changing the user_id after the feedback has been created. notes: convert to strong params. wiki pages: convert to strong params. * Also fix non-Builders being able to delete wiki pages. saved searches: convert to strong params. pools: convert to strong params. * Disallow setting `post_count` or `is_deleted` in create/update actions. janitor trials: convert to strong params. post disapprovals: convert to strong params. * Factor out quick-mod bar to shared partial. * Fix quick-mod bar to use `Post#is_approvable?` to determine visibility of Approve button. dmail filters: convert to strong params. password resets: convert to strong params. user name change requests: convert to strong params. posts: convert to strong params. users: convert to strong params. * Disallow setting password_hash, last_logged_in_at, last_forum_read_at, has_mail, and dmail_filter_attributes[user_id]. * Remove initialize_default_image_size (dead code). uploads: convert to strong params. * Remove `initialize_status` because status already defaults to pending in the database. tag aliases/implications: convert to strong params. tags: convert to strong params. forum posts: convert to strong params. * Disallow changing the topic_id after creating the post. * Disallow setting is_deleted (destroy/undelete actions should be used instead). * Remove is_sticky / is_locked (nonexistent attributes). forum topics: convert to strong params. * merges https://github.com/evazion/danbooru/tree/wip-rails-5.1 * lock pg gem to 0.21 (1.0.0 is incompatible with rails 5.1.4) * switch to factorybot and change all references Co-authored-by: r888888888 <r888888888@gmail.com> Co-authored-by: evazion <noizave@gmail.com> add diffs
2018-04-02 13:51:26 -04:00
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) }
2019-09-05 08:59:51 -04:00
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? }
2019-09-05 08:59:51 -04:00
validates :password, confirmation: true
2020-02-23 23:18:02 -05:00
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? }
2019-10-03 10:29:49 -04:00
before_validation :staff_cant_disable_dmail
before_validation :blank_out_nonexistent_avatars
2019-09-05 08:59:51 -04:00
validates :blacklisted_tags, length: { maximum: 150_000 }
2022-12-30 13:32:03 -05:00
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 }
2022-12-30 13:32:03 -05:00
validates :time_zone, inclusion: { in: ActiveSupport::TimeZone.all.map(&:name) }
before_create :encrypt_password_on_create
before_update :encrypt_password_on_update
2010-02-15 13:59:58 -05:00
after_save :update_cache
2016-08-24 18:58:22 -04:00
#after_create :notify_sock_puppets
after_create :create_user_status
2014-07-23 18:15:47 -04:00
has_one :api_key
2014-11-20 00:28:26 -05:00
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"
2019-11-04 17:06:19 -05:00
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
2014-11-20 00:28:26 -05:00
accepts_nested_attributes_for :dmail_filter
2013-03-19 08:10:10 -04:00
module BanMethods
def validate_ip_addr_is_not_banned
2011-02-02 15:53:28 -05:00
if IpBan.is_banned?(CurrentUser.ip_addr)
2021-02-07 21:56:22 -05:00
self.errors.add(:base, "IP address is banned")
return false
end
end
2013-03-19 08:10:10 -04:00
2010-08-18 18:42:33 -04:00
def unban!
2014-06-23 21:00:31 -04:00
self.is_banned = false
2020-03-06 15:04:08 -05:00
self.level = 20
2014-06-23 21:00:31 -04:00
save
end
def ban_expired?
is_banned? && recent_ban.try(:expired?)
2010-08-18 18:42:33 -04:00
end
end
2013-03-19 08:10:10 -04:00
module NameMethods
2010-11-19 16:24:17 -05:00
extend ActiveSupport::Concern
2013-03-19 08:10:10 -04:00
module ClassMethods
2010-08-18 18:42:33 -04:00
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)
2010-08-18 18:42:33 -04:00
end
end
2013-03-19 08:10:10 -04:00
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
2010-08-18 18:42:33 -04:00
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
2013-03-19 08:10:10 -04:00
2010-03-11 19:42:04 -05:00
def find_by_name(name)
where("lower(name) = ?", normalize_name(name)).first
2010-03-11 19:42:04 -05:00
end
2013-03-19 08:10:10 -04:00
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
2013-03-19 08:10:10 -04:00
def pretty_name
name.gsub(/([^_])_+(?=[^_])/, "\\1 \\2")
end
2013-03-19 08:10:10 -04:00
2010-02-15 13:59:58 -05:00
def update_cache
Cache.write("uin:#{id}", name, expires_in: 4.hours)
Cache.write("uni:#{User.normalize_name(name)}", id, expires_in: 4.hours)
2010-02-15 13:59:58 -05:00
end
2010-02-06 16:48:40 -05:00
end
2013-03-19 08:10:10 -04:00
module PasswordMethods
def password_token
Zlib::crc32(bcrypt_password_hash)
end
def bcrypt_password
BCrypt::Password.new(bcrypt_password_hash)
end
2013-03-19 08:10:10 -04:00
def encrypt_password_on_create
self.password_hash = ""
self.bcrypt_password_hash = User.bcrypt(password)
end
2013-03-19 08:10:10 -04:00
def encrypt_password_on_update
return if password.blank?
return if old_password.blank?
2013-03-19 08:10:10 -04:00
if bcrypt_password == old_password
self.bcrypt_password_hash = User.bcrypt(password)
return true
else
2021-02-07 21:56:22 -05:00
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
2013-03-19 08:10:10 -04:00
module AuthenticationMethods
extend ActiveSupport::Concern
2013-03-19 08:10:10 -04:00
module ClassMethods
def authenticate(name, pass)
user = find_by_name(name)
2019-09-13 13:18:20 -04:00
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
2014-07-23 18:15:47 -04:00
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
2013-03-19 08:10:10 -04:00
2010-02-19 17:30:11 -05:00
module LevelMethods
extend ActiveSupport::Concern
2013-03-19 08:10:10 -04:00
module ClassMethods
def system
User.find_by!(name: Danbooru.config.system_user)
end
def anonymous
user = User.new(name: "Anonymous", created_at: Time.now)
2019-09-13 16:34:20 -04:00
user.level = Levels::ANONYMOUS
user.freeze.readonly!
user
end
def level_hash
Danbooru.config.levels
end
2016-10-24 19:56:18 -04:00
def level_string(value)
Danbooru.config.levels.invert[value] || ""
2016-10-24 19:56:18 -04:00
end
end
2013-03-19 08:10:10 -04:00
2014-10-28 14:05:21 -04:00
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)
2016-10-24 19:56:18 -04:00
User.level_string(value || level)
2010-02-19 17:30:11 -05:00
end
2011-06-25 19:31:39 -04:00
def is_anonymous?
level == Levels::ANONYMOUS
end
2010-03-12 19:27:54 -05:00
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(' ', '_')
2013-03-19 08:10:10 -04:00
# Changed from e6 to match new Danbooru semantics.
define_method("is_#{normalized_name}?") do
is_verified? && self.level >= value && self.id.present?
end
2011-06-25 19:31:39 -04:00
end
2013-03-19 08:10:10 -04:00
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
2019-10-03 10:29:49 -04:00
def staff_cant_disable_dmail
self.disable_user_dmails = false if self.is_janitor?
end
2022-12-20 09:50:18 -05:00
def level_css_class
"user-#{level_string.parameterize}"
end
def create_user_status
UserStatus.create!(user_id: id)
end
2010-02-19 17:30:11 -05:00
end
2013-03-19 08:10:10 -04:00
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)
2021-02-07 21:56:22 -05:00
self.errors.add(:base, "Email address may not be used")
return false
end
end
end
2013-03-19 08:10:10 -04:00
module BlacklistMethods
def normalize_blacklisted_tags
self.blacklisted_tags = TagAlias.to_aliased_query(blacklisted_tags, comments: true) if blacklisted_tags.present?
end
2019-10-03 10:29:49 -04:00
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) }
2019-10-03 10:29:49 -04:00
end
end
2013-03-19 08:10:10 -04:00
2010-03-11 19:42:04 -05:00
module ForumMethods
def has_forum_been_updated?
2019-11-15 05:15:55 -05:00
return false unless is_member?
max_updated_at = ForumTopic.visible(self).order(updated_at: :desc).first&.updated_at
2014-07-17 18:53:36 -04:00
return false if max_updated_at.nil?
2011-01-13 18:16:39 -05:00
return true if last_forum_read_at.nil?
2014-07-17 18:53:36 -04:00
return max_updated_at > last_forum_read_at
2010-03-11 19:42:04 -05:00
end
2019-11-04 17:06:19 -05:00
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
2010-03-11 19:42:04 -05:00
end
2013-03-19 08:10:10 -04:00
2019-06-27 11:33:02 -04:00
module ThrottleMethods
def throttle_reason(reason, timeframe = "hourly")
2019-06-27 11:33:02 -04:00
reasons = {
REJ_NEWBIE: "can not yet perform this action. Account is too new",
REJ_LIMITED: "have reached the #{timeframe} limit for this action",
2019-06-27 11:33:02 -04:00
}
reasons.fetch(reason, "unknown throttle reason, please report this as a bug")
2019-06-27 11:33:02 -04:00
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
2019-06-27 11:33:02 -04:00
def self.create_user_throttle(name, limiter, checker, newbie_duration)
2023-12-03 09:36:37 -05:00
define_method(:"#{name}_limit", limiter)
2023-12-03 09:36:37 -05:00
define_method(:"can_#{name}_with_reason") do
return true if Danbooru.config.disable_throttles?
2019-06-27 11:33:02 -04:00
return send(checker) if checker && send(checker)
return :REJ_NEWBIE if newbie_duration && younger_than(newbie_duration)
2019-06-27 11:33:02 -04:00
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?
2019-09-04 00:56:41 -04:00
is_privileged?
2019-06-27 11:33:02 -04:00
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)
2019-06-27 11:33:02 -04:00
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)
2019-06-27 11:33:02 -04:00
create_user_throttle(:pool, ->{ Danbooru.config.pool_limit - Pool.for_user(id).where('created_at > ?', 1.hour.ago).count },
2020-09-15 03:33:40 -04:00
: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 },
2020-09-15 03:33:40 -04:00
: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)
2019-06-27 11:33:02 -04:00
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)
2019-06-27 11:33:02 -04:00
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)
2020-05-11 00:09:45 -04:00
create_user_throttle(:forum_post, ->{ Danbooru.config.member_comment_limit - ForumPost.for_user(id).where('created_at > ?', 1.hour.ago).count },
nil, 3.days)
2019-06-27 11:33:02 -04:00
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 },
2019-09-04 00:56:41 -04:00
:general_bypass_throttle?, nil)
2019-09-16 15:16:49 -04:00
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 },
2021-03-04 23:33:56 -05:00
:is_janitor?, 3.days)
2019-06-27 11:33:02 -04:00
def can_remove_from_pools?
2020-07-22 20:23:31 -04:00
is_member? && older_than(7.days)
end
2013-03-19 08:10:10 -04:00
2020-02-02 16:50:20 -05:00
def can_discord?
2020-07-22 20:23:31 -04:00
is_member? && older_than(7.days)
2020-02-02 16:50:20 -05:00
end
def can_view_flagger?(flagger_id)
2020-03-06 15:57:51 -05:00
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
2022-01-30 08:03:51 -05:00
pieces = upload_limit_pieces
base_upload_limit + (pieces[:approved] / 10) - (pieces[:deleted] / 4) - pieces[:pending]
end
2025-02-26 08:34:43 -05:00
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
2013-03-19 08:10:10 -04:00
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
2013-03-19 08:10:10 -04:00
def favorite_limit
Danbooru.config.legacy_favorite_limit.fetch(id, 80_000)
end
2015-06-23 15:25:54 -04:00
2017-01-06 18:40:49 -05:00
def api_regen_multiplier
2019-09-16 15:39:47 -04:00
1
2017-01-06 18:40:49 -05:00
end
2017-01-06 18:40:49 -05:00
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?
2019-09-16 15:39:47 -04:00
120
elsif is_privileged?
2019-09-16 15:39:47 -04:00
90
else
2019-09-16 15:39:47 -04:00
60
2013-03-20 19:35:35 -04:00
end
end
2014-06-01 13:39:19 -04:00
2017-01-06 18:40:49 -05:00
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
2013-03-19 08:10:10 -04:00
2011-09-10 15:58:04 -04:00
module ApiMethods
# blacklist all attributes by default. whitelist only safe attributes.
2011-09-10 15:58:04 -04:00
def hidden_attributes
super + attributes.keys.map(&:to_sym)
2011-09-10 15:58:04 -04:00
end
2013-03-19 08:10:10 -04:00
2014-06-01 13:39:19 -04:00
def method_attributes
list = super + [
2019-09-16 16:50:47 -04:00
:id, :created_at, :name, :level, :base_upload_limit,
:post_upload_count, :post_update_count, :note_update_count,
2019-06-01 11:46:03 -04:00
:is_banned, :can_approve_posts, :can_upload_free,
:level_string, :avatar_id
]
2014-06-01 13:39:19 -04:00
if id == CurrentUser.user.id
boolean_attributes = %i[
blacklist_users description_collapsed_initially
hide_comments show_hidden_comments show_post_statistics
2024-02-23 11:18:22 -05:00
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,
2019-03-23 03:57:37 -04:00
:statement_timeout, :favorite_limit,
2024-02-23 11:18:22 -05:00
:tag_query_limit, :has_mail?
]
2014-06-01 13:39:19 -04:00
end
2014-06-01 13:39:19 -04:00
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
2011-09-10 15:58:04 -04:00
end
2013-03-19 08:10:10 -04:00
2013-05-05 17:34:41 -04:00
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
2020-01-03 08:52:31 -05:00
def post_deleted_count
user_status.post_deleted_count
end
def note_version_count
user_status.note_count
2013-05-05 17:34:41 -04:00
end
def note_update_count
note_version_count
end
2013-05-05 17:34:41 -04:00
def artist_version_count
user_status.artist_edit_count
2013-05-05 17:34:41 -04:00
end
def pool_version_count
user_status.pool_edit_count
2013-05-05 17:34:41 -04:00
end
def forum_post_count
user_status.forum_post_count
end
def favorite_count
user_status.favorite_count
2013-05-05 17:34:41 -04:00
end
def comment_count
user_status.comment_count
2013-05-05 17:34:41 -04:00
end
2013-05-20 09:38:05 -04:00
2013-08-13 13:33:25 -04:00
def flag_count
user_status.post_flag_count
2013-08-13 13:33:25 -04:00
end
def ticket_count
user_status.ticket_count
end
2025-02-26 08:34:43 -05:00
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
2013-05-20 09:38:05 -04:00
def positive_feedback_count
feedback.active.positive.count
2013-05-20 09:38:05 -04:00
end
def neutral_feedback_count
feedback.active.neutral.count
2013-05-20 09:38:05 -04:00
end
def negative_feedback_count
feedback.active.negative.count
end
def deleted_feedback_count
feedback.deleted.count
2013-05-20 09:38:05 -04:00
end
2018-06-20 14:23:44 -04:00
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
2018-06-20 14:23:44 -04:00
def refresh_counts!
self.class.without_timeout do
UserStatus.where(user_id: id).update_all(
post_count: Post.for_user(id).count,
2020-01-03 08:52:31 -05:00
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,
2018-06-20 14:23:44 -04:00
)
end
end
2013-05-05 17:34:41 -04:00
end
2013-01-10 17:45:52 -05:00
module SearchMethods
def admins
2013-03-26 01:03:42 -04:00
where("level = ?", Levels::ADMIN)
2013-01-10 17:45:52 -05:00
end
2013-03-19 08:10:10 -04:00
2013-01-10 17:45:52 -05:00
def with_email(email)
2013-03-19 08:10:10 -04:00
if email.blank?
2013-01-10 17:45:52 -05:00
where("FALSE")
2013-03-19 08:10:10 -04:00
else
where("lower(email) = lower(?)", email)
2013-01-10 17:45:52 -05:00
end
end
2013-03-19 08:10:10 -04:00
2013-01-10 17:45:52 -05:00
def search(params)
q = super
q = q.joins(:user_status)
2013-03-19 08:10:10 -04:00
2018-10-04 14:03:18 -04:00
q = q.attribute_matches(:level, params[:level])
2023-05-18 14:48:01 -04:00
if params[:about_me].present?
q = q.attribute_matches(:profile_about, params[:about_me]).or(attribute_matches(:profile_artinfo, params[:about_me]))
end
2018-10-04 14:03:18 -04:00
if params[:avatar_id].present?
q = q.where(avatar_id: params[:avatar_id])
end
if params[:email_matches].present?
2020-03-06 14:37:13 -05:00
q = q.where_ilike(:email, params[:email_matches])
end
2013-02-19 12:27:17 -05:00
if params[:name_matches].present?
q = q.where_ilike(:name, normalize_name(params[:name_matches]))
end
2013-02-19 12:27:17 -05:00
if params[:min_level].present?
2013-01-10 17:45:52 -05:00
q = q.where("level >= ?", params[:min_level].to_i)
end
2013-02-22 12:27:19 -05:00
2013-04-28 14:11:21 -04:00
if params[:max_level].present?
q = q.where("level <= ?", params[:max_level].to_i)
end
bitprefs_length = BOOLEAN_ATTRIBUTES.length
2016-09-10 16:06:27 -04:00
bitprefs_include = nil
bitprefs_exclude = nil
2019-06-01 11:46:03 -04:00
[:can_approve_posts, :can_upload_free].each do |x|
2016-09-10 16:06:27 -04:00
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'
2016-09-10 16:06:27 -04:00
end
end
end
2016-09-10 16:06:27 -04:00
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})
2016-09-10 16:06:27 -04:00
end
2016-09-10 16:06:27 -04:00
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})
2016-09-10 16:06:27 -04:00
end
if params[:ip_addr].present?
q = q.where("last_ip_addr <<= ?", params[:ip_addr])
2015-04-23 09:43:19 -04:00
end
2013-03-08 16:52:24 -05:00
case params[:order]
when "name"
q = q.order("name")
when "post_upload_count"
q = q.order("user_statuses.post_count desc")
2013-03-08 16:52:24 -05:00
when "note_count"
q = q.order("user_statuses.note_count desc")
2013-03-19 08:10:10 -04:00
when "post_update_count"
q = q.order("user_statuses.post_update_count desc")
2013-03-08 16:52:24 -05:00
else
2023-07-07 08:32:57 -04:00
q = q.apply_basic_order(params)
2013-03-08 16:52:24 -05:00
end
2013-03-19 08:10:10 -04:00
2013-01-10 17:45:52 -05:00
q
end
end
2013-03-19 08:10:10 -04:00
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")
2016-08-24 18:58:22 -04:00
end
end
end
include BanMethods
include NameMethods
include PasswordMethods
include AuthenticationMethods
2010-02-19 17:30:11 -05:00
include LevelMethods
include EmailMethods
include BlacklistMethods
2010-03-11 19:42:04 -05:00
include ForumMethods
include LimitMethods
2011-09-10 15:58:04 -04:00
include ApiMethods
2013-05-05 17:34:41 -04:00
include CountMethods
2013-01-10 17:45:52 -05:00
extend SearchMethods
2019-06-27 11:33:02 -04:00
extend ThrottleMethods
2013-03-19 08:10:10 -04:00
2024-02-23 11:18:22 -05:00
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
Raise error on unpermitted params. Fail loudly if we forget to whitelist a param instead of silently ignoring it. misc models: convert to strong params. artist commentaries: convert to strong params. * Disallow changing or setting post_id to a nonexistent post. artists: convert to strong params. * Disallow setting `is_banned` in create/update actions. Changing it this way instead of with the ban/unban actions would leave the artist in a partially banned state. bans: convert to strong params. * Disallow changing the user_id after the ban has been created. comments: convert to strong params. favorite groups: convert to strong params. news updates: convert to strong params. post appeals: convert to strong params. post flags: convert to strong params. * Disallow users from setting the `is_deleted` / `is_resolved` flags. ip bans: convert to strong params. user feedbacks: convert to strong params. * Disallow users from setting `disable_dmail_notification` when creating feedbacks. * Disallow changing the user_id after the feedback has been created. notes: convert to strong params. wiki pages: convert to strong params. * Also fix non-Builders being able to delete wiki pages. saved searches: convert to strong params. pools: convert to strong params. * Disallow setting `post_count` or `is_deleted` in create/update actions. janitor trials: convert to strong params. post disapprovals: convert to strong params. * Factor out quick-mod bar to shared partial. * Fix quick-mod bar to use `Post#is_approvable?` to determine visibility of Approve button. dmail filters: convert to strong params. password resets: convert to strong params. user name change requests: convert to strong params. posts: convert to strong params. users: convert to strong params. * Disallow setting password_hash, last_logged_in_at, last_forum_read_at, has_mail, and dmail_filter_attributes[user_id]. * Remove initialize_default_image_size (dead code). uploads: convert to strong params. * Remove `initialize_status` because status already defaults to pending in the database. tag aliases/implications: convert to strong params. tags: convert to strong params. forum posts: convert to strong params. * Disallow changing the topic_id after creating the post. * Disallow setting is_deleted (destroy/undelete actions should be used instead). * Remove is_sticky / is_locked (nonexistent attributes). forum topics: convert to strong params. * merges https://github.com/evazion/danbooru/tree/wip-rails-5.1 * lock pg gem to 0.21 (1.0.0 is incompatible with rails 5.1.4) * switch to factorybot and change all references Co-authored-by: r888888888 <r888888888@gmail.com> Co-authored-by: evazion <noizave@gmail.com> add diffs
2018-04-02 13:51:26 -04:00
def initialize_attributes
return if Rails.env.test?
Danbooru.config.customize_new_user(self)
2014-06-19 03:07:19 -04:00
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
2010-02-06 16:48:40 -05:00
end