mirror of
https://github.com/manyfold3d/manyfold.git
synced 2026-10-22 03:32:18 -05:00
322 lines
9.1 KiB
Ruby
322 lines
9.1 KiB
Ruby
require "uri"
|
|
|
|
class User < ApplicationRecord
|
|
extend Memoist
|
|
|
|
include Lister
|
|
include Follower
|
|
include CaberSubject
|
|
include PublicIDable
|
|
|
|
DEFAULT_TOUR_STATE = {"completed" => []}
|
|
|
|
# Creator ownership relation used for auto-creation
|
|
has_many :creators, -> { where("caber_relations.permission": "own") }, through: :caber_relations, source_type: "Creator", source: :object
|
|
accepts_nested_attributes_for :creators
|
|
|
|
has_many :lists, -> { where("caber_relations.permission": "own") }, through: :caber_relations, source_type: "List", source: :object
|
|
|
|
has_many :notifications, as: :recipient, class_name: "Noticed::Notification", dependent: :destroy
|
|
|
|
before_validation :set_json_field_defaults
|
|
before_save :set_quota
|
|
|
|
acts_as_federails_actor(
|
|
username_field: :username,
|
|
name_field: :username,
|
|
user_count_method: :user_count
|
|
)
|
|
|
|
rolify
|
|
devise :database_authenticatable,
|
|
:registerable, :zxcvbnable,
|
|
:rememberable, :recoverable,
|
|
:lockable, :timeoutable,
|
|
:invitable
|
|
|
|
devise :omniauthable, omniauth_providers: %i[openid_connect] if SiteSettings.oidc_enabled?
|
|
|
|
# Reduce validations if only safe settings are being changed
|
|
with_options unless: :only_settings_changed? do
|
|
validates :username,
|
|
presence: true,
|
|
uniqueness: {case_sensitive: false},
|
|
format: {with: /\A[[:alnum:].\-_;]+\z/},
|
|
multimodel_uniqueness: {punctuation_sensitive: false, case_sensitive: false, check: FederailsCommon::FEDIVERSE_USERNAMES},
|
|
length: SAFE_NAME_LENGTH
|
|
validates :email,
|
|
presence: true,
|
|
uniqueness: {case_sensitive: false},
|
|
format: {with: URI::MailTo::EMAIL_REGEXP}
|
|
validates :password,
|
|
presence: true,
|
|
confirmation: true,
|
|
if: :password_required?
|
|
end
|
|
|
|
after_create :assign_default_role
|
|
after_create :create_special_lists
|
|
|
|
# Explicitly explain serialization for MariaDB
|
|
serialize :pagination_settings, coder: CrossDbJsonSerializer
|
|
serialize :renderer_settings, coder: CrossDbJsonSerializer
|
|
serialize :tag_cloud_settings, coder: CrossDbJsonSerializer
|
|
serialize :problem_settings, coder: CrossDbJsonSerializer
|
|
serialize :file_list_settings, coder: CrossDbJsonSerializer
|
|
serialize :tour_state, coder: CrossDbJsonSerializer
|
|
|
|
attribute :sort_order, :integer # Explicit declaration of attribute so as not to break old data migrations
|
|
enum :sort_order, {name: 0, recent: 1, updated: 2}, prefix: :sort_by, default: :name, validate: true
|
|
|
|
has_many :access_grants, # rubocop:disable Rails/InverseOf
|
|
class_name: "Doorkeeper::AccessGrant",
|
|
foreign_key: :resource_owner_id,
|
|
dependent: :delete_all
|
|
|
|
has_many :access_tokens, # rubocop:disable Rails/InverseOf
|
|
class_name: "Doorkeeper::AccessToken",
|
|
foreign_key: :resource_owner_id,
|
|
dependent: :delete_all
|
|
|
|
has_many :oauth_applications,
|
|
class_name: "Doorkeeper::Application",
|
|
as: :owner,
|
|
dependent: :delete_all,
|
|
inverse_of: :owner
|
|
|
|
has_many :memberships, dependent: :destroy
|
|
has_many :groups, through: :memberships
|
|
|
|
attr_writer :skip_invitation
|
|
|
|
scope :active, -> { where(invitation_token: nil) }
|
|
scope :invited, -> { where.not(invitation_token: nil) }
|
|
|
|
def federails_name
|
|
username
|
|
end
|
|
|
|
def to_param
|
|
username
|
|
end
|
|
|
|
def self.find_param(param)
|
|
find_by!(username: param)
|
|
end
|
|
|
|
def active?
|
|
invitation_token.nil?
|
|
end
|
|
|
|
def invited?
|
|
invitation_token.present?
|
|
end
|
|
|
|
def printed?(file)
|
|
listed?(file, :printed)
|
|
end
|
|
|
|
def is_administrator?
|
|
has_any_role_of? :administrator
|
|
end
|
|
|
|
def is_moderator?
|
|
has_any_role_of? :administrator, :moderator
|
|
end
|
|
|
|
def is_contributor?
|
|
has_any_role_of? :administrator, :moderator, :contributor
|
|
end
|
|
|
|
def is_member?
|
|
has_any_role_of? :administrator, :moderator, :contributor, :member
|
|
end
|
|
|
|
def problem_severity(category)
|
|
problem_settings[category.to_s]&.to_sym || Problem::DEFAULT_SEVERITIES[category.to_sym]
|
|
end
|
|
|
|
def self.from_omniauth(auth)
|
|
# Link existing users by verified email first
|
|
user = find_by(auth_provider: nil, auth_uid: nil, email: auth.info.email)
|
|
if user
|
|
raise OmniAuth::Strategies::OpenIDConnect::CallbackError, error: :email_unverified, reason: "Email not verified" unless auth.info.email_verified
|
|
user.update!(
|
|
auth_provider: auth.provider,
|
|
auth_uid: auth.uid
|
|
)
|
|
else
|
|
# Email isn't present, so let's match by ID
|
|
user = find_or_create_by!(auth_provider: auth.provider, auth_uid: auth.uid) do |user|
|
|
user.email = auth.info.email
|
|
# Find an unused username - get the first of a few options
|
|
user.username = [
|
|
auth.info.preferred_username,
|
|
auth.info.nickname,
|
|
auth.info.email&.split("@")&.[](0),
|
|
# Fallback to any of the above with some random numbers on the end
|
|
(auth.info.preferred_username || auth.info.nickname || auth.info.email&.split("@")&.[](0) || "") + SecureRandom.hex(2)
|
|
]
|
|
.compact
|
|
.map { |u| u.gsub(/[^[:alnum:].\-_;]/, "-") }
|
|
.find { |u| !User.exists?(username: u) }
|
|
end
|
|
end
|
|
user
|
|
end
|
|
|
|
def self.user_count(range)
|
|
return User.count if range.nil? # rubocop:disable Pundit/UsePolicyScope
|
|
|
|
# Updated date isn't a great proxy for activity, but it'll do for now
|
|
# We can improve this by using devise trackable to track logins at some point
|
|
User.where(updated_at: range).count # rubocop:disable Pundit/UsePolicyScope
|
|
end
|
|
|
|
# Devise approval checks
|
|
def active_for_authentication?
|
|
super && approved?
|
|
end
|
|
|
|
def inactive_message
|
|
approved? ? super : :not_approved
|
|
end
|
|
|
|
def self.send_reset_password_instructions(attributes = {})
|
|
recoverable = find_or_initialize_with_errors(reset_password_keys, attributes, :not_found)
|
|
if recoverable.persisted?
|
|
if recoverable.approved?
|
|
recoverable.send_reset_password_instructions
|
|
else
|
|
recoverable.errors.add(:base, :not_approved)
|
|
end
|
|
end
|
|
recoverable
|
|
end
|
|
|
|
def to_activitypub_object
|
|
ActivityPub::UserSerializer.new(self).serialize
|
|
end
|
|
|
|
def public?
|
|
true
|
|
end
|
|
|
|
# Quota is in MB and is referred to in the UI as file storage limits for clarity
|
|
def quota
|
|
quota_use_site_default ? SiteSettings.default_user_quota : attributes["quota"].to_i * 1.megabyte
|
|
end
|
|
|
|
def has_quota?
|
|
!(attributes["quota"] == 0) && SiteSettings.enable_user_quota
|
|
end
|
|
|
|
def current_space_used
|
|
permitted_models.with_permission("own").sum(&:size_on_disk)
|
|
end
|
|
|
|
def first_use?
|
|
reset_password_token == "first_use"
|
|
end
|
|
|
|
def owns?(thing)
|
|
has_permission_on?("own", thing)
|
|
end
|
|
|
|
def self.match!(identifier:, scope: User, invite: false)
|
|
raise ActiveRecord::RecordNotFound if identifier.blank?
|
|
query = case identifier
|
|
when URI::MailTo::EMAIL_REGEXP
|
|
{email: identifier}
|
|
when /\A(acct:|@)([a-z0-9\-_.]+)(@(.*))?\z/io
|
|
if SiteSettings.federation_enabled?
|
|
actor = Federails::Actor.find_by_account(identifier) # rubocop:disable Rails/DynamicFindBy
|
|
{id: actor&.entity&.id}
|
|
else
|
|
scope = scope.none
|
|
{}
|
|
end
|
|
else
|
|
{username: identifier}
|
|
end
|
|
scope.find_by! query
|
|
rescue ActiveRecord::RecordNotFound
|
|
if identifier =~ URI::MailTo::EMAIL_REGEXP && invite
|
|
scope.find(User.invite!(email: identifier, skip_invitation: true).id)
|
|
else
|
|
raise
|
|
end
|
|
end
|
|
|
|
def self.invite!(params)
|
|
options = params
|
|
options[:username] ||= "invite_#{SecureRandom.hex(8)}"
|
|
super(options)
|
|
end
|
|
|
|
def liked_list
|
|
lists.find_by(special: :liked)
|
|
end
|
|
memoize :liked_list
|
|
|
|
def liked?(listable)
|
|
liked_list&.list_items&.where(listable: listable)&.any?
|
|
end
|
|
memoize :liked?
|
|
|
|
private
|
|
|
|
def set_quota
|
|
attributes["quota"] = SiteSettings.default_user_quota if try(:quota_use_site_default)
|
|
end
|
|
|
|
def has_any_role_of?(*args)
|
|
args.map { |it| has_role? it }.any?
|
|
end
|
|
|
|
def assign_default_role
|
|
return unless roles.empty?
|
|
default_roles = [:member, SiteSettings.default_signup_role.to_sym].uniq
|
|
default_roles.each { |it| add_role(it) }
|
|
end
|
|
|
|
def password_required?
|
|
return false if try(:auth_provider) && try(:auth_uid)
|
|
!persisted? || !password.nil? || !password_confirmation.nil?
|
|
end
|
|
|
|
def weak_words
|
|
["manyfold", username]
|
|
end
|
|
|
|
def set_json_field_defaults
|
|
self.pagination_settings ||= SiteSettings::UserDefaults::PAGINATION
|
|
self.renderer_settings ||= SiteSettings::UserDefaults::RENDERER
|
|
self.tag_cloud_settings ||= SiteSettings::UserDefaults::TAG_CLOUD
|
|
self.problem_settings ||= Problem::DEFAULT_SEVERITIES
|
|
self.file_list_settings ||= SiteSettings::UserDefaults::FILE_LIST
|
|
self.tour_state ||= DEFAULT_TOUR_STATE
|
|
end
|
|
|
|
def only_settings_changed?
|
|
return false unless changed?
|
|
settings_attributes = [
|
|
"sensitive_content_handling",
|
|
"interface_language",
|
|
"sort_order",
|
|
"pagination_settings",
|
|
"renderer_settings",
|
|
"tag_cloud_settings",
|
|
"problem_settings",
|
|
"file_list_settings",
|
|
"tour_state"
|
|
].freeze
|
|
(changed - settings_attributes).empty?
|
|
end
|
|
|
|
def create_special_lists
|
|
# i18n-tasks-use t('lists.special.liked')
|
|
List.create(name: "lists.special.liked", special: :liked, owner: self) if lists.find_by(special: :liked).nil?
|
|
end
|
|
end
|