bih/spotify-ruby

View on GitHub
lib/spotify/accounts/session.rb

Summary

Maintainability
A
0 mins
Test Coverage
# frozen_string_literal: true

module Spotify
  class Accounts
    ##
    # A class representing an access token, with the ability to refresh.
    #
    class Session
      class << self
        ##
        # Parse the response we collect from the authorization code.
        #
        # @example
        #   @session = Spotify::Accounts.from_authorization_code(@accounts, "authorization code here")
        #
        # @param [Spotify::Accounts] accounts A valid instance of Spotify::Accounts.
        # @param [String] code The code provided in the Redirect URI from the Spotify Accounts API.
        # @return [Spotify::Accounts::Session] access_token An instance of Spotify::Accounts::Session
        # @see lib/spotify/accounts.rb
        #
        def from_authorization_code(accounts, code)
          params = {
            client_id:     @accounts.instance_variable_get(:@client_id),
            client_secret: @accounts.instance_variable_get(:@client_secret),
            redirect_uri:  @accounts.instance_variable_get(:@redirect_uri),
            grant_type:    "authorization_code",
            code:          code
          }
          request = HTTParty.post("https://accounts.spotify.com/api/token", body: params)
          response = request.parsed_response.with_indifferent_access
          raise response[:error_description] if response[:error]

          new(accounts, response[:access_token], response[:expires_in], response[:refresh_token], response[:scope])
        end

        ##
        # Set up an instance of Access Token with just a refresh_token.
        #
        # @example
        #   @access_token = Spotify::Accounts::Session.from_refresh_token(@accounts, "refresh token here")
        #   @access_token.force_refresh!
        #
        # @param [Spotify::Accounts] accounts A valid instance of Spotify::Accounts.
        # @param [String] refresh_token A valid refresh token. You'll want to store the refresh_token in your database.
        # @return [Spotify::Accounts::Session] access_token An instance of Spotify::Accounts::Session
        #
        def from_refresh_token(accounts, refresh_token)
          new(accounts, nil, nil, refresh_token, nil)
        end
      end

      def initialize(accounts, access_token, expires_in, refresh_token, scopes)
        unless accounts.instance_of?(Spotify::Accounts)
          raise "You need a valid Spotify::Accounts instance in order to use Spotify authentication."
        end

        @accounts = accounts
        @access_token = access_token
        @expires_in = expires_in
        @expires_at = expires_in + Time.now.to_i unless expires_in.nil?
        @refresh_token = refresh_token
        @scopes = scopes
      end

      attr_reader :accounts, :access_token, :expires_in, :refresh_token

      ##
      # Converts the space-delimited scope list to a symbolized array.
      #
      # @example
      #   @access_token.scopes # => [:"user-read-private", :"user-top-read", ...]
      #
      # @return [Array] scopes A symbolized list of scopes.
      #
      def scopes
        return [] if @scopes.nil?

        @scopes.split(" ").map(&:to_sym)
      end

      ##
      # Checks if a specific scope has been granted by the user.
      #
      # @example
      #   @access_token.contains_scope?("user-read-top")
      #   @access_token.contains_scope?(:"user-read-top")
      #
      # @param [String,Symbol] scope The name of the scope you'd like to check. For example, "user-read-private".
      # @return [TrueClass,FalseClass] scope_included A true/false boolean if the scope is included.
      #
      def contains_scope?(scope)
        scopes.include?(scope.downcase.to_sym)
      end

      ##
      # When will the access token expire? Returns nil if no expires_in is defined.
      #
      # @example
      #   @session.expires_at
      #
      # @return [Time] time When the access token will expire.
      #
      def expires_at
        return nil if @expires_in.nil?

        Time.at(@expires_at)
      end

      ##
      # Check if the access token has expired. Returns nil if no expires_in is defined.
      #
      # @example
      #   @session.expired?
      #
      # @return [TrueClass,FalseClass,NilClass] has_expired Has the access token expired?
      #
      def expired?
        return nil if expires_at.nil?

        Time.now > expires_at
      end

      ##
      # Refresh the access token.
      #
      # @example
      #   @session.refresh!
      #
      # @return [TrueClass,FalseClass] success Have we been able to refresh the access token?
      #
      # rubocop:disable AbcSize
      def refresh!
        raise "You cannot refresh without a valid refresh_token." if @refresh_token.nil?

        params = {
          client_id:     @accounts.instance_variable_get(:@client_id),
          client_secret: @accounts.instance_variable_get(:@client_secret),
          grant_type:    "refresh_token",
          refresh_token: @refresh_token
        }
        request = HTTParty.post("https://accounts.spotify.com/api/token", body: params)
        response = request.parsed_response.with_indifferent_access

        @access_token = response[:access_token]
        @expires_in = response[:expires_in]
        @expires_at = response[:expires_in] + Time.now.to_i
        @scopes = response[:scope]

        true
      rescue HTTParty::Error
        false
      end
      # rubocop:enable AbcSize

      ##
      # Export to JSON. Designed mostly for iOS, Android, or external use cases.
      #
      # @example
      #   @session.to_json
      #
      # @return [String] json The JSON output of the session instance.
      #
      def to_json(*_args)
        {
          access_token:  @access_token.presence,
          expires_at:    @expires_at.presence,
          refresh_token: @refresh_token.presence,
          scopes:        scopes
        }.to_json
      end

      def inspect # :nodoc:
        "#<%s:0x00%x>" % [self.class.name, (object_id << 1)]
      end
    end
  end
end