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
124
125
126
127
128
129
130
//! The client for Slack API. <https://api.slack.com/methods>.

use sfr_types as st;

use crate::Request;
use st::{OauthV2AccessRequest, OauthV2AccessResponse};
use std::collections::HashMap;

/// The client for Slack API without OAuth.
#[derive(Clone)]
pub struct Client {
    /// The HTTP Client.
    client: reqwest::Client,

    /// The access_token.
    token: String,
}

/// The client for Slack API about OAuth.
#[derive(Clone)]
pub struct OauthClient {
    /// The HTTP Client.
    client: reqwest::Client,
}

impl Client {
    /// The constructor.
    pub fn new(client: reqwest::Client, token: String) -> Self {
        Self { client, token }
    }

    /// Returns the cloned inner HTTP client.
    pub fn clone_http_client(&self) -> reqwest::Client {
        self.client.clone()
    }

    /// Requests to Slack API.
    pub async fn request<R>(&self, request: R) -> Result<R::Response, st::Error>
    where
        R: Request,
    {
        request.request(self).await
    }

    /// Uploads files by `upload_url` from `files.getUploadURLExternal`.
    ///
    /// - `set`: (filename, (MIME type, file bytes))
    pub async fn upload_file_by_upload_file<M>(
        &self,
        upload_url: &str,
        set: M,
    ) -> Result<(), st::Error>
    where
        M: Into<HashMap<String, (&'static str, Vec<u8>)>>,
    {
        use reqwest::multipart::{Form, Part};

        let mut form = Form::new();
        for (filename, (mime, bytes)) in set.into().into_iter() {
            let part = Part::bytes(bytes)
                .file_name(filename.clone())
                .mime_str(mime)
                .map_err(st::Error::failed_creating_mulipart_data)?;
            form = form.part(filename.clone(), part);
        }

        let response = self
            .client()
            .post(upload_url)
            .multipart(form)
            .send()
            .await
            .map_err(|e| st::Error::failed_to_request_by_http("upload_url", e))?;
        tracing::debug!("response = {response:?}");

        Ok(())
    }

    /// Returns the reference of the inner HTTP client.
    pub(crate) fn client(&self) -> &reqwest::Client {
        &self.client
    }

    /// Returns the access_token.
    pub(crate) fn token(&self) -> &str {
        &self.token
    }
}

impl OauthClient {
    /// The constructor.
    pub fn new(client: reqwest::Client) -> Self {
        Self { client }
    }

    /// Requests `oauth.v2.access`.
    ///
    /// <https://api.slack.com/methods/oauth.v2.access>
    ///
    /// Can't implicit in `Client` since the token is obtained from `oauth_v2_access()`.
    pub async fn oauth_v2_access(
        &self,
        form: OauthV2AccessRequest<'_>,
    ) -> Result<OauthV2AccessResponse, st::Error> {
        #[allow(clippy::missing_docs_in_private_items)] // https://github.com/rust-lang/rust-clippy/issues/13298
        const URL: &str = "https://slack.com/api/oauth.v2.access";

        #[allow(clippy::missing_docs_in_private_items)] // https://github.com/rust-lang/rust-clippy/issues/13298
        const API_CODE: &str = "oauth.v2.access";

        tracing::debug!("form = {form:?}");

        let response = self
            .client
            .post(URL)
            .form(&form)
            .send()
            .await
            .map_err(|e| st::Error::failed_to_request_by_http(API_CODE, e))?;
        tracing::debug!("response = {response:?}");

        let body: serde_json::Value = response
            .json()
            .await
            .map_err(|e| st::Error::failed_to_read_json(API_CODE, e))?;
        tracing::debug!("body = {body:?}");

        body.try_into()
    }
}