2019-05-03 06:17:27 +08:00
# frozen_string_literal: true
2013-02-06 03:16:51 +08:00
class User < ActiveRecord :: Base
2017-08-15 23:46:57 +08:00
include Searchable
2013-06-07 06:07:59 +08:00
include Roleable
2014-04-28 16:31:51 +08:00
include HasCustomFields
2018-02-20 14:44:51 +08:00
include SecondFactorManager
2018-10-05 16:53:59 +08:00
include HasDestroyedWebHook
2023-10-27 17:27:04 +08:00
include HasDeprecatedColumns
2013-06-07 06:07:59 +08:00
2020-05-23 12:56:13 +08:00
DEFAULT_FEATURED_BADGE_COUNT = 3
2023-04-11 17:16:28 +08:00
PASSWORD_SALT_LENGTH = 16
TARGET_PASSWORD_ALGORITHM =
" $pbkdf2- #{ Rails . configuration . pbkdf2_algorithm } $i= #{ Rails . configuration . pbkdf2_iterations } ,l=32$ "
2023-10-27 17:27:04 +08:00
deprecate_column :flag_level , drop_from : " 3.2 "
2020-05-23 12:56:13 +08:00
# not deleted on user delete
2013-02-06 03:16:51 +08:00
has_many :posts
2020-05-23 12:56:13 +08:00
has_many :topics
has_many :uploads
2015-09-03 02:43:15 +08:00
has_many :category_users , dependent : :destroy
2016-05-05 02:02:47 +08:00
has_many :tag_users , dependent : :destroy
2016-08-16 15:06:33 +08:00
has_many :user_api_keys , dependent : :destroy
2020-05-23 11:25:56 +08:00
has_many :topic_allowed_users , dependent : :destroy
2020-05-23 12:56:13 +08:00
has_many :user_archived_messages , dependent : :destroy
has_many :email_change_requests , dependent : :destroy
2020-05-23 11:25:56 +08:00
has_many :email_tokens , dependent : :destroy
has_many :topic_links , dependent : :destroy
has_many :user_uploads , dependent : :destroy
2022-06-09 07:24:30 +08:00
has_many :upload_references , as : :target , dependent : :destroy
2021-02-22 19:42:37 +08:00
has_many :user_emails , dependent : :destroy , autosave : true
2020-05-23 12:56:13 +08:00
has_many :user_associated_accounts , dependent : :destroy
has_many :oauth2_user_infos , dependent : :destroy
has_many :user_second_factors , dependent : :destroy
has_many :user_badges , - > { for_enabled_badges } , dependent : :destroy
has_many :user_auth_tokens , dependent : :destroy
has_many :group_users , dependent : :destroy
has_many :user_warnings , dependent : :destroy
has_many :api_keys , dependent : :destroy
has_many :push_subscriptions , dependent : :destroy
has_many :acting_group_histories ,
dependent : :destroy ,
foreign_key : :acting_user_id ,
class_name : " GroupHistory "
has_many :targeted_group_histories ,
dependent : :destroy ,
foreign_key : :target_user_id ,
class_name : " GroupHistory "
has_many :reviewable_scores , dependent : :destroy
2020-06-09 23:19:32 +08:00
has_many :invites , foreign_key : :invited_by_id , dependent : :destroy
2021-04-27 13:52:45 +08:00
has_many :user_custom_fields , dependent : :destroy
2021-12-09 20:30:27 +08:00
has_many :user_associated_groups , dependent : :destroy
2021-08-27 00:16:00 +08:00
has_many :pending_posts ,
- > { merge ( Reviewable . pending ) } ,
class_name : " ReviewableQueuedPost " ,
2023-07-18 19:50:31 +08:00
foreign_key : :target_created_by_id
2020-05-23 11:25:56 +08:00
has_one :user_option , dependent : :destroy
has_one :user_avatar , dependent : :destroy
2021-03-10 20:49:13 +08:00
has_one :primary_email ,
- > { where ( primary : true ) } ,
class_name : " UserEmail " ,
dependent : :destroy ,
autosave : true ,
validate : false
2020-05-23 12:56:13 +08:00
has_one :user_stat , dependent : :destroy
has_one :user_profile , dependent : :destroy , inverse_of : :user
has_one :single_sign_on_record , dependent : :destroy
has_one :anonymous_user_master , class_name : " AnonymousUser " , dependent : :destroy
has_one :anonymous_user_shadow ,
- > ( record ) { where ( active : true ) } ,
foreign_key : :master_user_id ,
class_name : " AnonymousUser " ,
dependent : :destroy
2020-06-09 23:19:32 +08:00
has_one :invited_user , dependent : :destroy
2021-01-21 00:31:52 +08:00
has_one :user_notification_schedule , dependent : :destroy
2024-06-04 15:42:53 +08:00
has_many :passwords , class_name : " UserPassword " , dependent : :destroy
2020-05-23 11:25:56 +08:00
2020-05-23 12:56:13 +08:00
# delete all is faster but bypasses callbacks
has_many :bookmarks , dependent : :delete_all
has_many :notifications , dependent : :delete_all
has_many :topic_users , dependent : :delete_all
has_many :incoming_emails , dependent : :delete_all
has_many :user_visits , dependent : :delete_all
has_many :user_auth_token_logs , dependent : :delete_all
has_many :group_requests , dependent : :delete_all
has_many :muted_user_records , class_name : " MutedUser " , dependent : :delete_all
has_many :ignored_user_records , class_name : " IgnoredUser " , dependent : :delete_all
2020-12-18 23:03:51 +08:00
has_many :do_not_disturb_timings , dependent : :delete_all
2023-02-03 11:44:40 +08:00
has_many :sidebar_sections , dependent : :destroy
2022-05-27 17:15:14 +08:00
has_one :user_status , dependent : :destroy
2019-05-29 12:26:06 +08:00
2020-05-23 12:56:13 +08:00
# dependent deleting handled via before_destroy (special cases)
has_many :user_actions
has_many :post_actions
has_many :post_timings
has_many :directory_items
2021-02-22 21:07:47 +08:00
has_many :email_logs
2020-05-23 11:25:56 +08:00
has_many :security_keys , - > { where ( enabled : true ) } , class_name : " UserSecurityKey "
2023-08-24 14:27:38 +08:00
has_many :all_security_keys , class_name : " UserSecurityKey "
2020-05-23 11:25:56 +08:00
2020-05-23 12:56:13 +08:00
has_many :badges , through : :user_badges
2021-06-22 23:58:03 +08:00
has_many :default_featured_user_badges ,
2023-11-29 13:38:07 +08:00
- > do
2021-06-22 23:58:03 +08:00
max_featured_rank =
2023-01-09 20:20:10 +08:00
(
2021-06-22 23:58:03 +08:00
if SiteSetting . max_favorite_badges > 0
SiteSetting . max_favorite_badges + 1
2023-01-09 20:20:10 +08:00
else
2021-06-22 23:58:03 +08:00
DEFAULT_FEATURED_BADGE_COUNT
2023-01-09 20:20:10 +08:00
end
)
2021-06-22 23:58:03 +08:00
for_enabled_badges . grouped_with_count . where ( " featured_rank <= ? " , max_featured_rank )
2023-11-29 13:38:07 +08:00
end ,
2021-06-22 23:58:03 +08:00
class_name : " UserBadge "
2020-05-23 12:56:13 +08:00
has_many :topics_allowed , through : :topic_allowed_users , source : :topic
has_many :groups , through : :group_users
2022-12-06 02:39:10 +08:00
has_many :secure_categories , - > { distinct } , through : :groups , source : :categories
2021-12-09 20:30:27 +08:00
has_many :associated_groups , through : :user_associated_groups , dependent : :destroy
2020-05-23 12:56:13 +08:00
# deleted in user_second_factors relationship
has_many :totps ,
- > { where ( method : UserSecondFactor . methods [ :totp ] , enabled : true ) } ,
class_name : " UserSecondFactor "
2020-05-23 11:25:56 +08:00
2019-05-29 12:26:06 +08:00
has_one :master_user , through : :anonymous_user_master
has_one :shadow_user , through : :anonymous_user_shadow , source : :user
2019-04-29 11:58:52 +08:00
has_one :profile_background_upload , through : :user_profile
has_one :card_background_upload , through : :user_profile
2013-02-06 03:16:51 +08:00
belongs_to :approved_by , class_name : " User "
2014-04-24 10:42:04 +08:00
belongs_to :primary_group , class_name : " Group "
2021-07-08 15:46:21 +08:00
belongs_to :flair_group , class_name : " Group "
2013-02-06 03:16:51 +08:00
2015-03-24 08:55:22 +08:00
has_many :muted_users , through : :muted_user_records
2020-01-02 21:04:08 +08:00
has_many :ignored_users , through : :ignored_user_records
2014-05-22 15:37:02 +08:00
belongs_to :uploaded_avatar , class_name : " Upload "
2013-08-14 04:08:29 +08:00
2022-06-30 14:54:20 +08:00
has_many :sidebar_section_links , dependent : :delete_all
2024-05-17 03:47:01 +08:00
has_many :embeddable_hosts
2022-06-30 14:54:20 +08:00
2013-11-15 23:27:43 +08:00
delegate :last_sent_email_address , to : :email_logs
2013-02-06 03:16:51 +08:00
validates_presence_of :username
2017-08-31 12:06:56 +08:00
validate :username_validator , if : :will_save_change_to_username?
2013-02-06 03:16:51 +08:00
validate :password_validator
2019-05-14 04:43:19 +08:00
validate :name_validator , if : :will_save_change_to_name?
2017-08-31 12:06:56 +08:00
validates :name , user_full_name : true , if : :will_save_change_to_name? , length : { maximum : 255 }
2023-10-27 15:22:38 +08:00
validates :ip_address , allowed_ip_address : { on : :create }
2023-03-30 11:52:10 +08:00
validates :primary_email , presence : true , unless : :skip_email_validation
2024-01-30 01:44:32 +08:00
validates :validatable_user_fields_values ,
watched_words : true ,
unless : :should_skip_user_fields_validation?
2017-09-12 01:22:04 +08:00
validates_associated :primary_email ,
2024-06-20 16:33:01 +08:00
message : - > ( _ , user_email ) do
user_email [ :value ] & . errors & . [] ( :email ) & . first . to_s
end
2013-02-06 03:16:51 +08:00
after_initialize :add_trust_level
2015-08-22 02:39:21 +08:00
2017-10-25 13:02:18 +08:00
before_validation :set_skip_validate_email
2017-08-09 10:56:08 +08:00
2013-02-06 10:44:49 +08:00
after_create :create_email_token
2013-09-12 02:50:26 +08:00
after_create :create_user_stat
2016-02-17 12:46:19 +08:00
after_create :create_user_option
2014-05-28 01:54:04 +08:00
after_create :create_user_profile
2018-07-18 18:57:43 +08:00
after_create :set_random_avatar
2014-06-17 08:46:30 +08:00
after_create :ensure_in_trust_level_group
2015-08-22 02:39:21 +08:00
after_create :set_default_categories_preferences
2019-11-01 15:10:13 +08:00
after_create :set_default_tags_preferences
2023-07-27 10:52:33 +08:00
after_create :set_default_sidebar_section_links
2024-05-15 08:06:58 +08:00
after_create :refresh_user_directory , if : Proc . new { SiteSetting . bootstrap_mode_enabled }
2023-07-27 10:52:33 +08:00
after_update :set_default_sidebar_section_links , if : Proc . new { self . saved_change_to_staged? }
2014-08-14 04:17:16 +08:00
2020-05-08 09:27:26 +08:00
after_update :trigger_user_updated_event ,
if : Proc . new { self . human? && self . saved_change_to_uploaded_avatar_id? }
2019-06-17 13:10:47 +08:00
after_update :trigger_user_automatic_group_refresh , if : :saved_change_to_staged?
2023-06-26 11:01:59 +08:00
after_update :change_display_name , if : :saved_change_to_name?
2019-05-28 00:12:26 +08:00
2019-04-23 18:22:47 +08:00
before_save :update_usernames
2014-08-14 04:17:16 +08:00
before_save :ensure_password_is_hashed
2021-07-08 15:46:21 +08:00
before_save :match_primary_group_changes
2018-09-21 10:06:08 +08:00
before_save :check_if_title_is_badged_granted
2024-01-30 01:44:32 +08:00
before_save :apply_watched_words , unless : :should_skip_user_fields_validation?
2014-08-14 04:17:16 +08:00
2017-02-01 06:21:37 +08:00
after_save :expire_tokens_if_password_changed
2014-08-14 04:17:16 +08:00
after_save :clear_global_notice_if_needed
2014-05-22 15:37:02 +08:00
after_save :refresh_avatar
2014-07-23 09:42:24 +08:00
after_save :badge_grant
2015-06-06 01:50:06 +08:00
after_save :expire_old_email_tokens
2016-12-22 10:13:14 +08:00
after_save :index_search
2018-12-15 05:52:37 +08:00
after_save :check_site_contact_username
2019-06-17 13:10:47 +08:00
2022-06-09 07:24:30 +08:00
after_save do
if saved_change_to_uploaded_avatar_id?
UploadReference . ensure_exist! ( upload_ids : [ self . uploaded_avatar_id ] , target : self )
end
end
2017-03-16 16:02:34 +08:00
after_commit :trigger_user_created_event , on : :create
2018-07-23 15:49:49 +08:00
after_commit :trigger_user_destroyed_event , on : :destroy
2013-02-06 03:16:51 +08:00
2013-09-04 05:19:29 +08:00
before_destroy do
# These tables don't have primary keys, so destroying them with activerecord is tricky:
2017-08-31 12:06:56 +08:00
PostTiming . where ( user_id : self . id ) . delete_all
TopicViewItem . where ( user_id : self . id ) . delete_all
2019-01-17 09:40:30 +08:00
UserAction . where (
" user_id = :user_id OR target_user_id = :user_id OR acting_user_id = :user_id " ,
user_id : self . id ,
) . delete_all
# we need to bypass the default scope here, which appears not bypassed for :delete_all
# however :destroy it is bypassed
PostAction . with_deleted . where ( user_id : self . id ) . delete_all
2019-04-26 16:11:39 +08:00
# This is a perf optimisation to ensure we hit the index
# without this we need to scan a much larger number of rows
DirectoryItem
. where ( user_id : self . id )
. where ( " period_type in (?) " , DirectoryItem . period_types . values )
. delete_all
2020-05-23 12:56:13 +08:00
# our relationship filters on enabled, this makes sure everything is deleted
UserSecurityKey . where ( user_id : self . id ) . delete_all
2020-06-19 05:42:39 +08:00
Developer . where ( user_id : self . id ) . delete_all
DraftSequence . where ( user_id : self . id ) . delete_all
GivenDailyLike . where ( user_id : self . id ) . delete_all
MutedUser . where ( user_id : self . id ) . or ( MutedUser . where ( muted_user_id : self . id ) ) . delete_all
IgnoredUser . where ( user_id : self . id ) . or ( IgnoredUser . where ( ignored_user_id : self . id ) ) . delete_all
UserAvatar . where ( user_id : self . id ) . delete_all
2013-09-04 05:19:29 +08:00
end
2016-09-08 02:05:46 +08:00
# Skip validating email, for example from a particular auth provider plugin
attr_accessor :skip_email_validation
2013-02-06 03:16:51 +08:00
# Whether we need to be sending a system message after creation
attr_accessor :send_welcome_message
# This is just used to pass some information into the serializer
attr_accessor :notification_channel_position
2014-08-14 04:17:16 +08:00
# set to true to optimize creation and save for imports
attr_accessor :import_mode
2021-04-27 13:52:45 +08:00
# Cache for user custom fields. Currently it is used to display quick search results
attr_accessor :custom_data
2024-07-24 15:19:58 +08:00
# Information if user was authenticated with OAuth
attr_accessor :authenticated_with_oauth
2018-03-19 11:31:14 +08:00
scope :with_email ,
2018-03-19 12:34:21 +08:00
- > ( email ) { joins ( :user_emails ) . where ( " lower(user_emails.email) IN (?) " , email ) }
2017-04-27 02:47:36 +08:00
2021-07-05 12:56:32 +08:00
scope :with_primary_email ,
2023-11-29 13:38:07 +08:00
- > ( email ) do
2021-07-05 12:56:32 +08:00
joins ( :user_emails ) . where (
" lower(user_emails.email) IN (?) AND user_emails.primary " ,
email ,
)
2023-11-29 13:38:07 +08:00
end
2021-07-05 12:56:32 +08:00
2024-03-27 05:55:53 +08:00
scope :human_users ,
- > ( allowed_bot_user_ids : nil ) do
if allowed_bot_user_ids . present?
where ( " users.id > 0 OR users.id IN (?) " , allowed_bot_user_ids )
else
where ( " users.id > 0 " )
end
end
2017-03-11 14:25:09 +08:00
2015-05-11 07:10:10 +08:00
# excluding fake users like the system user or anonymous users
2017-03-11 14:25:09 +08:00
scope :real ,
2024-03-27 05:55:53 +08:00
- > ( allowed_bot_user_ids : nil ) do
human_users ( allowed_bot_user_ids : allowed_bot_user_ids ) . where (
2017-03-11 14:25:09 +08:00
" NOT EXISTS(
2015-05-11 07:10:10 +08:00
SELECT 1
2019-05-29 12:26:06 +08:00
FROM anonymous_users a
WHERE a . user_id = users . id
2023-01-09 20:20:10 +08:00
) " ,
2019-05-29 12:26:06 +08:00
)
2023-11-29 13:38:07 +08:00
end
2013-03-29 14:29:58 +08:00
2014-09-04 05:50:19 +08:00
# TODO-PERF: There is no indexes on any of these
# and NotifyMailingListSubscribers does a select-all-and-loop
2017-11-11 01:18:08 +08:00
# may want to create an index on (active, silence, suspended_till)?
2017-11-14 02:41:36 +08:00
scope :silenced , - > { where ( " silenced_till IS NOT NULL AND silenced_till > ? " , Time . zone . now ) }
scope :not_silenced , - > { where ( " silenced_till IS NULL OR silenced_till <= ? " , Time . zone . now ) }
2014-09-04 05:50:19 +08:00
scope :suspended , - > { where ( " suspended_till IS NOT NULL AND suspended_till > ? " , Time . zone . now ) }
scope :not_suspended , - > { where ( " suspended_till IS NULL OR suspended_till <= ? " , Time . zone . now ) }
scope :activated , - > { where ( active : true ) }
2022-06-08 02:58:58 +08:00
scope :not_staged , - > { where ( staged : false ) }
2014-09-04 05:50:19 +08:00
2018-03-22 13:42:46 +08:00
scope :filter_by_username ,
2023-11-29 13:38:07 +08:00
- > ( filter ) do
2018-03-26 14:30:37 +08:00
if filter . is_a? ( Array )
where ( " username_lower ~* ? " , " ( #{ filter . join ( " | " ) } ) " )
else
where ( " username_lower ILIKE ? " , " % #{ filter } % " )
2018-03-22 13:42:46 +08:00
end
2023-11-29 13:38:07 +08:00
end
2018-03-26 14:30:37 +08:00
2018-03-22 13:42:46 +08:00
scope :filter_by_username_or_email ,
2023-11-29 13:38:07 +08:00
- > ( filter ) do
2022-12-16 09:08:05 +08:00
if filter . is_a? ( String ) && filter =~ / .+@.+ /
2018-03-22 13:42:46 +08:00
# probably an email so try the bypass
2023-02-13 12:39:45 +08:00
if user_id = UserEmail . where ( " lower(email) = ? " , filter . downcase ) . pick ( :user_id )
2018-03-22 13:42:46 +08:00
return where ( " users.id = ? " , user_id )
2023-01-09 20:20:10 +08:00
end
end
2018-03-26 14:30:37 +08:00
users = joins ( :primary_email )
2023-01-09 20:20:10 +08:00
2018-03-26 14:30:37 +08:00
if filter . is_a? ( Array )
users . where (
" username_lower ~* :filter OR lower(user_emails.email) SIMILAR TO :filter " ,
filter : " ( #{ filter . join ( " | " ) } ) " ,
)
else
users . where (
2018-03-22 13:42:46 +08:00
" username_lower ILIKE :filter OR lower(user_emails.email) ILIKE :filter " ,
filter : " % #{ filter } % " ,
)
end
2023-11-29 13:38:07 +08:00
end
2018-03-22 13:42:46 +08:00
2022-10-06 08:10:43 +08:00
scope :watching_topic ,
2023-11-29 13:38:07 +08:00
- > ( topic ) do
2020-12-16 06:30:21 +08:00
joins (
DB . sql_fragment (
" LEFT JOIN category_users ON category_users.user_id = users.id AND category_users.category_id = :category_id " ,
category_id : topic . category_id ,
2023-01-09 20:20:10 +08:00
) ,
2020-12-16 06:30:21 +08:00
)
. joins (
DB . sql_fragment (
" LEFT JOIN topic_users ON topic_users.user_id = users.id AND topic_users.topic_id = :topic_id " ,
topic_id : topic . id ,
2023-01-09 20:20:10 +08:00
) ,
2020-12-16 06:30:21 +08:00
)
. joins (
" LEFT JOIN tag_users ON tag_users.user_id = users.id AND tag_users.tag_id IN ( #{ topic . tag_ids . join ( " , " ) . presence || " NULL " } ) " ,
)
. where (
" category_users.notification_level > 0 OR topic_users.notification_level > 0 OR tag_users.notification_level > 0 " ,
2023-01-09 20:20:10 +08:00
)
2023-11-29 13:38:07 +08:00
end
2020-12-16 06:30:21 +08:00
2013-02-14 14:32:58 +08:00
module NewTopicDuration
2013-02-26 00:42:20 +08:00
ALWAYS = - 1
2013-02-14 14:32:58 +08:00
LAST_VISIT = - 2
end
2014-03-08 01:58:53 +08:00
2019-08-10 18:02:12 +08:00
MAX_STAFF_DELETE_POST_COUNT || = 5
2022-11-10 02:20:34 +08:00
def self . user_tips
@user_tips || =
Enum . new (
first_notification : 1 ,
topic_timeline : 2 ,
2022-11-15 23:36:08 +08:00
post_menu : 3 ,
topic_notification_levels : 4 ,
suggested_topics : 5 ,
2022-11-10 02:20:34 +08:00
)
end
2024-01-30 01:44:32 +08:00
def should_skip_user_fields_validation?
custom_fields_clean? || SiteSetting . disable_watched_word_checking_in_user_fields
end
2024-05-07 00:32:18 +08:00
def all_sidebar_sections
sidebar_sections
. or ( SidebarSection . public_sections )
. includes ( :sidebar_urls )
. order ( " (section_type IS NOT NULL) DESC, (public IS TRUE) DESC " )
end
2023-07-27 10:52:33 +08:00
def secured_sidebar_category_ids ( user_guardian = nil )
user_guardian || = guardian
SidebarSectionLink . where ( user_id : self . id , linkable_type : " Category " ) . pluck ( :linkable_id ) &
user_guardian . allowed_category_ids
end
2022-10-27 06:38:50 +08:00
def visible_sidebar_tags ( user_guardian = nil )
user_guardian || = guardian
2023-07-27 10:52:33 +08:00
DiscourseTagging . filter_visible (
Tag . where (
id : SidebarSectionLink . where ( user_id : self . id , linkable_type : " Tag " ) . select ( :linkable_id ) ,
) ,
user_guardian ,
)
2022-10-27 06:38:50 +08:00
end
2014-09-12 03:22:11 +08:00
def self . max_password_length
200
end
2013-02-06 03:16:51 +08:00
def self . username_length
2014-07-17 00:25:24 +08:00
SiteSetting . min_username_length . to_i .. SiteSetting . max_username_length . to_i
2013-02-06 03:16:51 +08:00
end
2019-04-23 18:22:47 +08:00
def self . normalize_username ( username )
2022-04-05 05:15:32 +08:00
username . to_s . unicode_normalize . downcase if username . present?
2019-04-23 18:22:47 +08:00
end
2018-08-01 11:08:45 +08:00
def self . username_available? ( username , email = nil , allow_reserved_username : false )
2019-04-23 18:22:47 +08:00
lower = normalize_username ( username )
2018-08-01 11:08:45 +08:00
return false if ! allow_reserved_username && reserved_username? ( lower )
2019-02-20 05:31:03 +08:00
return true if ! username_exists? ( lower )
2018-05-23 03:25:52 +08:00
2017-12-12 18:26:00 +08:00
# staged users can use the same username since they will take over the account
email . present? &&
User . joins ( :user_emails ) . exists? (
staged : true ,
username_lower : lower ,
user_emails : {
primary : true ,
email : email ,
} ,
)
2017-04-13 10:44:26 +08:00
end
def self . reserved_username? ( username )
2019-04-23 18:22:47 +08:00
username = normalize_username ( username )
2016-08-31 21:49:45 +08:00
2021-11-24 04:25:54 +08:00
return true if SiteSetting . here_mention == username
2019-04-23 18:22:47 +08:00
SiteSetting
. reserved_usernames
. unicode_normalize
. split ( " | " )
2023-01-21 02:52:49 +08:00
. any? { | reserved | username . match? ( / \ A #{ Regexp . escape ( reserved ) . gsub ( '\*' , " .* " ) } \ z / ) }
2013-02-06 03:16:51 +08:00
end
2019-10-11 16:57:55 +08:00
def self . editable_user_custom_fields ( by_staff : false )
2018-09-04 18:45:36 +08:00
fields = [ ]
2020-08-31 06:52:01 +08:00
fields . push ( * DiscoursePluginRegistry . self_editable_user_custom_fields )
fields . push ( * DiscoursePluginRegistry . staff_editable_user_custom_fields ) if by_staff
2019-10-11 16:57:55 +08:00
2018-09-04 18:45:36 +08:00
fields . uniq
end
2020-07-27 08:23:54 +08:00
def self . allowed_user_custom_fields ( guardian )
2016-03-12 04:52:18 +08:00
fields = [ ]
2020-08-31 06:52:01 +08:00
fields . push ( * DiscoursePluginRegistry . public_user_custom_fields )
2018-10-17 17:33:27 +08:00
2016-03-12 04:52:18 +08:00
if SiteSetting . public_user_custom_fields . present?
2020-08-31 06:52:01 +08:00
fields . push ( * SiteSetting . public_user_custom_fields . split ( " | " ) )
2016-03-12 04:52:18 +08:00
end
if guardian . is_staff?
if SiteSetting . staff_user_custom_fields . present?
2020-08-31 06:52:01 +08:00
fields . push ( * SiteSetting . staff_user_custom_fields . split ( " | " ) )
2016-03-12 04:52:18 +08:00
end
2020-05-15 21:04:38 +08:00
2020-08-31 06:52:01 +08:00
fields . push ( * DiscoursePluginRegistry . staff_user_custom_fields )
2016-03-12 04:52:18 +08:00
end
fields . uniq
end
2020-05-26 08:07:00 +08:00
def self . human_user_id? ( user_id )
user_id > 0
end
2019-02-09 02:34:54 +08:00
def human?
2020-05-26 08:07:00 +08:00
User . human_user_id? ( self . id )
2019-02-09 02:34:54 +08:00
end
2019-03-12 07:58:14 +08:00
def bot?
! self . human?
end
2015-02-06 11:38:51 +08:00
def effective_locale
if SiteSetting . allow_user_locale && self . locale . present?
self . locale
else
SiteSetting . default_locale
end
end
2022-04-22 06:23:42 +08:00
def bookmarks_of_type ( type )
bookmarks . where ( bookmarkable_type : type )
end
2013-04-01 00:51:13 +08:00
EMAIL = / ([^@]+)@([^ \ .]+) /
2020-04-30 14:48:34 +08:00
FROM_STAGED = " from_staged "
2013-04-01 00:51:13 +08:00
2013-04-13 06:46:55 +08:00
def self . new_from_params ( params )
user = User . new
user . name = params [ :name ]
user . email = params [ :email ]
user . password = params [ :password ]
user . username = params [ :username ]
user
end
2020-03-17 23:48:24 +08:00
def unstage!
2018-05-14 18:03:15 +08:00
if self . staged
2020-03-17 23:48:24 +08:00
ActiveRecord :: Base . transaction do
self . staged = false
self . custom_fields [ FROM_STAGED ] = true
self . notifications . destroy_all
save!
end
2018-05-13 23:00:02 +08:00
2020-03-17 23:48:24 +08:00
DiscourseEvent . trigger ( :user_unstaged , self )
2018-01-19 22:29:15 +08:00
end
end
2018-05-17 14:51:48 +08:00
def self . suggest_name ( string )
return " " if string . blank?
2018-05-17 16:34:16 +08:00
( string [ / \ A[^@]+ / ] . presence || string [ / [^@]+ \ z / ] ) . tr ( " . " , " " ) . titleize
2013-02-06 03:16:51 +08:00
end
2013-04-29 14:33:24 +08:00
def self . find_by_username_or_email ( username_or_email )
2013-10-28 13:29:07 +08:00
if username_or_email . include? ( " @ " )
find_by_email ( username_or_email )
2013-06-19 08:31:19 +08:00
else
2013-10-28 13:29:07 +08:00
find_by_username ( username_or_email )
2013-06-19 08:31:19 +08:00
end
2013-04-29 14:33:24 +08:00
end
2021-07-05 12:56:32 +08:00
def self . find_by_email ( email , primary : false )
if primary
self . with_primary_email ( Email . downcase ( email ) ) . first
else
self . with_email ( Email . downcase ( email ) ) . first
end
2013-10-24 15:59:58 +08:00
end
def self . find_by_username ( username )
2019-04-23 18:22:47 +08:00
find_by ( username_lower : normalize_username ( username ) )
2013-10-24 15:59:58 +08:00
end
2022-09-26 11:58:40 +08:00
def in_any_groups? ( group_ids )
2024-01-22 12:40:29 +08:00
group_ids . include? ( Group :: AUTO_GROUPS [ :everyone ] ) ||
( is_system_user? && ( Group . auto_groups_between ( :admins , :trust_level_4 ) & group_ids ) . any? ) ||
( group_ids & belonging_to_group_ids ) . any?
2022-09-26 11:58:40 +08:00
end
def belonging_to_group_ids
@belonging_to_group_ids || = group_users . pluck ( :group_id )
end
2018-08-25 06:41:03 +08:00
def group_granted_trust_level
GroupUser . where ( user_id : id ) . includes ( :group ) . maximum ( " groups.grant_trust_level " )
end
2018-12-18 15:41:42 +08:00
def visible_groups
groups . visible_groups ( self )
end
2018-08-25 06:41:03 +08:00
2013-04-29 14:33:24 +08:00
def enqueue_welcome_message ( message_type )
return unless SiteSetting . send_welcome_message?
Jobs . enqueue ( :send_system_message , user_id : id , message_type : message_type )
end
2018-06-23 00:51:07 +08:00
def enqueue_member_welcome_message
return unless SiteSetting . send_tl1_welcome_message?
Jobs . enqueue ( :send_system_message , user_id : id , message_type : " welcome_tl1_user " )
end
2020-09-22 08:17:52 +08:00
def enqueue_tl2_promotion_message
return unless SiteSetting . send_tl2_promotion_message
Jobs . enqueue ( :send_system_message , user_id : id , message_type : " tl2_promotion_message " )
end
2019-11-05 20:45:55 +08:00
def enqueue_staff_welcome_message ( role )
return unless staff?
2024-04-24 02:50:14 +08:00
return if is_singular_admin?
2019-11-05 20:45:55 +08:00
Jobs . enqueue (
:send_system_message ,
user_id : id ,
message_type : " welcome_staff " ,
message_options : {
2022-03-25 09:07:21 +08:00
role : role . to_s ,
2019-11-05 20:45:55 +08:00
} ,
)
2019-10-28 21:58:45 +08:00
end
2015-01-17 06:30:46 +08:00
def change_username ( new_username , actor = nil )
2015-03-07 05:44:54 +08:00
UsernameChanger . change ( self , new_username , actor )
2013-02-06 03:16:51 +08:00
end
2013-09-13 05:46:43 +08:00
def created_topic_count
2014-07-29 01:17:37 +08:00
stat . topic_count
2013-09-13 05:46:43 +08:00
end
2013-02-27 00:27:59 +08:00
2014-07-29 01:17:37 +08:00
alias_method :topic_count , :created_topic_count
2013-02-06 03:16:51 +08:00
# tricky, we need our bus to be subscribed from the right spot
def sync_notification_channel_position
@unread_notifications_by_type = nil
2015-05-04 10:21:00 +08:00
self . notification_channel_position = MessageBus . last_id ( " /notification/ #{ id } " )
2013-02-06 03:16:51 +08:00
end
def invited_by
2024-07-04 10:27:37 +08:00
# this is unfortunate, but when an invite is redeemed,
# any user created by the invite is created *after*
# the invite's redeemed_at
invite_redemption_delay = 5 . seconds
2021-03-06 19:29:35 +08:00
used_invite =
2024-07-04 10:27:37 +08:00
Invite
. with_deleted
. joins ( :invited_users )
. where (
" invited_users.user_id = ? AND invited_users.redeemed_at <= ? " ,
self . id ,
self . created_at + invite_redemption_delay ,
)
. first
2013-02-28 21:08:56 +08:00
used_invite . try ( :invited_by )
2013-02-06 03:16:51 +08:00
end
2017-08-09 10:56:08 +08:00
def should_validate_email_address?
2017-04-27 02:47:36 +08:00
! skip_email_validation && ! staged?
2016-09-08 02:05:46 +08:00
end
2013-02-06 03:16:51 +08:00
def self . email_hash ( email )
2013-02-06 10:44:49 +08:00
Digest :: MD5 . hexdigest ( email . strip . downcase )
2013-02-06 03:16:51 +08:00
end
def email_hash
2013-02-28 21:08:56 +08:00
User . email_hash ( email )
2013-02-06 03:16:51 +08:00
end
def reload
2015-04-17 14:01:20 +08:00
@unread_notifications = nil
2022-08-03 13:57:59 +08:00
@all_unread_notifications_count = nil
2014-10-13 18:26:30 +08:00
@unread_total_notifications = nil
2013-05-16 14:37:47 +08:00
@unread_pms = nil
2020-04-01 07:09:20 +08:00
@unread_bookmarks = nil
@unread_high_prios = nil
2020-01-02 21:04:08 +08:00
@ignored_user_ids = nil
@muted_user_ids = nil
2022-09-26 11:58:40 +08:00
@belonging_to_group_ids = nil
2013-02-06 03:16:51 +08:00
super
end
2020-01-02 21:04:08 +08:00
def ignored_user_ids
@ignored_user_ids || = ignored_users . pluck ( :id )
end
def muted_user_ids
@muted_user_ids || = muted_users . pluck ( :id )
end
2023-05-10 01:19:26 +08:00
def unread_notifications_of_type ( notification_type , since : nil )
2016-12-13 03:20:25 +08:00
# perf critical, much more efficient than AR
2018-05-26 08:09:48 +08:00
sql = << ~ SQL
SELECT COUNT ( * )
FROM notifications n
LEFT JOIN topics t ON t . id = n . topic_id
WHERE t . deleted_at IS NULL
2020-04-01 07:09:20 +08:00
AND n . notification_type = :notification_type
2018-05-26 08:09:48 +08:00
AND n . user_id = :user_id
AND NOT read
2023-05-10 01:19:26 +08:00
#{since ? "AND n.created_at > :since" : ""}
2018-05-26 08:09:48 +08:00
SQL
2016-12-13 03:20:25 +08:00
2018-06-19 14:13:14 +08:00
# to avoid coalesce we do to_i
2023-05-10 01:19:26 +08:00
DB . query_single ( sql , user_id : id , notification_type : notification_type , since : since ) [ 0 ] . to_i
2016-12-13 03:20:25 +08:00
end
2015-04-17 14:01:20 +08:00
2020-04-01 07:09:20 +08:00
def unread_notifications_of_priority ( high_priority : )
# perf critical, much more efficient than AR
sql = << ~ SQL
SELECT COUNT ( * )
FROM notifications n
LEFT JOIN topics t ON t . id = n . topic_id
WHERE t . deleted_at IS NULL
AND n . high_priority = :high_priority
AND n . user_id = :user_id
AND NOT read
SQL
# to avoid coalesce we do to_i
DB . query_single ( sql , user_id : id , high_priority : high_priority ) [ 0 ] . to_i
end
2022-08-31 09:16:28 +08:00
MAX_UNREAD_BACKLOG = 400
def grouped_unread_notifications
results = DB . query ( << ~ SQL , user_id : self . id , limit : MAX_UNREAD_BACKLOG )
2022-08-08 22:24:04 +08:00
SELECT X . notification_type AS type , COUNT ( * ) FROM (
SELECT n . notification_type
FROM notifications n
LEFT JOIN topics t ON t . id = n . topic_id
WHERE t . deleted_at IS NULL
AND n . user_id = :user_id
AND NOT n . read
LIMIT :limit
) AS X
GROUP BY X . notification_type
SQL
results . map! { | row | [ row . type , row . count ] }
results . to_h
end
2020-04-01 07:09:20 +08:00
def unread_high_priority_notifications
@unread_high_prios || = unread_notifications_of_priority ( high_priority : true )
2013-02-06 03:16:51 +08:00
end
2022-12-01 07:05:32 +08:00
def new_personal_messages_notifications_count
args = {
user_id : self . id ,
seen_notification_id : self . seen_notification_id ,
private_message : Notification . types [ :private_message ] ,
}
DB . query_single ( << ~ SQL , args ) . first
SELECT COUNT ( * )
FROM notifications
WHERE user_id = :user_id
AND id > :seen_notification_id
AND NOT read
AND notification_type = :private_message
SQL
end
2018-10-24 08:53:28 +08:00
# PERF: This safeguard is in place to avoid situations where
# a user with enormous amounts of unread data can issue extremely
# expensive queries
MAX_UNREAD_NOTIFICATIONS = 99
2018-10-24 09:10:27 +08:00
def self . max_unread_notifications
@max_unread_notifications || = MAX_UNREAD_NOTIFICATIONS
end
def self . max_unread_notifications = ( val )
@max_unread_notifications = val
end
2013-02-06 03:16:51 +08:00
def unread_notifications
2018-05-26 08:09:48 +08:00
@unread_notifications || =
begin
# perf critical, much more efficient than AR
sql = << ~ SQL
2018-10-24 09:10:27 +08:00
SELECT COUNT ( * ) FROM (
SELECT 1 FROM
notifications n
LEFT JOIN topics t ON t . id = n . topic_id
WHERE t . deleted_at IS NULL AND
2020-04-01 07:09:20 +08:00
n . high_priority = FALSE AND
2018-10-24 09:10:27 +08:00
n . user_id = :user_id AND
n . id > :seen_notification_id AND
NOT read
LIMIT :limit
) AS X
2018-05-26 08:09:48 +08:00
SQL
2018-06-19 14:13:14 +08:00
DB . query_single (
sql ,
2018-05-26 08:09:48 +08:00
user_id : id ,
seen_notification_id : seen_notification_id ,
2018-10-24 09:10:27 +08:00
limit : User . max_unread_notifications ,
2018-06-19 14:13:14 +08:00
) [
2023-01-09 20:20:10 +08:00
0
2018-06-19 14:13:14 +08:00
] . to_i
2018-05-26 08:09:48 +08:00
end
2013-02-06 03:16:51 +08:00
end
2013-02-06 10:44:49 +08:00
2022-08-03 13:57:59 +08:00
def all_unread_notifications_count
@all_unread_notifications_count || =
begin
sql = << ~ SQL
SELECT COUNT ( * ) FROM (
SELECT 1 FROM
notifications n
LEFT JOIN topics t ON t . id = n . topic_id
WHERE t . deleted_at IS NULL AND
n . user_id = :user_id AND
n . id > :seen_notification_id AND
NOT read
LIMIT :limit
) AS X
SQL
DB . query_single (
sql ,
user_id : id ,
seen_notification_id : seen_notification_id ,
limit : User . max_unread_notifications ,
) [
2023-01-09 20:20:10 +08:00
0
2022-08-03 13:57:59 +08:00
] . to_i
end
end
2014-10-13 18:26:30 +08:00
def total_unread_notifications
@unread_total_notifications || = notifications . where ( " read = false " ) . count
end
2022-07-28 16:16:33 +08:00
def reviewable_count
2023-05-18 00:16:42 +08:00
Reviewable . list_for ( self , include_claimed_by_others : false ) . count
2022-08-03 13:57:59 +08:00
end
2022-09-13 02:19:25 +08:00
def bump_last_seen_notification!
query = self . notifications . visible
query = query . where ( " notifications.id > ? " , seen_notification_id ) if seen_notification_id
if max_notification_id = query . maximum ( :id )
update! ( seen_notification_id : max_notification_id )
true
else
false
end
end
2022-08-03 13:57:59 +08:00
def bump_last_seen_reviewable!
query = Reviewable . unseen_list_for ( self , preload : false )
2022-12-01 07:09:57 +08:00
query = query . where ( " reviewables.id > ? " , last_seen_reviewable_id ) if last_seen_reviewable_id
2022-08-03 13:57:59 +08:00
max_reviewable_id = query . maximum ( :id )
if max_reviewable_id
update! ( last_seen_reviewable_id : max_reviewable_id )
2022-12-01 07:09:57 +08:00
publish_reviewable_counts
2022-08-03 13:57:59 +08:00
end
end
2022-12-01 07:09:57 +08:00
def publish_reviewable_counts ( extra_data = nil )
data = {
reviewable_count : self . reviewable_count ,
unseen_reviewable_count : Reviewable . unseen_reviewable_count ( self ) ,
}
data . merge! ( extra_data ) if extra_data . present?
2022-08-03 13:57:59 +08:00
MessageBus . publish ( " /reviewable_counts/ #{ self . id } " , data , user_ids : [ self . id ] )
end
2016-11-08 16:12:40 +08:00
def read_first_notification?
2023-06-26 23:39:29 +08:00
self . seen_notification_id != 0 || user_option . skip_new_user_tips
2016-11-08 16:12:40 +08:00
end
2013-02-06 03:16:51 +08:00
def publish_notifications_state
2021-11-22 11:38:49 +08:00
return if ! self . allow_live_notifications?
2018-05-26 08:09:48 +08:00
# publish last notification json with the message so we can apply an update
2018-06-28 23:04:40 +08:00
notification = notifications . visible . order ( " notifications.created_at desc " ) . first
2015-09-04 11:20:33 +08:00
json = NotificationSerializer . new ( notification ) . as_json if notification
2020-04-30 14:48:34 +08:00
sql = ( << ~ SQL )
2016-02-15 16:29:35 +08:00
SELECT * FROM (
SELECT n . id , n . read FROM notifications n
LEFT JOIN topics t ON n . topic_id = t . id
WHERE
t . deleted_at IS NULL AND
2020-04-01 07:09:20 +08:00
n . high_priority AND
2016-02-15 16:29:35 +08:00
n . user_id = :user_id AND
NOT read
ORDER BY n . id DESC
LIMIT 20
) AS x
UNION ALL
SELECT * FROM (
SELECT n . id , n . read FROM notifications n
LEFT JOIN topics t ON n . topic_id = t . id
WHERE
t . deleted_at IS NULL AND
2020-04-01 07:09:20 +08:00
( n . high_priority = FALSE OR read ) AND
2016-02-15 16:29:35 +08:00
n . user_id = :user_id
ORDER BY n . id DESC
LIMIT 20
) AS y
2018-06-19 14:13:14 +08:00
SQL
2016-02-15 16:29:35 +08:00
2020-04-01 07:09:20 +08:00
recent = DB . query ( sql , user_id : id ) . map! { | r | [ r . id , r . read ] }
2016-02-15 16:29:35 +08:00
2018-05-26 08:09:48 +08:00
payload = {
unread_notifications : unread_notifications ,
2020-04-01 07:09:20 +08:00
unread_high_priority_notifications : unread_high_priority_notifications ,
2018-05-26 08:09:48 +08:00
read_first_notification : read_first_notification? ,
last_notification : json ,
recent : recent ,
seen_notification_id : seen_notification_id ,
}
2023-05-18 00:16:42 +08:00
payload [ :all_unread_notifications_count ] = all_unread_notifications_count
payload [ :grouped_unread_notifications ] = grouped_unread_notifications
payload [ :new_personal_messages_notifications_count ] = new_personal_messages_notifications_count
2022-08-03 13:57:59 +08:00
2018-05-26 08:09:48 +08:00
MessageBus . publish ( " /notification/ #{ id } " , payload , user_ids : [ id ] )
2013-02-06 03:16:51 +08:00
end
2020-12-18 23:03:51 +08:00
def publish_do_not_disturb ( ends_at : nil )
2022-06-17 12:24:15 +08:00
MessageBus . publish ( " /do-not-disturb/ #{ id } " , { ends_at : ends_at & . httpdate } , user_ids : [ id ] )
2020-12-18 23:03:51 +08:00
end
2022-05-30 17:41:53 +08:00
def publish_user_status ( status )
2022-07-05 23:12:22 +08:00
if status
payload = {
description : status . description ,
emoji : status . emoji ,
ends_at : status . ends_at & . iso8601 ,
}
else
payload = nil
end
2022-06-17 12:24:15 +08:00
2022-07-07 21:37:05 +08:00
MessageBus . publish (
" /user-status " ,
{ id = > payload } ,
group_ids : [ Group :: AUTO_GROUPS [ :trust_level_0 ] ] ,
)
2022-05-30 17:41:53 +08:00
end
2013-02-06 03:16:51 +08:00
def password = ( password )
2013-02-06 10:44:49 +08:00
# special case for passwordless accounts
2024-05-27 18:27:13 +08:00
@raw_password = password if password . present?
2013-02-06 03:16:51 +08:00
end
2013-12-20 04:12:03 +08:00
def password
" " # so that validator doesn't complain that a password attribute doesn't exist
end
2013-02-13 04:42:04 +08:00
# Indicate that this is NOT a passwordless account for the purposes of validation
2013-02-28 21:08:56 +08:00
def password_required!
2013-02-13 04:42:04 +08:00
@password_required = true
end
2013-12-20 04:12:03 +08:00
def password_required?
! ! @password_required
end
2017-12-01 12:19:24 +08:00
def password_validation_required?
password_required? || @raw_password . present?
end
2014-01-22 01:42:20 +08:00
def has_password?
password_hash . present?
end
2013-12-20 04:12:03 +08:00
def password_validator
PasswordValidator . new ( attributes : :password ) . validate_each ( self , :password , @raw_password )
end
2024-06-04 15:42:53 +08:00
def password_expired? ( password )
passwords
. where ( " password_expired_at IS NOT NULL AND password_expired_at < ? " , Time . zone . now )
. any? do | user_password |
user_password . password_hash ==
hash_password ( password , user_password . password_salt , user_password . password_algorithm )
end
end
2013-02-06 03:16:51 +08:00
def confirm_password? ( password )
2023-04-11 17:16:28 +08:00
return false unless password_hash && salt && password_algorithm
confirmed = self . password_hash == hash_password ( password , salt , password_algorithm )
if confirmed && persisted? && password_algorithm != TARGET_PASSWORD_ALGORITHM
# Regenerate password_hash with new algorithm and persist
salt = SecureRandom . hex ( PASSWORD_SALT_LENGTH )
update_columns (
password_algorithm : TARGET_PASSWORD_ALGORITHM ,
salt : salt ,
password_hash : hash_password ( password , salt , TARGET_PASSWORD_ALGORITHM ) ,
)
end
confirmed
2013-02-06 03:16:51 +08:00
end
2013-10-12 01:33:23 +08:00
2016-06-21 04:38:15 +08:00
def new_user_posting_on_first_day?
2015-03-26 13:48:36 +08:00
! staff? && trust_level < TrustLevel [ 2 ] &&
2018-06-21 08:25:03 +08:00
(
trust_level == TrustLevel [ 0 ] || self . first_post_created_at . nil? ||
self . first_post_created_at > = 24 . hours . ago
)
2015-03-26 13:48:36 +08:00
end
2013-10-12 01:33:23 +08:00
def new_user?
2015-03-26 13:04:32 +08:00
( created_at > = 24 . hours . ago || trust_level == TrustLevel [ 0 ] ) && trust_level < TrustLevel [ 2 ] &&
! staff?
2013-10-12 01:33:23 +08:00
end
2013-02-06 03:16:51 +08:00
2013-02-12 13:41:04 +08:00
def seen_before?
last_seen_at . present?
end
2021-11-22 11:38:49 +08:00
def seen_since? ( datetime )
seen_before? && last_seen_at > = datetime
end
2015-07-08 00:31:07 +08:00
def create_visit_record! ( date , opts = { } )
user_stat . update_column ( :days_visited , user_stat . days_visited + 1 )
user_visits . create! (
visited_at : date ,
posts_read : opts [ :posts_read ] || 0 ,
mobile : opts [ :mobile ] || false ,
)
end
2014-01-25 04:19:20 +08:00
def visit_record_for ( date )
2014-05-06 21:41:59 +08:00
user_visits . find_by ( visited_at : date )
2013-02-12 13:41:04 +08:00
end
def update_visit_record! ( date )
2014-01-25 04:19:20 +08:00
create_visit_record! ( date ) unless visit_record_for ( date )
end
2019-11-25 08:49:27 +08:00
def update_timezone_if_missing ( timezone )
return if timezone . blank? || ! TimezoneValidator . valid? ( timezone )
# we only want to update the user's timezone if they have not set it themselves
UserOption . where ( user_id : self . id , timezone : nil ) . update_all ( timezone : timezone )
end
2014-01-25 04:19:20 +08:00
2015-07-08 00:31:07 +08:00
def update_posts_read! ( num_posts , opts = { } )
now = opts [ :at ] || Time . zone . now
_retry = opts [ :retry ] || false
2014-01-25 04:19:20 +08:00
if user_visit = visit_record_for ( now . to_date )
user_visit . posts_read += num_posts
2015-07-08 00:31:07 +08:00
user_visit . mobile = true if opts [ :mobile ]
2014-01-25 04:19:20 +08:00
user_visit . save
user_visit
else
2015-06-01 09:55:07 +08:00
begin
2015-07-08 00:31:07 +08:00
create_visit_record! ( now . to_date , posts_read : num_posts , mobile : opts . fetch ( :mobile , false ) )
2015-06-01 09:55:07 +08:00
rescue ActiveRecord :: RecordNotUnique
if ! _retry
2015-08-24 08:28:38 +08:00
update_posts_read! ( num_posts , opts . merge ( retry : true ) )
2015-06-01 09:55:07 +08:00
else
raise
end
end
2013-02-12 13:41:04 +08:00
end
end
2022-05-03 06:50:56 +08:00
def self . update_ip_address! ( user_id , new_ip : , old_ip : )
unless old_ip == new_ip || new_ip . blank?
DB . exec ( << ~ SQL , user_id : user_id , ip_address : new_ip )
UPDATE users
SET ip_address = :ip_address
WHERE id = :user_id
SQL
2020-09-17 10:55:29 +08:00
if SiteSetting . keep_old_ip_address_count > 0
2022-05-03 06:50:56 +08:00
DB . exec ( << ~ SQL , user_id : user_id , ip_address : new_ip , current_timestamp : Time . zone . now )
2020-09-17 10:55:29 +08:00
INSERT INTO user_ip_address_histories ( user_id , ip_address , created_at , updated_at )
VALUES ( :user_id , :ip_address , :current_timestamp , :current_timestamp )
ON CONFLICT ( user_id , ip_address )
DO
UPDATE SET updated_at = :current_timestamp
SQL
2022-05-03 06:50:56 +08:00
DB . exec ( << ~ SQL , user_id : user_id , offset : SiteSetting . keep_old_ip_address_count )
2020-09-17 10:55:29 +08:00
DELETE FROM user_ip_address_histories
WHERE id IN (
SELECT
id
FROM user_ip_address_histories
WHERE user_id = :user_id
ORDER BY updated_at DESC
OFFSET :offset
)
SQL
end
2013-02-24 18:42:04 +08:00
end
end
2022-05-03 06:50:56 +08:00
def update_ip_address! ( new_ip_address )
User . update_ip_address! ( id , new_ip : new_ip_address , old_ip : ip_address )
end
def self . last_seen_redis_key ( user_id , now )
2013-10-24 05:24:50 +08:00
now_date = now . to_date
2022-05-03 06:50:56 +08:00
" user: #{ user_id } : #{ now_date } "
end
def last_seen_redis_key ( now )
User . last_seen_redis_key ( id , now )
2020-08-31 06:54:42 +08:00
end
def clear_last_seen_cache! ( now = Time . zone . now )
Discourse . redis . del ( last_seen_redis_key ( now ) )
end
2022-05-03 06:50:56 +08:00
def self . should_update_last_seen? ( user_id , now = Time . zone . now )
return true if SiteSetting . active_user_rate_limit_secs < = 0
2020-08-31 06:54:42 +08:00
2022-05-03 06:50:56 +08:00
Discourse . redis . set (
last_seen_redis_key ( user_id , now ) ,
" 1 " ,
nx : true ,
ex : SiteSetting . active_user_rate_limit_secs ,
)
end
def update_last_seen! ( now = Time . zone . now , force : false )
if ! force
return if ! User . should_update_last_seen? ( self . id , now )
2020-08-31 06:54:42 +08:00
end
2013-02-06 03:16:51 +08:00
2013-10-24 05:24:50 +08:00
update_previous_visit ( now )
# using update_column to avoid the AR transaction
update_column ( :last_seen_at , now )
2016-05-21 21:17:54 +08:00
update_column ( :first_seen_at , now ) unless self . first_seen_at
2017-04-01 06:30:59 +08:00
DiscourseEvent . trigger ( :user_seen , self )
2013-02-06 03:16:51 +08:00
end
2013-08-14 04:08:29 +08:00
def self . gravatar_template ( email )
2020-03-12 23:23:55 +08:00
" // #{ SiteSetting . gravatar_base_url } /avatar/ #{ self . email_hash ( email ) } .png?s={size}&r=pg&d=identicon "
2013-02-06 03:16:51 +08:00
end
2013-03-09 04:58:37 +08:00
# Don't pass this up to the client - it's meant for server side use
2013-08-14 04:08:29 +08:00
# This is used in
# - self oneboxes in open graph data
# - emails
2013-03-09 04:58:37 +08:00
def small_avatar_url
2014-05-22 15:37:02 +08:00
avatar_template_url . gsub ( " {size} " , " 45 " )
2013-03-09 04:58:37 +08:00
end
2014-05-22 15:37:02 +08:00
def avatar_template_url
2015-06-12 18:02:36 +08:00
UrlHelper . schemaless UrlHelper . absolute avatar_template
2013-09-11 03:18:22 +08:00
end
2018-07-18 18:57:43 +08:00
def self . username_hash ( username )
username
. each_char
. reduce ( 0 ) do | result , char |
[ ( ( result << 5 ) - result ) + char . ord ] . pack ( " L " ) . unpack ( " l " ) . first
end
. abs
end
2015-06-27 01:37:50 +08:00
def self . default_template ( username )
if SiteSetting . default_avatars . present?
2018-07-18 18:57:43 +08:00
urls = SiteSetting . default_avatars . split ( " \n " )
return urls [ username_hash ( username ) % urls . size ] if urls . present?
2015-06-27 01:37:50 +08:00
end
2018-07-18 18:57:43 +08:00
system_avatar_template ( username )
2015-06-27 01:37:50 +08:00
end
2015-09-11 08:12:40 +08:00
def self . avatar_template ( username , uploaded_avatar_id )
2014-05-22 15:37:02 +08:00
username || = " "
2015-09-11 21:04:29 +08:00
return default_template ( username ) if ! uploaded_avatar_id
2015-05-30 00:51:17 +08:00
hostname = RailsMultisite :: ConnectionManagement . current_hostname
UserAvatar . local_avatar_template ( hostname , username . downcase , uploaded_avatar_id )
2014-05-22 15:37:02 +08:00
end
2015-09-11 16:14:34 +08:00
def self . system_avatar_template ( username )
2019-04-25 05:18:52 +08:00
normalized_username = normalize_username ( username )
2015-09-11 16:14:34 +08:00
# TODO it may be worth caching this in a distributed cache, should be benched
if SiteSetting . external_system_avatars_enabled
url = SiteSetting . external_system_avatars_url . dup
2023-01-21 02:52:49 +08:00
url = + " #{ Discourse . base_path } #{ url } " unless url =~ %r{ \ Ahttps?:// }
2019-04-25 05:18:52 +08:00
url . gsub! " {color} " , letter_avatar_color ( normalized_username )
2019-12-12 10:49:21 +08:00
url . gsub! " {username} " , UrlHelper . encode_component ( username )
url . gsub! " {first_letter} " ,
UrlHelper . encode_component ( normalized_username . grapheme_clusters . first )
2015-10-02 15:27:54 +08:00
url . gsub! " {hostname} " , Discourse . current_hostname
2015-09-11 16:14:34 +08:00
url
2015-09-11 08:12:40 +08:00
else
2020-10-09 19:51:24 +08:00
" #{ Discourse . base_path } /letter_avatar/ #{ normalized_username } /{size}/ #{ LetterAvatar . version } .png "
2015-09-11 08:12:40 +08:00
end
end
def self . letter_avatar_color ( username )
2015-09-11 21:04:29 +08:00
username || = " "
2019-03-18 23:24:21 +08:00
if SiteSetting . restrict_letter_avatar_colors . present?
hex_length = 6
colors = SiteSetting . restrict_letter_avatar_colors
length = colors . count ( " | " ) + 1
num = color_index ( username , length )
index = ( num * hex_length ) + num
colors [ index , hex_length ]
else
color = LetterAvatar :: COLORS [ color_index ( username , LetterAvatar :: COLORS . length ) ]
color . map { | c | c . to_s ( 16 ) . rjust ( 2 , " 0 " ) } . join
end
end
def self . color_index ( username , length )
Digest :: MD5 . hexdigest ( username ) [ 0 ... 15 ] . to_i ( 16 ) % length
2014-05-30 12:17:35 +08:00
end
2021-04-28 04:28:15 +08:00
def is_system_user?
id == Discourse :: SYSTEM_USER_ID
end
2013-02-06 03:16:51 +08:00
def avatar_template
2021-04-28 04:28:15 +08:00
use_small_logo =
2021-01-19 01:09:07 +08:00
is_system_user? && SiteSetting . logo_small && SiteSetting . use_site_small_logo_as_system_avatar
if use_small_logo
2021-02-01 09:35:41 +08:00
Discourse . store . cdn_url ( SiteSetting . logo_small . url )
2021-01-08 21:40:00 +08:00
else
self . class . avatar_template ( username , uploaded_avatar_id )
end
2013-02-06 03:16:51 +08:00
end
# The following count methods are somewhat slow - definitely don't use them in a loop.
2013-03-06 15:52:24 +08:00
# They might need to be denormalized
2013-02-06 03:16:51 +08:00
def like_count
2013-02-28 21:08:56 +08:00
UserAction . where ( user_id : id , action_type : UserAction :: WAS_LIKED ) . count
2013-02-06 03:16:51 +08:00
end
2014-08-23 03:23:10 +08:00
def like_given_count
UserAction . where ( user_id : id , action_type : UserAction :: LIKE ) . count
2014-08-23 02:37:00 +08:00
end
2013-02-06 03:16:51 +08:00
def post_count
2014-07-29 01:17:37 +08:00
stat . post_count
2014-02-21 01:29:40 +08:00
end
2021-08-02 22:15:53 +08:00
def post_edits_count
stat . post_edits_count
end
def increment_post_edits_count
stat . increment! ( :post_edits_count )
end
2013-02-06 03:16:51 +08:00
def flags_given_count
2017-10-18 01:31:45 +08:00
PostAction . where (
user_id : id ,
2024-07-18 08:10:22 +08:00
post_action_type_id : PostActionType . flag_types_without_additional_message . values ,
2017-10-18 01:31:45 +08:00
) . count
2013-02-06 03:16:51 +08:00
end
2014-09-08 23:11:56 +08:00
def warnings_received_count
2017-04-15 12:11:02 +08:00
user_warnings . count
2014-09-08 23:11:56 +08:00
end
2023-10-18 09:38:17 +08:00
def flags_received_count
posts
. includes ( :post_actions )
2024-07-18 08:10:22 +08:00
. where (
" post_actions.post_action_type_id " = >
PostActionType . flag_types_without_additional_message . values ,
)
2023-10-18 09:38:17 +08:00
. count
end
2013-02-06 03:16:51 +08:00
def private_topics_count
topics_allowed . where ( archetype : Archetype . private_message ) . count
end
2013-12-20 02:45:55 +08:00
def posted_too_much_in_topic? ( topic_id )
2016-04-19 04:08:42 +08:00
# Does not apply to staff and non-new members...
return false if staff? || ( trust_level != TrustLevel [ 0 ] )
# ... your own topics or in private messages
topic = Topic . where ( id : topic_id ) . first
return false if topic . try ( :private_message? ) || ( topic . try ( :user_id ) == self . id )
2014-01-03 01:57:40 +08:00
2014-04-30 00:59:14 +08:00
last_action_in_topic = UserAction . last_action_in_topic ( id , topic_id )
since_reply = Post . where ( user_id : id , topic_id : topic_id )
since_reply = since_reply . where ( " id > ? " , last_action_in_topic ) if last_action_in_topic
( since_reply . count > = SiteSetting . newuser_max_replies_per_topic )
2013-12-20 02:45:55 +08:00
end
2018-12-14 18:04:18 +08:00
def delete_posts_in_batches ( guardian , batch_size = 20 )
2013-02-07 15:11:56 +08:00
raise Discourse :: InvalidAccess unless guardian . can_delete_all_posts? self
2013-02-07 23:45:24 +08:00
2019-01-04 01:03:01 +08:00
Reviewable . where ( created_by_id : id ) . delete_all
2015-04-25 04:04:44 +08:00
2018-12-14 18:04:18 +08:00
posts
. order ( " post_number desc " )
. limit ( batch_size )
2013-06-06 04:00:45 +08:00
. each { | p | PostDestroyer . new ( guardian . user , p ) . destroy }
2013-02-07 15:11:56 +08:00
end
2013-11-08 02:53:32 +08:00
def suspended?
2017-11-29 02:44:24 +08:00
! ! ( suspended_till && suspended_till > Time . zone . now )
2013-02-06 03:16:51 +08:00
end
2017-11-14 02:41:36 +08:00
def silenced?
2017-11-29 02:44:24 +08:00
! ! ( silenced_till && silenced_till > Time . zone . now )
2017-11-14 02:41:36 +08:00
end
def silenced_record
UserHistory . for ( self , :silence_user ) . order ( " id DESC " ) . first
end
def silence_reason
silenced_record . try ( :details ) if silenced?
end
def silenced_at
silenced_record . try ( :created_at ) if silenced?
end
2021-07-20 18:42:08 +08:00
def silenced_forever?
silenced_till > 100 . years . from_now
end
2013-11-08 02:53:32 +08:00
def suspend_record
UserHistory . for ( self , :suspend_user ) . order ( " id DESC " ) . first
2013-11-01 22:47:03 +08:00
end
2017-12-08 02:20:42 +08:00
def full_suspend_reason
2023-08-10 08:03:38 +08:00
suspend_record . try ( :details ) if suspended?
2017-12-08 02:20:42 +08:00
end
2013-11-08 02:53:32 +08:00
def suspend_reason
2017-12-08 02:20:42 +08:00
if details = full_suspend_reason
return details . split ( " \n " ) [ 0 ]
end
nil
2013-11-01 22:47:03 +08:00
end
2021-07-20 18:42:08 +08:00
def suspended_message
return nil unless suspended?
message = " login.suspended "
if suspend_reason
if suspended_forever?
message = " login.suspended_with_reason_forever "
else
message = " login.suspended_with_reason "
end
end
I18n . t (
message ,
date : I18n . l ( suspended_till , format : :date_only ) ,
reason : Rack :: Utils . escape_html ( suspend_reason ) ,
)
end
def suspended_forever?
suspended_till > 100 . years . from_now
end
2013-02-06 03:16:51 +08:00
# Use this helper to determine if the user has a particular trust level.
# Takes into account admin, etc.
2013-02-06 10:44:49 +08:00
def has_trust_level? ( level )
2016-05-30 11:38:04 +08:00
raise InvalidTrustLevel . new ( " Invalid trust level #{ level } " ) unless TrustLevel . valid? ( level )
2015-12-08 00:01:08 +08:00
admin? || moderator? || staged? || TrustLevel . compare ( trust_level , level )
2013-02-06 03:16:51 +08:00
end
2021-11-23 02:18:53 +08:00
def has_trust_level_or_staff? ( level )
return admin? if level . to_s == " admin "
return staff? if level . to_s == " staff "
has_trust_level? ( level . to_i )
end
2013-03-20 07:51:39 +08:00
# a touch faster than automatic
2013-04-01 00:51:13 +08:00
def admin?
2013-03-20 07:51:39 +08:00
admin
end
2013-02-06 03:16:51 +08:00
def guardian
Guardian . new ( self )
end
2018-04-03 00:44:04 +08:00
def username_format_validator
UsernameValidator . perform_validation ( self , " username " )
2013-02-08 07:23:41 +08:00
end
2013-02-12 00:18:26 +08:00
def email_confirmed?
2018-05-15 07:48:30 +08:00
email_tokens . where ( email : email , confirmed : true ) . present? || email_tokens . empty? ||
2019-11-08 01:26:28 +08:00
single_sign_on_record & . external_email & . downcase == email
2013-02-12 00:18:26 +08:00
end
2013-05-08 09:58:34 +08:00
def activate
2021-11-25 15:34:39 +08:00
email_token = self . email_tokens . create! ( email : self . email , scope : EmailToken . scopes [ :signup ] )
EmailToken . confirm ( email_token . token , scope : EmailToken . scopes [ :signup ] )
reload
2013-05-08 09:58:34 +08:00
end
2019-04-04 00:04:05 +08:00
def deactivate ( performed_by )
2017-09-13 15:33:59 +08:00
self . update! ( active : false )
2019-04-04 00:04:05 +08:00
if reviewable = ReviewableUser . pending . find_by ( target : self )
2021-06-15 23:35:45 +08:00
reviewable . perform ( performed_by , :delete_user )
2019-04-04 00:04:05 +08:00
end
2013-05-08 09:58:34 +08:00
end
2014-06-17 08:46:30 +08:00
def change_trust_level! ( level , opts = nil )
Promotion . new ( self ) . change_trust_level! ( level , opts )
end
2013-02-22 02:20:00 +08:00
def readable_name
2018-05-15 07:48:30 +08:00
name . present? && name != username ? " #{ name } ( #{ username } ) " : username
2013-02-22 02:20:00 +08:00
end
2014-04-16 18:22:21 +08:00
def badge_count
2019-12-30 19:19:59 +08:00
user_stat & . distinct_badge_count
2014-04-16 18:22:21 +08:00
end
2021-06-22 23:58:03 +08:00
def featured_user_badges ( limit = nil )
if limit . nil?
2020-01-14 22:26:49 +08:00
default_featured_user_badges
else
user_badges . grouped_with_count . where ( " featured_rank <= ? " , limit )
end
2014-04-16 18:11:11 +08:00
end
2018-05-03 21:41:41 +08:00
def self . count_by_signup_date ( start_date = nil , end_date = nil , group_id = nil )
result = self
if start_date && end_date
2018-05-11 11:30:21 +08:00
result = result . group ( " date(users.created_at) " )
result = result . where ( " users.created_at >= ? AND users.created_at <= ? " , start_date , end_date )
2018-06-05 15:29:17 +08:00
result = result . order ( " date(users.created_at) " )
2018-05-03 21:41:41 +08:00
end
2016-02-03 10:29:51 +08:00
if group_id
result = result . joins ( " INNER JOIN group_users ON group_users.user_id = users.id " )
result = result . where ( " group_users.group_id = ? " , group_id )
end
2018-04-26 20:49:41 +08:00
result . count
end
2018-05-03 21:41:41 +08:00
def self . count_by_first_post ( start_date = nil , end_date = nil )
result = joins ( " INNER JOIN user_stats AS us ON us.user_id = users.id " )
if start_date && end_date
2018-05-11 11:30:21 +08:00
result = result . group ( " date(us.first_post_created_at) " )
result =
result . where (
" us.first_post_created_at > ? AND us.first_post_created_at < ? " ,
start_date ,
end_date ,
)
result = result . order ( " date(us.first_post_created_at) " )
2018-05-03 21:41:41 +08:00
end
result . count
2013-03-08 00:07:59 +08:00
end
2013-04-29 14:33:24 +08:00
def secure_category_ids
2022-11-18 11:37:36 +08:00
cats =
if self . admin? && ! SiteSetting . suppress_secured_categories_from_admin
Category . unscoped . where ( read_restricted : true )
else
secure_categories . references ( :categories )
end
2013-09-10 12:29:02 +08:00
cats . pluck ( " categories.id " ) . sort
2013-04-29 14:33:24 +08:00
end
2013-05-11 04:58:23 +08:00
# Flag all posts from a user as spam
def flag_linked_posts_as_spam
2019-01-04 01:03:01 +08:00
results = [ ]
2016-04-26 05:03:17 +08:00
disagreed_flag_post_ids =
PostAction
. where ( post_action_type_id : PostActionType . types [ :spam ] )
. where . not ( disagreed_at : nil )
. pluck ( :post_id )
2015-10-17 03:16:44 +08:00
2016-04-26 05:03:17 +08:00
topic_links
. includes ( :post )
. where . not ( post_id : disagreed_flag_post_ids )
. each do | tl |
2019-07-12 18:04:16 +08:00
message =
I18n . t (
" flag_reason.spam_hosts " ,
base_path : Discourse . base_path ,
locale : SiteSetting . default_locale ,
)
2019-01-04 01:03:01 +08:00
results << PostActionCreator . create ( Discourse . system_user , tl . post , :spam , message : message )
2013-05-11 04:58:23 +08:00
end
2019-01-04 01:03:01 +08:00
results
2013-05-11 04:58:23 +08:00
end
2013-05-13 16:04:03 +08:00
2013-08-14 04:08:29 +08:00
def has_uploaded_avatar
uploaded_avatar . present?
end
2013-05-24 18:58:26 +08:00
2013-11-15 23:27:43 +08:00
def find_email
2022-02-18 09:12:51 +08:00
if last_sent_email_address . present? &&
EmailAddressValidator . valid_value? ( last_sent_email_address )
last_sent_email_address
2023-01-09 20:20:10 +08:00
else
2022-02-18 09:12:51 +08:00
email
2023-01-09 20:20:10 +08:00
end
2013-11-15 23:27:43 +08:00
end
2014-09-25 08:19:26 +08:00
def tl3_requirements
2014-09-05 13:20:39 +08:00
@lq || = TrustLevel3Requirements . new ( self )
2014-01-23 06:09:56 +08:00
end
2014-09-25 08:19:26 +08:00
def on_tl3_grace_period?
2018-10-12 03:11:40 +08:00
return true if SiteSetting . tl3_promotion_min_duration . to_i . days . ago . year < 2013
2014-09-14 04:55:26 +08:00
UserHistory
. for ( self , :auto_trust_level_change )
. where ( " created_at >= ? " , SiteSetting . tl3_promotion_min_duration . to_i . days . ago )
. where ( previous_value : TrustLevel [ 2 ] . to_s )
. where ( new_value : TrustLevel [ 3 ] . to_s )
. exists?
end
2014-05-22 15:37:02 +08:00
def refresh_avatar
2014-08-14 04:17:16 +08:00
return if @import_mode
2014-05-28 14:54:21 +08:00
avatar = user_avatar || create_user_avatar
2014-05-22 15:37:02 +08:00
2020-09-03 10:12:24 +08:00
if self . primary_email . present? && SiteSetting . automatically_download_gravatars? &&
! avatar . last_gravatar_download_attempt
2016-04-18 18:44:09 +08:00
Jobs . cancel_scheduled_job ( :update_gravatar , user_id : self . id , avatar_id : avatar . id )
Jobs . enqueue_in ( 1 . second , :update_gravatar , user_id : self . id , avatar_id : avatar . id )
2014-07-03 15:29:44 +08:00
end
2015-03-30 18:31:10 +08:00
2015-04-24 17:14:10 +08:00
# mark all the user's quoted posts as "needing a rebake"
2024-05-27 15:57:48 +08:00
Post . rebake_all_quoted_posts ( self . id ) if saved_change_to_uploaded_avatar_id?
2014-07-03 15:29:44 +08:00
end
2014-07-29 01:17:37 +08:00
def first_post_created_at
user_stat . try ( :first_post_created_at )
end
2014-09-25 13:50:54 +08:00
def associated_accounts
result = [ ]
2018-07-23 23:51:57 +08:00
Discourse . authenticators . each do | authenticator |
account_description = authenticator . description_for_user ( self )
unless account_description . empty?
result << { name : authenticator . name , description : account_description }
end
2014-09-25 13:50:54 +08:00
end
2018-07-23 23:51:57 +08:00
result
2014-09-25 13:50:54 +08:00
end
2018-09-07 06:02:47 +08:00
USER_FIELD_PREFIX || = " user_field_ "
2022-07-18 23:35:47 +08:00
def user_fields ( field_ids = nil )
2020-03-03 03:22:49 +08:00
field_ids = ( @all_user_field_ids || = UserField . pluck ( :id ) ) if field_ids . nil?
2023-06-22 01:35:24 +08:00
field_ids . map { | fid | [ fid . to_s , custom_fields [ " #{ USER_FIELD_PREFIX } #{ fid } " ] ] } . to_h
2014-09-27 02:48:34 +08:00
end
2022-07-15 06:36:54 +08:00
def validatable_user_fields_values
validatable_user_fields . values . join ( " " )
2022-05-16 21:21:33 +08:00
end
2021-03-29 19:03:19 +08:00
def set_user_field ( field_id , value )
custom_fields [ " #{ USER_FIELD_PREFIX } #{ field_id } " ] = value
end
2022-06-15 00:27:01 +08:00
def apply_watched_words
2022-07-15 06:36:54 +08:00
validatable_user_fields . each do | id , value |
2023-03-01 10:43:34 +08:00
field = WordWatcher . censor_text ( value )
field = WordWatcher . replace_text ( field )
set_user_field ( id , field )
2022-06-15 00:27:01 +08:00
end
end
2022-07-15 06:36:54 +08:00
def validatable_user_fields
2022-07-18 23:35:47 +08:00
# ignore multiselect fields since they are admin-set and thus not user generated content
@public_user_field_ids || =
UserField . public_fields . where . not ( field_type : " multiselect " ) . pluck ( :id )
2022-07-15 06:36:54 +08:00
2022-06-15 00:27:01 +08:00
user_fields ( @public_user_field_ids )
end
2015-02-20 01:11:07 +08:00
def number_of_deleted_posts
Post . with_deleted . where ( user_id : self . id ) . where . not ( deleted_at : nil ) . count
end
def number_of_flagged_posts
2023-10-18 09:38:17 +08:00
ReviewableFlaggedPost . where ( target_created_by : self . id ) . count
2015-02-20 01:11:07 +08:00
end
2020-03-16 20:52:08 +08:00
def number_of_rejected_posts
2023-07-29 00:16:23 +08:00
ReviewableQueuedPost . rejected . where ( target_created_by_id : self . id ) . count
2020-03-16 20:52:08 +08:00
end
2015-02-20 01:11:07 +08:00
def number_of_flags_given
PostAction
. where ( user_id : self . id )
. where ( disagreed_at : nil )
. where ( post_action_type_id : PostActionType . notify_flag_type_ids )
. count
end
def number_of_suspensions
UserHistory . for ( self , :suspend_user ) . count
end
2015-03-07 05:44:54 +08:00
def create_user_profile
2019-05-24 12:06:58 +08:00
UserProfile . create! ( user_id : id )
2015-03-07 05:44:54 +08:00
end
2018-07-18 18:57:43 +08:00
def set_random_avatar
2022-02-25 04:57:39 +08:00
if SiteSetting . selectable_avatars_mode != " disabled "
2020-10-13 21:17:06 +08:00
if upload = SiteSetting . selectable_avatars . sample
update_column ( :uploaded_avatar_id , upload . id )
UserAvatar . create! ( user_id : id , custom_upload_id : upload . id )
2018-07-18 18:57:43 +08:00
end
end
end
2015-04-08 10:29:43 +08:00
def anonymous?
SiteSetting . allow_anonymous_posting && trust_level > = 1 && ! ! anonymous_user_master
end
2016-04-27 01:08:19 +08:00
def is_singular_admin?
2017-03-14 14:33:06 +08:00
User . where ( admin : true ) . where . not ( id : id ) . human_users . blank?
2016-04-27 01:08:19 +08:00
end
2016-07-04 17:20:30 +08:00
def logged_out
2023-01-05 03:55:52 +08:00
MessageBus . publish " /logout/ #{ self . id } " , self . id , user_ids : [ self . id ]
2016-07-04 17:20:30 +08:00
DiscourseEvent . trigger ( :user_logged_out , self )
end
2017-06-01 16:19:42 +08:00
def logged_in
DiscourseEvent . trigger ( :user_logged_in , self )
DiscourseEvent . trigger ( :user_first_logged_in , self ) if ! self . seen_before?
end
2017-06-15 01:20:18 +08:00
def set_automatic_groups
2018-05-15 07:48:30 +08:00
return if ! active || staged || ! email_confirmed?
2017-06-15 01:20:18 +08:00
Group
. where ( automatic : false )
. where ( " LENGTH(COALESCE(automatic_membership_email_domains, '')) > 0 " )
. each do | group |
domains = group . automatic_membership_email_domains . gsub ( " . " , '\.' )
if email =~ Regexp . new ( " @( #{ domains } )$ " , true ) && ! group . users . include? ( self )
group . add ( self )
GroupActionLogger . new ( Discourse . system_user , group ) . log_add_user_to_group ( self )
2023-01-09 20:20:10 +08:00
end
2017-06-15 01:20:18 +08:00
end
2022-09-26 11:58:40 +08:00
@belonging_to_group_ids = nil
2017-06-15 01:20:18 +08:00
end
2017-04-27 02:47:36 +08:00
def email
2020-12-10 01:14:45 +08:00
primary_email & . email
2017-04-27 02:47:36 +08:00
end
2021-02-22 19:42:37 +08:00
# Shortcut to set the primary email of the user.
# Automatically removes any identical secondary emails.
2018-03-02 16:41:02 +08:00
def email = ( new_email )
2017-04-27 02:47:36 +08:00
if primary_email
2021-02-22 19:42:37 +08:00
primary_email . email = new_email
2017-04-27 02:47:36 +08:00
else
2021-02-22 19:42:37 +08:00
build_primary_email email : new_email , skip_validate_email : ! should_validate_email_address?
2017-04-27 02:47:36 +08:00
end
2021-02-22 19:42:37 +08:00
if secondary_match =
user_emails . detect { | ue |
! ue . primary && Email . downcase ( ue . email ) == Email . downcase ( new_email )
}
secondary_match . mark_for_destruction
primary_email . skip_validate_unique_email = true
end
2017-04-27 02:47:36 +08:00
end
2018-07-03 19:51:22 +08:00
def emails
self . user_emails . order ( " user_emails.primary DESC NULLS LAST " ) . pluck ( :email )
end
def secondary_emails
self . user_emails . secondary . pluck ( :email )
end
2020-06-11 00:11:49 +08:00
def unconfirmed_emails
self
. email_change_requests
. where . not ( change_state : EmailChangeRequest . states [ :complete ] )
. pluck ( :new_email )
end
2020-03-03 21:57:46 +08:00
RECENT_TIME_READ_THRESHOLD || = 60 . days
def self . preload_recent_time_read ( users )
times =
UserVisit
. where ( user_id : users . map ( & :id ) )
. where ( " visited_at >= ? " , RECENT_TIME_READ_THRESHOLD . ago )
. group ( :user_id )
. sum ( :time_read )
users . each { | u | u . preload_recent_time_read ( times [ u . id ] || 0 ) }
end
def preload_recent_time_read ( time )
@recent_time_read = time
end
2017-11-15 05:39:07 +08:00
def recent_time_read
2020-03-03 21:57:46 +08:00
@recent_time_read || =
self . user_visits . where ( " visited_at >= ? " , RECENT_TIME_READ_THRESHOLD . ago ) . sum ( :time_read )
2017-11-15 05:39:07 +08:00
end
2018-01-19 22:29:15 +08:00
def from_staged?
custom_fields [ User :: FROM_STAGED ]
end
2018-06-19 08:05:04 +08:00
def mature_staged?
2018-06-20 00:41:10 +08:00
from_staged? && self . created_at && self . created_at < 1 . day . ago
2018-06-19 08:05:04 +08:00
end
2018-09-21 10:06:08 +08:00
def next_best_title
group_titles_query = groups . where ( " groups.title <> '' " )
group_titles_query =
group_titles_query . order ( " groups.id = #{ primary_group_id } DESC " ) if primary_group_id
group_titles_query = group_titles_query . order ( " groups.primary_group DESC " ) . limit ( 1 )
2023-02-13 12:39:45 +08:00
if next_best_group_title = group_titles_query . pick ( :title )
2018-09-21 10:06:08 +08:00
return next_best_group_title
end
2023-02-13 12:39:45 +08:00
next_best_badge_title = badges . where ( allow_title : true ) . pick ( :name )
2018-09-21 10:06:08 +08:00
next_best_badge_title ? Badge . display_name ( next_best_badge_title ) : nil
end
2019-04-04 04:10:36 +08:00
def create_reviewable
return unless SiteSetting . must_approve_users? || SiteSetting . invite_only?
return if approved?
Jobs . enqueue ( :create_user_reviewable , user_id : self . id )
end
2019-08-10 18:02:12 +08:00
def has_more_posts_than? ( max_post_count )
return true if user_stat && ( user_stat . topic_count + user_stat . post_count ) > max_post_count
2020-04-02 04:10:17 +08:00
return true if max_post_count < 0
2019-08-10 18:02:12 +08:00
DB . query_single ( << ~ SQL , user_id : self . id ) . first > max_post_count
SELECT COUNT ( 1 )
FROM (
SELECT 1
FROM posts p
JOIN topics t ON ( p . topic_id = t . id )
WHERE p . user_id = :user_id AND
p . deleted_at IS NULL AND
t . deleted_at IS NULL AND
(
t . archetype < > 'private_message' OR
EXISTS (
SELECT 1
FROM topic_allowed_users a
WHERE a . topic_id = t . id AND a . user_id > 0 AND a . user_id < > :user_id
) OR
EXISTS (
SELECT 1
FROM topic_allowed_groups g
WHERE g . topic_id = p . topic_id
)
)
LIMIT #{max_post_count + 1}
) x
SQL
end
2019-10-02 10:08:41 +08:00
def create_or_fetch_secure_identifier
return secure_identifier if secure_identifier . present?
new_secure_identifier = SecureRandom . hex ( 20 )
self . update ( secure_identifier : new_secure_identifier )
new_secure_identifier
end
2023-10-04 02:59:28 +08:00
def second_factor_security_keys
security_keys . where ( factor_type : UserSecurityKey . factor_types [ :second_factor ] )
end
2019-10-02 10:08:41 +08:00
def second_factor_security_key_credential_ids
2023-10-04 02:59:28 +08:00
second_factor_security_keys . pluck ( :credential_id )
end
def passkey_credential_ids
security_keys . where ( factor_type : UserSecurityKey . factor_types [ :first_factor ] ) . pluck (
:credential_id ,
)
2019-10-02 10:08:41 +08:00
end
2020-06-06 00:31:58 +08:00
def encoded_username ( lower : false )
UrlHelper . encode_component ( lower ? username_lower : username )
end
2020-12-18 23:03:51 +08:00
def do_not_disturb?
active_do_not_disturb_timings . exists?
end
def active_do_not_disturb_timings
now = Time . zone . now
do_not_disturb_timings . where ( " starts_at <= ? AND ends_at > ? " , now , now )
end
2021-01-08 00:49:49 +08:00
def do_not_disturb_until
active_do_not_disturb_timings . maximum ( :ends_at )
end
2021-01-28 00:29:24 +08:00
def shelved_notifications
ShelvedNotification . joins ( :notification ) . where ( " notifications.user_id = ? " , self . id )
end
2021-11-22 11:38:49 +08:00
def allow_live_notifications?
seen_since? ( 30 . days . ago )
end
2021-12-02 21:42:23 +08:00
def username_equals_to? ( another_username )
username_lower == User . normalize_username ( another_username )
end
2024-05-28 03:30:19 +08:00
def relative_url
" #{ Discourse . base_path } /u/ #{ encoded_username } "
end
2022-04-13 21:52:56 +08:00
def full_url
" #{ Discourse . base_url } /u/ #{ encoded_username } "
end
def display_name
if SiteSetting . prioritize_username_in_ux?
username
else
name . presence || username
end
end
2022-05-27 17:15:14 +08:00
def clear_status!
user_status . destroy! if user_status
2022-05-30 17:41:53 +08:00
publish_user_status ( nil )
2022-05-27 17:15:14 +08:00
end
2022-08-09 18:54:33 +08:00
def set_status! ( description , emoji , ends_at = nil )
2023-02-06 22:56:28 +08:00
status = {
description : description ,
emoji : emoji ,
set_at : Time . zone . now ,
ends_at : ends_at ,
user_id : id ,
}
validate_status! ( status )
UserStatus . upsert ( status )
2022-05-30 17:41:53 +08:00
2023-02-06 22:56:28 +08:00
reload_user_status
2022-05-30 17:41:53 +08:00
publish_user_status ( user_status )
2022-05-27 17:15:14 +08:00
end
2022-07-05 23:12:22 +08:00
def has_status?
user_status && ! user_status . expired?
end
2023-02-27 20:11:01 +08:00
def new_new_view_enabled?
in_any_groups? ( SiteSetting . experimental_new_new_view_groups_map )
end
2024-02-24 02:08:15 +08:00
2023-07-04 13:08:29 +08:00
def watched_precedence_over_muted
if user_option . watched_precedence_over_muted . nil?
SiteSetting . watched_precedence_over_muted
else
user_option . watched_precedence_over_muted
end
end
2024-06-25 19:32:18 +08:00
def populated_required_custom_fields?
UserField
. required
. pluck ( :id )
. all? { | field_id | custom_fields [ " #{ User :: USER_FIELD_PREFIX } #{ field_id } " ] . present? }
end
def needs_required_fields_check?
( required_fields_version || 0 ) < UserRequiredFieldsVersion . current
end
def bump_required_fields_version
update ( required_fields_version : UserRequiredFieldsVersion . current )
end
2013-02-06 03:16:51 +08:00
protected
2014-07-23 09:42:24 +08:00
def badge_grant
BadgeGranter . queue_badge_grant ( Badge :: Trigger :: UserChange , user : self )
end
2015-06-06 01:50:06 +08:00
def expire_old_email_tokens
2017-08-31 12:06:56 +08:00
if saved_change_to_password_hash? && ! saved_change_to_id?
2015-06-06 01:50:06 +08:00
email_tokens . where ( " not expired " ) . update_all ( expired : true )
end
end
2016-12-22 10:13:14 +08:00
def index_search
2021-04-27 13:52:45 +08:00
# force is needed as user custom fields are updated using SQL and after_save callback is not triggered
SearchIndexer . index ( self , force : true )
2016-12-22 10:13:14 +08:00
end
2014-03-24 15:03:39 +08:00
def clear_global_notice_if_needed
2017-03-14 14:33:06 +08:00
return if id < 0
2017-02-13 23:53:45 +08:00
2014-03-24 15:03:39 +08:00
if admin && SiteSetting . has_login_hint
SiteSetting . has_login_hint = false
SiteSetting . global_notice = " "
end
end
2014-06-17 08:46:30 +08:00
def ensure_in_trust_level_group
Group . user_trust_level_change! ( id , trust_level )
end
2013-09-12 02:50:26 +08:00
def create_user_stat
2021-08-02 22:15:53 +08:00
UserStat . create! ( new_since : Time . zone . now , user_id : id )
2013-09-12 02:50:26 +08:00
end
2016-02-17 12:46:19 +08:00
def create_user_option
2019-05-24 12:06:58 +08:00
UserOption . create! ( user_id : id )
2016-02-17 12:46:19 +08:00
end
2013-06-06 22:40:10 +08:00
def create_email_token
2021-11-25 15:34:39 +08:00
email_tokens . create! ( email : email , scope : EmailToken . scopes [ :signup ] )
2013-06-06 22:40:10 +08:00
end
2013-02-06 03:16:51 +08:00
2013-06-06 22:40:10 +08:00
def ensure_password_is_hashed
if @raw_password
2023-04-11 17:16:28 +08:00
self . salt = SecureRandom . hex ( PASSWORD_SALT_LENGTH )
self . password_algorithm = TARGET_PASSWORD_ALGORITHM
self . password_hash = hash_password ( @raw_password , salt , password_algorithm )
2013-02-06 03:16:51 +08:00
end
2013-06-06 22:40:10 +08:00
end
2013-02-06 03:16:51 +08:00
2017-02-01 06:21:37 +08:00
def expire_tokens_if_password_changed
# NOTE: setting raw password is the only valid way of changing a password
# the password field in the DB is actually hashed, nobody should be amending direct
if @raw_password
# Association in model may be out-of-sync
UserAuthToken . where ( user_id : id ) . destroy_all
# We should not carry this around after save
@raw_password = nil
2017-12-01 12:19:24 +08:00
@password_required = false
2017-02-01 06:21:37 +08:00
end
end
2023-04-11 17:16:28 +08:00
def hash_password ( password , salt , algorithm )
2016-05-30 11:38:04 +08:00
raise StandardError . new ( " password is too long " ) if password . size > User . max_password_length
2023-04-11 17:16:28 +08:00
PasswordHasher . hash_password ( password : password , salt : salt , algorithm : algorithm )
2013-06-06 22:40:10 +08:00
end
2013-02-06 03:16:51 +08:00
2013-06-06 22:40:10 +08:00
def add_trust_level
2013-12-21 15:19:22 +08:00
# there is a possibility we did not load trust level column, skip it
2013-06-06 22:40:10 +08:00
return unless has_attribute? :trust_level
self . trust_level || = SiteSetting . default_trust_level
end
2013-02-06 03:16:51 +08:00
2019-04-23 18:22:47 +08:00
def update_usernames
self . username . unicode_normalize!
2013-06-06 22:40:10 +08:00
self . username_lower = username . downcase
end
2013-02-06 10:44:49 +08:00
2018-04-03 00:44:04 +08:00
USERNAME_EXISTS_SQL = << ~ SQL
2019-04-23 18:22:47 +08:00
( SELECT users . id AS id , true as is_user FROM users
WHERE users . username_lower = :username )
2018-04-03 00:44:04 +08:00
2019-04-23 18:22:47 +08:00
UNION ALL
2018-04-03 00:44:04 +08:00
2019-04-23 18:22:47 +08:00
( SELECT groups . id , false as is_user FROM groups
WHERE lower ( groups . name ) = :username )
2018-04-03 00:44:04 +08:00
SQL
2019-04-23 18:22:47 +08:00
def self . username_exists? ( username )
username = normalize_username ( username )
DB . exec ( User :: USERNAME_EXISTS_SQL , username : username ) > 0
2019-02-20 05:31:03 +08:00
end
2018-04-03 00:44:04 +08:00
def username_validator
username_format_validator ||
begin
2019-05-14 04:43:19 +08:00
if will_save_change_to_username?
existing =
DB . query ( USERNAME_EXISTS_SQL , username : self . class . normalize_username ( username ) )
user_id = existing . select { | u | u . is_user } . first & . id
same_user = user_id && user_id == self . id
2018-06-19 14:13:14 +08:00
2019-05-14 04:43:19 +08:00
errors . add ( :username , I18n . t ( :" user.username.unique " ) ) if existing . present? && ! same_user
2018-04-03 00:44:04 +08:00
2019-05-14 04:43:19 +08:00
if confirm_password? ( username ) || confirm_password? ( username . downcase )
errors . add ( :username , :same_as_password )
2023-01-09 20:20:10 +08:00
end
2019-05-14 04:43:19 +08:00
end
2018-04-03 00:44:04 +08:00
end
end
2019-05-14 04:43:19 +08:00
def name_validator
2020-02-04 03:12:45 +08:00
if name . present?
name_pw = name [ 0 ... User . max_password_length ]
if confirm_password? ( name_pw ) || confirm_password? ( name_pw . downcase )
errors . add ( :name , :same_as_password )
end
2019-05-14 04:43:19 +08:00
end
end
2015-08-22 02:39:21 +08:00
def set_default_categories_preferences
2016-06-14 22:45:47 +08:00
return if self . staged?
2015-08-22 02:39:21 +08:00
values = [ ]
2021-05-06 07:14:07 +08:00
# The following site settings are used to pre-populate default category
# tracking settings for a user:
#
# * default_categories_watching
# * default_categories_tracking
# * default_categories_watching_first_post
2022-06-20 11:49:33 +08:00
# * default_categories_normal
2021-05-06 07:14:07 +08:00
# * default_categories_muted
2022-06-20 11:49:33 +08:00
%w[ watching watching_first_post tracking normal muted ] . each do | setting |
2021-05-06 07:14:07 +08:00
category_ids = SiteSetting . get ( " default_categories_ #{ setting } " ) . split ( " | " ) . map ( & :to_i )
2015-08-22 02:39:21 +08:00
category_ids . each do | category_id |
2019-07-12 01:41:51 +08:00
next if category_id == 0
2021-05-06 07:14:07 +08:00
values << {
user_id : self . id ,
category_id : category_id ,
notification_level : CategoryUser . notification_levels [ setting . to_sym ] ,
}
2013-08-24 05:35:01 +08:00
end
end
2023-03-28 22:13:01 +08:00
CategoryUser . insert_all ( values ) if values . present?
2014-01-03 04:27:26 +08:00
end
2019-11-01 15:10:13 +08:00
def set_default_tags_preferences
return if self . staged?
values = [ ]
2021-05-06 07:14:07 +08:00
# The following site settings are used to pre-populate default tag
# tracking settings for a user:
#
# * default_tags_watching
# * default_tags_tracking
# * default_tags_watching_first_post
# * default_tags_muted
%w[ watching watching_first_post tracking muted ] . each do | setting |
tag_names = SiteSetting . get ( " default_tags_ #{ setting } " ) . split ( " | " )
2019-11-01 15:10:13 +08:00
now = Time . zone . now
Tag
. where ( name : tag_names )
. pluck ( :id )
. each do | tag_id |
2021-05-06 07:14:07 +08:00
values << {
user_id : self . id ,
tag_id : tag_id ,
notification_level : TagUser . notification_levels [ setting . to_sym ] ,
created_at : now ,
updated_at : now ,
}
2019-11-01 15:10:13 +08:00
end
end
2024-07-04 14:23:28 +08:00
TagUser . insert_all ( values ) if values . present?
2019-11-01 15:10:13 +08:00
end
2014-12-03 13:36:25 +08:00
def self . purge_unactivated
2017-08-26 03:20:06 +08:00
return [ ] if SiteSetting . purge_unactivated_users_grace_period_days < = 0
2018-05-17 00:24:11 +08:00
destroyer = UserDestroyer . new ( Discourse . system_user )
User
2024-04-03 11:06:31 +08:00
. joins (
2024-04-18 07:53:43 +08:00
" LEFT JOIN user_histories ON user_histories.target_user_id = users.id AND action = #{ UserHistory . actions [ :deactivate_user ] } AND acting_user_id IS NOT NULL " ,
2024-04-03 11:06:31 +08:00
)
2018-05-17 00:24:11 +08:00
. where ( active : false )
2024-04-03 11:06:31 +08:00
. where ( " users.created_at < ? " , SiteSetting . purge_unactivated_users_grace_period_days . days . ago )
2018-05-17 00:24:11 +08:00
. where ( " NOT admin AND NOT moderator " )
2018-06-29 11:33:54 +08:00
. where (
" NOT EXISTS
2018-07-13 10:28:27 +08:00
( SELECT 1 FROM topic_allowed_users tu JOIN topics t ON t . id = tu . topic_id AND t . user_id > 0 WHERE tu . user_id = users . id LIMIT 1 )
2018-06-29 11:33:54 +08:00
" ,
)
2021-03-01 13:46:28 +08:00
. where (
" NOT EXISTS
( SELECT 1 FROM posts p WHERE p . user_id = users . id LIMIT 1 )
" ,
)
2024-04-03 11:06:31 +08:00
. where ( " user_histories.id IS NULL " )
2016-07-27 18:28:56 +08:00
. limit ( 200 )
2018-05-17 00:24:11 +08:00
. find_each do | user |
2014-08-20 01:46:40 +08:00
begin
2018-05-17 00:24:11 +08:00
destroyer . destroy ( user , context : I18n . t ( :purge_reason ) )
2021-03-01 13:46:28 +08:00
rescue Discourse :: InvalidAccess
2018-05-17 00:24:11 +08:00
# keep going
2023-01-09 20:20:10 +08:00
end
2014-08-20 01:46:40 +08:00
end
2014-08-14 02:13:41 +08:00
end
2021-07-08 15:46:21 +08:00
def match_primary_group_changes
2018-09-17 13:08:39 +08:00
return unless primary_group_id_changed?
2023-02-13 12:39:45 +08:00
self . title = primary_group & . title if Group . exists? ( id : primary_group_id_was , title : title )
2021-07-08 15:46:21 +08:00
self . flair_group_id = primary_group & . id if flair_group_id == primary_group_id_was
2018-09-17 13:08:39 +08:00
end
2022-08-10 00:22:39 +08:00
def self . first_login_admin_id
User
. where ( admin : true )
. human_users
. joins ( :user_auth_tokens )
. order ( " user_auth_tokens.created_at " )
2023-02-13 12:39:45 +08:00
. pick ( :id )
2022-08-10 00:22:39 +08:00
end
2013-07-07 18:40:35 +08:00
private
2022-12-06 02:39:10 +08:00
def set_default_sidebar_section_links ( update : false )
2022-10-27 06:38:50 +08:00
return if staged? || bot?
2023-06-15 07:31:28 +08:00
if SiteSetting . default_navigation_menu_categories . present?
categories_to_update = SiteSetting . default_navigation_menu_categories . split ( " | " )
2022-12-06 02:39:10 +08:00
2022-12-01 09:32:35 +08:00
SidebarSectionLinksUpdater . update_category_section_links (
self ,
2022-12-06 02:39:10 +08:00
category_ids : categories_to_update ,
2022-12-01 09:32:35 +08:00
)
2022-10-27 06:38:50 +08:00
end
2023-06-15 07:31:28 +08:00
if SiteSetting . tagging_enabled && SiteSetting . default_navigation_menu_tags . present?
2023-07-27 10:52:33 +08:00
SidebarSectionLinksUpdater . update_tag_section_links (
self ,
tag_ids : Tag . where ( name : SiteSetting . default_navigation_menu_tags . split ( " | " ) ) . pluck ( :id ) ,
)
2022-10-27 06:38:50 +08:00
end
end
2021-08-02 22:15:53 +08:00
def stat
user_stat || create_user_stat
end
2019-06-17 13:10:47 +08:00
def trigger_user_automatic_group_refresh
Group . user_trust_level_change! ( id , trust_level ) if ! staged
true
end
2019-05-28 00:12:26 +08:00
def trigger_user_updated_event
DiscourseEvent . trigger ( :user_updated , self )
true
end
2018-09-21 10:06:08 +08:00
def check_if_title_is_badged_granted
if title_changed? && ! new_record? && user_profile
2019-11-08 13:34:24 +08:00
badge_matching_title =
title &&
badges . find do | badge |
badge . allow_title? && ( badge . display_name == title || badge . name == title )
end
2023-03-08 20:37:20 +08:00
user_profile . update! ( granted_title_badge_id : badge_matching_title & . id )
2018-09-21 10:06:08 +08:00
end
end
2013-10-24 05:24:50 +08:00
def previous_visit_at_update_required? ( timestamp )
2014-01-17 11:38:08 +08:00
seen_before? && ( last_seen_at < ( timestamp - SiteSetting . previous_visit_timeout_hours . hours ) )
2013-10-24 05:24:50 +08:00
end
def update_previous_visit ( timestamp )
update_visit_record! ( timestamp . to_date )
update_column ( :previous_visit_at , last_seen_at ) if previous_visit_at_update_required? ( timestamp )
end
2023-06-26 11:01:59 +08:00
def change_display_name
Jobs . enqueue ( :change_display_name , user_id : id , old_name : name_before_last_save , new_name : name )
end
2017-03-16 15:36:27 +08:00
def trigger_user_created_event
2017-03-16 16:02:34 +08:00
DiscourseEvent . trigger ( :user_created , self )
2017-03-16 15:36:27 +08:00
true
end
2018-07-23 15:49:49 +08:00
def trigger_user_destroyed_event
DiscourseEvent . trigger ( :user_destroyed , self )
true
end
2017-10-25 13:02:18 +08:00
def set_skip_validate_email
self . primary_email . skip_validate_email = ! should_validate_email_address? if self . primary_email
2017-08-09 10:56:08 +08:00
true
end
2018-12-15 05:52:37 +08:00
def check_site_contact_username
if ( saved_change_to_admin? || saved_change_to_moderator? ) &&
self . username == SiteSetting . site_contact_username && ! staff?
SiteSetting . set_and_log ( :site_contact_username , SiteSetting . defaults [ :site_contact_username ] )
end
end
2018-08-31 12:46:22 +08:00
def self . ensure_consistency!
DB . exec << ~ SQL
UPDATE users
SET uploaded_avatar_id = NULL
WHERE uploaded_avatar_id IN (
SELECT u1 . uploaded_avatar_id FROM users u1
LEFT JOIN uploads up
ON u1 . uploaded_avatar_id = up . id
WHERE u1 . uploaded_avatar_id IS NOT NULL AND
up . id IS NULL
)
SQL
end
2023-02-06 22:56:28 +08:00
def validate_status! ( status )
UserStatus . new ( status ) . validate!
end
2024-05-15 08:06:58 +08:00
def refresh_user_directory
DirectoryItem . refresh!
end
2013-02-06 03:16:51 +08:00
end
2013-05-24 10:48:32 +08:00
# == Schema Information
#
# Table name: users
#
2017-11-24 04:55:44 +08:00
# id :integer not null, primary key
# username :string(60) not null
# created_at :datetime not null
# updated_at :datetime not null
2019-01-12 03:29:56 +08:00
# name :string
2017-11-24 04:55:44 +08:00
# seen_notification_id :integer default(0), not null
# last_posted_at :datetime
# password_hash :string(64)
# salt :string(32)
# active :boolean default(FALSE), not null
# username_lower :string(60) not null
# last_seen_at :datetime
# admin :boolean default(FALSE), not null
# last_emailed_at :datetime
# trust_level :integer not null
# approved :boolean default(FALSE), not null
# approved_by_id :integer
# approved_at :datetime
# previous_visit_at :datetime
# suspended_at :datetime
# suspended_till :datetime
# date_of_birth :date
# views :integer default(0), not null
# flag_level :integer default(0), not null
# ip_address :inet
# moderator :boolean default(FALSE)
2019-01-12 03:29:56 +08:00
# title :string
2017-11-24 04:55:44 +08:00
# uploaded_avatar_id :integer
2019-01-12 01:19:23 +08:00
# locale :string(10)
2019-01-12 03:29:56 +08:00
# primary_group_id :integer
2017-11-24 04:55:44 +08:00
# registration_ip_address :inet
# staged :boolean default(FALSE), not null
# first_seen_at :datetime
# silenced_till :datetime
2019-01-04 01:03:01 +08:00
# group_locked_trust_level :integer
2017-11-24 04:55:44 +08:00
# manual_locked_trust_level :integer
2019-10-17 13:57:53 +08:00
# secure_identifier :string
2021-07-08 15:46:21 +08:00
# flair_group_id :integer
2022-05-06 15:11:16 +08:00
# last_seen_reviewable_id :integer
2023-04-11 17:16:28 +08:00
# password_algorithm :string(64)
2024-06-25 19:32:18 +08:00
# required_fields_version :integer
2013-05-24 10:48:32 +08:00
#
# Indexes
#
2023-02-03 00:35:04 +08:00
# idx_users_admin (id) WHERE admin
# idx_users_moderator (id) WHERE moderator
# index_users_on_last_posted_at (last_posted_at)
# index_users_on_last_seen_at (last_seen_at)
# index_users_on_name_trgm (name) USING gist
# index_users_on_secure_identifier (secure_identifier) UNIQUE
# index_users_on_uploaded_avatar_id (uploaded_avatar_id)
# index_users_on_username (username) UNIQUE
# index_users_on_username_lower (username_lower) UNIQUE
# index_users_on_username_lower_trgm (username_lower) USING gist
2013-05-24 10:48:32 +08:00
#