hubbub/
context.rs

1use anyhow::Result;
2use http::{HeaderMap, HeaderValue, Method, StatusCode};
3use reqwest::Url;
4use serde_json::Value as JSON;
5
6use crate::error::Error;
7use crate::types::{
8    guild::CachedGuild,
9    user::{BotUser, User},
10};
11
12static BASE_URL: &str = "https://discord.com/";
13
14#[derive(Debug)]
15pub struct ResumeInfo {
16    pub url: String,
17    pub id: String,
18}
19
20#[derive(Default, Debug)]
21pub struct Cache {
22    pub users: Vec<User>,
23    pub guilds: Vec<CachedGuild>,
24}
25
26#[derive(Debug)]
27pub struct Context {
28    pub user: Option<BotUser>,
29    pub resume_info: Option<ResumeInfo>,
30    pub cache: Cache,
31    pub auth: Option<String>,
32    client: reqwest::Client,
33}
34
35impl Default for Context {
36    fn default() -> Self {
37        let mut headers: HeaderMap<HeaderValue> = HeaderMap::with_capacity(8);
38        headers.insert("Referrer", HeaderValue::from_static("https://discord.com"));
39        headers.insert(
40            "Sec-Ch-Ua",
41            HeaderValue::from_static(r#""Not(A:Brand";v="24", "Chromium";v="122""#),
42        );
43        headers.insert("Sec-Ch-Ua-Mobile", HeaderValue::from_static("?0"));
44        headers.insert("Sec-Ch-Ua-Platform", HeaderValue::from_static("\"Linux\""));
45
46        Self {
47            user: None,
48            resume_info: None,
49            cache: Cache::default(),
50            client: reqwest::Client::builder()
51                .default_headers(headers)
52                .user_agent("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36")
53                .build().expect("Couldn't build client"),
54            auth: None,
55        }
56    }
57}
58
59#[derive(Debug)]
60pub struct Response {
61    pub status: StatusCode,
62    pub headers: HeaderMap<HeaderValue>,
63    pub body: JSON,
64}
65
66impl Context {
67    pub fn set_auth(&mut self, token: String) {
68        self.auth = Some(token);
69    }
70
71    pub async fn request(
72        &mut self,
73        method: Method,
74        endpoint: &str,
75        body: Option<JSON>,
76    ) -> Result<Response> {
77        log::debug!(">> {} {}", method, endpoint);
78
79        let builder = self.client.request(
80            method,
81            Url::parse(BASE_URL)?.join(format!("/api/{}", endpoint).as_str())?,
82        );
83
84        let builder = match &self.auth {
85            Some(a) => builder.header("Authorization", HeaderValue::from_str(a.as_str())?),
86            None => builder,
87        };
88
89        let builder = match body {
90            Some(b) => builder
91                .body(serde_json::to_vec(&b)?)
92                .header("Content-Type", HeaderValue::from_str("application/json")?),
93            None => builder,
94        };
95
96        let res = self.client.execute(builder.build()?).await?;
97
98        let status = res.status();
99        let headers = res.headers().clone();
100        let text = res.text().await?;
101        log::debug!("<< {}", status);
102        log::trace!("{headers:?}");
103        log::trace!("{text}");
104
105        match status.as_u16() {
106            429 => {
107                Err(anyhow::anyhow!(Error::Ratelimit(
108                    std::time::Duration::from_secs(headers.get("Retry-After").unwrap().to_str().unwrap().parse::<u64>().unwrap() + 1)
109                )))
110            },
111            _ => Ok(Response {
112                status,
113                headers,
114                body: serde_json::from_str(text.as_str()).unwrap_or(JSON::Null),
115            }),
116        }
117    }
118}