1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
use itertools::Itertools;
use url::Url;

/// A scope that the user can grant access to.
///
/// [Reference](https://developer.spotify.com/documentation/general/guides/scopes/).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[allow(missing_docs)]
pub enum Scope {
    UgcImageUpload,
    UserReadPlaybackState,
    UserModifyPlaybackState,
    UserReadCurrentlyPlaying,
    Streaming,
    AppRemoteControl,
    UserReadEmail,
    UserReadPrivate,
    PlaylistReadCollaborative,
    PlaylistModifyPublic,
    PlaylistReadPrivate,
    PlaylistModifyPrivate,
    UserLibraryModify,
    UserLibraryRead,
    UserTopRead,
    UserReadRecentlyPlayed,
    UserReadPlaybackPosition,
    UserFollowRead,
    UserFollowModify,
}

impl Scope {
    /// Get the scope as a string (in `kebab-case` like Spotify requires).
    ///
    /// # Examples
    ///
    /// ```
    /// let scope = aspotify::Sope::UserReadEmail;
    ///
    /// assert_eq!(scope.as_str(), "user-read-email");
    /// ```
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::UgcImageUpload => "ugc-image-upload",
            Self::UserReadPlaybackState => "user-read-playback-state",
            Self::UserModifyPlaybackState => "user-modify-playback-state",
            Self::UserReadCurrentlyPlaying => "user-read-currently-playing",
            Self::Streaming => "streaming",
            Self::AppRemoteControl => "app-remote-control",
            Self::UserReadEmail => "user-read-email",
            Self::UserReadPrivate => "user-read-private",
            Self::PlaylistReadCollaborative => "playlist-read-collaborative",
            Self::PlaylistModifyPublic => "playlist-modify-public",
            Self::PlaylistReadPrivate => "playlist-read-private",
            Self::PlaylistModifyPrivate => "playlist-modify-private",
            Self::UserLibraryModify => "user-library-modify",
            Self::UserLibraryRead => "user-library-read",
            Self::UserTopRead => "user-top-read",
            Self::UserReadRecentlyPlayed => "user-read-recently-played",
            Self::UserReadPlaybackPosition => "user-read-playback-position",
            Self::UserFollowRead => "user-follow-read",
            Self::UserFollowModify => "user-follow-modify",
        }
    }
}

/// Like [`authorization_url`], but you supply your own state.
///
/// It is recommended to use randomly generated state for security, so use this if you wish to use
/// your own random state generator.
///
/// This function, unlike [`authorization_url`] does not require features to be activated.
///
/// See the docs of the other function for information about the parameters.
pub fn authorization_url_with_state(
    client_id: &str,
    scopes: impl IntoIterator<Item = Scope>,
    force_approve: bool,
    redirect_uri: &str,
    state: &str,
) -> String {
    Url::parse_with_params(
        "https://accounts.spotify.com/authorize",
        &[
            ("response_type", "code"),
            ("state", &state),
            ("client_id", client_id),
            ("scope", &scopes.into_iter().map(Scope::as_str).join(" ")),
            ("show_dialog", if force_approve { "true" } else { "false" }),
            ("redirect_uri", redirect_uri),
        ],
    )
    .unwrap()
    .into_string()
}

/// Get the URL to redirect the user's browser to so that the URL can be generated for the
/// [`Client::redirected`](super::Client::redirected) function.
///
/// `force_approve`, if set, forces the user to approve the app again even if they already have.
/// Make sure that you have whitelisted the redirect uri in your Spotify dashboard, and
/// `redirect_uri` must not contain any query strings.
///
/// This method returns a tuple of the generated url and the state parameter, which is randomly
/// generated for security.
///
/// This function is only available when the `rand` feature of this library is activated, and it is
/// activated by default.
///
/// [Reference](https://developer.spotify.com/documentation/general/guides/authorization-guide/#1-have-your-application-request-authorization-the-user-logs-in-and-authorizes-access).
#[cfg(feature = "rand")]
pub fn authorization_url(
    client_id: &str,
    scopes: impl IntoIterator<Item = Scope>,
    force_approve: bool,
    redirect_uri: &str,
) -> (String, String) {
    use rand::Rng as _;

    const STATE_LEN: usize = 16;
    const STATE_CHARS: &[u8] =
        b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~";

    let mut rng = rand::thread_rng();
    let mut state = String::with_capacity(STATE_LEN);
    for _ in 0..STATE_LEN {
        state.push(STATE_CHARS[rng.gen_range(0..STATE_CHARS.len())].into());
    }

    (
        authorization_url_with_state(client_id, scopes, force_approve, redirect_uri, &state),
        state,
    )
}