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 pub async fn post_message(&self, request: PostMessageRequest) -> Result<MessageResponse> {
145 self.post("chat.postMessage", &request).await
146 }
147
148 pub async fn update_message(&self, request: UpdateMessageRequest) -> Result<MessageResponse> {
150 self.post("chat.update", &request).await
151 }
152
153 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 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", ¶ms).await
172 }
173
174 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 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", ¶ms).await
191 }
192
193 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 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 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 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 pub async fn auth_test(&self) -> Result<AuthTestResponse> {
246 self.post("auth.test", &serde_json::json!({})).await
247 }
248}
249
250#[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}