280 lines
7.8 KiB
Ruby
280 lines
7.8 KiB
Ruby
# 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
|