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
pub mod api;
pub mod fetch;
pub mod ratelimit;

use api::stashes::PublicStashesResponse;
use ratelimit::limiter::RateLimiter;
use reqwest::{
    header::{HeaderMap, HeaderValue, ACCEPT},
    StatusCode,
};
use thiserror::Error;

pub type HttpStatusCode = StatusCode;

#[derive(Debug, Error)]
pub enum ClientError {
    #[error("encountered error with HTTP status code: {0}")]
    HttpError(StatusCode),
    #[error("reqwest failed to process: {0}")]
    ReqwestError(#[from] reqwest::Error),
    #[error("encountered rate limit")]
    RateLimited,
    #[error("failed to authenticate or authentication was rejected")]
    AuthError,
    #[error("invalid request")]
    BadRequest,
    #[error("unexpected internal error")]
    UnknownError,
}

pub struct Client<L: RateLimiter> {
    access_token: Option<String>,
    http_client: reqwest::Client,
    limiter: L,
}

impl<L: RateLimiter> Client<L> {
    pub fn new(user_agent: &str, rate_limiter: L) -> Result<Self, ClientError> {
        let mut default_headers = HeaderMap::new();
        default_headers.insert(ACCEPT, HeaderValue::from_static("application/json"));

        let http_client = reqwest::Client::builder()
            .user_agent(user_agent)
            .default_headers(default_headers)
            .build()
            .expect("API client should build successfully, did you provide a valid user agent?");

        Ok(Self {
            access_token: None,
            http_client,
            limiter: rate_limiter,
        })
    }

    pub async fn authorize(
        &mut self,
        client_id: &str,
        client_secret: &str,
    ) -> Result<(), ClientError> {
        let endpoint = "oauth/token";
        let request = self
            .http_client
            .post(format!("https://www.pathofexile.com/{endpoint}"))
            .form(&[
                ("client_id", client_id),
                ("client_secret", client_secret),
                ("grant_type", "client_credentials"),
            ]);

        let response = self.fetch_api_response(endpoint, request).await?;
        match response.status() {
            StatusCode::OK => {
                let body = response
                    .json::<serde_json::Value>()
                    .await
                    .map_err(ClientError::ReqwestError)?;

                self.access_token = Some(body["access_token"].as_str().unwrap().to_owned());

                Ok(())
            }
            _ => Err(ClientError::HttpError(response.status())),
        }
    }

    pub async fn get_public_stashes(
        &mut self,
        next_change_id: Option<&str>,
    ) -> Result<(PublicStashesResponse, StatusCode), ClientError> {
        let endpoint = "public-stash-tabs";

        let token = match &self.access_token {
            Some(t) => t,
            None => return Err(ClientError::AuthError),
        };

        let stash_url = match next_change_id {
            Some(id) => format!("https://api.pathofexile.com/{endpoint}?id={id}"),
            None => format!("https://api.pathofexile.com/{endpoint}"),
        };

        let request = self.http_client.get(stash_url).bearer_auth(token);

        let response = self.fetch_api_response(endpoint, request).await?;
        let status = response.status();
        match status {
            StatusCode::OK => {
                let body = response
                    .json::<PublicStashesResponse>()
                    .await
                    .map_err(ClientError::ReqwestError)?;

                Ok((body, status))
            }
            StatusCode::UNAUTHORIZED => {
                println!("unauthorized. debug headers: {:#?}", response.headers());

                Err(ClientError::AuthError)
            }
            _ => Err(ClientError::HttpError(response.status())),
        }
    }
}