# frozen_string_literal: true class UserUpdater CATEGORY_IDS = { watched_first_post_category_ids: :watching_first_post, watched_category_ids: :watching, tracked_category_ids: :tracking, regular_category_ids: :regular, muted_category_ids: :muted, } TAG_NAMES = { watching_first_post_tags: :watching_first_post, watched_tags: :watching, tracked_tags: :tracking, muted_tags: :muted, } OPTION_ATTR = %i[ mailing_list_mode mailing_list_mode_frequency email_digests email_level email_messages_level external_links_in_new_tab enable_quoting enable_defer color_scheme_id dark_scheme_id dynamic_favicon automatically_unpin_topics digest_after_minutes new_topic_duration_minutes auto_track_topics_after_msecs notification_level_when_replying email_previous_replies email_in_reply_to like_notification_frequency include_tl0_in_digests theme_ids allow_private_messages enable_allowed_pm_users homepage_id hide_profile_and_presence text_size title_count_mode timezone skip_new_user_tips seen_popups default_calendar sidebar_list_destination bookmark_auto_delete_preference ] NOTIFICATION_SCHEDULE_ATTRS = -> do attrs = [:enabled] 7.times do |n| attrs.push("day_#{n}_start_time".to_sym) attrs.push("day_#{n}_end_time".to_sym) end { user_notification_schedule: attrs } end.call def initialize(actor, user) @user = user @guardian = Guardian.new(actor) @actor = actor end def update(attributes = {}) user_profile = user.user_profile user_profile.dismissed_banner_key = attributes[:dismissed_banner_key] if attributes[ :dismissed_banner_key ].present? unless SiteSetting.enable_discourse_connect && SiteSetting.discourse_connect_overrides_bio user_profile.bio_raw = attributes.fetch(:bio_raw) { user_profile.bio_raw } end unless SiteSetting.enable_discourse_connect && SiteSetting.discourse_connect_overrides_location user_profile.location = attributes.fetch(:location) { user_profile.location } end unless SiteSetting.enable_discourse_connect && SiteSetting.discourse_connect_overrides_website user_profile.website = format_url(attributes.fetch(:website) { user_profile.website }) end if attributes[:profile_background_upload_url] == "" || !guardian.can_upload_profile_header?(user) user_profile.profile_background_upload_id = nil elsif upload = Upload.get_from_url(attributes[:profile_background_upload_url]) user_profile.profile_background_upload_id = upload.id end if attributes[:card_background_upload_url] == "" || !guardian.can_upload_user_card_background?(user) user_profile.card_background_upload_id = nil elsif upload = Upload.get_from_url(attributes[:card_background_upload_url]) user_profile.card_background_upload_id = upload.id end if attributes[:user_notification_schedule] user_notification_schedule = user.user_notification_schedule || UserNotificationSchedule.new(user: user) user_notification_schedule.assign_attributes(attributes[:user_notification_schedule]) end old_user_name = user.name.present? ? user.name : "" user.name = attributes.fetch(:name) { user.name } if guardian.can_edit_name?(user) user.locale = attributes.fetch(:locale) { user.locale } user.date_of_birth = attributes.fetch(:date_of_birth) { user.date_of_birth } if attributes[:title] && attributes[:title] != user.title && guardian.can_grant_title?(user, attributes[:title]) user.title = attributes[:title] end if SiteSetting.user_selected_primary_groups && attributes[:primary_group_id] && attributes[:primary_group_id] != user.primary_group_id && guardian.can_use_primary_group?(user, attributes[:primary_group_id]) user.primary_group_id = attributes[:primary_group_id] elsif SiteSetting.user_selected_primary_groups && attributes[:primary_group_id] && attributes[:primary_group_id].blank? user.primary_group_id = nil end if attributes[:flair_group_id] && attributes[:flair_group_id] != user.flair_group_id && ( attributes[:flair_group_id].blank? || guardian.can_use_flair_group?(user, attributes[:flair_group_id]) ) user.flair_group_id = attributes[:flair_group_id] end if @guardian.can_change_tracking_preferences?(user) CATEGORY_IDS.each do |attribute, level| if ids = attributes[attribute] CategoryUser.batch_set(user, level, ids) end end TAG_NAMES.each do |attribute, level| if attributes.has_key?(attribute) TagUser.batch_set(user, level, attributes[attribute]&.split(",") || []) end end end save_options = false # special handling for theme_id cause we need to bump a sequence number if attributes.key?(:theme_ids) user_guardian = Guardian.new(user) attributes[:theme_ids].reject!(&:blank?) attributes[:theme_ids].map!(&:to_i) if user_guardian.allow_themes?(attributes[:theme_ids]) user.user_option.theme_key_seq += 1 if user.user_option.theme_ids != attributes[:theme_ids] else attributes.delete(:theme_ids) end end if attributes.key?(:text_size) user.user_option.text_size_seq += 1 if user.user_option.text_size.to_s != attributes[:text_size] end OPTION_ATTR.each do |attribute| if attributes.key?(attribute) save_options = true if [true, false].include?(user.user_option.public_send(attribute)) val = attributes[attribute].to_s == "true" user.user_option.public_send("#{attribute}=", val) else user.user_option.public_send("#{attribute}=", attributes[attribute]) end end end if attributes.key?(:skip_new_user_tips) && user.user_option.skip_new_user_tips user.user_option.seen_popups = [-1] end # automatically disable digests when mailing_list_mode is enabled user.user_option.email_digests = false if user.user_option.mailing_list_mode fields = attributes[:custom_fields] user.custom_fields = user.custom_fields.merge(fields) if fields.present? saved = nil User.transaction do update_muted_users(attributes[:muted_usernames]) if attributes.key?(:muted_usernames) if attributes.key?(:allowed_pm_usernames) update_allowed_pm_users(attributes[:allowed_pm_usernames]) end if attributes.key?(:discourse_connect) update_discourse_connect(attributes[:discourse_connect]) end if attributes.key?(:user_associated_accounts) updated_associated_accounts(attributes[:user_associated_accounts]) end if attributes.key?(:sidebar_category_ids) SidebarSectionLinksUpdater.update_category_section_links( user, category_ids: attributes[:sidebar_category_ids], ) end if attributes.key?(:sidebar_tag_names) && SiteSetting.tagging_enabled SidebarSectionLinksUpdater.update_tag_section_links( user, tag_names: attributes[:sidebar_tag_names], ) end if SiteSetting.enable_user_status? update_user_status(attributes[:status]) if attributes.has_key?(:status) end name_changed = user.name_changed? saved = (!save_options || user.user_option.save) && (user_notification_schedule.nil? || user_notification_schedule.save) && user_profile.save && user.save if saved && (name_changed && old_user_name.casecmp(attributes.fetch(:name)) != 0) StaffActionLogger.new(@actor).log_name_change( user.id, old_user_name, attributes.fetch(:name) { "" }, ) end DiscourseEvent.trigger(:within_user_updater_transaction, user, attributes) rescue Addressable::URI::InvalidURIError => e # Prevent 500 for crazy url input return saved end if saved if user_notification_schedule if user_notification_schedule.enabled user_notification_schedule.create_do_not_disturb_timings(delete_existing: true) else user_notification_schedule.destroy_scheduled_timings end end if attributes.key?(:seen_popups) || attributes.key?(:skip_new_user_tips) MessageBus.publish( "/user-tips/#{user.id}", user.user_option.seen_popups, user_ids: [user.id], ) end DiscourseEvent.trigger(:user_updated, user) end saved end def update_muted_users(usernames) usernames ||= "" desired_usernames = usernames.split(",").reject { |username| user.username == username } desired_ids = User.where(username: desired_usernames).pluck(:id) if desired_ids.empty? MutedUser.where(user_id: user.id).destroy_all else MutedUser.where("user_id = ? AND muted_user_id not in (?)", user.id, desired_ids).destroy_all # SQL is easier here than figuring out how to do the same in AR DB.exec(<<~SQL, now: Time.now, user_id: user.id, desired_ids: desired_ids) INSERT into muted_users(user_id, muted_user_id, created_at, updated_at) SELECT :user_id, id, :now, :now FROM users WHERE id in (:desired_ids) ON CONFLICT DO NOTHING SQL end end def update_allowed_pm_users(usernames) usernames ||= "" desired_usernames = usernames.split(",").reject { |username| user.username == username } desired_ids = User.where(username: desired_usernames).pluck(:id) if desired_ids.empty? AllowedPmUser.where(user_id: user.id).destroy_all else AllowedPmUser.where( "user_id = ? AND allowed_pm_user_id not in (?)", user.id, desired_ids, ).destroy_all # SQL is easier here than figuring out how to do the same in AR DB.exec(<<~SQL, now: Time.zone.now, user_id: user.id, desired_ids: desired_ids) INSERT into allowed_pm_users(user_id, allowed_pm_user_id, created_at, updated_at) SELECT :user_id, id, :now, :now FROM users WHERE id in (:desired_ids) ON CONFLICT DO NOTHING SQL end end def updated_associated_accounts(associations) associations.each do |association| user_associated_account = UserAssociatedAccount.find_or_initialize_by( user_id: user.id, provider_name: association[:provider_name], ) if association[:provider_uid].present? user_associated_account.update!(provider_uid: association[:provider_uid]) else user_associated_account.destroy! end end end private def update_user_status(status) if status.blank? @user.clear_status! else @user.set_status!(status[:description], status[:emoji], status[:ends_at]) end end def update_discourse_connect(discourse_connect) external_id = discourse_connect[:external_id] sso = SingleSignOnRecord.find_or_initialize_by(user_id: user.id) if external_id.present? sso.update!( external_id: discourse_connect[:external_id], last_payload: "external_id=#{discourse_connect[:external_id]}", ) else sso.destroy! end end attr_reader :user, :guardian def format_url(website) return nil if website.blank? website =~ /\Ahttp/ ? website : "http://#{website}" end end