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$ "
2024-08-22 19:38:56 +08:00
MAX_SIMILAR_USERS = 10
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
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?
2024-08-26 23:01:24 +08:00
before_save :check_qualification_for_users_directory ,
if : Proc . new { SiteSetting . bootstrap_mode_enabled }
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
2024-08-26 23:01:24 +08:00
after_save :add_to_user_directory ,
if : Proc . new { SiteSetting . bootstrap_mode_enabled && @qualified_for_users_directory }
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 : )
2024-08-09 02:06:08 +08:00
can_update_ip_address =
DiscoursePluginRegistry . apply_modifier ( :user_can_update_ip_address , user_id : user_id )
return if ! can_update_ip_address
2022-05-03 06:50:56 +08:00
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
FIX: serialize Flags instead of PostActionType (#28362)
### Why?
Before, all flags were static. Therefore, they were stored in class variables and serialized by SiteSerializer. Recently, we added an option for admins to add their own flags or disable existing flags. Therefore, the class variable had to be dropped because it was unsafe for a multisite environment. However, it started causing performance problems.
### Solution
When a new Flag system is used, instead of using PostActionType, we can serialize Flags and use fragment cache for performance reasons.
At the same time, we are still supporting deprecated `replace_flags` API call. When it is used, we fall back to the old solution and the admin cannot add custom flags. In a couple of months, we will be able to drop that API function and clean that code properly. However, because it may still be used, redis cache was introduced to improve performance.
To test backward compatibility you can add this code to any plugin
```ruby
replace_flags do |flag_settings|
flag_settings.add(
4,
:inappropriate,
topic_type: true,
notify_type: true,
auto_action_type: true,
)
flag_settings.add(1001, :trolling, topic_type: true, notify_type: true, auto_action_type: true)
end
```
2024-08-14 10:13:46 +08:00
def post_action_type_view
@post_action_type_view || = PostActionTypeView . new
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 ,
FIX: serialize Flags instead of PostActionType (#28362)
### Why?
Before, all flags were static. Therefore, they were stored in class variables and serialized by SiteSerializer. Recently, we added an option for admins to add their own flags or disable existing flags. Therefore, the class variable had to be dropped because it was unsafe for a multisite environment. However, it started causing performance problems.
### Solution
When a new Flag system is used, instead of using PostActionType, we can serialize Flags and use fragment cache for performance reasons.
At the same time, we are still supporting deprecated `replace_flags` API call. When it is used, we fall back to the old solution and the admin cannot add custom flags. In a couple of months, we will be able to drop that API function and clean that code properly. However, because it may still be used, redis cache was introduced to improve performance.
To test backward compatibility you can add this code to any plugin
```ruby
replace_flags do |flag_settings|
flag_settings.add(
4,
:inappropriate,
topic_type: true,
notify_type: true,
auto_action_type: true,
)
flag_settings.add(1001, :trolling, topic_type: true, notify_type: true, auto_action_type: true)
end
```
2024-08-14 10:13:46 +08:00
post_action_type_id : post_action_type_view . 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 " = >
FIX: serialize Flags instead of PostActionType (#28362)
### Why?
Before, all flags were static. Therefore, they were stored in class variables and serialized by SiteSerializer. Recently, we added an option for admins to add their own flags or disable existing flags. Therefore, the class variable had to be dropped because it was unsafe for a multisite environment. However, it started causing performance problems.
### Solution
When a new Flag system is used, instead of using PostActionType, we can serialize Flags and use fragment cache for performance reasons.
At the same time, we are still supporting deprecated `replace_flags` API call. When it is used, we fall back to the old solution and the admin cannot add custom flags. In a couple of months, we will be able to drop that API function and clean that code properly. However, because it may still be used, redis cache was introduced to improve performance.
To test backward compatibility you can add this code to any plugin
```ruby
replace_flags do |flag_settings|
flag_settings.add(
4,
:inappropriate,
topic_type: true,
notify_type: true,
auto_action_type: true,
)
flag_settings.add(1001, :trolling, topic_type: true, notify_type: true, auto_action_type: true)
end
```
2024-08-14 10:13:46 +08:00
post_action_type_view . flag_types_without_additional_message . values ,
2024-07-18 08:10:22 +08:00
)
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
FIX: serialize Flags instead of PostActionType (#28362)
### Why?
Before, all flags were static. Therefore, they were stored in class variables and serialized by SiteSerializer. Recently, we added an option for admins to add their own flags or disable existing flags. Therefore, the class variable had to be dropped because it was unsafe for a multisite environment. However, it started causing performance problems.
### Solution
When a new Flag system is used, instead of using PostActionType, we can serialize Flags and use fragment cache for performance reasons.
At the same time, we are still supporting deprecated `replace_flags` API call. When it is used, we fall back to the old solution and the admin cannot add custom flags. In a couple of months, we will be able to drop that API function and clean that code properly. However, because it may still be used, redis cache was introduced to improve performance.
To test backward compatibility you can add this code to any plugin
```ruby
replace_flags do |flag_settings|
flag_settings.add(
4,
:inappropriate,
topic_type: true,
notify_type: true,
auto_action_type: true,
)
flag_settings.add(1001, :trolling, topic_type: true, notify_type: true, auto_action_type: true)
end
```
2024-08-14 10:13:46 +08:00
. where ( post_action_type_id : post_action_type_view . types [ :spam ] )
2016-04-26 05:03:17 +08:00
. 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 )
FIX: serialize Flags instead of PostActionType (#28362)
### Why?
Before, all flags were static. Therefore, they were stored in class variables and serialized by SiteSerializer. Recently, we added an option for admins to add their own flags or disable existing flags. Therefore, the class variable had to be dropped because it was unsafe for a multisite environment. However, it started causing performance problems.
### Solution
When a new Flag system is used, instead of using PostActionType, we can serialize Flags and use fragment cache for performance reasons.
At the same time, we are still supporting deprecated `replace_flags` API call. When it is used, we fall back to the old solution and the admin cannot add custom flags. In a couple of months, we will be able to drop that API function and clean that code properly. However, because it may still be used, redis cache was introduced to improve performance.
To test backward compatibility you can add this code to any plugin
```ruby
replace_flags do |flag_settings|
flag_settings.add(
4,
:inappropriate,
topic_type: true,
notify_type: true,
auto_action_type: true,
)
flag_settings.add(1001, :trolling, topic_type: true, notify_type: true, auto_action_type: true)
end
```
2024-08-14 10:13:46 +08:00
. where ( post_action_type_id : post_action_type_view . notify_flag_type_ids )
2015-02-20 01:11:07 +08:00
. 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
2024-08-26 15:33:19 +08:00
. for_all_users
2024-06-25 19:32:18 +08:00
. 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
2024-08-20 20:27:29 +08:00
def similar_users
User
. real
. where . not ( id : self . id )
. where ( ip_address : self . ip_address , admin : false , moderator : false )
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
2024-08-26 23:01:24 +08:00
def check_qualification_for_users_directory
if ( ! self . active_was && self . active ) || ( ! self . approved_was && self . approved ) ||
( self . id_was . nil? && self . id . present? )
@qualified_for_users_directory = true
end
end
def add_to_user_directory
DirectoryItem . add_missing_users_all_periods
@qualified_for_users_directory = false
2024-05-15 08:06:58 +08:00
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
#