Skip to main content

fluxer/
http.rs

1//! HTTP client for the Fluxer REST API.
2//!
3//! Handles auth headers, serialization, and error handling. You'll usually
4//! access this through `ctx.http` in your event handlers.
5
6use reqwest::{ header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE, USER_AGENT}, StatusCode, };
7use serde::de::DeserializeOwned;
8use serde_json::json;
9use crate::error::ClientError;
10use crate::model::*;
11
12/// HTTP client for making REST API calls.
13///
14/// Created automatically by the client builder. Available as `ctx.http` in event handlers
15/// or directly if you just need REST calls without a gateway connection:
16///
17/// ```rust,no_run
18/// use fluxer::http::Http;
19///
20/// #[tokio::main]
21/// async fn main() {
22///     let http = Http::new("your-bot-token", "https://api.fluxer.app/v1".to_string());
23///     let me = http.get_me().await.unwrap();
24///     println!("Bot user: {}", me.username);
25/// }
26/// ```
27pub struct Http {
28    pub client: reqwest::Client,
29    pub base_url: String,
30    token: String,
31}
32
33impl Http {
34    /// Creates a new HTTP client. The token is sent as `Bot {token}` in the
35    /// Authorization header on every request.
36    pub fn new(token: &str, base_url: String) -> Self {
37        Self::new_with_prefix(token, base_url, &format!("Bot {}", token))
38    }
39
40    /// Creates a new HTTP client for a user token (no `Bot ` prefix).
41    pub fn new_user(token: &str, base_url: String) -> Self {
42        Self::new_with_prefix(token, base_url, token)
43    }
44
45    fn new_with_prefix(token: &str, base_url: String, auth_value: &str) -> Self {
46        let mut headers = HeaderMap::new();
47        headers.insert(AUTHORIZATION, HeaderValue::from_str(auth_value).unwrap());
48        headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
49        headers.insert(USER_AGENT, HeaderValue::from_static("fluxer-rust"));
50
51        Self {
52            client: reqwest::Client::builder()
53                .default_headers(headers)
54                .build()
55                .unwrap(),
56            base_url,
57            token: token.to_string(),
58        }
59    }
60
61    pub fn get_token(&self) -> &str {
62        &self.token
63    }
64
65    async fn request_json<T: DeserializeOwned>(
66        &self,
67        req: reqwest::RequestBuilder,
68    ) -> Result<T, ClientError> {
69        let resp = req.send().await.map_err(ClientError::Http)?;
70        let status = resp.status();
71        if status == StatusCode::NO_CONTENT {
72            return Err(ClientError::Api("Expected body but got 204".into()));
73        }
74        if !status.is_success() {
75            let text = resp.text().await.unwrap_or_default();
76            return Err(ClientError::Api(format!("HTTP {}: {}", status, text)));
77        }
78        resp.json::<T>().await.map_err(ClientError::Http)
79    }
80
81    async fn request_empty(&self, req: reqwest::RequestBuilder) -> Result<(), ClientError> {
82        let resp = req.send().await.map_err(ClientError::Http)?;
83        let status = resp.status();
84        if !status.is_success() {
85            let text = resp.text().await.unwrap_or_default();
86            return Err(ClientError::Api(format!("HTTP {}: {}", status, text)));
87        }
88        Ok(())
89    }
90
91    /// Fetches the gateway URL. Used internally during connection setup.
92    pub async fn get_gateway(&self) -> Result<String, ClientError> {
93        let url = format!("{}/gateway/bot", self.base_url);
94        let res = self
95            .request_json::<GatewayBotResponse>(self.client.get(&url))
96            .await?;
97        Ok(res.url)
98    }
99
100    /// Fetches the bot's own user object.
101    pub async fn get_me(&self) -> Result<User, ClientError> {
102        let url = format!("{}/users/@me", self.base_url);
103        self.request_json(self.client.get(&url)).await
104    }
105
106    /// Fetches a user by ID.
107    pub async fn get_user(&self, user_id: &str) -> Result<User, ClientError> {
108        let url = format!("{}/users/{}", self.base_url, user_id);
109        self.request_json(self.client.get(&url)).await
110    }
111
112    /// Returns all guilds the bot is in.
113    pub async fn get_current_user_guilds(&self) -> Result<Vec<Guild>, ClientError> {
114        let url = format!("{}/users/@me/guilds", self.base_url);
115        self.request_json(self.client.get(&url)).await
116    }
117
118    /// Fetches a channel by ID.
119    pub async fn get_channel(&self, channel_id: &str) -> Result<Channel, ClientError> {
120        let url = format!("{}/channels/{}", self.base_url, channel_id);
121        self.request_json(self.client.get(&url)).await
122    }
123
124    /// Edits a channel. Only the fields you set in the payload will change.
125    pub async fn edit_channel(
126        &self,
127        channel_id: &str,
128        payload: &ChannelCreatePayload,
129    ) -> Result<Channel, ClientError> {
130        let url = format!("{}/channels/{}", self.base_url, channel_id);
131        self.request_json(self.client.patch(&url).json(payload)).await
132    }
133
134    /// Permanently deletes a channel. Can't be undone.
135    pub async fn delete_channel(&self, channel_id: &str) -> Result<(), ClientError> {
136        let url = format!("{}/channels/{}", self.base_url, channel_id);
137        self.request_empty(self.client.delete(&url)).await
138    }
139
140    /// Triggers the "Bot is typing..." indicator. Lasts ~10 seconds or until
141    /// the bot sends a message. (I actually haven't tested this)
142    pub async fn trigger_typing(&self, channel_id: &str) -> Result<(), ClientError> {
143        let url = format!("{}/channels/{}/typing", self.base_url, channel_id);
144        self.request_empty(self.client.post(&url).body("{}")).await
145    }
146
147    /// Fetches messages from a channel. Use [`GetMessagesQuery`] to paginate.
148    ///
149    /// ```rust,no_run
150    /// # async fn example(http: &fluxer::http::Http) {
151    /// use fluxer::prelude::*;
152    ///
153    /// let query = GetMessagesQuery {
154    ///     limit: Some(10),
155    ///     ..Default::default()
156    /// };
157    /// let messages = http.get_messages("channel_id", query).await.unwrap();
158    /// # }
159    /// ```
160    pub async fn get_messages(
161        &self,
162        channel_id: &str,
163        query: GetMessagesQuery,
164    ) -> Result<Vec<Message>, ClientError> {
165        let url = format!(
166            "{}/channels/{}/messages{}",
167            self.base_url,
168            channel_id,
169            query.to_query_string()
170        );
171        self.request_json(self.client.get(&url)).await
172    }
173
174    /// Fetches a single message by ID.
175    pub async fn get_message(
176        &self,
177        channel_id: &str,
178        message_id: &str,
179    ) -> Result<Message, ClientError> {
180        let url = format!(
181            "{}/channels/{}/messages/{}",
182            self.base_url, channel_id, message_id
183        );
184        self.request_json(self.client.get(&url)).await
185    }
186
187    /// Sends a text message. For embeds or other options, use
188    /// [`send_message_advanced`](Http::send_message_advanced).
189    pub async fn send_message(
190        &self,
191        channel_id: &str,
192        content: &str,
193    ) -> Result<Message, ClientError> {
194        let url = format!("{}/channels/{}/messages", self.base_url, channel_id);
195        let body = json!({ "content": content });
196        self.request_json(self.client.post(&url).json(&body)).await
197    }
198
199    /// Sends a message with full control over the payload (embeds, TTS, replies, etc).
200    pub async fn send_message_advanced(
201        &self,
202        channel_id: &str,
203        payload: &MessageCreatePayload,
204    ) -> Result<Message, ClientError> {
205        let url = format!("{}/channels/{}/messages", self.base_url, channel_id);
206        self.request_json(self.client.post(&url).json(payload)).await
207    }
208
209    /// Shorthand for sending embeds. Wraps [`send_message_advanced`](Http::send_message_advanced).
210    pub async fn send_embed(
211        &self,
212        channel_id: &str,
213        content: Option<&str>,
214        embeds: Vec<Embed>,
215    ) -> Result<Message, ClientError> {
216        let payload = MessageCreatePayload {
217            content: content.map(|s| s.to_string()),
218            embeds: Some(embeds),
219            ..Default::default()
220        };
221        self.send_message_advanced(channel_id, &payload).await
222    }
223
224    /// Replies to a specific message. The client will render it as a reply thread.
225    /// Fails with a 404 if the referenced message no longer exists.
226    pub async fn reply_to_message(
227        &self,
228        channel_id: &str,
229        message_id: &str,
230        content: &str,
231    ) -> Result<Message, ClientError> {
232        let payload = MessageCreatePayload {
233            content: Some(content.to_string()),
234            message_reference: Some(MessageReference {
235                message_id: message_id.to_string(),
236                channel_id: None,
237                guild_id: None,
238                fail_if_not_exists: Some(true),
239            }),
240            ..Default::default()
241        };
242        self.send_message_advanced(channel_id, &payload).await
243    }
244
245    /// Edits a message's content. Bot must be the author.
246    pub async fn edit_message(
247        &self,
248        channel_id: &str,
249        message_id: &str,
250        content: &str,
251    ) -> Result<Message, ClientError> {
252        let url = format!(
253            "{}/channels/{}/messages/{}",
254            self.base_url, channel_id, message_id
255        );
256        let body = json!({ "content": content });
257        self.request_json(self.client.patch(&url).json(&body)).await
258    }
259
260    /// Edits a message with full control over the payload.
261    pub async fn edit_message_advanced(
262        &self,
263        channel_id: &str,
264        message_id: &str,
265        payload: &MessageCreatePayload,
266    ) -> Result<Message, ClientError> {
267        let url = format!(
268            "{}/channels/{}/messages/{}",
269            self.base_url, channel_id, message_id
270        );
271        self.request_json(self.client.patch(&url).json(payload)).await
272    }
273
274    /// Deletes a message. Bot must be the author, or have Manage Messages.
275    pub async fn delete_message(
276        &self,
277        channel_id: &str,
278        message_id: &str,
279    ) -> Result<(), ClientError> {
280        let url = format!(
281            "{}/channels/{}/messages/{}",
282            self.base_url, channel_id, message_id
283        );
284        self.request_empty(self.client.delete(&url)).await
285    }
286
287    /// Deletes multiple messages at once. Way faster than deleting one by one.
288    pub async fn bulk_delete_messages(
289        &self,
290        channel_id: &str,
291        message_ids: Vec<&str>,
292    ) -> Result<(), ClientError> {
293        let url = format!(
294            "{}/channels/{}/messages/bulk-delete",
295            self.base_url, channel_id
296        );
297        let body = json!({ "message_ids": message_ids });
298        self.request_empty(self.client.post(&url).json(&body)).await
299    }
300
301    /// Reacts to a message as the bot. `emoji` should be a unicode emoji like
302    /// `"👍"` or a custom emoji in `name:id` format. You can use
303    /// [`Emoji::to_reaction_string`] to get the right format.
304    pub async fn add_reaction(
305        &self,
306        channel_id: &str,
307        message_id: &str,
308        emoji: &str,
309    ) -> Result<(), ClientError> {
310        let encoded = urlencoded(emoji);
311        let url = format!(
312            "{}/channels/{}/messages/{}/reactions/{}/@me",
313            self.base_url, channel_id, message_id, encoded
314        );
315        self.request_empty(self.client.put(&url).body("")).await
316    }
317
318    /// Removes the bot's own reaction from a message.
319    pub async fn remove_own_reaction(
320        &self,
321        channel_id: &str,
322        message_id: &str,
323        emoji: &str,
324    ) -> Result<(), ClientError> {
325        let encoded = urlencoded(emoji);
326        let url = format!(
327            "{}/channels/{}/messages/{}/reactions/{}/@me",
328            self.base_url, channel_id, message_id, encoded
329        );
330        self.request_empty(self.client.delete(&url)).await
331    }
332
333    /// Removes someone else's reaction. Needs Manage Messages permission.
334    pub async fn remove_user_reaction(
335        &self,
336        channel_id: &str,
337        message_id: &str,
338        emoji: &str,
339        user_id: &str,
340    ) -> Result<(), ClientError> {
341        let encoded = urlencoded(emoji);
342        let url = format!(
343            "{}/channels/{}/messages/{}/reactions/{}/{}",
344            self.base_url, channel_id, message_id, encoded, user_id
345        );
346        self.request_empty(self.client.delete(&url)).await
347    }
348
349    /// Gets the list of users who reacted with a specific emoji.
350    pub async fn get_reactions(
351        &self,
352        channel_id: &str,
353        message_id: &str,
354        emoji: &str,
355    ) -> Result<Vec<User>, ClientError> {
356        let encoded = urlencoded(emoji);
357        let url = format!(
358            "{}/channels/{}/messages/{}/reactions/{}",
359            self.base_url, channel_id, message_id, encoded
360        );
361        self.request_json(self.client.get(&url)).await
362    }
363
364    /// Removes all reactions from a message. Needs Manage Messages.
365    pub async fn clear_reactions(
366        &self,
367        channel_id: &str,
368        message_id: &str,
369    ) -> Result<(), ClientError> {
370        let url = format!(
371            "{}/channels/{}/messages/{}/reactions",
372            self.base_url, channel_id, message_id
373        );
374        self.request_empty(self.client.delete(&url)).await
375    }
376
377    /// Removes all reactions for a specific emoji. Needs Manage Messages.
378    pub async fn clear_reactions_for_emoji(
379        &self,
380        channel_id: &str,
381        message_id: &str,
382        emoji: &str,
383    ) -> Result<(), ClientError> {
384        let encoded = urlencoded(emoji);
385        let url = format!(
386            "{}/channels/{}/messages/{}/reactions/{}",
387            self.base_url, channel_id, message_id, encoded
388        );
389        self.request_empty(self.client.delete(&url)).await
390    }
391
392    /// Fetches pinned messages in a channel.
393    pub async fn get_pins(&self, channel_id: &str) -> Result<PinsResponse, ClientError> {
394        let url = format!("{}/channels/{}/messages/pins", self.base_url, channel_id);
395        self.request_json(self.client.get(&url)).await
396    }
397
398    /// Pins a message. Needs Manage Messages.
399    pub async fn pin_message(
400        &self,
401        channel_id: &str,
402        message_id: &str,
403    ) -> Result<(), ClientError> {
404        let url = format!(
405            "{}/channels/{}/pins/{}",
406            self.base_url, channel_id, message_id
407        );
408        self.request_empty(self.client.put(&url).body("")).await
409    }
410
411    /// Unpins a message. Needs Manage Messages.
412    pub async fn unpin_message(
413        &self,
414        channel_id: &str,
415        message_id: &str,
416    ) -> Result<(), ClientError> {
417        let url = format!(
418            "{}/channels/{}/pins/{}",
419            self.base_url, channel_id, message_id
420        );
421        self.request_empty(self.client.delete(&url)).await
422    }
423
424    /// Fetches an invite by code. Includes approximate member counts.
425    pub async fn get_invite(&self, invite_code: &str) -> Result<Invite, ClientError> {
426        let url = format!(
427            "{}/invites/{}?with_counts=true",
428            self.base_url, invite_code
429        );
430        self.request_json(self.client.get(&url)).await
431    }
432
433    /// Creates an invite for a channel.
434    pub async fn create_invite(
435        &self,
436        channel_id: &str,
437        payload: &CreateInvitePayload,
438    ) -> Result<Invite, ClientError> {
439        let url = format!("{}/channels/{}/invites", self.base_url, channel_id);
440        self.request_json(self.client.post(&url).json(payload)).await
441    }
442
443    /// Deletes an invite by code.
444    pub async fn delete_invite(&self, invite_code: &str) -> Result<(), ClientError> {
445        let url = format!("{}/invites/{}", self.base_url, invite_code);
446        self.request_empty(self.client.delete(&url)).await
447    }
448
449    /// Returns all active invites for a channel.
450    pub async fn get_channel_invites(&self, channel_id: &str) -> Result<Vec<Invite>, ClientError> {
451        let url = format!("{}/channels/{}/invites", self.base_url, channel_id);
452        self.request_json(self.client.get(&url)).await
453    }
454
455    /// Returns all active invites for a guild.
456    pub async fn get_guild_invites(&self, guild_id: &str) -> Result<Vec<Invite>, ClientError> {
457        let url = format!("{}/guilds/{}/invites", self.base_url, guild_id);
458        self.request_json(self.client.get(&url)).await
459    }
460
461    /// Fetches a guild by ID.
462    pub async fn get_guild(&self, guild_id: &str) -> Result<Guild, ClientError> {
463        let url = format!("{}/guilds/{}", self.base_url, guild_id);
464        self.request_json(self.client.get(&url)).await
465    }
466
467    /// Edits guild settings. Only fields you set in the payload will change.
468    pub async fn edit_guild(
469        &self,
470        guild_id: &str,
471        payload: &EditGuildPayload,
472    ) -> Result<Guild, ClientError> {
473        let url = format!("{}/guilds/{}", self.base_url, guild_id);
474        self.request_json(self.client.patch(&url).json(payload)).await
475    }
476
477    /// Permanently deletes a guild. The bot must be the owner. (not tested)
478    pub async fn delete_guild(&self, guild_id: &str) -> Result<(), ClientError> {
479        let url = format!("{}/guilds/{}", self.base_url, guild_id);
480        self.request_empty(self.client.delete(&url)).await
481    }
482
483    /// Returns all channels in a guild.
484    pub async fn get_guild_channels(&self, guild_id: &str) -> Result<Vec<Channel>, ClientError> {
485        let url = format!("{}/guilds/{}/channels", self.base_url, guild_id);
486        self.request_json(self.client.get(&url)).await
487    }
488
489    /// Creates a channel in a guild. You need at least `name` in the payload.
490    pub async fn create_channel(
491        &self,
492        guild_id: &str,
493        payload: &ChannelCreatePayload,
494    ) -> Result<Channel, ClientError> {
495        let url = format!("{}/guilds/{}/channels", self.base_url, guild_id);
496        self.request_json(self.client.post(&url).json(payload)).await
497    }
498
499    /// Fetches a single guild member.
500    pub async fn get_guild_member(
501        &self,
502        guild_id: &str,
503        user_id: &str,
504    ) -> Result<Member, ClientError> {
505        let url = format!("{}/guilds/{}/members/{}", self.base_url, guild_id, user_id);
506        self.request_json(self.client.get(&url)).await
507    }
508
509    /// Fetches guild members. `limit` caps at 1000, `after` is a user ID for pagination.
510    pub async fn get_guild_members(
511        &self,
512        guild_id: &str,
513        limit: Option<u16>,
514        after: Option<&str>,
515    ) -> Result<Vec<Member>, ClientError> {
516        let mut url = format!("{}/guilds/{}/members?", self.base_url, guild_id);
517        if let Some(l) = limit {
518            url.push_str(&format!("limit={}&", l.min(1000)));
519        }
520        if let Some(a) = after {
521            url.push_str(&format!("after={}", a));
522        }
523        self.request_json(self.client.get(&url)).await
524    }
525
526    /// Kicks a member from the guild.
527    pub async fn kick_member(
528        &self,
529        guild_id: &str,
530        user_id: &str,
531    ) -> Result<(), ClientError> {
532        let url = format!("{}/guilds/{}/members/{}", self.base_url, guild_id, user_id);
533        self.request_empty(self.client.delete(&url)).await
534    }
535
536    /// Edits a member's properties. To clear a nullable field like `nick`,
537    /// set it to `Some(None)`.
538    pub async fn edit_member(
539        &self,
540        guild_id: &str,
541        user_id: &str,
542        payload: &EditMemberPayload,
543    ) -> Result<Member, ClientError> {
544        let url = format!("{}/guilds/{}/members/{}", self.base_url, guild_id, user_id);
545        self.request_json(self.client.patch(&url).json(payload)).await
546    }
547
548    /// Grants a role to a guild member. Requires Manage Roles.
549    pub async fn add_member_role(
550        &self,
551        guild_id: &str,
552        user_id: &str,
553        role_id: &str,
554    ) -> Result<(), ClientError> {
555        let url = format!(
556            "{}/guilds/{}/members/{}/roles/{}",
557            self.base_url, guild_id, user_id, role_id
558        );
559        self.request_empty(self.client.put(&url).body("")).await
560    }
561
562    /// Removes a role from a guild member. Requires Manage Roles.
563    pub async fn remove_member_role(
564        &self,
565        guild_id: &str,
566        user_id: &str,
567        role_id: &str,
568    ) -> Result<(), ClientError> {
569        let url = format!(
570            "{}/guilds/{}/members/{}/roles/{}",
571            self.base_url, guild_id, user_id, role_id
572        );
573        self.request_empty(self.client.delete(&url)).await
574    }
575
576    /// Bans a member. `reason` is stored in the audit log.
577    pub async fn ban_member(
578        &self,
579        guild_id: &str,
580        user_id: &str,
581        reason: &str,
582    ) -> Result<(), ClientError> {
583        let url = format!("{}/guilds/{}/bans/{}", self.base_url, guild_id, user_id);
584        let body = json!({ "reason": reason });
585        self.request_empty(self.client.put(&url).json(&body)).await
586    }
587
588    /// Removes a ban.
589    pub async fn unban_member(
590        &self,
591        guild_id: &str,
592        user_id: &str,
593    ) -> Result<(), ClientError> {
594        let url = format!("{}/guilds/{}/bans/{}", self.base_url, guild_id, user_id);
595        self.request_empty(self.client.delete(&url)).await
596    }
597
598    /// Returns the guild's ban list.
599    pub async fn get_guild_bans(&self, guild_id: &str) -> Result<Vec<serde_json::Value>, ClientError> {
600        let url = format!("{}/guilds/{}/bans", self.base_url, guild_id);
601        self.request_json(self.client.get(&url)).await
602    }
603
604    /// Returns all roles in a guild.
605    pub async fn get_guild_roles(&self, guild_id: &str) -> Result<Vec<Role>, ClientError> {
606        let url = format!("{}/guilds/{}/roles", self.base_url, guild_id);
607        self.request_json(self.client.get(&url)).await
608    }
609
610    /// Creates a new role in a guild.
611    pub async fn create_role(
612        &self,
613        guild_id: &str,
614        payload: &CreateRolePayload,
615    ) -> Result<Role, ClientError> {
616        let url = format!("{}/guilds/{}/roles", self.base_url, guild_id);
617        self.request_json(self.client.post(&url).json(payload)).await
618    }
619
620    /// Edits a role's properties.
621    pub async fn edit_role(
622        &self,
623        guild_id: &str,
624        role_id: &str,
625        payload: &EditRolePayload,
626    ) -> Result<Role, ClientError> {
627        let url = format!("{}/guilds/{}/roles/{}", self.base_url, guild_id, role_id);
628        self.request_json(self.client.patch(&url).json(payload)).await
629    }
630
631    /// Deletes a role.
632    pub async fn delete_role(
633        &self,
634        guild_id: &str,
635        role_id: &str,
636    ) -> Result<(), ClientError> {
637        let url = format!("{}/guilds/{}/roles/{}", self.base_url, guild_id, role_id);
638        self.request_empty(self.client.delete(&url)).await
639    }
640
641    /// Returns all custom emojis in a guild.
642    pub async fn get_guild_emojis(&self, guild_id: &str) -> Result<Vec<Emoji>, ClientError> {
643        let url = format!("{}/guilds/{}/emojis", self.base_url, guild_id);
644        self.request_json(self.client.get(&url)).await
645    }
646
647    /// Deletes a custom emoji from a guild.
648    pub async fn delete_guild_emoji(
649        &self,
650        guild_id: &str,
651        emoji_id: &str,
652    ) -> Result<(), ClientError> {
653        let url = format!("{}/guilds/{}/emojis/{}", self.base_url, guild_id, emoji_id);
654        self.request_empty(self.client.delete(&url)).await
655    }
656
657    /// Returns all webhooks for a channel.
658    pub async fn get_channel_webhooks(
659        &self,
660        channel_id: &str,
661    ) -> Result<Vec<Webhook>, ClientError> {
662        let url = format!("{}/channels/{}/webhooks", self.base_url, channel_id);
663        self.request_json(self.client.get(&url)).await
664    }
665
666    /// Returns all webhooks for a guild.
667    pub async fn get_guild_webhooks(&self, guild_id: &str) -> Result<Vec<Webhook>, ClientError> {
668        let url = format!("{}/guilds/{}/webhooks", self.base_url, guild_id);
669        self.request_json(self.client.get(&url)).await
670    }
671
672    /// `avatar` should be a data URI if provided.
673    pub async fn create_webhook(
674        &self,
675        channel_id: &str,
676        name: &str,
677        avatar: Option<&str>,
678    ) -> Result<Webhook, ClientError> {
679        let url = format!("{}/channels/{}/webhooks", self.base_url, channel_id);
680        let mut body = json!({ "name": name });
681        if let Some(av) = avatar {
682            body["avatar"] = serde_json::Value::String(av.to_string());
683        }
684        self.request_json(self.client.post(&url).json(&body)).await
685    }
686
687    /// Deletes a webhook.
688    pub async fn delete_webhook(&self, webhook_id: &str) -> Result<(), ClientError> {
689        let url = format!("{}/webhooks/{}", self.base_url, webhook_id);
690        self.request_empty(self.client.delete(&url)).await
691    }
692
693    /// Sends a message with one or more file attachments. `content` is optional
694    /// if you just want to upload files with no text.
695    pub async fn send_files(
696        &self,
697        channel_id: &str,
698        files: Vec<AttachmentFile>,
699        content: Option<&str>,
700    ) -> Result<Message, ClientError> {
701        let payload = MessageCreatePayload {
702            content: content.map(|s| s.to_string()),
703            ..Default::default()
704        };
705        self.send_message_with_files(channel_id, &payload, files).await
706    }
707
708    /// Like [`send_message_advanced`](Http::send_message_advanced) but also uploads files as attachments.
709    pub async fn send_message_with_files(
710        &self,
711        channel_id: &str,
712        payload: &MessageCreatePayload,
713        files: Vec<AttachmentFile>,
714    ) -> Result<Message, ClientError> {
715        use reqwest::multipart::{Form, Part};
716
717        let url = format!("{}/channels/{}/messages", self.base_url, channel_id);
718
719        let mut payload = payload.clone();
720        payload.attachments = Some(
721            files
722                .iter()
723                .enumerate()
724                .map(|(i, f)| AttachmentMetadata {
725                    id: i as u64,
726                    filename: f.filename.clone(),
727                    description: None,
728                })
729                .collect(),
730        );
731        let json_string = serde_json::to_string(&payload)?;
732
733        let mut form = Form::new().text("payload_json", json_string);
734        for (i, file) in files.into_iter().enumerate() {
735            let content_type = file
736                .content_type
737                .unwrap_or_else(|| "application/octet-stream".to_string());
738            let part = Part::bytes(file.data)
739                .file_name(file.filename)
740                .mime_str(&content_type)
741                .map_err(ClientError::Http)?;
742            form = form.part(format!("files[{}]", i), part);
743        }
744
745        let req = self.client.post(&url).multipart(form);
746        self.request_json(req).await
747    }
748
749    /// Searches messages across the platform. See [`SearchMessagesQuery`] for
750    /// all available filters (content, author, channel, attachments, etc).
751    pub async fn search_messages(
752        &self,
753        query: &SearchMessagesQuery,
754    ) -> Result<SearchMessagesResponse, ClientError> {
755        let url = format!("{}/search/messages", self.base_url);
756        self.request_json(self.client.post(&url).json(query)).await
757    }
758
759    /// Marks multiple channels as read in a single request. Pass up to 100
760    /// `(channel_id, last_message_id)` pairs.
761    pub async fn ack_bulk(&self, states: Vec<ReadStateAck>) -> Result<(), ClientError> {
762        let url = format!("{}/read-states/ack-bulk", self.base_url);
763        let body = json!({ "read_states": states });
764        self.request_empty(self.client.post(&url).json(&body)).await
765    }
766
767    /// Executes a webhook (sends a message through it). Uses `wait=true` so
768    /// the response includes the full message object.
769    pub async fn execute_webhook(
770        &self,
771        webhook_id: &str,
772        webhook_token: &str,
773        payload: &WebhookExecutePayload,
774    ) -> Result<Option<Message>, ClientError> {
775        let url = format!(
776            "{}/webhooks/{}/{}?wait=true",
777            self.base_url, webhook_id, webhook_token
778        );
779        self.request_json(self.client.post(&url).json(payload)).await
780    }
781
782    /// Gets a message previously sent by a webhook.
783    pub async fn get_webhook_message(
784        &self,
785        webhook_id: &str,
786        webhook_token: &str,
787        message_id: &str,
788    ) -> Result<Message, ClientError> {
789        let url = format!(
790            "{}/webhooks/{}/{}/messages/{}",
791            self.base_url, webhook_id, webhook_token, message_id
792        );
793        self.request_json(self.client.get(&url)).await
794    }
795
796    /// Edits a message previously sent by a webhook.
797    pub async fn edit_webhook_message(
798        &self,
799        webhook_id: &str,
800        webhook_token: &str,
801        message_id: &str,
802        payload: &WebhookEditPayload,
803    ) -> Result<Message, ClientError> {
804        let url = format!(
805            "{}/webhooks/{}/{}/messages/{}",
806            self.base_url, webhook_id, webhook_token, message_id
807        );
808        self.request_json(self.client.patch(&url).json(payload)).await
809    }
810
811}
812
813fn urlencoded(s: &str) -> String {
814    s.chars()
815        .flat_map(|c| {
816            let mut buf = [0u8; 4];
817            c.encode_utf8(&mut buf);
818            let bytes = &buf[..c.len_utf8()];
819            bytes
820                .iter()
821                .map(|b| match b {
822                    b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9'
823                    | b'-' | b'_' | b'.' | b'~' | b':' => {
824                        char::from(*b).to_string()
825                    }
826                    other => format!("%{:02X}", other),
827                })
828                .collect::<Vec<_>>()
829        })
830        .collect()
831}