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
#![doc = include_str!("../README.md")]

mod authentication;
mod bots;
mod channels;
mod customisation;
mod invites;
mod miscellaneous;
mod platform_administration;
mod revolt;
mod servers;
mod users;

use rive_models::{authentication::Authentication, ApiError};

type Result<T> = std::result::Result<T, Error>;

pub(crate) mod prelude {
    pub(crate) use crate::{ep, Client, RequestBuilderExt, ResponseExt, Result};
}

/// Base URL of the official Revolt instance API
pub const BASE_URL: &str = "https://api.revolt.chat";

/// Client error
#[derive(Debug, thiserror::Error)]
pub enum Error {
    /// Data serialization/deserialization error
    #[error("Serde JSON serialization/deserialization error: {0}")]
    Serialization(#[from] serde_json::Error),

    /// HTTP error
    #[error("Error while processing an HTTP request: {0}")]
    HttpRequest(#[from] reqwest::Error),

    /// An error returned from Revolt API
    #[error("Error returned from API: {0:#?}")]
    Api(ApiError),
}

macro_rules! ep {
    ($self:ident, $ep:literal, $($args:tt)*) => {
        format!(concat!("{}", $ep), $self.base_url, $($args)*)
    };

    ($self:ident, $ep:literal) => {
        format!(concat!("{}", $ep), $self.base_url)
    };

    (api_root = $api_root:expr, $ep:literal $($args:tt)*) => {
        format!(concat!("{}", $ep), $api_root, $($args)*)
    };
}
pub(crate) use ep;

trait RequestBuilderExt {
    fn auth(self, authentication: &Authentication) -> Self;
}

impl RequestBuilderExt for reqwest::RequestBuilder {
    fn auth(self, authentication: &Authentication) -> Self {
        self.header(authentication.header_key(), authentication.value())
    }
}

#[async_trait::async_trait]
trait ResponseExt {
    async fn process_error(self) -> Result<Self>
    where
        Self: Sized;
}

#[async_trait::async_trait]
impl ResponseExt for reqwest::Response {
    async fn process_error(self) -> Result<Self>
    where
        Self: Sized,
    {
        match self.status().as_u16() {
            200..=299 => Ok(self),
            // NOTE: it's a workaround thing but there are no alternative ways
            // because API returns some rocket's HTML instead of parseable JSON
            401 => Err(Error::Api(ApiError::Unauthenticated)),
            _ => Err(Error::Api(self.json().await?)),
        }
    }
}

#[derive(Debug, Clone)]
pub struct Client {
    base_url: String,
    client: reqwest::Client,
    authentication: Authentication,
}

impl Client {
    /// Create a client instance with the API base URL of Revolt official instance.
    pub fn new(authentication: Authentication) -> Self {
        Client::new_base_url(authentication, BASE_URL)
    }

    /// Create a client instance with given base URL.
    pub fn new_base_url(authentication: Authentication, base_url: impl Into<String>) -> Self {
        Client {
            base_url: base_url.into(),
            client: reqwest::Client::new(),
            authentication,
        }
    }
}