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