Class: User

Inherits:
ActiveRecord::Base
  • Object
show all
Includes:
HasCustomFields, HasDestroyedWebHook, Roleable, Searchable, SecondFactorManager
Defined in:
app/models/user.rb

Defined Under Namespace

Modules: NewTopicDuration

Constant Summary collapse

3
PASSWORD_SALT_LENGTH =
16
TARGET_PASSWORD_ALGORITHM =
"$pbkdf2-#{Rails.configuration.pbkdf2_algorithm}$i=#{Rails.configuration.pbkdf2_iterations},l=32$"
EMAIL =
/([^@]+)@([^\.]+)/
FROM_STAGED =
"from_staged"
MAX_UNREAD_BACKLOG =
400
MAX_UNREAD_NOTIFICATIONS =

PERF: This safeguard is in place to avoid situations where a user with enormous amounts of unread data can issue extremely expensive queries

99

Constants included from SecondFactorManager

SecondFactorManager::TOTP_ALLOWED_DRIFT_SECONDS

Constants included from HasCustomFields

HasCustomFields::CUSTOM_FIELDS_MAX_ITEMS, HasCustomFields::CUSTOM_FIELDS_MAX_VALUE_LENGTH

Constants included from Searchable

Searchable::PRIORITIES

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from HasDestroyedWebHook

#enqueue_destroyed_web_hook

Methods included from SecondFactorManager

#authenticate_backup_code, #authenticate_second_factor, #authenticate_security_key, #authenticate_totp, #backup_codes_enabled?, #create_backup_codes, #create_totp, #generate_backup_codes, #get_totp_object, #has_any_second_factor_methods_enabled?, #has_multiple_second_factor_methods?, #hash_backup_code, #invalid_second_factor_authentication_result, #invalid_second_factor_method_result, #invalid_security_key_result, #invalid_totp_or_backup_code_result, #not_enabled_second_factor_method_result, #only_security_keys_enabled?, #only_totp_or_backup_codes_enabled?, #remaining_backup_codes, #require_rotp, #security_keys_enabled?, #totp_enabled?, #totp_or_backup_codes_enabled?, #totp_provisioning_uri, #valid_second_factor_method_for_user?

Methods included from HasCustomFields

#clear_custom_fields, #create_singular, #custom_field_preloaded?, #custom_fields, #custom_fields=, #custom_fields_clean?, #custom_fields_preloaded?, #on_custom_fields_change, #save_custom_fields, #set_preloaded_custom_fields, #upsert_custom_fields

Methods included from Roleable

#grant_admin!, #grant_moderation!, #regular?, #revoke_admin!, #revoke_moderation!, #save_and_refresh_staff_groups!, #set_default_notification_levels, #set_permission, #staff?, #whisperer?

Instance Attribute Details

#custom_dataObject

Cache for user custom fields. Currently it is used to display quick search results



243
244
245
# File 'app/models/user.rb', line 243

def custom_data
  @custom_data
end

#import_modeObject

set to true to optimize creation and save for imports



240
241
242
# File 'app/models/user.rb', line 240

def import_mode
  @import_mode
end

#notification_channel_positionObject

This is just used to pass some information into the serializer



237
238
239
# File 'app/models/user.rb', line 237

def notification_channel_position
  @notification_channel_position
end

#send_welcome_messageObject

Whether we need to be sending a system message after creation



234
235
236
# File 'app/models/user.rb', line 234

def send_welcome_message
  @send_welcome_message
end

#skip_email_validationObject

Skip validating email, for example from a particular auth provider plugin



231
232
233
# File 'app/models/user.rb', line 231

def skip_email_validation
  @skip_email_validation
end

Class Method Details

.allowed_user_custom_fields(guardian) ⇒ Object



421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
# File 'app/models/user.rb', line 421

def self.allowed_user_custom_fields(guardian)
  fields = []

  fields.push(*DiscoursePluginRegistry.public_user_custom_fields)

  if SiteSetting.public_user_custom_fields.present?
    fields.push(*SiteSetting.public_user_custom_fields.split("|"))
  end

  if guardian.is_staff?
    if SiteSetting.staff_user_custom_fields.present?
      fields.push(*SiteSetting.staff_user_custom_fields.split("|"))
    end

    fields.push(*DiscoursePluginRegistry.staff_user_custom_fields)
  end

  fields.uniq
end

.avatar_template(username, uploaded_avatar_id) ⇒ Object



1105
1106
1107
1108
1109
1110
# File 'app/models/user.rb', line 1105

def self.avatar_template(username, uploaded_avatar_id)
  username ||= ""
  return default_template(username) if !uploaded_avatar_id
  hostname = RailsMultisite::ConnectionManagement.current_hostname
  UserAvatar.local_avatar_template(hostname, username.downcase, uploaded_avatar_id)
end

.color_index(username, length) ⇒ Object



1145
1146
1147
# File 'app/models/user.rb', line 1145

def self.color_index(username, length)
  Digest::MD5.hexdigest(username)[0...15].to_i(16) % length
end

.count_by_first_post(start_date = nil, end_date = nil) ⇒ Object



1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
# File 'app/models/user.rb', line 1372

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
    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)")
  end

  result.count
end

.count_by_signup_date(start_date = nil, end_date = nil, group_id = nil) ⇒ Object



1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
# File 'app/models/user.rb', line 1355

def self.(start_date = nil, end_date = nil, group_id = nil)
  result = self

  if start_date && end_date
    result = result.group("date(users.created_at)")
    result = result.where("users.created_at >= ? AND users.created_at <= ?", start_date, end_date)
    result = result.order("date(users.created_at)")
  end

  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

  result.count
end

.default_template(username) ⇒ Object



1096
1097
1098
1099
1100
1101
1102
1103
# File 'app/models/user.rb', line 1096

def self.default_template(username)
  if SiteSetting.default_avatars.present?
    urls = SiteSetting.default_avatars.split("\n")
    return urls[username_hash(username) % urls.size] if urls.present?
  end

  system_avatar_template(username)
end

.editable_user_custom_fields(by_staff: false) ⇒ Object



413
414
415
416
417
418
419
# File 'app/models/user.rb', line 413

def self.editable_user_custom_fields(by_staff: false)
  fields = []
  fields.push(*DiscoursePluginRegistry.self_editable_user_custom_fields)
  fields.push(*DiscoursePluginRegistry.staff_editable_user_custom_fields) if by_staff

  fields.uniq
end

.email_hash(email) ⇒ Object



586
587
588
# File 'app/models/user.rb', line 586

def self.email_hash(email)
  Digest::MD5.hexdigest(email.strip.downcase)
end

.find_by_email(email, primary: false) ⇒ Object



503
504
505
506
507
508
509
# File 'app/models/user.rb', line 503

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
end

.find_by_username(username) ⇒ Object



511
512
513
# File 'app/models/user.rb', line 511

def self.find_by_username(username)
  find_by(username_lower: normalize_username(username))
end

.find_by_username_or_email(username_or_email) ⇒ Object



495
496
497
498
499
500
501
# File 'app/models/user.rb', line 495

def self.find_by_username_or_email(username_or_email)
  if username_or_email.include?("@")
    find_by_email(username_or_email)
  else
    find_by_username(username_or_email)
  end
end

.gravatar_template(email) ⇒ Object



1071
1072
1073
# File 'app/models/user.rb', line 1071

def self.gravatar_template(email)
  "//#{SiteSetting.gravatar_base_url}/avatar/#{self.email_hash(email)}.png?s={size}&r=pg&d=identicon"
end

.human_user_id?(user_id) ⇒ Boolean

Returns:

  • (Boolean)


441
442
443
# File 'app/models/user.rb', line 441

def self.human_user_id?(user_id)
  user_id > 0
end

.last_seen_redis_key(user_id, now) ⇒ Object



1034
1035
1036
1037
# File 'app/models/user.rb', line 1034

def self.last_seen_redis_key(user_id, now)
  now_date = now.to_date
  "user:#{user_id}:#{now_date}"
end

.letter_avatar_color(username) ⇒ Object



1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
# File 'app/models/user.rb', line 1130

def self.letter_avatar_color(username)
  username ||= ""
  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

.max_password_lengthObject



372
373
374
# File 'app/models/user.rb', line 372

def self.max_password_length
  200
end

.max_unread_notificationsObject



692
693
694
# File 'app/models/user.rb', line 692

def self.max_unread_notifications
  @max_unread_notifications ||= MAX_UNREAD_NOTIFICATIONS
end

.max_unread_notifications=(val) ⇒ Object



696
697
698
# File 'app/models/user.rb', line 696

def self.max_unread_notifications=(val)
  @max_unread_notifications = val
end

.new_from_params(params) ⇒ Object



468
469
470
471
472
473
474
475
# File 'app/models/user.rb', line 468

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

.normalize_username(username) ⇒ Object



380
381
382
# File 'app/models/user.rb', line 380

def self.normalize_username(username)
  username.to_s.unicode_normalize.downcase if username.present?
end

.preload_recent_time_read(users) ⇒ Object



1639
1640
1641
1642
1643
1644
1645
1646
1647
# File 'app/models/user.rb', line 1639

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

.reserved_username?(username) ⇒ Boolean

Returns:

  • (Boolean)


401
402
403
404
405
406
407
408
409
410
411
# File 'app/models/user.rb', line 401

def self.reserved_username?(username)
  username = normalize_username(username)

  return true if SiteSetting.here_mention == username

  SiteSetting
    .reserved_usernames
    .unicode_normalize
    .split("|")
    .any? { |reserved| username.match?(/\A#{Regexp.escape(reserved).gsub('\*', ".*")}\z/) }
end

.should_update_last_seen?(user_id, now = Time.zone.now) ⇒ Boolean

Returns:

  • (Boolean)


1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
# File 'app/models/user.rb', line 1047

def self.should_update_last_seen?(user_id, now = Time.zone.now)
  return true if SiteSetting.active_user_rate_limit_secs <= 0

  Discourse.redis.set(
    last_seen_redis_key(user_id, now),
    "1",
    nx: true,
    ex: SiteSetting.active_user_rate_limit_secs,
  )
end

.suggest_name(string) ⇒ Object



490
491
492
493
# File 'app/models/user.rb', line 490

def self.suggest_name(string)
  return "" if string.blank?
  (string[/\A[^@]+/].presence || string[/[^@]+\z/]).tr(".", " ").titleize
end

.system_avatar_template(username) ⇒ Object



1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
# File 'app/models/user.rb', line 1112

def self.system_avatar_template(username)
  normalized_username = normalize_username(username)

  # 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
    url = +"#{Discourse.base_path}#{url}" unless url =~ %r{\Ahttps?://}
    url.gsub! "{color}", letter_avatar_color(normalized_username)
    url.gsub! "{username}", UrlHelper.encode_component(username)
    url.gsub! "{first_letter}",
              UrlHelper.encode_component(normalized_username.grapheme_clusters.first)
    url.gsub! "{hostname}", Discourse.current_hostname
    url
  else
    "#{Discourse.base_path}/letter_avatar/#{normalized_username}/{size}/#{LetterAvatar.version}.png"
  end
end

.update_ip_address!(user_id, new_ip:, old_ip:) ⇒ Object



998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
# File 'app/models/user.rb', line 998

def self.update_ip_address!(user_id, new_ip:, old_ip:)
  unless old_ip == new_ip || new_ip.blank?
    DB.exec(<<~SQL, user_id: user_id, ip_address: new_ip)
      UPDATE users
      SET ip_address = :ip_address
      WHERE id = :user_id
    SQL

    if SiteSetting.keep_old_ip_address_count > 0
      DB.exec(<<~SQL, user_id: user_id, ip_address: new_ip, current_timestamp: Time.zone.now)
      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

      DB.exec(<<~SQL, user_id: user_id, offset: SiteSetting.keep_old_ip_address_count)
      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
  end
end

.user_tipsObject



342
343
344
345
346
347
348
349
350
351
352
# File 'app/models/user.rb', line 342

def self.user_tips
  @user_tips ||=
    Enum.new(
      first_notification: 1,
      topic_timeline: 2,
      post_menu: 3,
      topic_notification_levels: 4,
      suggested_topics: 5,
      admin_guide: 6,
    )
end

.username_available?(username, email = nil, allow_reserved_username: false) ⇒ Boolean

Returns:

  • (Boolean)


384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
# File 'app/models/user.rb', line 384

def self.username_available?(username, email = nil, allow_reserved_username: false)
  lower = normalize_username(username)
  return false if !allow_reserved_username && reserved_username?(lower)
  return true if !username_exists?(lower)

  # 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,
      },
    )
end

.username_hash(username) ⇒ Object



1087
1088
1089
1090
1091
1092
1093
1094
# File 'app/models/user.rb', line 1087

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

.username_lengthObject



376
377
378
# File 'app/models/user.rb', line 376

def self.username_length
  SiteSetting.min_username_length.to_i..SiteSetting.max_username_length.to_i
end

Instance Method Details

#activateObject



1321
1322
1323
1324
1325
# File 'app/models/user.rb', line 1321

def activate
  email_token = self.email_tokens.create!(email: self.email, scope: EmailToken.scopes[:signup])
  EmailToken.confirm(email_token.token, scope: EmailToken.scopes[:signup])
  reload
end

#active_do_not_disturb_timingsObject



1740
1741
1742
1743
# File 'app/models/user.rb', line 1740

def active_do_not_disturb_timings
  now = Time.zone.now
  do_not_disturb_timings.where("starts_at <= ? AND ends_at > ?", now, now)
end

#admin?Boolean

a touch faster than automatic

Returns:

  • (Boolean)


1304
1305
1306
# File 'app/models/user.rb', line 1304

def admin?
  admin
end

#all_unread_notifications_countObject



729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
# File 'app/models/user.rb', line 729

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,
      )[
        0
      ].to_i
    end
end

#allow_live_notifications?Boolean

Returns:

  • (Boolean)


1753
1754
1755
# File 'app/models/user.rb', line 1753

def allow_live_notifications?
  seen_since?(30.days.ago)
end

#anonymous?Boolean

Returns:

  • (Boolean)


1561
1562
1563
# File 'app/models/user.rb', line 1561

def anonymous?
  SiteSetting.allow_anonymous_posting && trust_level >= 1 && !!anonymous_user_master
end

#apply_watched_wordsObject



1502
1503
1504
1505
1506
1507
1508
# File 'app/models/user.rb', line 1502

def apply_watched_words
  validatable_user_fields.each do |id, value|
    field = WordWatcher.censor_text(value)
    field = WordWatcher.replace_text(field)
    set_user_field(id, field)
  end
end

#associated_accountsObject



1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
# File 'app/models/user.rb', line 1473

def associated_accounts
  result = []

  Discourse.authenticators.each do |authenticator|
     = authenticator.description_for_user(self)
    unless .empty?
      result << { name: authenticator.name, description:  }
    end
  end

  result
end

#avatar_templateObject



1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
# File 'app/models/user.rb', line 1153

def avatar_template
   =
    is_system_user? && SiteSetting.logo_small && SiteSetting.use_site_small_logo_as_system_avatar

  if 
    Discourse.store.cdn_url(SiteSetting.logo_small.url)
  else
    self.class.avatar_template(username, uploaded_avatar_id)
  end
end

#avatar_template_urlObject



1083
1084
1085
# File 'app/models/user.rb', line 1083

def avatar_template_url
  UrlHelper.schemaless UrlHelper.absolute avatar_template
end

#badge_countObject



1343
1344
1345
# File 'app/models/user.rb', line 1343

def badge_count
  user_stat&.distinct_badge_count
end

#belonging_to_group_idsObject



519
520
521
# File 'app/models/user.rb', line 519

def belonging_to_group_ids
  @belonging_to_group_ids ||= group_users.pluck(:group_id)
end

#bookmarks_of_type(type) ⇒ Object



461
462
463
# File 'app/models/user.rb', line 461

def bookmarks_of_type(type)
  bookmarks.where(bookmarkable_type: type)
end

#bot?Boolean

Returns:

  • (Boolean)


449
450
451
# File 'app/models/user.rb', line 449

def bot?
  !self.human?
end

#bump_last_seen_notification!Object



776
777
778
779
780
781
782
783
784
785
# File 'app/models/user.rb', line 776

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

#bump_last_seen_reviewable!Object



787
788
789
790
791
792
793
794
795
796
797
# File 'app/models/user.rb', line 787

def bump_last_seen_reviewable!
  query = Reviewable.unseen_list_for(self, preload: false)

  query = query.where("reviewables.id > ?", last_seen_reviewable_id) if last_seen_reviewable_id
  max_reviewable_id = query.maximum(:id)

  if max_reviewable_id
    update!(last_seen_reviewable_id: max_reviewable_id)
    publish_reviewable_counts
  end
end

#change_trust_level!(level, opts = nil) ⇒ Object



1335
1336
1337
# File 'app/models/user.rb', line 1335

def change_trust_level!(level, opts = nil)
  Promotion.new(self).change_trust_level!(level, opts)
end

#change_username(new_username, actor = nil) ⇒ Object



560
561
562
# File 'app/models/user.rb', line 560

def change_username(new_username, actor = nil)
  UsernameChanger.change(self, new_username, actor)
end

#clear_last_seen_cache!(now = Time.zone.now) ⇒ Object



1043
1044
1045
# File 'app/models/user.rb', line 1043

def clear_last_seen_cache!(now = Time.zone.now)
  Discourse.redis.del(last_seen_redis_key(now))
end

#clear_status!Object



1773
1774
1775
1776
# File 'app/models/user.rb', line 1773

def clear_status!
  user_status.destroy! if user_status
  publish_user_status(nil)
end

#confirm_password?(password) ⇒ Boolean

Returns:

  • (Boolean)


914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
# File 'app/models/user.rb', line 914

def confirm_password?(password)
  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
end

#create_or_fetch_secure_identifierObject



1718
1719
1720
1721
1722
1723
# File 'app/models/user.rb', line 1718

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

#create_reviewableObject



1680
1681
1682
1683
1684
1685
# File 'app/models/user.rb', line 1680

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

#create_user_profileObject



1548
1549
1550
# File 'app/models/user.rb', line 1548

def 
  UserProfile.create!(user_id: id)
end

#create_visit_record!(date, opts = {}) ⇒ Object



952
953
954
955
956
957
958
959
# File 'app/models/user.rb', line 952

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

#created_topic_countObject Also known as: topic_count



564
565
566
# File 'app/models/user.rb', line 564

def created_topic_count
  stat.topic_count
end

#deactivate(performed_by) ⇒ Object



1327
1328
1329
1330
1331
1332
1333
# File 'app/models/user.rb', line 1327

def deactivate(performed_by)
  self.update!(active: false)

  if reviewable = ReviewableUser.pending.find_by(target: self)
    reviewable.perform(performed_by, :delete_user)
  end
end

#delete_posts_in_batches(guardian, batch_size = 20) ⇒ Object



1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
# File 'app/models/user.rb', line 1215

def delete_posts_in_batches(guardian, batch_size = 20)
  raise Discourse::InvalidAccess unless guardian.can_delete_all_posts? self

  Reviewable.where(created_by_id: id).delete_all

  posts
    .order("post_number desc")
    .limit(batch_size)
    .each { |p| PostDestroyer.new(guardian.user, p).destroy }
end

#display_nameObject



1765
1766
1767
1768
1769
1770
1771
# File 'app/models/user.rb', line 1765

def display_name
  if SiteSetting.prioritize_username_in_ux?
    username
  else
    name.presence || username
  end
end

#do_not_disturb?Boolean

Returns:

  • (Boolean)


1736
1737
1738
# File 'app/models/user.rb', line 1736

def do_not_disturb?
  active_do_not_disturb_timings.exists?
end

#do_not_disturb_untilObject



1745
1746
1747
# File 'app/models/user.rb', line 1745

def do_not_disturb_until
  active_do_not_disturb_timings.maximum(:ends_at)
end

#effective_localeObject



453
454
455
456
457
458
459
# File 'app/models/user.rb', line 453

def effective_locale
  if SiteSetting.allow_user_locale && self.locale.present?
    self.locale
  else
    SiteSetting.default_locale
  end
end

#emailObject



1598
1599
1600
# File 'app/models/user.rb', line 1598

def email
  primary_email&.email
end

#email=(new_email) ⇒ Object

Shortcut to set the primary email of the user. Automatically removes any identical secondary emails.



1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
# File 'app/models/user.rb', line 1604

def email=(new_email)
  if primary_email
    primary_email.email = new_email
  else
    build_primary_email email: new_email, skip_validate_email: !should_validate_email_address?
  end

  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

  new_email
end

#email_confirmed?Boolean

Returns:

  • (Boolean)


1316
1317
1318
1319
# File 'app/models/user.rb', line 1316

def email_confirmed?
  email_tokens.where(email: email, confirmed: true).present? || email_tokens.empty? ||
    single_sign_on_record&.external_email&.downcase == email
end

#email_hashObject



590
591
592
# File 'app/models/user.rb', line 590

def email_hash
  User.email_hash(email)
end

#emailsObject



1622
1623
1624
# File 'app/models/user.rb', line 1622

def emails
  self.user_emails.order("user_emails.primary DESC NULLS LAST").pluck(:email)
end

#encoded_username(lower: false) ⇒ Object



1732
1733
1734
# File 'app/models/user.rb', line 1732

def encoded_username(lower: false)
  UrlHelper.encode_component(lower ? username_lower : username)
end

#enqueue_member_welcome_messageObject



536
537
538
539
# File 'app/models/user.rb', line 536

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

#enqueue_staff_welcome_message(role) ⇒ Object



546
547
548
549
550
551
552
553
554
555
556
557
558
# File 'app/models/user.rb', line 546

def enqueue_staff_welcome_message(role)
  return unless staff?
  return if role == :admin && User.real.where(admin: true).count == 1

  Jobs.enqueue(
    :send_system_message,
    user_id: id,
    message_type: "welcome_staff",
    message_options: {
      role: role.to_s,
    },
  )
end

#enqueue_tl2_promotion_messageObject



541
542
543
544
# File 'app/models/user.rb', line 541

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

#enqueue_welcome_message(message_type) ⇒ Object



531
532
533
534
# File 'app/models/user.rb', line 531

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

#experimental_search_menu_groups_enabled?Boolean

Returns:

  • (Boolean)


1801
1802
1803
# File 'app/models/user.rb', line 1801

def experimental_search_menu_groups_enabled?
  in_any_groups?(SiteSetting.experimental_search_menu_groups_map)
end


1347
1348
1349
1350
1351
1352
1353
# File 'app/models/user.rb', line 1347

def featured_user_badges(limit = nil)
  if limit.nil?
    default_featured_user_badges
  else
    user_badges.grouped_with_count.where("featured_rank <= ?", limit)
  end
end

#find_emailObject



1430
1431
1432
1433
1434
1435
1436
1437
# File 'app/models/user.rb', line 1430

def find_email
  if last_sent_email_address.present? &&
       EmailAddressValidator.valid_value?(last_sent_email_address)
    last_sent_email_address
  else
    email
  end
end

#first_post_created_atObject



1469
1470
1471
# File 'app/models/user.rb', line 1469

def first_post_created_at
  user_stat.try(:first_post_created_at)
end

#flag_linked_posts_as_spamObject

Flag all posts from a user as spam



1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
# File 'app/models/user.rb', line 1401

def flag_linked_posts_as_spam
  results = []

  disagreed_flag_post_ids =
    PostAction
      .where(post_action_type_id: PostActionType.types[:spam])
      .where.not(disagreed_at: nil)
      .pluck(:post_id)

  topic_links
    .includes(:post)
    .where.not(post_id: disagreed_flag_post_ids)
    .each do |tl|
      message =
        I18n.t(
          "flag_reason.spam_hosts",
          base_path: Discourse.base_path,
          locale: SiteSetting.default_locale,
        )
      results << PostActionCreator.create(Discourse.system_user, tl.post, :spam, message: message)
    end

  results
end

#flags_given_countObject



1186
1187
1188
1189
1190
1191
# File 'app/models/user.rb', line 1186

def flags_given_count
  PostAction.where(
    user_id: id,
    post_action_type_id: PostActionType.flag_types_without_custom.values,
  ).count
end

#from_staged?Boolean

Returns:

  • (Boolean)


1658
1659
1660
# File 'app/models/user.rb', line 1658

def from_staged?
  custom_fields[User::FROM_STAGED]
end

#full_suspend_reasonObject



1254
1255
1256
# File 'app/models/user.rb', line 1254

def full_suspend_reason
  suspend_record.try(:details) if suspended?
end

#full_urlObject



1761
1762
1763
# File 'app/models/user.rb', line 1761

def full_url
  "#{Discourse.base_url}/u/#{encoded_username}"
end

#group_granted_trust_levelObject



523
524
525
# File 'app/models/user.rb', line 523

def group_granted_trust_level
  GroupUser.where(user_id: id).includes(:group).maximum("groups.grant_trust_level")
end

#grouped_unread_notificationsObject



649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
# File 'app/models/user.rb', line 649

def grouped_unread_notifications
  results = DB.query(<<~SQL, user_id: self.id, limit: MAX_UNREAD_BACKLOG)
    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

#guardianObject



1308
1309
1310
# File 'app/models/user.rb', line 1308

def guardian
  Guardian.new(self)
end

Returns:

  • (Boolean)


1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
# File 'app/models/user.rb', line 1687

def has_more_posts_than?(max_post_count)
  return true if user_stat && (user_stat.topic_count + user_stat.post_count) > max_post_count
  return true if max_post_count < 0

  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

#has_password?Boolean

Returns:

  • (Boolean)


906
907
908
# File 'app/models/user.rb', line 906

def has_password?
  password_hash.present?
end

#has_status?Boolean

Returns:

  • (Boolean)


1793
1794
1795
# File 'app/models/user.rb', line 1793

def has_status?
  user_status && !user_status.expired?
end

#has_trust_level?(level) ⇒ Boolean

Use this helper to determine if the user has a particular trust level. Takes into account admin, etc.

Returns:

  • (Boolean)

Raises:



1291
1292
1293
1294
1295
# File 'app/models/user.rb', line 1291

def has_trust_level?(level)
  raise InvalidTrustLevel.new("Invalid trust level #{level}") unless TrustLevel.valid?(level)

  admin? || moderator? || staged? || TrustLevel.compare(trust_level, level)
end

#has_trust_level_or_staff?(level) ⇒ Boolean

Returns:

  • (Boolean)


1297
1298
1299
1300
1301
# File 'app/models/user.rb', line 1297

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

#has_uploaded_avatarObject



1426
1427
1428
# File 'app/models/user.rb', line 1426

def has_uploaded_avatar
  uploaded_avatar.present?
end

#human?Boolean

Returns:

  • (Boolean)


445
446
447
# File 'app/models/user.rb', line 445

def human?
  User.human_user_id?(self.id)
end

#ignored_user_idsObject



607
608
609
# File 'app/models/user.rb', line 607

def ignored_user_ids
  @ignored_user_ids ||= ignored_users.pluck(:id)
end

#in_any_groups?(group_ids) ⇒ Boolean

Returns:

  • (Boolean)


515
516
517
# File 'app/models/user.rb', line 515

def in_any_groups?(group_ids)
  group_ids.include?(Group::AUTO_GROUPS[:everyone]) || (group_ids & belonging_to_group_ids).any?
end

#increment_post_edits_countObject



1182
1183
1184
# File 'app/models/user.rb', line 1182

def increment_post_edits_count
  stat.increment!(:post_edits_count)
end

#invited_byObject



576
577
578
579
580
# File 'app/models/user.rb', line 576

def invited_by
  used_invite =
    Invite.with_deleted.joins(:invited_users).where("invited_users.user_id = ?", self.id).first
  used_invite.try(:invited_by)
end

#is_singular_admin?Boolean

Returns:

  • (Boolean)


1565
1566
1567
# File 'app/models/user.rb', line 1565

def is_singular_admin?
  User.where(admin: true).where.not(id: id).human_users.blank?
end

#is_system_user?Boolean

Returns:

  • (Boolean)


1149
1150
1151
# File 'app/models/user.rb', line 1149

def is_system_user?
  id == Discourse::SYSTEM_USER_ID
end

#last_seen_redis_key(now) ⇒ Object



1039
1040
1041
# File 'app/models/user.rb', line 1039

def last_seen_redis_key(now)
  User.last_seen_redis_key(id, now)
end

#like_countObject

The following count methods are somewhat slow - definitely don’t use them in a loop. They might need to be denormalized



1166
1167
1168
# File 'app/models/user.rb', line 1166

def like_count
  UserAction.where(user_id: id, action_type: UserAction::WAS_LIKED).count
end

#like_given_countObject



1170
1171
1172
# File 'app/models/user.rb', line 1170

def like_given_count
  UserAction.where(user_id: id, action_type: UserAction::LIKE).count
end

#logged_inObject



1574
1575
1576
1577
1578
# File 'app/models/user.rb', line 1574

def logged_in
  DiscourseEvent.trigger(:user_logged_in, self)

  DiscourseEvent.trigger(:user_first_logged_in, self) if !self.seen_before?
end

#logged_outObject



1569
1570
1571
1572
# File 'app/models/user.rb', line 1569

def logged_out
  MessageBus.publish "/logout/#{self.id}", self.id, user_ids: [self.id]
  DiscourseEvent.trigger(:user_logged_out, self)
end

#mature_staged?Boolean

Returns:

  • (Boolean)


1662
1663
1664
# File 'app/models/user.rb', line 1662

def mature_staged?
  from_staged? && self.created_at && self.created_at < 1.day.ago
end

#muted_user_idsObject



611
612
613
# File 'app/models/user.rb', line 611

def muted_user_ids
  @muted_user_ids ||= muted_users.pluck(:id)
end

#new_new_view_enabled?Boolean

Returns:

  • (Boolean)


1797
1798
1799
# File 'app/models/user.rb', line 1797

def new_new_view_enabled?
  in_any_groups?(SiteSetting.experimental_new_new_view_groups_map)
end

#new_personal_messages_notifications_countObject



670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
# File 'app/models/user.rb', line 670

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

#new_user?Boolean

Returns:

  • (Boolean)


939
940
941
942
# File 'app/models/user.rb', line 939

def new_user?
  (created_at >= 24.hours.ago || trust_level == TrustLevel[0]) && trust_level < TrustLevel[2] &&
    !staff?
end

#new_user_posting_on_first_day?Boolean

Returns:

  • (Boolean)


931
932
933
934
935
936
937
# File 'app/models/user.rb', line 931

def new_user_posting_on_first_day?
  !staff? && trust_level < TrustLevel[2] &&
    (
      trust_level == TrustLevel[0] || self.first_post_created_at.nil? ||
        self.first_post_created_at >= 24.hours.ago
    )
end

#next_best_titleObject



1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
# File 'app/models/user.rb', line 1666

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)

  if next_best_group_title = group_titles_query.pick(:title)
    return next_best_group_title
  end

  next_best_badge_title = badges.where(allow_title: true).pick(:name)
  next_best_badge_title ? Badge.display_name(next_best_badge_title) : nil
end

#number_of_deleted_postsObject



1518
1519
1520
# File 'app/models/user.rb', line 1518

def number_of_deleted_posts
  Post.with_deleted.where(user_id: self.id).where.not(deleted_at: nil).count
end

#number_of_flagged_postsObject Also known as: flags_received_count



1522
1523
1524
1525
1526
1527
1528
1529
# File 'app/models/user.rb', line 1522

def number_of_flagged_posts
  posts
    .with_deleted
    .includes(:post_actions)
    .where("post_actions.post_action_type_id" => PostActionType.flag_types_without_custom.values)
    .where("post_actions.agreed_at IS NOT NULL")
    .count
end

#number_of_flags_givenObject



1536
1537
1538
1539
1540
1541
1542
# File 'app/models/user.rb', line 1536

def number_of_flags_given
  PostAction
    .where(user_id: self.id)
    .where(disagreed_at: nil)
    .where(post_action_type_id: PostActionType.notify_flag_type_ids)
    .count
end

#number_of_rejected_postsObject



1532
1533
1534
# File 'app/models/user.rb', line 1532

def number_of_rejected_posts
  ReviewableQueuedPost.rejected.where(target_created_by_id: self.id).count
end

#number_of_suspensionsObject



1544
1545
1546
# File 'app/models/user.rb', line 1544

def number_of_suspensions
  UserHistory.for(self, :suspend_user).count
end

#on_tl3_grace_period?Boolean

Returns:

  • (Boolean)


1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
# File 'app/models/user.rb', line 1443

def on_tl3_grace_period?
  return true if SiteSetting.tl3_promotion_min_duration.to_i.days.ago.year < 2013

  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

#passwordObject



889
890
891
# File 'app/models/user.rb', line 889

def password
  "" # so that validator doesn't complain that a password attribute doesn't exist
end

#password=(password) ⇒ Object



884
885
886
887
# File 'app/models/user.rb', line 884

def password=(password)
  # special case for passwordless accounts
  @raw_password = password unless password.blank?
end

#password_required!Object

Indicate that this is NOT a passwordless account for the purposes of validation



894
895
896
# File 'app/models/user.rb', line 894

def password_required!
  @password_required = true
end

#password_required?Boolean

Returns:

  • (Boolean)


898
899
900
# File 'app/models/user.rb', line 898

def password_required?
  !!@password_required
end

#password_validation_required?Boolean

Returns:

  • (Boolean)


902
903
904
# File 'app/models/user.rb', line 902

def password_validation_required?
  password_required? || @raw_password.present?
end

#password_validatorObject



910
911
912
# File 'app/models/user.rb', line 910

def password_validator
  PasswordValidator.new(attributes: :password).validate_each(self, :password, @raw_password)
end

#post_countObject



1174
1175
1176
# File 'app/models/user.rb', line 1174

def post_count
  stat.post_count
end

#post_edits_countObject



1178
1179
1180
# File 'app/models/user.rb', line 1178

def post_edits_count
  stat.post_edits_count
end

#posted_too_much_in_topic?(topic_id) ⇒ Boolean

Returns:

  • (Boolean)


1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
# File 'app/models/user.rb', line 1201

def posted_too_much_in_topic?(topic_id)
  # 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)

  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)
end

#preload_recent_time_read(time) ⇒ Object



1649
1650
1651
# File 'app/models/user.rb', line 1649

def preload_recent_time_read(time)
  @recent_time_read = time
end

#private_topics_countObject



1197
1198
1199
# File 'app/models/user.rb', line 1197

def private_topics_count
  topics_allowed.where(archetype: Archetype.private_message).count
end

#publish_do_not_disturb(ends_at: nil) ⇒ Object



862
863
864
# File 'app/models/user.rb', line 862

def publish_do_not_disturb(ends_at: nil)
  MessageBus.publish("/do-not-disturb/#{id}", { ends_at: ends_at&.httpdate }, user_ids: [id])
end

#publish_notifications_stateObject



812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
# File 'app/models/user.rb', line 812

def publish_notifications_state
  return if !self.allow_live_notifications?

  # publish last notification json with the message so we can apply an update
  notification = notifications.visible.order("notifications.created_at desc").first
  json = NotificationSerializer.new(notification).as_json if notification

  sql = (<<~SQL)
     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
        n.high_priority AND
        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
      (n.high_priority = FALSE OR read) AND
      n.user_id = :user_id
     ORDER BY n.id DESC
     LIMIT 20
    ) AS y
  SQL

  recent = DB.query(sql, user_id: id).map! { |r| [r.id, r.read] }

  payload = {
    unread_notifications: unread_notifications,
    unread_high_priority_notifications: unread_high_priority_notifications,
    read_first_notification: read_first_notification?,
    last_notification: json,
    recent: recent,
    seen_notification_id: seen_notification_id,
  }

  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

  MessageBus.publish("/notification/#{id}", payload, user_ids: [id])
end

#publish_reviewable_counts(extra_data = nil) ⇒ Object



799
800
801
802
803
804
805
806
# File 'app/models/user.rb', line 799

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?
  MessageBus.publish("/reviewable_counts/#{self.id}", data, user_ids: [self.id])
end

#publish_user_status(status) ⇒ Object



866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
# File 'app/models/user.rb', line 866

def publish_user_status(status)
  if status
    payload = {
      description: status.description,
      emoji: status.emoji,
      ends_at: status.ends_at&.iso8601,
    }
  else
    payload = nil
  end

  MessageBus.publish(
    "/user-status",
    { id => payload },
    group_ids: [Group::AUTO_GROUPS[:trust_level_0]],
  )
end

#read_first_notification?Boolean

Returns:

  • (Boolean)


808
809
810
# File 'app/models/user.rb', line 808

def read_first_notification?
  self.seen_notification_id != 0 || user_option.skip_new_user_tips
end

#readable_nameObject



1339
1340
1341
# File 'app/models/user.rb', line 1339

def readable_name
  name.present? && name != username ? "#{name} (#{username})" : username
end

#recent_time_readObject



1653
1654
1655
1656
# File 'app/models/user.rb', line 1653

def recent_time_read
  @recent_time_read ||=
    self.user_visits.where("visited_at >= ?", RECENT_TIME_READ_THRESHOLD.ago).sum(:time_read)
end

#refresh_avatarObject



1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
# File 'app/models/user.rb', line 1454

def refresh_avatar
  return if @import_mode

  avatar = user_avatar || create_user_avatar

  if self.primary_email.present? && SiteSetting.automatically_download_gravatars? &&
       !avatar.last_gravatar_download_attempt
    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)
  end

  # mark all the user's quoted posts as "needing a rebake"
  Post.rebake_all_quoted_posts(self.id) if self.will_save_change_to_uploaded_avatar_id?
end

#reloadObject



594
595
596
597
598
599
600
601
602
603
604
605
# File 'app/models/user.rb', line 594

def reload
  @unread_notifications = nil
  @all_unread_notifications_count = nil
  @unread_total_notifications = nil
  @unread_pms = nil
  @unread_bookmarks = nil
  @unread_high_prios = nil
  @ignored_user_ids = nil
  @muted_user_ids = nil
  @belonging_to_group_ids = nil
  super
end

#reviewable_countObject



760
761
762
# File 'app/models/user.rb', line 760

def reviewable_count
  Reviewable.list_for(self, include_claimed_by_others: false).count
end

#saw_notification_id(notification_id) ⇒ Object



764
765
766
767
768
769
770
771
772
773
774
# File 'app/models/user.rb', line 764

def saw_notification_id(notification_id)
  Discourse.deprecate(<<~TEXT, since: "2.9", drop_from: "3.0")
    User#saw_notification_id is deprecated. Please use User#bump_last_seen_notification! instead.
  TEXT
  if seen_notification_id.to_i < notification_id.to_i
    update_columns(seen_notification_id: notification_id.to_i)
    true
  else
    false
  end
end

#second_factor_security_key_credential_idsObject



1725
1726
1727
1728
1729
1730
# File 'app/models/user.rb', line 1725

def second_factor_security_key_credential_ids
  security_keys
    .select(:credential_id)
    .where(factor_type: UserSecurityKey.factor_types[:second_factor])
    .pluck(:credential_id)
end

#secondary_emailsObject



1626
1627
1628
# File 'app/models/user.rb', line 1626

def secondary_emails
  self.user_emails.secondary.pluck(:email)
end

#secure_category_idsObject



1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
# File 'app/models/user.rb', line 1389

def secure_category_ids
  cats =
    if self.admin? && !SiteSetting.suppress_secured_categories_from_admin
      Category.unscoped.where(read_restricted: true)
    else
      secure_categories.references(:categories)
    end

  cats.pluck("categories.id").sort
end

#secured_sidebar_category_ids(user_guardian = nil) ⇒ Object



354
355
356
357
358
359
# File 'app/models/user.rb', line 354

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

#seen_before?Boolean

Returns:

  • (Boolean)


944
945
946
# File 'app/models/user.rb', line 944

def seen_before?
  last_seen_at.present?
end

#seen_since?(datetime) ⇒ Boolean

Returns:

  • (Boolean)


948
949
950
# File 'app/models/user.rb', line 948

def seen_since?(datetime)
  seen_before? && last_seen_at >= datetime
end

#set_automatic_groupsObject



1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
# File 'app/models/user.rb', line 1580

def set_automatic_groups
  return if !active || staged || !email_confirmed?

  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)
      end
    end

  @belonging_to_group_ids = nil
end

#set_random_avatarObject



1552
1553
1554
1555
1556
1557
1558
1559
# File 'app/models/user.rb', line 1552

def set_random_avatar
  if SiteSetting.selectable_avatars_mode != "disabled"
    if upload = SiteSetting.selectable_avatars.sample
      update_column(:uploaded_avatar_id, upload.id)
      UserAvatar.create!(user_id: id, custom_upload_id: upload.id)
    end
  end
end

#set_status!(description, emoji, ends_at = nil) ⇒ Object



1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
# File 'app/models/user.rb', line 1778

def set_status!(description, emoji, ends_at = nil)
  status = {
    description: description,
    emoji: emoji,
    set_at: Time.zone.now,
    ends_at: ends_at,
    user_id: id,
  }
  validate_status!(status)
  UserStatus.upsert(status)

  reload_user_status
  publish_user_status(user_status)
end

#set_user_field(field_id, value) ⇒ Object



1498
1499
1500
# File 'app/models/user.rb', line 1498

def set_user_field(field_id, value)
  custom_fields["#{USER_FIELD_PREFIX}#{field_id}"] = value
end

#shelved_notificationsObject



1749
1750
1751
# File 'app/models/user.rb', line 1749

def shelved_notifications
  ShelvedNotification.joins(:notification).where("notifications.user_id = ?", self.id)
end

#should_validate_email_address?Boolean

Returns:

  • (Boolean)


582
583
584
# File 'app/models/user.rb', line 582

def should_validate_email_address?
  !skip_email_validation && !staged?
end

#silence_reasonObject



1238
1239
1240
# File 'app/models/user.rb', line 1238

def silence_reason
  silenced_record.try(:details) if silenced?
end

#silenced?Boolean

Returns:

  • (Boolean)


1230
1231
1232
# File 'app/models/user.rb', line 1230

def silenced?
  !!(silenced_till && silenced_till > Time.zone.now)
end

#silenced_atObject



1242
1243
1244
# File 'app/models/user.rb', line 1242

def silenced_at
  silenced_record.try(:created_at) if silenced?
end

#silenced_forever?Boolean

Returns:

  • (Boolean)


1246
1247
1248
# File 'app/models/user.rb', line 1246

def silenced_forever?
  silenced_till > 100.years.from_now
end

#silenced_recordObject



1234
1235
1236
# File 'app/models/user.rb', line 1234

def silenced_record
  UserHistory.for(self, :silence_user).order("id DESC").first
end

#small_avatar_urlObject

Don’t pass this up to the client - it’s meant for server side use This is used in

- self oneboxes in open graph data
- emails


1079
1080
1081
# File 'app/models/user.rb', line 1079

def small_avatar_url
  avatar_template_url.gsub("{size}", "45")
end

#suspend_reasonObject



1258
1259
1260
1261
1262
1263
1264
# File 'app/models/user.rb', line 1258

def suspend_reason
  if details = full_suspend_reason
    return details.split("\n")[0]
  end

  nil
end

#suspend_recordObject



1250
1251
1252
# File 'app/models/user.rb', line 1250

def suspend_record
  UserHistory.for(self, :suspend_user).order("id DESC").first
end

#suspended?Boolean

Returns:

  • (Boolean)


1226
1227
1228
# File 'app/models/user.rb', line 1226

def suspended?
  !!(suspended_till && suspended_till > Time.zone.now)
end

#suspended_forever?Boolean

Returns:

  • (Boolean)


1285
1286
1287
# File 'app/models/user.rb', line 1285

def suspended_forever?
  suspended_till > 100.years.from_now
end

#suspended_messageObject



1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
# File 'app/models/user.rb', line 1266

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

#sync_notification_channel_positionObject

tricky, we need our bus to be subscribed from the right spot



571
572
573
574
# File 'app/models/user.rb', line 571

def sync_notification_channel_position
  @unread_notifications_by_type = nil
  self.notification_channel_position = MessageBus.last_id("/notification/#{id}")
end

#tl3_requirementsObject



1439
1440
1441
# File 'app/models/user.rb', line 1439

def tl3_requirements
  @lq ||= TrustLevel3Requirements.new(self)
end

#total_unread_notificationsObject



756
757
758
# File 'app/models/user.rb', line 756

def total_unread_notifications
  @unread_total_notifications ||= notifications.where("read = false").count
end

#unconfirmed_emailsObject



1630
1631
1632
1633
1634
1635
# File 'app/models/user.rb', line 1630

def unconfirmed_emails
  self
    .email_change_requests
    .where.not(change_state: EmailChangeRequest.states[:complete])
    .pluck(:new_email)
end

#unread_high_priority_notificationsObject



666
667
668
# File 'app/models/user.rb', line 666

def unread_high_priority_notifications
  @unread_high_prios ||= unread_notifications_of_priority(high_priority: true)
end

#unread_notificationsObject



700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
# File 'app/models/user.rb', line 700

def unread_notifications
  @unread_notifications ||=
    begin
      # perf critical, much more efficient than AR
      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.high_priority = FALSE 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,
      )[
        0
      ].to_i
    end
end

#unread_notifications_of_priority(high_priority:) ⇒ Object



632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
# File 'app/models/user.rb', line 632

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

#unread_notifications_of_type(notification_type, since: nil) ⇒ Object



615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
# File 'app/models/user.rb', line 615

def unread_notifications_of_type(notification_type, since: nil)
  # 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.notification_type = :notification_type
         AND n.user_id = :user_id
         AND NOT read
         #{since ? "AND n.created_at > :since" : ""}
  SQL

  # to avoid coalesce we do to_i
  DB.query_single(sql, user_id: id, notification_type: notification_type, since: since)[0].to_i
end

#unstage!Object



477
478
479
480
481
482
483
484
485
486
487
488
# File 'app/models/user.rb', line 477

def unstage!
  if self.staged
    ActiveRecord::Base.transaction do
      self.staged = false
      self.custom_fields[FROM_STAGED] = true
      self.notifications.destroy_all
      save!
    end

    DiscourseEvent.trigger(:user_unstaged, self)
  end
end

#update_ip_address!(new_ip_address) ⇒ Object



1030
1031
1032
# File 'app/models/user.rb', line 1030

def update_ip_address!(new_ip_address)
  User.update_ip_address!(id, new_ip: new_ip_address, old_ip: ip_address)
end

#update_last_seen!(now = Time.zone.now, force: false) ⇒ Object



1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
# File 'app/models/user.rb', line 1058

def update_last_seen!(now = Time.zone.now, force: false)
  if !force
    return if !User.should_update_last_seen?(self.id, now)
  end

  update_previous_visit(now)
  # using update_column to avoid the AR transaction
  update_column(:last_seen_at, now)
  update_column(:first_seen_at, now) unless self.first_seen_at

  DiscourseEvent.trigger(:user_seen, self)
end

#update_posts_read!(num_posts, opts = {}) ⇒ Object



976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
# File 'app/models/user.rb', line 976

def update_posts_read!(num_posts, opts = {})
  now = opts[:at] || Time.zone.now
  _retry = opts[:retry] || false

  if user_visit = visit_record_for(now.to_date)
    user_visit.posts_read += num_posts
    user_visit.mobile = true if opts[:mobile]
    user_visit.save
    user_visit
  else
    begin
      create_visit_record!(now.to_date, posts_read: num_posts, mobile: opts.fetch(:mobile, false))
    rescue ActiveRecord::RecordNotUnique
      if !_retry
        update_posts_read!(num_posts, opts.merge(retry: true))
      else
        raise
      end
    end
  end
end

#update_timezone_if_missing(timezone) ⇒ Object



969
970
971
972
973
974
# File 'app/models/user.rb', line 969

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

#update_visit_record!(date) ⇒ Object



965
966
967
# File 'app/models/user.rb', line 965

def update_visit_record!(date)
  create_visit_record!(date) unless visit_record_for(date)
end

#user_fields(field_ids = nil) ⇒ Object



1488
1489
1490
1491
1492
# File 'app/models/user.rb', line 1488

def user_fields(field_ids = nil)
  field_ids = (@all_user_field_ids ||= UserField.pluck(:id)) if field_ids.nil?

  field_ids.map { |fid| [fid.to_s, custom_fields["#{USER_FIELD_PREFIX}#{fid}"]] }.to_h
end

#username_equals_to?(another_username) ⇒ Boolean

Returns:

  • (Boolean)


1757
1758
1759
# File 'app/models/user.rb', line 1757

def username_equals_to?(another_username)
  username_lower == User.normalize_username(another_username)
end

#username_format_validatorObject



1312
1313
1314
# File 'app/models/user.rb', line 1312

def username_format_validator
  UsernameValidator.perform_validation(self, "username")
end

#validatable_user_fieldsObject



1510
1511
1512
1513
1514
1515
1516
# File 'app/models/user.rb', line 1510

def validatable_user_fields
  # 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)

  user_fields(@public_user_field_ids)
end

#validatable_user_fields_valuesObject



1494
1495
1496
# File 'app/models/user.rb', line 1494

def validatable_user_fields_values
  validatable_user_fields.values.join(" ")
end

#visible_groupsObject



527
528
529
# File 'app/models/user.rb', line 527

def visible_groups
  groups.visible_groups(self)
end

#visible_sidebar_tags(user_guardian = nil) ⇒ Object



361
362
363
364
365
366
367
368
369
370
# File 'app/models/user.rb', line 361

def visible_sidebar_tags(user_guardian = nil)
  user_guardian ||= guardian

  DiscourseTagging.filter_visible(
    Tag.where(
      id: SidebarSectionLink.where(user_id: self.id, linkable_type: "Tag").select(:linkable_id),
    ),
    user_guardian,
  )
end

#visit_record_for(date) ⇒ Object



961
962
963
# File 'app/models/user.rb', line 961

def visit_record_for(date)
  user_visits.find_by(visited_at: date)
end

#warnings_received_countObject



1193
1194
1195
# File 'app/models/user.rb', line 1193

def warnings_received_count
  user_warnings.count
end

#watched_precedence_over_mutedObject



1805
1806
1807
1808
1809
1810
1811
# File 'app/models/user.rb', line 1805

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