class User < ApplicationRecord include Avatar, Bannable, Bot, Mentionable, Role, Transferable has_many :memberships, dependent: :delete_all has_many :rooms, through: :memberships has_many :reachable_messages, through: :rooms, source: :messages has_many :messages, dependent: :destroy, foreign_key: :creator_id has_many :push_subscriptions, class_name: "Push::Subscription", dependent: :delete_all has_many :boosts, dependent: :destroy, foreign_key: :booster_id has_many :searches, dependent: :delete_all has_many :sessions, dependent: :destroy has_many :bans, dependent: :destroy enum :status, %i[ active deactivated banned ], default: :active has_secure_password validations: false after_create_commit :grant_membership_to_open_rooms after_update :sign_out_all_sessions, if: :saved_change_to_password_digest? after_update_commit :disconnect_signed_out_sessions, if: :saved_change_to_password_digest? scope :ordered, -> { order("LOWER(name)") } scope :filtered_by, ->(query) { where("name like ?", "%#{query}%") } def initials name.scan(/\b\w/).join end def title [ name, bio ].compact_blank.join(" – ") end def deactivate transaction do close_remote_connections memberships.without_direct_rooms.delete_all push_subscriptions.delete_all searches.delete_all sessions.delete_all update! status: :deactivated, email_address: deactived_email_address end end def reset_remote_connections close_remote_connections reconnect: true end private # A new password is how someone shuts out whoever else got into their account, so it ends every session opened # with the old one, copies of the changing browser's cookie included. The profile page signs that browser back in. def sign_out_all_sessions sessions.delete_all end # After commit, so a connection that drops can't come back on a session that's still there. The password is # saved by then, so a realtime service that can't be reached mustn't fail the change. def disconnect_signed_out_sessions reset_remote_connections rescue => error Rails.logger.warn "Could not disconnect remote connections after a password change: #{error.class}" end # One statement, which SQLite runs under the write lock, so no room is closed between reading the open ones and # inserting. A room that Room#destroy_later closes is either closed before, and skipped, or after, and takes this # membership away with the others. A room that granted itself to every user when it was created is skipped too, # as insert_all did. def grant_membership_to_open_rooms now = Time.current Membership.connection.execute Membership.sanitize_sql([ <<~SQL, id, now, now ]) insert into memberships (room_id, user_id, created_at, updated_at) select id, ?, ?, ? from rooms where type = 'Rooms::Open' on conflict do nothing SQL end def deactived_email_address email_address&.gsub(/@/, "-deactivated-#{SecureRandom.uuid}@") end def close_remote_connections(reconnect: false) ActionCable.server.remote_connections.where(current_user: self).disconnect reconnect: reconnect end end