Skip to content

Commit c955ddb

Browse files
committed
add scope checks
Signed-off-by: Adam Ratzman <[email protected]>
1 parent d84bf8d commit c955ddb

File tree

15 files changed

+353
-216
lines changed

15 files changed

+353
-216
lines changed

src/commonMain/kotlin/com.adamratzman.spotify/SpotifyApi.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import com.adamratzman.spotify.endpoints.client.ClientPersonalizationApi
1111
import com.adamratzman.spotify.endpoints.client.ClientPlayerApi
1212
import com.adamratzman.spotify.endpoints.client.ClientPlaylistApi
1313
import com.adamratzman.spotify.endpoints.client.ClientProfileApi
14-
import com.adamratzman.spotify.endpoints.client.ClientSearchApi
1514
import com.adamratzman.spotify.endpoints.client.ClientShowApi
1615
import com.adamratzman.spotify.endpoints.pub.AlbumApi
1716
import com.adamratzman.spotify.endpoints.pub.ArtistApi
@@ -491,8 +490,7 @@ public open class SpotifyClientApi(
491490
override val browse: BrowseApi = BrowseApi(this)
492491
override val artists: ArtistApi = ArtistApi(this)
493492
override val tracks: TrackApi = TrackApi(this)
494-
495-
override val search: ClientSearchApi = ClientSearchApi(this)
493+
override val search: SearchApi = SearchApi(this)
496494

497495
override val episodes: ClientEpisodeApi = ClientEpisodeApi(this)
498496
override val shows: ClientShowApi = ClientShowApi(this)

src/commonMain/kotlin/com.adamratzman.spotify/SpotifyExceptions.kt

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,45 +9,74 @@ public sealed class SpotifyException(message: String, cause: Throwable? = null)
99
public abstract class UnNullableException(message: String) : SpotifyException(message)
1010

1111
/**
12-
* Thrown when a request fails
12+
* Thrown when a request fails.
13+
*
14+
* @param statusCode The status code of the request, if this exception is thrown after the completion of an HTTP request.
15+
* @param reason The reason for the failure, as a readable message.
1316
*/
14-
public open class BadRequestException(message: String, public val statusCode: Int? = null, public val reason: String? = null, cause: Throwable? = null) :
15-
SpotifyException(message, cause) {
17+
public open class BadRequestException(
18+
message: String,
19+
public val statusCode: Int? = null,
20+
public val reason: String? = null,
21+
cause: Throwable? = null
22+
) :
23+
SpotifyException(message, cause) {
1624
public constructor(message: String, cause: Throwable? = null) : this(message, null, null, cause)
1725
public constructor(error: ErrorObject?, cause: Throwable? = null) : this(
18-
"Received Status Code ${error?.status}. Error cause: ${error?.message}" + (error?.reason?.let { ". Reason: ${error.reason}" }
19-
?: ""),
20-
error?.status,
21-
error?.reason,
22-
cause
26+
"Received Status Code ${error?.status}. Error cause: ${error?.message}" + (error?.reason?.let { ". Reason: ${error.reason}" }
27+
?: ""),
28+
error?.status,
29+
error?.reason,
30+
cause
2331
)
2432

2533
public constructor(authenticationError: AuthenticationError) :
2634
this(
27-
"Authentication error: ${authenticationError.error}. Description: ${authenticationError.description}",
28-
401
35+
"Authentication error: ${authenticationError.error}. Description: ${authenticationError.description}",
36+
401
2937
)
3038

3139
public constructor(responseException: ResponseException) :
3240
this(
33-
responseException.message ?: "Bad Request",
34-
responseException.response.status.value,
35-
null,
36-
responseException
41+
responseException.message ?: "Bad Request",
42+
responseException.response.status.value,
43+
null,
44+
responseException
3745
)
3846
}
3947

48+
/**
49+
* Exception signifying that JSON (de)serialization failed. This is likely a library error rather than user error.
50+
*/
4051
public class ParseException(message: String, cause: Throwable? = null) : SpotifyException(message, cause)
4152

53+
/**
54+
* Exception signifying that authentication (via token or code) was unsuccessful, likely due to an invalid access token, code,
55+
* or refresh token.
56+
*/
4257
public class AuthenticationException(message: String, cause: Throwable? = null) : SpotifyException(message, cause) {
4358
public constructor(authenticationError: AuthenticationError?) :
4459
this("Authentication error: ${authenticationError?.error}. Description: ${authenticationError?.description}")
4560
}
4661

62+
/**
63+
* Exception signifying that the HTTP request associated with this API endpoint timed out (by default, after 100 seconds).
64+
*/
4765
public class TimeoutException(message: String, cause: Throwable? = null) : SpotifyException(message, cause)
4866

4967
/**
5068
* Exception signifying that re-authentication via spotify-auth is necessary. Thrown by default when refreshTokenProducer is null.
5169
*/
52-
public class ReAuthenticationNeededException(cause: Throwable? = null, message: String? = null) : SpotifyException(message ?: "Re-authentication is needed.", cause)
70+
public class ReAuthenticationNeededException(cause: Throwable? = null, message: String? = null) :
71+
SpotifyException(message ?: "Re-authentication is needed.", cause)
72+
73+
/**
74+
* Exception signifying that the current api token does not have the necessary scope to complete this request
75+
*
76+
*/
77+
public class SpotifyScopesNeededException(cause: Throwable? = null, public val missingScopes: List<SpotifyScope>) :
78+
BadRequestException(
79+
cause = cause,
80+
message = "You tried to call a method that requires the following missing scopes: $missingScopes. Please make sure that your token is requested with these scopes."
81+
)
5382
}

src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientEpisodeApi.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ public class ClientEpisodeApi(api: GenericSpotifyApi) : EpisodeApi(api) {
5252
*
5353
* @return possibly-null [Episode].
5454
*/
55-
public fun getEpisodeRestAction(id: String): SpotifyRestAction<Episode?> = SpotifyRestAction { getEpisode(id) }
55+
public fun getEpisodeRestAction(id: String): SpotifyRestAction<Episode?> {
56+
return SpotifyRestAction { getEpisode(id) }
57+
}
5658

5759
/**
5860
* Get Spotify catalog information for multiple episodes based on their Spotify IDs. The [Market] associated with
@@ -70,7 +72,9 @@ public class ClientEpisodeApi(api: GenericSpotifyApi) : EpisodeApi(api) {
7072
* @throws BadRequestException If any invalid show id is provided
7173
*/
7274
public suspend fun getEpisodes(vararg ids: String): List<Episode?> {
75+
requireScopes(SpotifyScope.USER_READ_PLAYBACK_POSITION)
7376
checkBulkRequesting(50, ids.size)
77+
7478
return bulkRequest(50, ids.toList()) { chunk ->
7579
get(
7680
endpointBuilder("/episodes")
@@ -95,7 +99,9 @@ public class ClientEpisodeApi(api: GenericSpotifyApi) : EpisodeApi(api) {
9599
* @return List of possibly-null [Episode] objects.
96100
* @throws BadRequestException If any invalid show id is provided
97101
*/
98-
public fun getEpisodesRestAction(vararg ids: String): SpotifyRestAction<List<Episode?>> = SpotifyRestAction {
99-
getEpisodes(*ids)
102+
public fun getEpisodesRestAction(vararg ids: String): SpotifyRestAction<List<Episode?>> {
103+
return SpotifyRestAction {
104+
getEpisodes(*ids)
105+
}
100106
}
101107
}

src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientFollowingApi.kt

Lines changed: 54 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ public class ClientFollowingApi(api: GenericSpotifyApi) : FollowingApi(api) {
3636
* @throws BadRequestException if [user] is a non-existing id
3737
* @return Whether the current user is following [user]
3838
*/
39-
public suspend fun isFollowingUser(user: String): Boolean = isFollowingUsers(user)[0]
39+
public suspend fun isFollowingUser(user: String): Boolean {
40+
requireScopes(SpotifyScope.USER_FOLLOW_READ)
41+
return isFollowingUsers(user)[0]
42+
}
4043

4144
/**
4245
* Check to see if the current user is following another Spotify user.
@@ -50,8 +53,9 @@ public class ClientFollowingApi(api: GenericSpotifyApi) : FollowingApi(api) {
5053
* @throws BadRequestException if [user] is a non-existing id
5154
* @return Whether the current user is following [user]
5255
*/
53-
public fun isFollowingUserRestAction(user: String): SpotifyRestAction<Boolean> =
54-
SpotifyRestAction { isFollowingUser(user) }
56+
public fun isFollowingUserRestAction(user: String): SpotifyRestAction<Boolean> {
57+
return SpotifyRestAction { isFollowingUser(user) }
58+
}
5559

5660
/**
5761
* Check to see if the current Spotify user is following the specified playlist.
@@ -68,11 +72,12 @@ public class ClientFollowingApi(api: GenericSpotifyApi) : FollowingApi(api) {
6872
* @throws [BadRequestException] if the playlist is not found
6973
* @return Whether the current user is following [playlistId]
7074
*/
71-
public suspend fun isFollowingPlaylist(playlistId: String): Boolean =
72-
isFollowingPlaylist(
75+
public suspend fun isFollowingPlaylist(playlistId: String): Boolean {
76+
return isFollowingPlaylist(
7377
playlistId,
7478
(api as SpotifyClientApi).getUserId()
7579
)
80+
}
7681

7782
/**
7883
* Check to see if the current Spotify user is following the specified playlist.
@@ -105,6 +110,7 @@ public class ClientFollowingApi(api: GenericSpotifyApi) : FollowingApi(api) {
105110
* @return A list of booleans corresponding to [users] of whether the current user is following that user
106111
*/
107112
public suspend fun isFollowingUsers(vararg users: String): List<Boolean> {
113+
requireScopes(SpotifyScope.USER_FOLLOW_READ)
108114
checkBulkRequesting(50, users.size)
109115
return bulkRequest(50, users.toList()) { chunk ->
110116
get(
@@ -126,8 +132,9 @@ public class ClientFollowingApi(api: GenericSpotifyApi) : FollowingApi(api) {
126132
* @throws BadRequestException if [users] contains a non-existing id
127133
* @return A list of booleans corresponding to [users] of whether the current user is following that user
128134
*/
129-
public fun isFollowingUsersRestAction(vararg users: String): SpotifyRestAction<List<Boolean>> =
130-
SpotifyRestAction { isFollowingUsers(*users) }
135+
public fun isFollowingUsersRestAction(vararg users: String): SpotifyRestAction<List<Boolean>> {
136+
return SpotifyRestAction { isFollowingUsers(*users) }
137+
}
131138

132139
/**
133140
* Check to see if the current user is following a Spotify artist.
@@ -155,8 +162,9 @@ public class ClientFollowingApi(api: GenericSpotifyApi) : FollowingApi(api) {
155162
* @throws BadRequestException if [artist] is a non-existing id
156163
* @return Whether the current user is following [artist]
157164
*/
158-
public fun isFollowingArtistRestAction(artist: String): SpotifyRestAction<Boolean> =
159-
SpotifyRestAction { isFollowingArtist(artist) }
165+
public fun isFollowingArtistRestAction(artist: String): SpotifyRestAction<Boolean> {
166+
return SpotifyRestAction { isFollowingArtist(artist) }
167+
}
160168

161169
/**
162170
* Check to see if the current user is following one or more artists.
@@ -171,6 +179,7 @@ public class ClientFollowingApi(api: GenericSpotifyApi) : FollowingApi(api) {
171179
* @return A list of booleans corresponding to [artists] of whether the current user is following that artist
172180
*/
173181
public suspend fun isFollowingArtists(vararg artists: String): List<Boolean> {
182+
requireScopes(SpotifyScope.USER_FOLLOW_READ)
174183
checkBulkRequesting(50, artists.size)
175184
return bulkRequest(50, artists.toList()) { chunk ->
176185
get(
@@ -192,8 +201,9 @@ public class ClientFollowingApi(api: GenericSpotifyApi) : FollowingApi(api) {
192201
* @throws BadRequestException if [artists] contains a non-existing id
193202
* @return A list of booleans corresponding to [artists] of whether the current user is following that artist
194203
*/
195-
public fun isFollowingArtistsRestAction(vararg artists: String): SpotifyRestAction<List<Boolean>> =
196-
SpotifyRestAction { isFollowingArtists(*artists) }
204+
public fun isFollowingArtistsRestAction(vararg artists: String): SpotifyRestAction<List<Boolean>> {
205+
return SpotifyRestAction { isFollowingArtists(*artists) }
206+
}
197207

198208
/**
199209
* Get the current user’s followed artists.
@@ -211,12 +221,15 @@ public class ClientFollowingApi(api: GenericSpotifyApi) : FollowingApi(api) {
211221
public suspend fun getFollowedArtists(
212222
limit: Int? = api.spotifyApiOptions.defaultLimit,
213223
after: String? = null
214-
): CursorBasedPagingObject<Artist> = get(
215-
endpointBuilder("/me/following").with("type", "artist").with("limit", limit).with(
216-
"after",
217-
after
218-
).toString()
219-
).toCursorBasedPagingObject(Artist::class, Artist.serializer(), "artists", api, json)
224+
): CursorBasedPagingObject<Artist> {
225+
requireScopes(SpotifyScope.USER_FOLLOW_READ)
226+
return get(
227+
endpointBuilder("/me/following").with("type", "artist").with("limit", limit).with(
228+
"after",
229+
after
230+
).toString()
231+
).toCursorBasedPagingObject(Artist::class, Artist.serializer(), "artists", api, json)
232+
}
220233

221234
/**
222235
* Get the current user’s followed artists.
@@ -270,6 +283,7 @@ public class ClientFollowingApi(api: GenericSpotifyApi) : FollowingApi(api) {
270283
* @throws BadRequestException if an invalid id is provided
271284
*/
272285
public suspend fun followUsers(vararg users: String) {
286+
requireScopes(SpotifyScope.USER_FOLLOW_MODIFY)
273287
checkBulkRequesting(50, users.size)
274288
bulkRequest(50, users.toList()) { chunk ->
275289
put(
@@ -313,7 +327,8 @@ public class ClientFollowingApi(api: GenericSpotifyApi) : FollowingApi(api) {
313327
*
314328
* @throws BadRequestException if an invalid id is provided
315329
*/
316-
public fun followArtistRestAction(artistId: String): SpotifyRestAction<Unit> = SpotifyRestAction { followArtist(artistId) }
330+
public fun followArtistRestAction(artistId: String): SpotifyRestAction<Unit> =
331+
SpotifyRestAction { followArtist(artistId) }
317332

318333
/**
319334
* Add the current user as a follower of other artists
@@ -327,6 +342,7 @@ public class ClientFollowingApi(api: GenericSpotifyApi) : FollowingApi(api) {
327342
* @throws BadRequestException if an invalid id is provided
328343
*/
329344
public suspend fun followArtists(vararg artists: String) {
345+
requireScopes(SpotifyScope.USER_FOLLOW_MODIFY)
330346
checkBulkRequesting(50, artists.size)
331347
bulkRequest(50, artists.toList()) { chunk ->
332348
put(
@@ -369,10 +385,14 @@ public class ClientFollowingApi(api: GenericSpotifyApi) : FollowingApi(api) {
369385
*
370386
* @throws BadRequestException if the playlist is not found
371387
*/
372-
public suspend fun followPlaylist(playlist: String, followPublicly: Boolean = true): String = put(
373-
endpointBuilder("/playlists/${PlaylistUri(playlist).id}/followers").toString(),
374-
"{\"public\": $followPublicly}"
375-
)
388+
public suspend fun followPlaylist(playlist: String, followPublicly: Boolean = true): String {
389+
requireScopes(SpotifyScope.PLAYLIST_MODIFY_PUBLIC, SpotifyScope.PLAYLIST_MODIFY_PRIVATE, anyOf = true)
390+
391+
return put(
392+
endpointBuilder("/playlists/${PlaylistUri(playlist).id}/followers").toString(),
393+
"{\"public\": $followPublicly}"
394+
)
395+
}
376396

377397
/**
378398
* Add the current user as a follower of a playlist.
@@ -393,10 +413,13 @@ public class ClientFollowingApi(api: GenericSpotifyApi) : FollowingApi(api) {
393413
*
394414
* @throws BadRequestException if the playlist is not found
395415
*/
396-
public fun followPlaylistRestAction(playlist: String, followPublicly: Boolean): SpotifyRestAction<String> =
397-
SpotifyRestAction {
416+
public fun followPlaylistRestAction(playlist: String, followPublicly: Boolean): SpotifyRestAction<String> {
417+
requireScopes(SpotifyScope.PLAYLIST_MODIFY_PUBLIC, SpotifyScope.PLAYLIST_MODIFY_PRIVATE, anyOf = true)
418+
419+
return SpotifyRestAction {
398420
followPlaylist(playlist, followPublicly)
399421
}
422+
}
400423

401424
/**
402425
* Remove the current user as a follower of another user
@@ -437,6 +460,7 @@ public class ClientFollowingApi(api: GenericSpotifyApi) : FollowingApi(api) {
437460
* @throws BadRequestException if an invalid id is provided
438461
*/
439462
public suspend fun unfollowUsers(vararg users: String) {
463+
requireScopes(SpotifyScope.USER_FOLLOW_MODIFY)
440464
checkBulkRequesting(50, users.size)
441465
bulkRequest(50, users.toList()) { list ->
442466
delete(
@@ -502,6 +526,7 @@ public class ClientFollowingApi(api: GenericSpotifyApi) : FollowingApi(api) {
502526
* @throws BadRequestException if an invalid id is provided
503527
*/
504528
public suspend fun unfollowArtists(vararg artists: String) {
529+
requireScopes(SpotifyScope.USER_FOLLOW_MODIFY)
505530
checkBulkRequesting(50, artists.size)
506531
bulkRequest(50, artists.toList()) { list ->
507532
delete(
@@ -542,8 +567,11 @@ public class ClientFollowingApi(api: GenericSpotifyApi) : FollowingApi(api) {
542567
*
543568
* @throws BadRequestException if the playlist is not found
544569
*/
545-
public suspend fun unfollowPlaylist(playlist: String): String =
546-
delete(endpointBuilder("/playlists/${PlaylistUri(playlist).id}/followers").toString())
570+
public suspend fun unfollowPlaylist(playlist: String): String {
571+
requireScopes(SpotifyScope.PLAYLIST_MODIFY_PUBLIC, SpotifyScope.PLAYLIST_MODIFY_PRIVATE, anyOf = true)
572+
573+
return delete(endpointBuilder("/playlists/${PlaylistUri(playlist).id}/followers").toString())
574+
}
547575

548576
/**
549577
* Remove the current user as a follower of a playlist.

0 commit comments

Comments
 (0)