Spanfile / Ferrispot

A wrapper for the Spotify Web API
Apache License 2.0
1 stars 1 forks source link

0.4.0 The endpoint is forbidden #3

Closed ShayBox closed 1 year ago

ShayBox commented 1 year ago

I'm using 0.4.0 and getting The endpoint is forbidden with any playback state control such as pause, next, previous, etc.
I believe I have all the required scopes and I've re-authorized to make sure my token is updated.

Log: https://pastebin.com/QvqnnNes
Source: https://pastebin.com/jBZvMD1M
Full Source: https://pastebin.com/b9qA7Jaq

Spanfile commented 1 year ago

I can't reproduce the issue on my end so it must be that your token doesn't actually have the required scopes. I see you're requesting the correct scopes, however you should set .show_dialog(true) on the authorization code client builder to force the user to have to re-approve the application with the new scopes. Adding new scopes won't retroactively apply to existing tokens.

ShayBox commented 1 year ago

I tried show_dialog, removing access to my app, creating a new app, and creating a test project, I can't get it to work

use anyhow::bail;
use ferrispot::{client::SpotifyClientBuilder, prelude::*, scope::Scope};
use tiny_http::{Header, Response, Server};
use url::Url;

fn main() -> Result<(), anyhow::Error> {
    let spotify_client = SpotifyClientBuilder::new("fea982db1b3e4f3993fb1d1e87903d56")
        .client_secret("c58e9fd4814c4ffca383389453ecd5b7")
        .build_sync()?;

    let redirect_uri = "http://127.0.0.1:2345";
    let incomplete_auth_code_client = spotify_client
        .authorization_code_client(redirect_uri)
        .scopes([Scope::UserModifyPlaybackState, Scope::UserReadPlaybackState])
        .show_dialog(true)
        .build();

    let authorize_url = incomplete_auth_code_client.get_authorize_url();
    let (code, state) = get_user_authorization(&authorize_url, redirect_uri)?;
    let user_client = incomplete_auth_code_client.finalize(code.trim(), state.trim())?;

    user_client.pause().send_sync()?;

    Ok(())
}

fn get_user_authorization(
    authorize_url: &str,
    redirect_uri: &str,
) -> Result<(String, String), anyhow::Error> {
    match webbrowser::open(authorize_url) {
        Ok(ok) => ok,
        Err(why) => eprintln!(
            "Error when trying to open an URL in your browser: {:?}. \
             Please navigate here manually: {}",
            why, authorize_url
        ),
    }

    let addr = redirect_uri.replace("http://", "").replace("https://", "");
    let server = Server::http(addr).expect("Failed to bind server");
    let request = match server.recv() {
        Ok(rq) => rq,
        Err(e) => panic!("Failed to get request: {e}"),
    };

    let request_url = redirect_uri.to_owned() + request.url();
    let parsed_url = Url::parse(&request_url)?;

    let header = Header::from_bytes(&b"Content-Type"[..], &b"text/html"[..]).unwrap();
    let mut response;
    if parsed_url.query_pairs().count() == 2 {
        response = Response::from_string(
            "<h1>You may close this tab</h1> \
                <script>window.close()</script>",
        );
    } else {
        response = Response::from_string("<h1>An error has occurred</h1>");
    }

    response.add_header(header);
    request.respond(response).expect("Failed to send response");

    let Some(code) = parsed_url.query_pairs().next() else {
        bail!("None")
    };
    let Some(state) = parsed_url.query_pairs().nth(1) else {
        bail!("None")
    };

    Ok((code.1.into(), state.1.into()))
}
[package]
name = "test"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
anyhow = "1.0.66"
ferrispot = { version = "0.4.0", features = ["sync"] }
tiny_http = "0.12.0"
url = "2.3.1"
webbrowser = "0.8.2"

If you'd like to try it on my app I can whitelist you, but like I said I tried two apps with the same results, I have no idea why it works for you but not me 🤔

Spanfile commented 1 year ago

Alright, I've figured it out. I was confused what Spotify responds with when the access token expires, which is indeed a 401 Unauthorized, not a 403 Forbidden. The 403 here is caused by the player command being restricted for the current playback. CurrentlyPlayingItem has an .actions() object that has a disallows field that contains booleans that tell whether or not certain actions are disallowed on the current playback. For example, pausing an already paused playback is disallowed (pausing = true), or skipping an ad with .next() is disallowed (skipping_next = true). Attempting a disallowed action will return a 403 Forbidden. I'll have to update the error handling to account for this case and return a more descriptive error.

Also, you should rotate that client secret if you haven't already.

ShayBox commented 1 year ago

I don't know if you finished working or not, but I tested the latest commit
I logged the actions and got

Actions {
    disallows: Disallows {
        interrupting_playback: false,
        pausing: false,
        resuming: true,
        seeking: false,
        skipping_next: false,
        skipping_prev: false,
        toggling_repeat_context: false,
        toggling_shuffle: false,
        toggling_repeat_track: false,
        transferring_playback: false,
    },
}
Error: The endpoint is forbidden

The attempted action was pause, which should be allowed (and appears to be?)

Spanfile commented 1 year ago

Can you run the program with logging enabled, or run the same request in the Spotify developer console? I'd like to know what the API error response is if it's not about restricted player controls.

ShayBox commented 1 year ago

Ah, well that's dumb

{
  "error": {
    "status": 403,
    "message": "Player command failed: Premium required",
    "reason": "PREMIUM_REQUIRED"
  }
}
Spanfile commented 1 year ago

I had a hunch that was it. I'll add an error for it, thanks!