# typed: strict class Domain::User < ReduxApplicationRecord extend T::Helpers include HasAuxTable include AttrJsonRecordAliases include HasCompositeToParam include HasViewPrefix include HasDescriptionHtmlForView include HasTimestampsWithDueAt include HasDomainType self.table_name = "domain_users" abstract! class_attribute :class_has_created_posts, :class_has_faved_posts, :class_has_followed_users, :class_has_followed_by_users, :due_at_timestamp_fields sig(:final) { returns(T::Boolean) } def has_created_posts? class_has_created_posts.present? end sig(:final) { returns(T::Boolean) } def has_faved_posts? class_has_faved_posts.present? end sig(:final) { returns(T::Boolean) } def has_followed_users? class_has_followed_users.present? end sig(:final) { returns(T::Boolean) } def has_followed_by_users? class_has_followed_by_users.present? end sig { overridable.returns(T.class_of(Domain::UserPostFav)) } def self.fav_model_type if self.is_a?(Domain::User::FaUser) Domain::UserPostFav::FaUserPostFav else Domain::UserPostFav end end sig { returns(String) } def type super end sig { params(value: String).returns(String) } def type=(value) super end after_save :set_names_for_search sig { void } def set_names_for_search values = names_for_search_values models = user_search_names.pluck(:name) missing_values = values - models extra_values = models - values if missing_values.any? ::Domain::UserSearchName.upsert_all( missing_values.map { |name| { user_id: id, name: name } }, unique_by: %i[user_id name], ) end if extra_values.any? ::Domain::UserSearchName.where(user_id: id, name: extra_values).delete_all end end sig(:final) { returns(T::Array[String]) } def names_for_search_values names_for_search .map { |name| name.downcase.strip } .reject(&:blank?) .uniq .sort end attr_json :migrated_user_favs_at, ActiveModelUtcTimeValue.new attr_json_due_timestamp :scanned_favs_at, 3.months has_many :user_search_names, class_name: "::Domain::UserSearchName", inverse_of: :user, dependent: :destroy has_many :user_post_creations, -> { extending CounterCacheWithFallback[:user_post_creations] }, class_name: "::Domain::UserPostCreation", inverse_of: :user, dependent: :destroy has_many :user_post_favs, -> { extending CounterCacheWithFallback[:user_post_favs] }, class_name: "::Domain::UserPostFav", inverse_of: :user, dependent: :destroy has_many :user_user_follows_from, -> { extending CounterCacheWithFallback[:user_user_follows_from] }, class_name: "::Domain::UserUserFollow", foreign_key: :from_id, inverse_of: :from, dependent: :destroy has_many :user_user_follows_to, -> { extending CounterCacheWithFallback[:user_user_follows_to] }, class_name: "::Domain::UserUserFollow", foreign_key: :to_id, inverse_of: :to, dependent: :destroy has_many :posts, through: :user_post_creations, source: :post has_many :faved_posts, through: :user_post_favs, source: :post has_many :followed_users, through: :user_user_follows_from, source: :to has_many :followed_by_users, through: :user_user_follows_to, source: :from has_many :follows_scans, -> { where(kind: "follows").order(created_at: :asc) }, inverse_of: :user, class_name: "Domain::UserJobEvent::FollowScan" has_many :followed_by_scans, -> { where(kind: "followed_by").order(created_at: :asc) }, inverse_of: :user, class_name: "Domain::UserJobEvent::FollowScan" sig { params(klass: T.class_of(Domain::Post)).void } def self.has_created_posts!(klass) self.class_has_created_posts = klass has_many :posts, -> { order(klass.param_order_attribute => :desc) }, through: :user_post_creations, source: :post, class_name: klass.name end sig do params( klass: T.class_of(Domain::Post), fav_model_type: T.class_of(Domain::UserPostFav), fav_model_order: T.untyped, ).void end def self.has_faved_posts!( klass, fav_model_type = Domain::UserPostFav, fav_model_order: nil ) self.class_has_faved_posts = klass has_many :user_post_favs, -> do rel = extending(CounterCacheWithFallback[:user_post_favs]) rel = rel.order(fav_model_order) if fav_model_order rel end, class_name: fav_model_type.name, inverse_of: :user, dependent: :destroy has_many :faved_posts, -> { order(klass.param_order_attribute => :desc) }, through: :user_post_favs, source: :post, class_name: klass.name end has_many :add_tracked_objects, -> { order(created_at: :desc) }, inverse_of: :user, class_name: Domain::UserJobEvent::AddTrackedObject.name has_many :favs_scans, -> { where(kind: "favs").order(requested_at: :asc) }, inverse_of: :user, class_name: Domain::UserJobEvent::AddTrackedObject.name sig { params(post_ids: T::Array[Integer], log_entry: HttpLogEntry).void } def upsert_new_favs(post_ids, log_entry:) self.class.transaction do if post_ids.any? post_ids.each_slice(1000) do |slice| self.user_post_favs.upsert_all( slice.map { |post_id| { post_id: } }, unique_by: %i[user_id post_id], ) end end current_time = Time.now user_post_favs_count = self.user_post_favs.count self[:user_post_favs_count] = user_post_favs_count self.favs_scans.create!( num_added: post_ids.count, num_total: user_post_favs_count, duration_since_last_scan: duration_since_last_scan, log_entry:, ) self.scanned_favs_at = current_time logger.info(format_tags(make_tag("upsert new favs", post_ids.size))) end end sig { returns(T.nilable(ActiveSupport::Duration)) } def duration_since_last_scan if (sfa = self.scanned_favs_at) ActiveSupport::Duration.build(Time.now - sfa.utc) else nil end end sig { params(klass: T.class_of(Domain::User)).void } def self.has_followed_users!(klass) self.class_has_followed_users = klass has_many :followed_users, through: :user_user_follows_from, source: :to, class_name: klass.name end sig { params(klass: T.class_of(Domain::User)).void } def self.has_followed_by_users!(klass) self.class_has_followed_by_users = klass has_many :followed_by_users, through: :user_user_follows_to, source: :from, class_name: klass.name end has_one :avatar, -> { order(created_at: :desc) }, class_name: "::Domain::UserAvatar", inverse_of: :user has_many :avatars, -> { order(created_at: :desc) }, class_name: "::Domain::UserAvatar", inverse_of: :user, dependent: :destroy sig { abstract.returns(String) } def account_status_for_view end sig { abstract.returns(T.nilable(String)) } def external_url_for_view end sig { abstract.returns(T.nilable(String)) } def name_for_view end sig { overridable.returns(T::Array[String]) } def names_for_search [] end sig { abstract.returns(String) } def site_name_for_view end end # eager load all subclasses Dir[Rails.root.join("app/models/domain/user/**/*.rb")].each do |file| require_dependency file end