sparkle_impostor/
lib.rs

1#![warn(
2    clippy::cargo,
3    clippy::nursery,
4    clippy::pedantic,
5    clippy::allow_attributes,
6    clippy::allow_attributes_without_reason,
7    clippy::arithmetic_side_effects,
8    clippy::as_underscore,
9    clippy::assertions_on_result_states,
10    clippy::clone_on_ref_ptr,
11    clippy::create_dir,
12    clippy::dbg_macro,
13    clippy::default_numeric_fallback,
14    clippy::empty_drop,
15    clippy::empty_structs_with_brackets,
16    clippy::exit,
17    clippy::filetype_is_file,
18    clippy::float_cmp_const,
19    clippy::fn_to_numeric_cast_any,
20    clippy::format_push_string,
21    clippy::if_then_some_else_none,
22    clippy::indexing_slicing,
23    clippy::integer_division,
24    clippy::large_include_file,
25    clippy::let_underscore_must_use,
26    clippy::lossy_float_literal,
27    clippy::mem_forget,
28    clippy::mixed_read_write_in_expression,
29    clippy::mod_module_files,
30    clippy::multiple_unsafe_ops_per_block,
31    clippy::mutex_atomic,
32    clippy::rc_buffer,
33    clippy::rc_mutex,
34    clippy::rest_pat_in_fully_bound_structs,
35    clippy::same_name_method,
36    clippy::semicolon_inside_block,
37    clippy::shadow_reuse,
38    clippy::shadow_same,
39    clippy::shadow_unrelated,
40    clippy::str_to_string,
41    clippy::string_add,
42    clippy::string_slice,
43    clippy::string_to_string,
44    clippy::suspicious_xor_used_as_pow,
45    clippy::tests_outside_test_module,
46    clippy::try_err,
47    clippy::unnecessary_safety_comment,
48    clippy::unnecessary_safety_doc,
49    clippy::unneeded_field_pattern,
50    clippy::unseparated_literal_suffix,
51    clippy::verbose_file_reads,
52    rustdoc::missing_crate_level_docs,
53    rustdoc::private_doc_tests,
54    absolute_paths_not_starting_with_crate,
55    elided_lifetimes_in_paths,
56    explicit_outlives_requirements,
57    keyword_idents,
58    let_underscore_drop,
59    macro_use_extern_crate,
60    meta_variable_misuse,
61    missing_abi,
62    missing_copy_implementations,
63    missing_debug_implementations,
64    missing_docs,
65    non_ascii_idents,
66    noop_method_call,
67    pointer_structural_match,
68    rust_2021_incompatible_or_patterns,
69    rust_2021_prefixes_incompatible_syntax,
70    rust_2021_prelude_collisions,
71    single_use_lifetimes,
72    trivial_casts,
73    trivial_numeric_casts,
74    unreachable_pub,
75    unsafe_code,
76    unsafe_op_in_unsafe_fn,
77    unused_crate_dependencies,
78    unused_extern_crates,
79    unused_import_braces,
80    unused_lifetimes,
81    unused_macro_rules,
82    unused_qualifications,
83    unused_tuple_struct_fields,
84    variant_size_differences,
85    // nightly lints:
86    // fuzzy_provenance_casts,
87    // lossy_provenance_casts,
88    // must_not_suspend,
89    // non_exhaustive_omitted_patterns,
90)]
91#![allow(clippy::redundant_pub_crate)]
92#![doc = include_str!("../README.md")]
93
94#[cfg(test)]
95use anyhow as _;
96#[cfg(test)]
97use dotenvy as _;
98#[cfg(test)]
99use tokio as _;
100use twilight_http::{request::channel::webhook::ExecuteWebhookAndWait, Client};
101#[cfg(doc)]
102use twilight_model::guild::Permissions;
103use twilight_model::{
104    channel::{
105        message::{Embed, MessageFlags},
106        Message,
107    },
108    id::{
109        marker::{ChannelMarker, EmojiMarker, GuildMarker, MessageMarker, WebhookMarker},
110        Id,
111    },
112};
113
114use crate::error::Error;
115
116pub mod attachment_sticker;
117pub mod avatar;
118pub mod component;
119mod constructor;
120mod delete;
121pub mod error;
122pub mod later_messages;
123pub mod reaction;
124pub mod reference;
125pub mod response;
126pub mod thread;
127mod username;
128
129/// A message that can be cloned
130///
131/// # Mutation
132///
133/// Can be mutated to override some fields, for example to clone it to another
134/// channel
135///
136/// Since most methods mutate the source, it's recommend to mutate the message
137/// right before calling [`MessageSource::create`]
138///
139/// Fields starting with `source` shouldn't be mutated, in other
140/// words, "message" refers to the created message while "source message" refers
141/// to the message to be cloned from
142///
143/// You can also provide some of the fields, for example from your cache, so
144/// that they won't be received over the HTTP API
145///
146/// # Warnings
147///
148/// Many of the fields here are stateful, there are no guarantees on the
149/// validity of these since this doesn't have access to the gateway, this means
150/// you should use and drop this struct as fast as you can
151#[derive(Debug)]
152pub struct MessageSource<'a> {
153    /// Source message's ID
154    pub source_id: Id<MessageMarker>,
155    /// ID of the channel the source message is in
156    pub source_channel_id: Id<ChannelMarker>,
157    /// ID of the thread the source message is in
158    pub source_thread_id: Option<Id<ChannelMarker>>,
159    /// Content of the message
160    pub content: String,
161    /// Embeds in the message
162    pub embeds: Vec<Embed>,
163    /// Whether the message has text-to-speech enabled
164    pub tts: bool,
165    /// Flags of the message
166    pub flags: Option<MessageFlags>,
167    /// ID of the channel the message is in
168    ///
169    /// If the message is in a thread, this should be the parent thread's ID
170    pub channel_id: Id<ChannelMarker>,
171    /// ID of the guild the message is in
172    pub guild_id: Id<GuildMarker>,
173    /// Emoji IDs of the guild the message is in
174    ///
175    /// `None` if it has never been needed
176    pub guild_emoji_ids: Option<Vec<Id<EmojiMarker>>>,
177    /// Username of the message's author
178    pub username: String,
179    /// Name to be used for the webhook that will be used to create the message
180    pub webhook_name: String,
181    /// Info about the message's avatar
182    pub avatar_info: avatar::Info,
183    /// Info about the message's reference
184    pub reference_info: reference::Info<'a>,
185    /// Info about the message's reactions
186    pub reaction_info: reaction::Info<'a>,
187    /// Info about the message's attachments
188    pub attachment_sticker_info: attachment_sticker::Info<'a>,
189    /// Info about the message's components
190    pub component_info: component::Info,
191    /// Info about the message's thread
192    pub thread_info: thread::Info,
193    /// Messages sent after the source
194    pub later_messages: later_messages::Info,
195    /// Webhook ID and token to execute to clone messages with
196    pub webhook: Option<(Id<WebhookMarker>, String)>,
197    /// Cloned message's response
198    ///
199    /// `None` if [`MessageSource::create`] wasn't called
200    pub response: Option<response::MaybeDeserialized<Message>>,
201    /// The client to use for requests
202    pub http: &'a Client,
203}
204
205impl<'a> MessageSource<'a> {
206    /// Executes a webhook using the given source
207    ///
208    /// If a webhook called the set name or *Message Cloner* in the channel
209    /// doesn't exist, creates it
210    ///
211    /// Make sure the bot has these required permissions:
212    /// - [`Permissions::SEND_TTS_MESSAGES`]
213    /// - [`Permissions::MENTION_EVERYONE`]
214    /// - [`Permissions::USE_EXTERNAL_EMOJIS`]
215    /// - [`Permissions::MANAGE_WEBHOOKS`]
216    ///
217    /// Because rate-limits for webhook executions can't be handled
218    /// beforehand, retries each execution up to 5 times, if all of these
219    /// are rate-limited, returns the HTTP error
220    ///
221    /// # Warnings
222    ///
223    /// Other methods on [`MessageSource`] are provided to handle edge-cases,
224    /// not calling them before this may make this method fail
225    ///
226    /// If calling this on the same webhook repeatedly, it's rate-limited on
227    /// every try after the 50th execution in tests, though Discord may change
228    /// this in the future
229    ///
230    /// # Errors
231    ///
232    /// Returns [`Error::Http`] if getting, creating or executing the webhook
233    /// fails
234    ///
235    /// Returns [`Error::DeserializeBody`] if deserializing the webhook
236    ///
237    /// Returns [`Error::Validation`] if the webhook name is invalid
238    ///
239    /// Returns [`Error::MessageValidation`] if the given message is invalid,
240    /// shouldn't happen unless the message was mutated
241    pub async fn create(mut self) -> Result<MessageSource<'a>, Error> {
242        self.set_webhook().await?;
243        self.avatar_info.set_url();
244
245        for i in 0..=5_u8 {
246            match self.webhook_exec()?.await {
247                Ok(response) => {
248                    self.response = Some(response::MaybeDeserialized::Response(response));
249                    break;
250                }
251                Err(err)
252                    if matches!(
253                        err.kind(),
254                        twilight_http::error::ErrorType::Response {
255                            error: twilight_http::api_error::ApiError::Ratelimited(_),
256                            ..
257                        }
258                    ) =>
259                {
260                    if i == 5 {
261                        return Err(Error::Http(err));
262                    }
263                    continue;
264                }
265                Err(err) => return Err(Error::Http(err)),
266            }
267        }
268
269        self.later_messages.is_source_created = true;
270
271        Ok(self)
272    }
273
274    /// Set the name of the webhook to use for creating messages
275    ///
276    /// Defaults to *Message Cloner* if not called
277    #[must_use]
278    #[allow(clippy::missing_const_for_fn)]
279    pub fn webhook_name(mut self, name: String) -> Self {
280        self.webhook_name = name;
281        self
282    }
283
284    async fn set_webhook(&mut self) -> Result<(), Error> {
285        if self.webhook.is_some() {
286            return Ok(());
287        }
288
289        let webhook = if let Some(webhook) = self
290            .http
291            .channel_webhooks(self.channel_id)
292            .await?
293            .models()
294            .await?
295            .into_iter()
296            .find(|webhook| {
297                webhook.token.is_some() && webhook.name.as_ref() == Some(&self.webhook_name)
298            }) {
299            webhook
300        } else {
301            self.http
302                .create_webhook(self.channel_id, &self.webhook_name)?
303                .await?
304                .model()
305                .await?
306        };
307        self.webhook = Some((webhook.id, webhook.token.unwrap()));
308
309        Ok(())
310    }
311
312    fn webhook_exec(&self) -> Result<ExecuteWebhookAndWait<'_>, Error> {
313        let (webhook_id, webhook_token) = self.webhook.as_ref().unwrap();
314
315        let mut execute_webhook = self
316            .http
317            .execute_webhook(*webhook_id, webhook_token)
318            .content(&self.content)?
319            .embeds(&self.embeds)?
320            .components(&self.component_info.url_components)?
321            .username(&self.username)?
322            .avatar_url(self.avatar_info.url.as_ref().unwrap())
323            .tts(self.tts);
324
325        match &self.thread_info {
326            thread::Info::In(thread_id) => execute_webhook = execute_webhook.thread_id(*thread_id),
327            thread::Info::CreatedPost(channel) => {
328                execute_webhook = execute_webhook.thread_name(channel.name.as_ref().unwrap());
329            }
330            _ => {}
331        }
332
333        if let Some(flags) = self.flags {
334            execute_webhook = execute_webhook.flags(flags);
335        }
336
337        #[cfg(feature = "upload")]
338        {
339            execute_webhook =
340                execute_webhook.attachments(&self.attachment_sticker_info.attachments_upload)?;
341        }
342
343        // not waiting causes race condition issues in the client
344        Ok(execute_webhook.wait())
345    }
346
347    async fn set_guild_emojis(&mut self) -> Result<(), Error> {
348        if self.guild_emoji_ids.is_some() {
349            return Ok(());
350        }
351
352        self.guild_emoji_ids = Some(
353            self.http
354                .emojis(self.guild_id)
355                .await?
356                .models()
357                .await?
358                .into_iter()
359                .map(|emoji| emoji.id)
360                .collect(),
361        );
362
363        Ok(())
364    }
365}