Files
manyfold/app/models/user.rb

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