mirl 9.2.0

Miners Rust Lib - A massive collection of ever growing and changing functions, structs, and enums. Check the description for compatibility and toggleable features! (Most of the lib is controlled by flags/features so the lib can continue to be lightweight despite its size)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
use serde::Serialize;
#[allow(clippy::doc_markdown)]
/// Main webhook payload structure for sending messages to Discord
#[derive(Serialize, Default, Debug, Clone)]
#[cfg_attr(feature = "c_compatible", repr(C))]
pub struct WebhookPayload {
    /// Message content (up to 2000 characters)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub content: Option<String>,
    /// Override the webhook's default username
    #[serde(skip_serializing_if = "Option::is_none")]
    pub username: Option<String>,
    /// Override the webhook's default avatar
    #[serde(skip_serializing_if = "Option::is_none")]
    pub avatar_url: Option<String>,
    /// Enable text-to-speech for the message
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tts: Option<bool>,
    /// Array of rich embed objects (up to 10)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub embeds: Option<Vec<Embed>>,
    /// Control which mentions are parsed from the content
    #[serde(skip_serializing_if = "Option::is_none")]
    pub allowed_mentions: Option<AllowedMentions>,
    /// Message components (buttons, select menus)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub components: Option<Vec<Component>>,
    /// Attachment objects with filenames and descriptions
    #[serde(skip_serializing_if = "Option::is_none")]
    pub attachments: Option<Vec<Attachment>>,
    /// Name of thread to create (requires thread in forum/media channel)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub thread_name: Option<String>,
    /// Message flags (e.g., SUPPRESS_EMBEDS = 1 << 2)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub flags: Option<u64>,
}

/// Rich embed object for formatted messages
#[derive(Serialize, Default, Debug, Clone)]
#[cfg_attr(feature = "c_compatible", repr(C))]
pub struct Embed {
    /// Title of the embed
    #[serde(skip_serializing_if = "Option::is_none")]
    pub title: Option<String>,
    /// Description text (supports Markdown)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    /// URL to make the title a hyperlink
    #[serde(skip_serializing_if = "Option::is_none")]
    pub url: Option<String>,
    /// ISO8601 timestamp for the embed
    #[serde(skip_serializing_if = "Option::is_none")]
    pub timestamp: Option<String>,
    /// Color code as a decimal integer (not hex)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub color: Option<u32>,
    /// Footer information
    #[serde(skip_serializing_if = "Option::is_none")]
    pub footer: Option<EmbedFooter>,
    /// Full-size image displayed in the embed
    #[serde(skip_serializing_if = "Option::is_none")]
    pub image: Option<EmbedImage>,
    /// Small thumbnail image in top-right corner
    #[serde(skip_serializing_if = "Option::is_none")]
    pub thumbnail: Option<EmbedThumbnail>,
    /// Author information displayed above the title
    #[serde(skip_serializing_if = "Option::is_none")]
    pub author: Option<EmbedAuthor>,
    /// Array of field objects (up to 25)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub fields: Option<Vec<EmbedField>>,
}

/// Footer section of an embed
#[derive(Serialize, Debug, Clone)]
#[cfg_attr(feature = "c_compatible", repr(C))]
pub struct EmbedFooter {
    /// Footer text
    pub text: String,
    /// URL of footer icon
    #[serde(skip_serializing_if = "Option::is_none")]
    pub icon_url: Option<String>,
}

/// Image displayed in an embed
#[derive(Serialize, Debug, Clone)]
#[cfg_attr(feature = "c_compatible", repr(C))]
pub struct EmbedImage {
    /// Source URL of the image
    pub url: String,
}

/// Thumbnail image for an embed
#[derive(Serialize, Debug, Clone)]
#[cfg_attr(feature = "c_compatible", repr(C))]
pub struct EmbedThumbnail {
    /// Source URL of the thumbnail
    pub url: String,
}

/// Author information for an embed
#[derive(Serialize, Debug, Clone)]
#[cfg_attr(feature = "c_compatible", repr(C))]
pub struct EmbedAuthor {
    /// Name of the author
    pub name: String,
    /// URL to make the name a hyperlink
    #[serde(skip_serializing_if = "Option::is_none")]
    pub url: Option<String>,
    /// URL of author icon
    #[serde(skip_serializing_if = "Option::is_none")]
    pub icon_url: Option<String>,
}

/// Individual field within an embed
#[derive(Serialize, Debug, Clone)]
#[cfg_attr(feature = "c_compatible", repr(C))]
pub struct EmbedField {
    /// Name of the field
    pub name: String,
    /// Value of the field
    pub value: String,
    /// Whether to display inline (max 3 per row)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub inline: Option<bool>,
}

/// Configuration for which mentions are parsed
#[derive(Serialize, Default, Debug, Clone)]
#[cfg_attr(feature = "c_compatible", repr(C))]
pub struct AllowedMentions {
    /// Types of mentions to parse: "roles", "users", "everyone"
    #[serde(skip_serializing_if = "Option::is_none")]
    pub parse: Option<Vec<String>>,
    /// Array of role IDs to allow mentions for
    #[serde(skip_serializing_if = "Option::is_none")]
    pub roles: Option<Vec<String>>,
    /// Array of user IDs to allow mentions for
    #[serde(skip_serializing_if = "Option::is_none")]
    pub users: Option<Vec<String>>,
    /// Whether to mention the author of the message being replied to
    #[serde(skip_serializing_if = "Option::is_none")]
    pub replied_user: Option<bool>,
}

#[allow(clippy::doc_markdown)]
/// Message component (buttons, select menus)
#[derive(Serialize, Debug, Clone)]
#[cfg_attr(feature = "c_compatible", repr(C))]
pub struct Component {
    /// Component type: 1 = ActionRow, 2 = Button, 3 = SelectMenu, 4 = TextInput
    #[serde(rename = "type")]
    pub component_type: u8,
    /// Child components (for ActionRow)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub components: Option<Vec<Self>>,
    /// Button style: 1 = Primary, 2 = Secondary, 3 = Success, 4 = Danger, 5 = Link
    #[serde(skip_serializing_if = "Option::is_none")]
    pub style: Option<u8>,
    /// Label text for buttons
    #[serde(skip_serializing_if = "Option::is_none")]
    pub label: Option<String>,
    /// Emoji to display on component
    #[serde(skip_serializing_if = "Option::is_none")]
    pub emoji: Option<Emoji>,
    /// Developer-defined identifier
    #[serde(skip_serializing_if = "Option::is_none")]
    pub custom_id: Option<String>,
    /// URL for link-style buttons
    #[serde(skip_serializing_if = "Option::is_none")]
    pub url: Option<String>,
    /// Whether the component is disabled
    #[serde(skip_serializing_if = "Option::is_none")]
    pub disabled: Option<bool>,
    /// Choices for select menus
    #[serde(skip_serializing_if = "Option::is_none")]
    pub options: Option<Vec<SelectOption>>,
    /// Placeholder text for select menus
    #[serde(skip_serializing_if = "Option::is_none")]
    pub placeholder: Option<String>,
    /// Minimum number of selections
    #[serde(skip_serializing_if = "Option::is_none")]
    pub min_values: Option<u8>,
    /// Maximum number of selections
    #[serde(skip_serializing_if = "Option::is_none")]
    pub max_values: Option<u8>,
}

/// Emoji for components or reactions
#[derive(Serialize, Debug, Clone)]
#[cfg_attr(feature = "c_compatible", repr(C))]
pub struct Emoji {
    /// Custom emoji ID
    #[serde(skip_serializing_if = "Option::is_none")]
    pub id: Option<String>,
    /// Unicode emoji or custom emoji name
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    /// Whether the emoji is animated
    #[serde(skip_serializing_if = "Option::is_none")]
    pub animated: Option<bool>,
}

/// Option in a select menu
#[derive(Serialize, Debug, Clone)]
#[cfg_attr(feature = "c_compatible", repr(C))]
pub struct SelectOption {
    /// User-facing option label
    pub label: String,
    /// Developer-defined value
    pub value: String,
    /// Additional description
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    /// Emoji to display with the option
    #[serde(skip_serializing_if = "Option::is_none")]
    pub emoji: Option<Emoji>,
    /// Whether this option is selected by default
    #[serde(skip_serializing_if = "Option::is_none")]
    pub default: Option<bool>,
}

/// Attachment metadata for files
#[derive(Serialize, Debug, Clone)]
#[cfg_attr(feature = "c_compatible", repr(C))]
pub struct Attachment {
    /// Attachment ID
    pub id: String,
    /// Name of the file
    pub filename: String,
    /// Description of the file
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
}

// Convenience implementations

impl WebhookPayload {
    /// Create a simple text message
    pub fn new_text(content: impl Into<String>) -> Self {
        Self {
            content: Some(content.into()),
            ..Default::default()
        }
    }
    #[must_use]
    /// Create a message with a single embed
    pub fn new_embed(embed: Embed) -> Self {
        Self {
            embeds: Some(vec![embed]),
            ..Default::default()
        }
    }
    #[must_use]
    /// Add or replace message content
    pub fn with_content(mut self, content: impl Into<String>) -> Self {
        self.content = Some(content.into());
        self
    }
    #[must_use]
    /// Override the webhook's username
    pub fn with_username(mut self, username: impl Into<String>) -> Self {
        self.username = Some(username.into());
        self
    }
    #[must_use]
    /// Override the webhook's avatar
    pub fn with_avatar_url(mut self, url: impl Into<String>) -> Self {
        self.avatar_url = Some(url.into());
        self
    }
    #[must_use]
    /// Add an embed to the message
    pub fn add_embed(mut self, embed: Embed) -> Self {
        self.embeds.get_or_insert_with(Vec::new).push(embed);
        self
    }
    #[must_use]
    /// Enable text-to-speech
    pub const fn with_tts(mut self, tts: bool) -> Self {
        self.tts = Some(tts);
        self
    }
}

impl Embed {
    #[must_use]
    /// Create a new embed with a title
    pub fn new() -> Self {
        Self::default()
    }

    #[must_use]
    /// Set the embed title
    pub fn with_title(mut self, title: impl Into<String>) -> Self {
        self.title = Some(title.into());
        self
    }

    #[must_use]
    /// Set the embed description
    pub fn with_description(mut self, description: impl Into<String>) -> Self {
        self.description = Some(description.into());
        self
    }

    #[must_use]
    /// Set the embed color (as decimal)
    pub const fn with_color(mut self, color: u32) -> Self {
        self.color = Some(color);
        self
    }

    #[must_use]
    /// Set the embed URL
    pub fn with_url(mut self, url: impl Into<String>) -> Self {
        self.url = Some(url.into());
        self
    }

    #[must_use]
    /// Add a field to the embed
    pub fn add_field(
        mut self,
        name: impl Into<String>,
        value: impl Into<String>,
        inline: bool,
    ) -> Self {
        self.fields.get_or_insert_with(Vec::new).push(EmbedField {
            name: name.into(),
            value: value.into(),
            inline: Some(inline),
        });
        self
    }

    #[must_use]
    /// Set the footer
    pub fn with_footer(
        mut self,
        text: impl Into<String>,
        icon_url: Option<String>,
    ) -> Self {
        self.footer = Some(EmbedFooter {
            text: text.into(),
            icon_url,
        });
        self
    }

    #[must_use]
    /// Set the author
    pub fn with_author(
        mut self,
        name: impl Into<String>,
        url: Option<String>,
        icon_url: Option<String>,
    ) -> Self {
        self.author = Some(EmbedAuthor {
            name: name.into(),
            url,
            icon_url,
        });
        self
    }

    #[must_use]
    /// Set the thumbnail
    pub fn with_thumbnail(mut self, url: impl Into<String>) -> Self {
        self.thumbnail = Some(EmbedThumbnail {
            url: url.into(),
        });
        self
    }

    #[must_use]
    /// Set the image
    pub fn with_image(mut self, url: impl Into<String>) -> Self {
        self.image = Some(EmbedImage {
            url: url.into(),
        });
        self
    }

    // /// Set the timestamp to now
    // pub fn with_timestamp_now(mut self) -> Self {
    //     self.timestamp = Some(chrono::Utc::now().to_rfc3339());
    //     self
    // }
}

impl AllowedMentions {
    #[must_use]
    /// Create allowed mentions config that allows nothing
    pub fn none() -> Self {
        Self {
            parse: Some(vec![]),
            ..Default::default()
        }
    }

    #[must_use]
    /// Create allowed mentions config that allows everything
    pub fn all() -> Self {
        Self {
            parse: Some(vec![
                "roles".to_string(),
                "users".to_string(),
                "everyone".to_string(),
            ]),
            ..Default::default()
        }
    }

    #[must_use]
    /// Allow only user mentions
    pub fn users_only() -> Self {
        Self {
            parse: Some(vec!["users".to_string()]),
            ..Default::default()
        }
    }
}

use reqwest::blocking::Client;
/// Send a message to Discord via webhook
///
/// Use [`WebhookPayload::default()`](WebhookPayload::default) to customize the payload you wanna send
///
/// If the message is longer than 2000 characters, it will automatically split and send in 2000 character chunks
///
/// # Example
/// ```
/// fn function() {
///     let url = "{Your webhook}";
///     let payload = mirl::misc::discord::WebhookPayload::default()
///         .with_content("This message will be send :)");
///     mirl::misc::discord::send_discord_message(url, &payload).unwrap();
/// }
/// ```
/// # Errors
/// When unable to send the message or when the server gives a non 2xx return code
pub fn send_discord_message(
    webhook_url: &str,
    payload: &WebhookPayload,
) -> Result<(), Box<dyn core::error::Error>> {
    const MAX_CONTENT_LENGTH: usize = 2000;

    // Check if we need to chunk the content
    if let Some(content) = &payload.content {
        if content.len() > MAX_CONTENT_LENGTH {
            // Split content into chunks
            let chunks: Vec<String> = content
                .chars()
                .collect::<Vec<char>>()
                .chunks(MAX_CONTENT_LENGTH)
                .map(|chunk| chunk.iter().collect())
                .collect();

            // Send each chunk
            for chunk in chunks {
                let mut chunk_payload = payload.clone();
                chunk_payload.content = Some(chunk);
                // Only include embeds, components, etc. in the first message
                if chunk_payload.content.as_ref().is_some_and(|c| c != content)
                {
                    chunk_payload.embeds = None;
                    chunk_payload.components = None;
                    chunk_payload.attachments = None;
                }

                send_discord_message_single(webhook_url, &chunk_payload)?;
            }
            return Ok(());
        }
    }
    send_discord_message_single(webhook_url, payload)
}
/// Same as [`send_discord_message`] but doesn't chunk messages that are too large
/// # Errors
/// When unable to send the message or when the server gives a non 2xx return code
pub fn send_discord_message_single(
    webhook_url: &str,
    payload: &WebhookPayload,
) -> Result<(), Box<dyn core::error::Error>> {
    let client = Client::new();
    client
        .post(webhook_url)
        .header("Content-Type", "application/json")
        .json(&payload)
        .send()?
        .error_for_status()?;
    Ok(())
}