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