lib_client_slack/
client.rs

1use reqwest::header::HeaderMap;
2use std::sync::Arc;
3use tracing::{debug, warn};
4
5use crate::auth::AuthStrategy;
6use crate::error::{Error, Result};
7use crate::types::*;
8
9const DEFAULT_BASE_URL: &str = "https://slack.com/api";
10
11pub struct ClientBuilder<A> {
12    auth: A,
13    base_url: String,
14}
15
16impl ClientBuilder<()> {
17    pub fn new() -> Self {
18        Self {
19            auth: (),
20            base_url: DEFAULT_BASE_URL.to_string(),
21        }
22    }
23
24    pub fn auth<S: AuthStrategy + 'static>(self, auth: S) -> ClientBuilder<S> {
25        ClientBuilder {
26            auth,
27            base_url: self.base_url,
28        }
29    }
30}
31
32impl Default for ClientBuilder<()> {
33    fn default() -> Self {
34        Self::new()
35    }
36}
37
38impl<A: AuthStrategy + 'static> ClientBuilder<A> {
39    pub fn base_url(mut self, url: impl Into<String>) -> Self {
40        self.base_url = url.into();
41        self
42    }
43
44    pub fn build(self) -> Client {
45        Client {
46            http: reqwest::Client::new(),
47            auth: Arc::new(self.auth),
48            base_url: self.base_url,
49        }
50    }
51}
52
53#[derive(Clone)]
54pub struct Client {
55    http: reqwest::Client,
56    auth: Arc<dyn AuthStrategy>,
57    base_url: String,
58}
59
60impl Client {
61    pub fn builder() -> ClientBuilder<()> {
62        ClientBuilder::new()
63    }
64
65    async fn post<T, B>(&self, method: &str, body: &B) -> Result<T>
66    where
67        T: serde::de::DeserializeOwned,
68        B: serde::Serialize,
69    {
70        let url = format!("{}/{}", self.base_url, method);
71        debug!("Slack API request: POST {}", url);
72
73        let mut headers = HeaderMap::new();
74        self.auth.apply(&mut headers).await?;
75        headers.insert("Content-Type", "application/json; charset=utf-8".parse().unwrap());
76
77        let response = self
78            .http
79            .post(&url)
80            .headers(headers)
81            .json(body)
82            .send()
83            .await?;
84
85        self.handle_response(response).await
86    }
87
88    async fn get<T>(&self, method: &str, params: &[(&str, &str)]) -> Result<T>
89    where
90        T: serde::de::DeserializeOwned,
91    {
92        let url = format!("{}/{}", self.base_url, method);
93        debug!("Slack API request: GET {}", url);
94
95        let mut headers = HeaderMap::new();
96        self.auth.apply(&mut headers).await?;
97
98        let response = self
99            .http
100            .get(&url)
101            .headers(headers)
102            .query(params)
103            .send()
104            .await?;
105
106        self.handle_response(response).await
107    }
108
109    async fn handle_response<T>(&self, response: reqwest::Response) -> Result<T>
110    where
111        T: serde::de::DeserializeOwned,
112    {
113        let status = response.status();
114
115        if status == 429 {
116            let retry_after = response
117                .headers()
118                .get("Retry-After")
119                .and_then(|v| v.to_str().ok())
120                .and_then(|v| v.parse().ok())
121                .unwrap_or(60);
122            return Err(Error::RateLimited { retry_after });
123        }
124
125        let body = response.text().await?;
126        let slack_resp: SlackResponse<T> = serde_json::from_str(&body)?;
127
128        if slack_resp.ok {
129            slack_resp.data.ok_or_else(|| Error::SlackApi("No data returned".to_string()))
130        } else {
131            let error = slack_resp.error.unwrap_or_else(|| "unknown_error".to_string());
132            warn!("Slack API error: {}", error);
133
134            match error.as_str() {
135                "invalid_auth" | "not_authed" => Err(Error::Unauthorized),
136                "channel_not_found" => Err(Error::ChannelNotFound(error)),
137                "user_not_found" => Err(Error::UserNotFound(error)),
138                _ => Err(Error::SlackApi(error)),
139            }
140        }
141    }
142
143    /// Post a message to a channel.
144    pub async fn post_message(&self, request: PostMessageRequest) -> Result<MessageResponse> {
145        self.post("chat.postMessage", &request).await
146    }
147
148    /// Update a message.
149    pub async fn update_message(&self, request: UpdateMessageRequest) -> Result<MessageResponse> {
150        self.post("chat.update", &request).await
151    }
152
153    /// Delete a message.
154    pub async fn delete_message(&self, channel: &str, ts: &str) -> Result<()> {
155        #[derive(serde::Serialize)]
156        struct Request<'a> {
157            channel: &'a str,
158            ts: &'a str,
159        }
160
161        let _: serde_json::Value = self.post("chat.delete", &Request { channel, ts }).await?;
162        Ok(())
163    }
164
165    /// List channels.
166    pub async fn list_channels(&self, cursor: Option<&str>) -> Result<ChannelsListResponse> {
167        let mut params = vec![("types", "public_channel,private_channel")];
168        if let Some(c) = cursor {
169            params.push(("cursor", c));
170        }
171        self.get("conversations.list", &params).await
172    }
173
174    /// Get channel info.
175    pub async fn get_channel(&self, channel_id: &str) -> Result<Channel> {
176        #[derive(serde::Deserialize)]
177        struct Response {
178            channel: Channel,
179        }
180        let resp: Response = self.get("conversations.info", &[("channel", channel_id)]).await?;
181        Ok(resp.channel)
182    }
183
184    /// List users.
185    pub async fn list_users(&self, cursor: Option<&str>) -> Result<UsersListResponse> {
186        let mut params = vec![];
187        if let Some(c) = cursor {
188            params.push(("cursor", c));
189        }
190        self.get("users.list", &params).await
191    }
192
193    /// Get user info.
194    pub async fn get_user(&self, user_id: &str) -> Result<User> {
195        #[derive(serde::Deserialize)]
196        struct Response {
197            user: User,
198        }
199        let resp: Response = self.get("users.info", &[("user", user_id)]).await?;
200        Ok(resp.user)
201    }
202
203    /// Add a reaction to a message.
204    pub async fn add_reaction(&self, request: ReactionRequest) -> Result<()> {
205        let _: serde_json::Value = self.post("reactions.add", &request).await?;
206        Ok(())
207    }
208
209    /// Remove a reaction from a message.
210    pub async fn remove_reaction(&self, request: ReactionRequest) -> Result<()> {
211        let _: serde_json::Value = self.post("reactions.remove", &request).await?;
212        Ok(())
213    }
214
215    /// Upload a file.
216    pub async fn upload_file(
217        &self,
218        channels: &[&str],
219        content: Vec<u8>,
220        filename: &str,
221        title: Option<&str>,
222    ) -> Result<FileUploadResponse> {
223        let url = format!("{}/files.upload", self.base_url);
224
225        let mut headers = HeaderMap::new();
226        self.auth.apply(&mut headers).await?;
227
228        let mut form = reqwest::multipart::Form::new()
229            .text("channels", channels.join(","))
230            .text("filename", filename.to_string())
231            .part(
232                "file",
233                reqwest::multipart::Part::bytes(content).file_name(filename.to_string()),
234            );
235
236        if let Some(t) = title {
237            form = form.text("title", t.to_string());
238        }
239
240        let response = self.http.post(&url).headers(headers).multipart(form).send().await?;
241        self.handle_response(response).await
242    }
243
244    /// Test authentication.
245    pub async fn auth_test(&self) -> Result<AuthTestResponse> {
246        self.post("auth.test", &serde_json::json!({})).await
247    }
248}
249
250/// Auth test response.
251#[derive(Debug, Clone, serde::Deserialize)]
252pub struct AuthTestResponse {
253    pub url: String,
254    pub team: String,
255    pub user: String,
256    pub team_id: String,
257    pub user_id: String,
258    pub bot_id: Option<String>,
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264    use crate::auth::BotTokenAuth;
265
266    #[test]
267    fn test_builder() {
268        let client = Client::builder()
269            .auth(BotTokenAuth::new("xoxb-token"))
270            .build();
271        assert_eq!(client.base_url, DEFAULT_BASE_URL);
272    }
273}