"delete" is generally used for immediate SQL deletion without running any callbacks or other ruby code, whereas "destroy" will trigger callbacks. Although we don't currently use any callbacks, let's rename this method to align better with the convention.
344 lines
13 KiB
Ruby
344 lines
13 KiB
Ruby
# == Schema Information
|
|
#
|
|
# Table name: users
|
|
#
|
|
# email :string not null
|
|
# id :bigint(8) not null, primary key
|
|
# pass_crypt :string not null
|
|
# creation_time :datetime not null
|
|
# display_name :string default(""), not null
|
|
# data_public :boolean default(FALSE), not null
|
|
# description :text default(""), not null
|
|
# home_lat :float
|
|
# home_lon :float
|
|
# home_zoom :integer default(3)
|
|
# nearby :integer default(50)
|
|
# pass_salt :string
|
|
# email_valid :boolean default(FALSE), not null
|
|
# new_email :string
|
|
# creation_ip :string
|
|
# languages :string
|
|
# status :enum default("pending"), not null
|
|
# terms_agreed :datetime
|
|
# consider_pd :boolean default(FALSE), not null
|
|
# auth_uid :string
|
|
# preferred_editor :string
|
|
# terms_seen :boolean default(FALSE), not null
|
|
# description_format :enum default("markdown"), not null
|
|
# changesets_count :integer default(0), not null
|
|
# traces_count :integer default(0), not null
|
|
# diary_entries_count :integer default(0), not null
|
|
# image_use_gravatar :boolean default(FALSE), not null
|
|
# auth_provider :string
|
|
# home_tile :bigint(8)
|
|
# tou_agreed :datetime
|
|
#
|
|
# Indexes
|
|
#
|
|
# users_auth_idx (auth_provider,auth_uid) UNIQUE
|
|
# users_display_name_idx (display_name) UNIQUE
|
|
# users_display_name_lower_idx (lower((display_name)::text))
|
|
# users_email_idx (email) UNIQUE
|
|
# users_email_lower_idx (lower((email)::text))
|
|
# users_home_idx (home_tile)
|
|
#
|
|
|
|
class User < ApplicationRecord
|
|
require "digest"
|
|
require "xml/libxml"
|
|
|
|
has_many :traces, -> { where(:visible => true) }
|
|
has_many :diary_entries, -> { order(:created_at => :desc) }
|
|
has_many :diary_comments, -> { order(:created_at => :desc) }
|
|
has_many :diary_entry_subscriptions, :class_name => "DiaryEntrySubscription"
|
|
has_many :diary_subscriptions, :through => :diary_entry_subscriptions, :source => :diary_entry
|
|
has_many :messages, -> { where(:to_user_visible => true).order(:sent_on => :desc).preload(:sender, :recipient) }, :foreign_key => :to_user_id
|
|
has_many :new_messages, -> { where(:to_user_visible => true, :message_read => false).order(:sent_on => :desc) }, :class_name => "Message", :foreign_key => :to_user_id
|
|
has_many :sent_messages, -> { where(:from_user_visible => true).order(:sent_on => :desc).preload(:sender, :recipient) }, :class_name => "Message", :foreign_key => :from_user_id
|
|
has_many :friendships, -> { joins(:befriendee).where(:users => { :status => %w[active confirmed] }) }
|
|
has_many :friends, :through => :friendships, :source => :befriendee
|
|
has_many :tokens, :class_name => "UserToken", :dependent => :destroy
|
|
has_many :preferences, :class_name => "UserPreference"
|
|
has_many :changesets, -> { order(:created_at => :desc) }
|
|
has_many :changeset_comments, :foreign_key => :author_id
|
|
has_and_belongs_to_many :changeset_subscriptions, :class_name => "Changeset", :join_table => "changesets_subscribers", :foreign_key => "subscriber_id"
|
|
has_many :note_comments, :foreign_key => :author_id
|
|
has_many :notes, :through => :note_comments
|
|
|
|
has_many :client_applications
|
|
has_many :oauth_tokens, -> { order(:authorized_at => :desc).preload(:client_application) }, :class_name => "OauthToken"
|
|
|
|
has_many :oauth2_applications, :class_name => Doorkeeper.config.application_model.name, :as => :owner
|
|
has_many :access_grants, :class_name => Doorkeeper.config.access_grant_model.name, :foreign_key => :resource_owner_id
|
|
has_many :access_tokens, :class_name => Doorkeeper.config.access_token_model.name, :foreign_key => :resource_owner_id
|
|
|
|
has_many :blocks, :class_name => "UserBlock"
|
|
has_many :blocks_created, :class_name => "UserBlock", :foreign_key => :creator_id
|
|
has_many :blocks_revoked, :class_name => "UserBlock", :foreign_key => :revoker_id
|
|
|
|
has_many :roles, :class_name => "UserRole"
|
|
|
|
has_many :issues, :class_name => "Issue", :foreign_key => :reported_user_id
|
|
has_many :issue_comments
|
|
|
|
has_many :reports
|
|
|
|
scope :visible, -> { where(:status => %w[pending active confirmed]) }
|
|
scope :active, -> { where(:status => %w[active confirmed]) }
|
|
scope :identifiable, -> { where(:data_public => true) }
|
|
|
|
has_one_attached :avatar
|
|
|
|
validates :display_name, :presence => true, :length => 3..255,
|
|
:exclusion => %w[new terms save confirm confirm-email go_public reset-password forgot-password suspended]
|
|
validates :display_name, :if => proc { |u| u.display_name_changed? },
|
|
:uniqueness => { :case_sensitive => false }
|
|
validates :display_name, :if => proc { |u| u.display_name_changed? },
|
|
:characters => { :url_safe => true },
|
|
:whitespace => { :leading => false, :trailing => false }
|
|
validates :email, :presence => true, :confirmation => true, :characters => true
|
|
validates :email, :if => proc { |u| u.email_changed? },
|
|
:uniqueness => { :case_sensitive => false }
|
|
validates :email, :if => proc { |u| u.email_changed? },
|
|
:whitespace => { :leading => false, :trailing => false }
|
|
validates :pass_crypt, :confirmation => true, :length => 8..255
|
|
validates :home_lat, :allow_nil => true, :numericality => true, :inclusion => { :in => -90..90 }
|
|
validates :home_lon, :allow_nil => true, :numericality => true, :inclusion => { :in => -180..180 }
|
|
validates :home_zoom, :allow_nil => true, :numericality => { :only_integer => true }
|
|
validates :preferred_editor, :inclusion => Editors::ALL_EDITORS, :allow_nil => true
|
|
validates :auth_uid, :unless => proc { |u| u.auth_provider.nil? },
|
|
:uniqueness => { :scope => :auth_provider }
|
|
validates :avatar, :if => proc { |u| u.attachment_changes["avatar"] },
|
|
:image => true
|
|
|
|
validates_email_format_of :email, :if => proc { |u| u.email_changed? }
|
|
validates_email_format_of :new_email, :allow_blank => true, :if => proc { |u| u.new_email_changed? }
|
|
|
|
after_initialize :set_defaults
|
|
before_save :encrypt_password
|
|
before_save :update_tile
|
|
after_save :spam_check
|
|
|
|
def to_param
|
|
display_name
|
|
end
|
|
|
|
def self.authenticate(options)
|
|
if options[:username] && options[:password]
|
|
user = find_by("email = ? OR display_name = ?", options[:username].strip, options[:username])
|
|
|
|
if user.nil?
|
|
users = where("LOWER(email) = LOWER(?) OR LOWER(display_name) = LOWER(?)", options[:username].strip, options[:username])
|
|
|
|
user = users.first if users.count == 1
|
|
end
|
|
|
|
if user && PasswordHash.check(user.pass_crypt, user.pass_salt, options[:password])
|
|
if PasswordHash.upgrade?(user.pass_crypt, user.pass_salt)
|
|
user.pass_crypt, user.pass_salt = PasswordHash.create(options[:password])
|
|
user.save
|
|
end
|
|
else
|
|
user = nil
|
|
end
|
|
elsif options[:token]
|
|
token = UserToken.find_by(:token => options[:token])
|
|
user = token.user if token
|
|
end
|
|
|
|
if user &&
|
|
(user.status == "deleted" ||
|
|
(user.status == "pending" && !options[:pending]) ||
|
|
(user.status == "suspended" && !options[:suspended]))
|
|
user = nil
|
|
end
|
|
|
|
token.update(:expiry => 1.week.from_now) if token && user
|
|
|
|
user
|
|
end
|
|
|
|
def description
|
|
RichText.new(self[:description_format], self[:description])
|
|
end
|
|
|
|
def languages
|
|
attribute_present?(:languages) ? self[:languages].split(/ *[, ] */) : []
|
|
end
|
|
|
|
def languages=(languages)
|
|
self[:languages] = languages.join(",")
|
|
end
|
|
|
|
def preferred_language
|
|
languages.find { |l| Language.exists?(:code => l) }
|
|
end
|
|
|
|
def preferred_languages
|
|
@preferred_languages ||= Locale.list(languages)
|
|
end
|
|
|
|
def nearby(radius = Settings.nearby_radius, num = Settings.nearby_users)
|
|
if home_lon && home_lat
|
|
gc = OSM::GreatCircle.new(home_lat, home_lon)
|
|
sql_for_area = QuadTile.sql_for_area(gc.bounds(radius), "home_")
|
|
sql_for_distance = gc.sql_for_distance("home_lat", "home_lon")
|
|
nearby = User.active.identifiable
|
|
.where.not(:id => id)
|
|
.where(sql_for_area)
|
|
.where("#{sql_for_distance} <= ?", radius)
|
|
.order(Arel.sql(sql_for_distance))
|
|
.limit(num)
|
|
else
|
|
nearby = []
|
|
end
|
|
nearby
|
|
end
|
|
|
|
def distance(nearby_user)
|
|
OSM::GreatCircle.new(home_lat, home_lon).distance(nearby_user.home_lat, nearby_user.home_lon)
|
|
end
|
|
|
|
def is_friends_with?(new_friend)
|
|
friendships.exists?(:befriendee => new_friend)
|
|
end
|
|
|
|
##
|
|
# returns true if a user is visible
|
|
def visible?
|
|
%w[pending active confirmed].include? status
|
|
end
|
|
|
|
##
|
|
# returns true if a user is active
|
|
def active?
|
|
%w[active confirmed].include? status
|
|
end
|
|
|
|
##
|
|
# returns true if the user has the moderator role, false otherwise
|
|
def moderator?
|
|
has_role? "moderator"
|
|
end
|
|
|
|
##
|
|
# returns true if the user has the administrator role, false otherwise
|
|
def administrator?
|
|
has_role? "administrator"
|
|
end
|
|
|
|
##
|
|
# returns true if the user has the requested role
|
|
def has_role?(role)
|
|
roles.any? { |r| r.role == role }
|
|
end
|
|
|
|
##
|
|
# returns the first active block which would require users to view
|
|
# a message, or nil if there are none.
|
|
def blocked_on_view
|
|
blocks.active.detect(&:needs_view?)
|
|
end
|
|
|
|
##
|
|
# destroy a user - leave the account but purge most personal data
|
|
def destroy
|
|
avatar.purge_later
|
|
|
|
self.display_name = "user_#{id}"
|
|
self.description = ""
|
|
self.home_lat = nil
|
|
self.home_lon = nil
|
|
self.email_valid = false
|
|
self.new_email = nil
|
|
self.auth_provider = nil
|
|
self.auth_uid = nil
|
|
self.status = "deleted"
|
|
|
|
save
|
|
end
|
|
|
|
##
|
|
# return a spam score for a user
|
|
def spam_score
|
|
changeset_score = changesets.size * 50
|
|
trace_score = traces.size * 50
|
|
diary_entry_score = diary_entries.visible.inject(0) { |acc, elem| acc + elem.body.spam_score }
|
|
diary_comment_score = diary_comments.visible.inject(0) { |acc, elem| acc + elem.body.spam_score }
|
|
|
|
score = description.spam_score / 4.0
|
|
score += diary_entries.where("created_at > ?", 1.day.ago).count * 10
|
|
score += diary_entry_score / diary_entries.length unless diary_entries.empty?
|
|
score += diary_comment_score / diary_comments.length unless diary_comments.empty?
|
|
score -= changeset_score
|
|
score -= trace_score
|
|
|
|
score.to_i
|
|
end
|
|
|
|
##
|
|
# perform a spam check on a user
|
|
def spam_check
|
|
update(:status => "suspended") if status == "active" && spam_score > Settings.spam_threshold
|
|
end
|
|
|
|
##
|
|
# return an oauth 1 access token for a specified application
|
|
def access_token(application_key)
|
|
ClientApplication.find_by(:key => application_key).access_token_for_user(self)
|
|
end
|
|
|
|
##
|
|
# return an oauth 2 access token for a specified application
|
|
def oauth_token(application_id)
|
|
application = Doorkeeper.config.application_model.find_by(:uid => application_id)
|
|
|
|
Doorkeeper.config.access_token_model.find_or_create_for(
|
|
:application => application,
|
|
:resource_owner => self,
|
|
:scopes => application.scopes
|
|
)
|
|
end
|
|
|
|
def fingerprint
|
|
digest = Digest::SHA256.new
|
|
digest.update(email)
|
|
digest.update(pass_crypt)
|
|
digest.hexdigest
|
|
end
|
|
|
|
def max_messages_per_hour
|
|
account_age_in_seconds = Time.now.utc - creation_time
|
|
account_age_in_hours = account_age_in_seconds / 3600
|
|
recent_messages = messages.where("sent_on >= ?", Time.now.utc - 3600).count
|
|
active_reports = issues.with_status(:open).sum(:reports_count)
|
|
max_messages = account_age_in_hours.ceil + recent_messages - active_reports * 10
|
|
max_messages.clamp(0, Settings.max_messages_per_hour)
|
|
end
|
|
|
|
def max_friends_per_hour
|
|
account_age_in_seconds = Time.now.utc - creation_time
|
|
account_age_in_hours = account_age_in_seconds / 3600
|
|
recent_friends = Friendship.where(:befriendee => self).where("created_at >= ?", Time.now.utc - 3600).count
|
|
active_reports = issues.with_status(:open).sum(:reports_count)
|
|
max_friends = account_age_in_hours.ceil + recent_friends - active_reports * 10
|
|
max_friends.clamp(0, Settings.max_friends_per_hour)
|
|
end
|
|
|
|
private
|
|
|
|
def set_defaults
|
|
self.creation_time = Time.now.getutc unless attribute_present?(:creation_time)
|
|
end
|
|
|
|
def encrypt_password
|
|
if pass_crypt_confirmation
|
|
self.pass_crypt, self.pass_salt = PasswordHash.create(pass_crypt)
|
|
self.pass_crypt_confirmation = nil
|
|
end
|
|
end
|
|
|
|
def update_tile
|
|
self.home_tile = QuadTile.tile_for_point(home_lat, home_lon) if home_lat && home_lon
|
|
end
|
|
end
|