| 1 | class User < ApplicationRecord # rubocop:disable Metrics/ClassLength |
| 2 | # Virtual attribute to skip password validation while saving new user from |
| 3 | # a wallet connection. |
| 4 | attr_accessor :skip_password_validation |
| 5 | |
| 6 | has_merit |
| 7 | |
| 8 | # Include default devise modules. Others available are: |
| 9 | # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable |
| 10 | devise :invitable, :database_authenticatable, |
| 11 | :registerable, |
| 12 | :confirmable, |
| 13 | :recoverable, |
| 14 | :rememberable, |
| 15 | :validatable, |
| 16 | :jwt_authenticatable, |
| 17 | jwt_revocation_strategy: JwtDenylist |
| 18 | |
| 19 | before_save { email.downcase! } |
| 20 | |
| 21 | VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d-]+(\.[a-z\d-]+)*\.[a-z]+\z/i |
| 22 | validates :email, presence: true, |
| 23 | length: { maximum: 255 }, |
| 24 | format: { with: VALID_EMAIL_REGEX }, |
| 25 | uniqueness: { case_sensitive: false } |
| 26 | |
| 27 | validates :password, presence: true, length: { minimum: 6 }, allow_nil: true |
| 28 | devise :invitable, :omniauthable, omniauth_providers: %i[facebook spotify google_oauth2 apple] |
| 29 | |
| 30 | has_many :chords, dependent: :destroy |
| 31 | has_many :lyrics, dependent: :destroy |
| 32 | has_many :songs, dependent: :destroy |
| 33 | has_many :albums, dependent: :destroy |
| 34 | has_many :artists, dependent: :destroy |
| 35 | has_many :javanese_gendings, dependent: :destroy |
| 36 | has_many :comments, dependent: :delete_all |
| 37 | |
| 38 | # Invitable |
| 39 | has_many :invitees, class_name: 'User', foreign_key: :invited_by_id |
| 40 | |
| 41 | scope :all_public, -> { where(is_private: false).all } |
| 42 | |
| 43 | has_many :active_relationships, class_name: 'Relationship', |
| 44 | foreign_key: 'follower_id', |
| 45 | dependent: :destroy |
| 46 | has_many :passive_relationships, class_name: 'Relationship', |
| 47 | foreign_key: 'followed_id', |
| 48 | dependent: :destroy |
| 49 | has_many :following, through: :active_relationships, source: :followed |
| 50 | has_many :followers, through: :passive_relationships, source: :follower |
| 51 | |
| 52 | has_many :authorizations, dependent: :destroy |
| 53 | |
| 54 | has_many :song_chord_pleas, dependent: :destroy |
| 55 | has_many :javanese_gending_notations, dependent: :destroy |
| 56 | |
| 57 | has_one :user_setting, dependent: :destroy |
| 58 | has_many :wallets, dependent: :destroy |
| 59 | |
| 60 | has_many :comments, as: :commentable |
| 61 | |
| 62 | # Follows a user. |
| 63 | def follow(other_user) |
| 64 | active_relationships.create(followed_id: other_user.id) |
| 65 | end |
| 66 | |
| 67 | # Unfollows a user. |
| 68 | def unfollow(other_user) |
| 69 | active_relationships.find_by(followed_id: other_user.id).destroy |
| 70 | end |
| 71 | |
| 72 | # Returns true if the current user is following the other user. |
| 73 | def following?(other_user) |
| 74 | following.include?(other_user) |
| 75 | end |
| 76 | |
| 77 | # Returns the hash digest of the given string. |
| 78 | def self.digest(string) |
| 79 | cost = if ActiveModel::SecurePassword.min_cost |
| 80 | BCrypt::Engine::MIN_COST |
| 81 | else |
| 82 | BCrypt::Engine.cost |
| 83 | end |
| 84 | BCrypt::Password.create(string, cost: cost) |
| 85 | end |
| 86 | |
| 87 | # Returns a random token |
| 88 | def self.new_token |
| 89 | SecureRandom.urlsafe_base64 |
| 90 | end |
| 91 | |
| 92 | # Remembers a user in the database for use in persistent sessions |
| 93 | def remember |
| 94 | self.remember_token = User.new_token |
| 95 | update_attribute(:remember_digest, User.digest(remember_token)) |
| 96 | end |
| 97 | |
| 98 | # Returns true if the given token matches the digest |
| 99 | def authenticated?(remember_token) |
| 100 | return false if remember_digest.nil? |
| 101 | |
| 102 | BCrypt::Password.new(remember_digest).is_password?(remember_token) |
| 103 | end |
| 104 | |
| 105 | # Forgets a user |
| 106 | def forget |
| 107 | update_attribute(:remember_digest, nil) |
| 108 | end |
| 109 | |
| 110 | # Omniauth helpers |
| 111 | def self.new_with_session(params, session) |
| 112 | super.tap do |user| |
| 113 | if data == session['devise.facebook_data'] && session['devise.facebook_data']['extra']['raw_info'] && user.email.blank? |
| 114 | user.email = data['email'] |
| 115 | end |
| 116 | end |
| 117 | end |
| 118 | |
| 119 | def self.from_omniauth(auth) |
| 120 | return_user = where(email: auth.info.email).first |
| 121 | return_user ||= create do |user| |
| 122 | user.name = auth.info.name |
| 123 | user.email = auth.info.email |
| 124 | end |
| 125 | save_or_update_provider(return_user, auth) |
| 126 | return_user |
| 127 | end |
| 128 | |
| 129 | def self.from_omniauth_web3(auth, pubkey_email) |
| 130 | return_user = where(email: pubkey_email).first |
| 131 | # Throw error if we could not find user |
| 132 | raise 'User not found' if return_user.nil? |
| 133 | |
| 134 | save_or_update_provider(return_user, auth) |
| 135 | return_user |
| 136 | end |
| 137 | |
| 138 | def spotify |
| 139 | authorizations.find_by(user_id: id, provider: 'spotify') |
| 140 | end |
| 141 | |
| 142 | def google |
| 143 | authorizations.find_by(user_id: id, provider: 'google_oauth2') |
| 144 | end |
| 145 | |
| 146 | # General helpers |
| 147 | |
| 148 | def email_client |
| 149 | email.split('@').last |
| 150 | end |
| 151 | |
| 152 | def need_onboarding? |
| 153 | username.presence.nil? || need_onboarding_spotify? || need_onboarding_google? |
| 154 | end |
| 155 | |
| 156 | def need_onboarding_spotify? |
| 157 | (spotify.nil? || spotify&.pending?) |
| 158 | end |
| 159 | |
| 160 | def need_onboarding_google? |
| 161 | (google.nil? || google&.pending?) |
| 162 | end |
| 163 | |
| 164 | def as_json(options = {}) |
| 165 | options[:methods] = %i[gravatar_url followers_count following_count] |
| 166 | super |
| 167 | end |
| 168 | |
| 169 | def as_json_packed |
| 170 | as_json(only: %i[id email username name about]) |
| 171 | end |
| 172 | |
| 173 | def followers_count |
| 174 | followers.size |
| 175 | end |
| 176 | |
| 177 | def following_count |
| 178 | following.size |
| 179 | end |
| 180 | |
| 181 | def gravatar_url |
| 182 | gravatar_id = Digest::MD5.hexdigest(email.downcase) |
| 183 | "https://secure.gravatar.com/avatar/#{gravatar_id}" |
| 184 | end |
| 185 | |
| 186 | def username_or_id |
| 187 | username.presence ? username : id |
| 188 | end |
| 189 | |
| 190 | def username_or_name |
| 191 | username.presence ? username : name |
| 192 | end |
| 193 | |
| 194 | def splitfire_link(item) |
| 195 | "#{ENV['SF_HOST']}/@#{username_or_id}/#{item.source_file.filename.to_s.downcase.parameterize}-#{item.id}" |
| 196 | end |
| 197 | |
| 198 | def splitfire_profile_link |
| 199 | "#{ENV['SF_HOST']}/@#{username_or_id}" |
| 200 | end |
| 201 | |
| 202 | def verified? |
| 203 | %w[seto splitfire bonzo karokowe bass64 ahmad yovie nowhereman].include? username |
| 204 | end |
| 205 | |
| 206 | def reviewer? |
| 207 | verified? |
| 208 | end |
| 209 | |
| 210 | def self.save_or_update_provider(user, auth) |
| 211 | authorization = user.authorizations.find_by(provider: auth.provider, uid: auth.uid) |
| 212 | authorization ||= Authorization.new(user_id: user.id, provider: auth.provider, uid: auth.uid) |
| 213 | authorization.update_token(auth.credentials.token, auth.credentials.refresh_token) |
| 214 | end |
| 215 | |
| 216 | protected |
| 217 | |
| 218 | # Disable password for wallet connection |
| 219 | |
| 220 | def password_required? |
| 221 | return false if skip_password_validation |
| 222 | |
| 223 | super |
| 224 | end |
| 225 | end |