Prior to this commit, we trusted all user profiles. There are folks using DEV.to profiles to help with their spam efforts. With this commit, we're trying to thwart their efforts. Closes forem/rfcs#334
676 lines
24 KiB
Ruby
676 lines
24 KiB
Ruby
class User < ApplicationRecord
|
|
resourcify
|
|
rolify
|
|
|
|
include CloudinaryHelper
|
|
include Storext.model
|
|
|
|
# NOTE: we are using an inline module to keep profile related things together.
|
|
concerning :Profiles do
|
|
included do
|
|
has_one :profile, dependent: :delete
|
|
|
|
# NOTE: There are rare cases were we want to skip this callback, primarily
|
|
# in tests. `skip_callback` modifies global state, which is not thread-safe
|
|
# and can cause hard to track down bugs. We use an instance-level attribute
|
|
# instead. See `spec/factories/profiles.rb` for an example.
|
|
attr_accessor :_skip_creating_profile
|
|
|
|
# All new users should automatically have a profile
|
|
after_create_commit -> { Profile.create(user: self) }, unless: :_skip_creating_profile
|
|
end
|
|
end
|
|
|
|
ANY_ADMIN_ROLES = %i[admin super_admin].freeze
|
|
USERNAME_MAX_LENGTH = 30
|
|
USERNAME_REGEXP = /\A[a-zA-Z0-9_]+\z/
|
|
MESSAGES = {
|
|
reserved_username: "username is reserved"
|
|
}.freeze
|
|
# follow the syntax in https://interledger.org/rfcs/0026-payment-pointers/#payment-pointer-syntax
|
|
PAYMENT_POINTER_REGEXP = %r{
|
|
\A # start
|
|
\$ # starts with a dollar sign
|
|
([a-zA-Z0-9\-.])+ # matches the hostname (ex ilp.uphold.com)
|
|
(/[\x20-\x7F]+)? # optional forward slash and identifier with printable ASCII characters
|
|
\z
|
|
}x
|
|
|
|
attr_accessor :scholar_email, :new_note, :note_for_current_role, :user_status, :merge_user_id,
|
|
:add_credits, :remove_credits, :add_org_credits, :remove_org_credits, :ip_address,
|
|
:current_password
|
|
|
|
acts_as_followable
|
|
acts_as_follower
|
|
|
|
has_one :notification_setting, class_name: "Users::NotificationSetting", dependent: :delete
|
|
has_one :setting, class_name: "Users::Setting", dependent: :delete
|
|
|
|
has_many :access_grants, class_name: "Doorkeeper::AccessGrant", foreign_key: :resource_owner_id,
|
|
inverse_of: :resource_owner, dependent: :delete_all
|
|
has_many :access_tokens, class_name: "Doorkeeper::AccessToken", foreign_key: :resource_owner_id,
|
|
inverse_of: :resource_owner, dependent: :delete_all
|
|
has_many :affected_feedback_messages, class_name: "FeedbackMessage",
|
|
inverse_of: :affected, foreign_key: :affected_id, dependent: :nullify
|
|
has_many :ahoy_events, class_name: "Ahoy::Event", dependent: :delete_all
|
|
has_many :ahoy_visits, class_name: "Ahoy::Visit", dependent: :delete_all
|
|
has_many :api_secrets, dependent: :delete_all
|
|
has_many :articles, dependent: :destroy
|
|
has_many :audit_logs, dependent: :nullify
|
|
has_many :authored_notes, inverse_of: :author, class_name: "Note", foreign_key: :author_id, dependent: :delete_all
|
|
has_many :badge_achievements, dependent: :delete_all
|
|
has_many :badge_achievements_rewarded, class_name: "BadgeAchievement", foreign_key: :rewarder_id,
|
|
inverse_of: :rewarder, dependent: :nullify
|
|
has_many :badges, through: :badge_achievements
|
|
has_many :banished_users, class_name: "BanishedUser", foreign_key: :banished_by_id,
|
|
inverse_of: :banished_by, dependent: :nullify
|
|
has_many :blocked_blocks, class_name: "UserBlock", foreign_key: :blocked_id,
|
|
inverse_of: :blocked, dependent: :delete_all
|
|
has_many :blocker_blocks, class_name: "UserBlock", foreign_key: :blocker_id,
|
|
inverse_of: :blocker, dependent: :delete_all
|
|
has_many :collections, dependent: :destroy
|
|
has_many :comments, dependent: :destroy
|
|
has_many :created_podcasts, class_name: "Podcast", foreign_key: :creator_id, inverse_of: :creator, dependent: :nullify
|
|
has_many :credits, dependent: :destroy
|
|
has_many :discussion_locks, dependent: :delete_all, inverse_of: :locking_user, foreign_key: :locking_user_id
|
|
has_many :display_ad_events, dependent: :delete_all
|
|
has_many :email_authorizations, dependent: :delete_all
|
|
has_many :email_messages, class_name: "Ahoy::Message", dependent: :destroy
|
|
has_many :field_test_memberships, class_name: "FieldTest::Membership", as: :participant, dependent: :destroy
|
|
# Consider that we might be able to use dependent: :delete_all as the GithubRepo busts the user cache
|
|
has_many :github_repos, dependent: :destroy
|
|
has_many :html_variants, dependent: :destroy
|
|
has_many :identities, dependent: :delete_all
|
|
has_many :identities_enabled, -> { enabled }, class_name: "Identity", inverse_of: false
|
|
has_many :listings, dependent: :destroy
|
|
has_many :mentions, dependent: :delete_all
|
|
has_many :notes, as: :noteable, inverse_of: :noteable, dependent: :delete_all
|
|
has_many :notification_subscriptions, dependent: :delete_all
|
|
has_many :notifications, dependent: :delete_all
|
|
has_many :offender_feedback_messages, class_name: "FeedbackMessage",
|
|
inverse_of: :offender, foreign_key: :offender_id, dependent: :nullify
|
|
has_many :organization_memberships, dependent: :delete_all
|
|
has_many :organizations, through: :organization_memberships
|
|
# we keep page views as they belong to the article, not to the user who viewed it
|
|
has_many :page_views, dependent: :nullify
|
|
has_many :podcast_episode_appearances, dependent: :delete_all, inverse_of: :user
|
|
has_many :podcast_episodes, through: :podcast_episode_appearances, source: :podcast_episode
|
|
has_many :podcast_ownerships, dependent: :delete_all, inverse_of: :owner
|
|
has_many :podcasts_owned, through: :podcast_ownerships, source: :podcast
|
|
has_many :poll_skips, dependent: :delete_all
|
|
has_many :poll_votes, dependent: :delete_all
|
|
has_many :profile_pins, as: :profile, inverse_of: :profile, dependent: :delete_all
|
|
|
|
# we keep rating votes as they belong to the article, not to the user who viewed it
|
|
has_many :rating_votes, dependent: :nullify
|
|
|
|
has_many :reactions, dependent: :destroy
|
|
has_many :reporter_feedback_messages, class_name: "FeedbackMessage",
|
|
inverse_of: :reporter, foreign_key: :reporter_id, dependent: :nullify
|
|
has_many :response_templates, inverse_of: :user, dependent: :delete_all
|
|
has_many :source_authored_user_subscriptions, class_name: "UserSubscription",
|
|
foreign_key: :author_id, inverse_of: :author, dependent: :destroy
|
|
has_many :subscribed_to_user_subscriptions, class_name: "UserSubscription",
|
|
foreign_key: :subscriber_id, inverse_of: :subscriber, dependent: :destroy
|
|
has_many :subscribers, through: :source_authored_user_subscriptions, dependent: :destroy
|
|
has_many :tweets, dependent: :nullify
|
|
has_many :webhook_endpoints, class_name: "Webhook::Endpoint", inverse_of: :user, dependent: :delete_all
|
|
has_many :devices, dependent: :delete_all
|
|
has_many :sponsorships, dependent: :delete_all
|
|
|
|
mount_uploader :profile_image, ProfileImageUploader
|
|
|
|
devise :invitable, :omniauthable, :registerable, :database_authenticatable, :confirmable, :rememberable,
|
|
:recoverable, :lockable
|
|
|
|
validates :articles_count, presence: true
|
|
validates :badge_achievements_count, presence: true
|
|
validates :blocked_by_count, presence: true
|
|
validates :blocking_others_count, presence: true
|
|
validates :comments_count, presence: true
|
|
validates :credits_count, presence: true
|
|
validates :email, length: { maximum: 50 }, email: true, allow_nil: true
|
|
validates :email, uniqueness: { allow_nil: true, case_sensitive: false }, if: :email_changed?
|
|
validates :following_orgs_count, presence: true
|
|
validates :following_tags_count, presence: true
|
|
validates :following_users_count, presence: true
|
|
validates :name, length: { in: 1..100 }
|
|
validates :password, length: { in: 8..100 }, allow_nil: true
|
|
validates :payment_pointer, format: PAYMENT_POINTER_REGEXP, allow_blank: true
|
|
validates :rating_votes_count, presence: true
|
|
validates :reactions_count, presence: true
|
|
validates :sign_in_count, presence: true
|
|
validates :spent_credits_count, presence: true
|
|
validates :subscribed_to_user_subscriptions_count, presence: true
|
|
validates :unspent_credits_count, presence: true
|
|
validates :username, length: { in: 2..USERNAME_MAX_LENGTH }, format: USERNAME_REGEXP
|
|
validates :username, presence: true, exclusion: { in: ReservedWords.all, message: MESSAGES[:invalid_username] }
|
|
validates :username, uniqueness: { case_sensitive: false, message: lambda do |_obj, data|
|
|
"#{data[:value]} is taken."
|
|
end }, if: :username_changed?
|
|
|
|
# add validators for provider related usernames
|
|
Authentication::Providers.username_fields.each do |username_field|
|
|
# make sure usernames are not empty string, to be able to use the database unique index
|
|
clean_provider_username = proc do |record|
|
|
cleaned_username = record.attributes[username_field.to_s].presence
|
|
record.assign_attributes(username_field => cleaned_username)
|
|
end
|
|
before_validation clean_provider_username
|
|
|
|
validates username_field, uniqueness: { allow_nil: true }, if: :"#{username_field}_changed?"
|
|
end
|
|
|
|
validate :non_banished_username, :username_changed?
|
|
validate :unique_including_orgs_and_podcasts, if: :username_changed?
|
|
validate :can_send_confirmation_email
|
|
validate :update_rate_limit
|
|
# NOTE: when updating the password on a Devise enabled model, the :encrypted_password
|
|
# field will be marked as dirty, not :password.
|
|
validate :password_matches_confirmation, if: :encrypted_password_changed?
|
|
|
|
alias_attribute :public_reactions_count, :reactions_count
|
|
|
|
scope :eager_load_serialized_data, -> { includes(:roles) }
|
|
scope :registered, -> { where(registered: true) }
|
|
# Unfortunately pg_search's default SQL query is not performant enough in this
|
|
# particular case (~ 500ms). There are multiple reasons:
|
|
# => creates a complex query like `SELECT FROM users INNER JOIN users` to compute ranking.
|
|
# See https://github.com/Casecommons/pg_search/issues/292#issuecomment-202604151
|
|
# => it concatenates the content of `name` and the content of `username` to match
|
|
# against the search term. By doing that, it can't use `tsvector` indexes correctly
|
|
#
|
|
# For these reasons we need to build a query manually using an `OR` condition,
|
|
# thus allowing the database to use the indexes properly. With this the SQL time is ~ 8-10ms.
|
|
#
|
|
# NOTE: we can't use unaccent() on the `tsvector` document because `unaccent()` can't be
|
|
# used in expression indexes as it's a mutable function and depends on server settings
|
|
# => https://stackoverflow.com/a/11007216/4186181
|
|
#
|
|
scope :search_by_name_and_username, lambda { |term|
|
|
where(
|
|
sanitize_sql_array(
|
|
[
|
|
"to_tsvector('simple', coalesce(name::text, '')) @@ to_tsquery('simple', ? || ':*')",
|
|
connection.quote(term),
|
|
],
|
|
),
|
|
).or(
|
|
where(
|
|
sanitize_sql_array(
|
|
[
|
|
"to_tsvector('simple', coalesce(username::text, '')) @@ to_tsquery('simple', ? || ':*')",
|
|
connection.quote(term),
|
|
],
|
|
),
|
|
),
|
|
)
|
|
}
|
|
before_validation :check_for_username_change
|
|
before_validation :downcase_email
|
|
# make sure usernames are not empty, to be able to use the database unique index
|
|
before_validation :verify_email
|
|
before_validation :set_username
|
|
before_validation :strip_payment_pointer
|
|
before_create :create_users_settings_and_notification_settings_records
|
|
before_destroy :unsubscribe_from_newsletters, prepend: true
|
|
before_destroy :destroy_follows, prepend: true
|
|
|
|
after_create_commit :send_welcome_notification
|
|
|
|
after_save :create_conditional_autovomits
|
|
after_commit :subscribe_to_mailchimp_newsletter
|
|
after_commit :bust_cache
|
|
|
|
def self.staff_account
|
|
find_by(id: Settings::Community.staff_user_id)
|
|
end
|
|
|
|
def self.mascot_account
|
|
find_by(id: Settings::General.mascot_user_id)
|
|
end
|
|
|
|
def tag_line
|
|
profile.summary
|
|
end
|
|
|
|
def twitter_url
|
|
"https://twitter.com/#{twitter_username}" if twitter_username.present?
|
|
end
|
|
|
|
def github_url
|
|
"https://github.com/#{github_username}" if github_username.present?
|
|
end
|
|
|
|
def set_remember_fields
|
|
self.remember_token ||= self.class.remember_token if respond_to?(:remember_token)
|
|
self.remember_created_at ||= Time.now.utc
|
|
end
|
|
|
|
def calculate_score
|
|
# User score is used to mitigate spam by reducing visibility of flagged users
|
|
# It can generally be used as a baseline for affecting certain functionality which
|
|
# relies on trust gray area.
|
|
|
|
# Current main use: If score is below zero, the user's profile page will render noindex
|
|
# meta tag. This is a subtle anti-spam mechanism.
|
|
|
|
# It can be changed as frequently as needed to do a better job reflecting its purpose
|
|
# Changes should generally keep the score within the same order of magnitude so that
|
|
# mass re-calculation is needed.
|
|
user_reaction_points = Reaction.user_vomits.where(reactable_id: id).sum(:points)
|
|
calculated_score = (badge_achievements_count * 10) + user_reaction_points
|
|
update_column(:score, calculated_score)
|
|
end
|
|
|
|
def path
|
|
"/#{username}"
|
|
end
|
|
|
|
# NOTE: This method is only used in the EmailDigestArticleCollector and does
|
|
# not perform particularly well. It should most likely not be used in other
|
|
# parts of the app.
|
|
def followed_articles
|
|
relation = Article
|
|
if cached_antifollowed_tag_names.any?
|
|
relation = relation.not_cached_tagged_with_any(cached_antifollowed_tag_names)
|
|
end
|
|
|
|
relation
|
|
.cached_tagged_with_any(cached_followed_tag_names)
|
|
.unscope(:select)
|
|
.union(Article.where(user_id: cached_following_users_ids))
|
|
end
|
|
|
|
def cached_following_users_ids
|
|
cache_key = "user-#{id}-#{last_followed_at}-#{following_users_count}/following_users_ids"
|
|
Rails.cache.fetch(cache_key, expires_in: 12.hours) do
|
|
Follow.follower_user(id).limit(150).pluck(:followable_id)
|
|
end
|
|
end
|
|
|
|
def cached_following_organizations_ids
|
|
cache_key = "user-#{id}-#{last_followed_at}-#{following_orgs_count}/following_organizations_ids"
|
|
Rails.cache.fetch(cache_key, expires_in: 12.hours) do
|
|
Follow.follower_organization(id).limit(150).pluck(:followable_id)
|
|
end
|
|
end
|
|
|
|
def cached_following_podcasts_ids
|
|
cache_key = "user-#{id}-#{last_followed_at}/following_podcasts_ids"
|
|
Rails.cache.fetch(cache_key, expires_in: 12.hours) do
|
|
Follow.follower_podcast(id).pluck(:followable_id)
|
|
end
|
|
end
|
|
|
|
def cached_reading_list_article_ids
|
|
Rails.cache.fetch("reading_list_ids_of_articles_#{id}_#{public_reactions_count}_#{last_reacted_at}") do
|
|
Reaction.readinglist.where(
|
|
user_id: id, reactable_type: "Article",
|
|
).where.not(status: "archived").order(created_at: :desc).pluck(:reactable_id)
|
|
end
|
|
end
|
|
|
|
def processed_website_url
|
|
profile.website_url.to_s.strip if profile.website_url.present?
|
|
end
|
|
|
|
def remember_me
|
|
true
|
|
end
|
|
|
|
def cached_followed_tag_names
|
|
cache_name = "user-#{id}-#{following_tags_count}-#{last_followed_at&.rfc3339}/followed_tag_names"
|
|
Rails.cache.fetch(cache_name, expires_in: 24.hours) do
|
|
Tag.where(
|
|
id: Follow.where(
|
|
follower_id: id,
|
|
followable_type: "ActsAsTaggableOn::Tag",
|
|
points: 1..,
|
|
).select(:followable_id),
|
|
).pluck(:name)
|
|
end
|
|
end
|
|
|
|
def cached_antifollowed_tag_names
|
|
cache_name = "user-#{id}-#{following_tags_count}-#{last_followed_at&.rfc3339}/antifollowed_tag_names"
|
|
Rails.cache.fetch(cache_name, expires_in: 24.hours) do
|
|
Tag.where(
|
|
id: Follow.where(
|
|
follower_id: id,
|
|
followable_type: "ActsAsTaggableOn::Tag",
|
|
points: ...1,
|
|
).select(:followable_id),
|
|
).pluck(:name)
|
|
end
|
|
end
|
|
|
|
def suspended?
|
|
has_role?(:suspended)
|
|
end
|
|
|
|
def warned
|
|
has_role?(:warned)
|
|
end
|
|
|
|
def admin?
|
|
has_role?(:super_admin)
|
|
end
|
|
|
|
def creator?
|
|
has_role?(:creator)
|
|
end
|
|
|
|
def any_admin?
|
|
@any_admin ||= roles.where(name: ANY_ADMIN_ROLES).any?
|
|
end
|
|
|
|
def tech_admin?
|
|
has_role?(:tech_admin) || has_role?(:super_admin)
|
|
end
|
|
|
|
def vomitted_on?
|
|
Reaction.exists?(reactable_id: id, reactable_type: "User", category: "vomit", status: "confirmed")
|
|
end
|
|
|
|
def trusted
|
|
return @trusted if defined? @trusted
|
|
|
|
@trusted = Rails.cache.fetch("user-#{id}/has_trusted_role", expires_in: 200.hours) do
|
|
has_role?(:trusted)
|
|
end
|
|
end
|
|
|
|
alias trusted? trusted
|
|
|
|
def moderator_for_tags
|
|
Rails.cache.fetch("user-#{id}/tag_moderators_list", expires_in: 200.hours) do
|
|
tag_ids = roles.where(name: "tag_moderator").pluck(:resource_id)
|
|
Tag.where(id: tag_ids).pluck(:name)
|
|
end
|
|
end
|
|
|
|
def comment_suspended?
|
|
has_role?(:comment_suspended)
|
|
end
|
|
|
|
def workshop_eligible?
|
|
has_any_role?(:workshop_pass)
|
|
end
|
|
|
|
def admin_organizations
|
|
org_ids = organization_memberships.where(type_of_user: "admin").pluck(:organization_id)
|
|
organizations.where(id: org_ids)
|
|
end
|
|
|
|
def member_organizations
|
|
org_ids = organization_memberships.where(type_of_user: %w[admin member]).pluck(:organization_id)
|
|
organizations.where(id: org_ids)
|
|
end
|
|
|
|
def org_member?(organization)
|
|
OrganizationMembership.exists?(user: self, organization: organization, type_of_user: %w[admin member])
|
|
end
|
|
|
|
def org_admin?(organization)
|
|
OrganizationMembership.exists?(user: self, organization: organization, type_of_user: "admin")
|
|
end
|
|
|
|
def block; end
|
|
|
|
def all_blocked_by
|
|
UserBlock.where(blocked_id: id)
|
|
end
|
|
|
|
def blocking?(blocked_id)
|
|
UserBlock.blocking?(id, blocked_id)
|
|
end
|
|
|
|
def blocked_by?(blocker_id)
|
|
UserBlock.blocking?(blocker_id, id)
|
|
end
|
|
|
|
def unique_including_orgs_and_podcasts
|
|
username_taken = (
|
|
Organization.exists?(slug: username) ||
|
|
Podcast.exists?(slug: username) ||
|
|
Page.exists?(slug: username)
|
|
)
|
|
|
|
errors.add(:username, "is taken.") if username_taken
|
|
end
|
|
|
|
def non_banished_username
|
|
errors.add(:username, "has been banished.") if BanishedUser.exists?(username: username)
|
|
end
|
|
|
|
def banished?
|
|
username.starts_with?("spam_")
|
|
end
|
|
|
|
def subscribe_to_mailchimp_newsletter
|
|
return unless registered && email.present?
|
|
return if Settings::General.mailchimp_api_key.blank?
|
|
return if saved_changes.key?(:unconfirmed_email) && saved_changes.key?(:confirmation_sent_at)
|
|
return unless saved_changes.key?(:email)
|
|
|
|
Users::SubscribeToMailchimpNewsletterWorker.perform_async(id)
|
|
end
|
|
|
|
def a_sustaining_member?
|
|
monthly_dues.positive?
|
|
end
|
|
|
|
def resave_articles
|
|
articles.find_each do |article|
|
|
if article.path
|
|
cache_bust = EdgeCache::Bust.new
|
|
cache_bust.call(article.path)
|
|
cache_bust.call("#{article.path}?i=i")
|
|
end
|
|
article.save
|
|
end
|
|
end
|
|
|
|
def profile_image_90
|
|
Images::Profile.call(profile_image_url, length: 90)
|
|
end
|
|
|
|
def unsubscribe_from_newsletters
|
|
return if email.blank?
|
|
return if Settings::General.mailchimp_api_key.blank?
|
|
|
|
Mailchimp::Bot.new(self).unsubscribe_all_newsletters
|
|
end
|
|
|
|
def auditable?
|
|
trusted || tag_moderator? || any_admin?
|
|
end
|
|
|
|
def tag_moderator?
|
|
roles.where(name: "tag_moderator").any?
|
|
end
|
|
|
|
def enough_credits?(num_credits_needed)
|
|
credits.unspent.size >= num_credits_needed
|
|
end
|
|
|
|
def receives_follower_email_notifications?
|
|
email.present? && subscribed_to_email_follower_notifications?
|
|
end
|
|
|
|
def hotness_score
|
|
search_score
|
|
end
|
|
|
|
def authenticated_through?(provider_name)
|
|
return false unless Authentication::Providers.available?(provider_name)
|
|
return false unless Authentication::Providers.enabled?(provider_name)
|
|
|
|
identities_enabled.exists?(provider: provider_name)
|
|
end
|
|
|
|
def authenticated_with_all_providers?
|
|
# ga_providers refers to Generally Available (not in beta)
|
|
ga_providers = Authentication::Providers.enabled.reject { |sym| sym == :apple }
|
|
enabled_providers = identities.pluck(:provider).map(&:to_sym)
|
|
(ga_providers - enabled_providers).empty?
|
|
end
|
|
|
|
def rate_limiter
|
|
RateLimitChecker.new(self)
|
|
end
|
|
|
|
def flipper_id
|
|
"User:#{id}"
|
|
end
|
|
|
|
def subscribed_to_welcome_notifications?
|
|
notification_setting.welcome_notifications
|
|
end
|
|
|
|
def subscribed_to_mod_roundrobin_notifications?
|
|
notification_setting.mod_roundrobin_notifications
|
|
end
|
|
|
|
def subscribed_to_email_follower_notifications?
|
|
notification_setting.email_follower_notifications
|
|
end
|
|
|
|
protected
|
|
|
|
# Send emails asynchronously
|
|
# see https://github.com/heartcombo/devise#activejob-integration
|
|
def send_devise_notification(notification, *args)
|
|
message = devise_mailer.public_send(notification, self, *args)
|
|
message.deliver_later
|
|
end
|
|
|
|
private
|
|
|
|
def create_users_settings_and_notification_settings_records
|
|
self.setting = Users::Setting.create
|
|
self.notification_setting = Users::NotificationSetting.create
|
|
end
|
|
|
|
def send_welcome_notification
|
|
return unless (set_up_profile_broadcast = Broadcast.active.find_by(title: "Welcome Notification: set_up_profile"))
|
|
|
|
Notification.send_welcome_notification(id, set_up_profile_broadcast.id)
|
|
end
|
|
|
|
def verify_email
|
|
self.email = nil if email == ""
|
|
end
|
|
|
|
def set_username
|
|
set_temp_username if username.blank?
|
|
self.username = username&.downcase
|
|
end
|
|
|
|
def set_temp_username
|
|
self.username = if temp_name_exists?
|
|
"#{temp_username}_#{rand(100)}"
|
|
else
|
|
temp_username
|
|
end
|
|
end
|
|
|
|
def temp_name_exists?
|
|
User.exists?(username: temp_username) || Organization.exists?(slug: temp_username)
|
|
end
|
|
|
|
def temp_username
|
|
Authentication::Providers.username_fields.each do |username_field|
|
|
value = public_send(username_field)
|
|
next if value.blank?
|
|
|
|
return value.downcase.gsub(/[^0-9a-z_]/i, "").delete(" ")
|
|
end
|
|
end
|
|
|
|
def downcase_email
|
|
self.email = email.downcase if email
|
|
end
|
|
|
|
def check_for_username_change
|
|
return unless username_changed?
|
|
|
|
self.old_old_username = old_username
|
|
self.old_username = username_was
|
|
articles.find_each do |article|
|
|
article.path = article.path.gsub(username_was, username)
|
|
article.save
|
|
end
|
|
end
|
|
|
|
def bust_cache
|
|
Users::BustCacheWorker.perform_async(id)
|
|
end
|
|
|
|
def create_conditional_autovomits
|
|
return unless Settings::RateLimit.spam_trigger_terms.any? do |term|
|
|
name.match?(/#{term}/i)
|
|
end
|
|
|
|
Reaction.create!(
|
|
user_id: Settings::General.mascot_user_id,
|
|
reactable_id: id,
|
|
reactable_type: "User",
|
|
category: "vomit",
|
|
)
|
|
end
|
|
|
|
# TODO: @citizen428 I don't want to completely remove this method yet, as we
|
|
# have similar methods in other models. But the previous implementation used
|
|
# three profile fields that we can't guarantee to exist across all Forems. So
|
|
# for now this method will just return an empty string.
|
|
def tag_keywords_for_search
|
|
""
|
|
end
|
|
|
|
# TODO: this can be removed once we migrate away from ES
|
|
def search_score
|
|
counts_score = (articles_count + comments_count + reactions_count + badge_achievements_count) * 10
|
|
score = (counts_score + tag_keywords_for_search.size) * reputation_modifier
|
|
score.to_i
|
|
end
|
|
|
|
def destroy_follows
|
|
follower_relationships = Follow.followable_user(id)
|
|
follower_relationships.destroy_all
|
|
follows.destroy_all
|
|
end
|
|
|
|
def can_send_confirmation_email
|
|
return if changes[:email].blank? || id.blank?
|
|
|
|
rate_limiter.track_limit_by_action(:send_email_confirmation)
|
|
rate_limiter.check_limit!(:send_email_confirmation)
|
|
rescue RateLimitChecker::LimitReached => e
|
|
errors.add(:email, "confirmation could not be sent. #{e.message}")
|
|
end
|
|
|
|
def update_rate_limit
|
|
return unless persisted?
|
|
|
|
rate_limiter.track_limit_by_action(:user_update)
|
|
rate_limiter.check_limit!(:user_update)
|
|
rescue RateLimitChecker::LimitReached => e
|
|
errors.add(:base, "User could not be saved. #{e.message}")
|
|
end
|
|
|
|
def password_matches_confirmation
|
|
return true if password == password_confirmation
|
|
|
|
errors.add(:password, "doesn't match password confirmation")
|
|
end
|
|
|
|
def strip_payment_pointer
|
|
self.payment_pointer = payment_pointer.strip if payment_pointer
|
|
end
|
|
|
|
def confirmation_required?
|
|
ForemInstance.smtp_enabled?
|
|
end
|
|
end
|