use serde::Serialize;
#[allow(clippy::doc_markdown)]
#[derive(Serialize, Default, Debug, Clone)]
#[cfg_attr(feature = "c_compatible", repr(C))]
pub struct WebhookPayload {
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub username: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub avatar_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tts: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub embeds: Option<Vec<Embed>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allowed_mentions: Option<AllowedMentions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub components: Option<Vec<Component>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub attachments: Option<Vec<Attachment>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thread_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub flags: Option<u64>,
}
#[derive(Serialize, Default, Debug, Clone)]
#[cfg_attr(feature = "c_compatible", repr(C))]
pub struct Embed {
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timestamp: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub color: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub footer: Option<EmbedFooter>,
#[serde(skip_serializing_if = "Option::is_none")]
pub image: Option<EmbedImage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail: Option<EmbedThumbnail>,
#[serde(skip_serializing_if = "Option::is_none")]
pub author: Option<EmbedAuthor>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fields: Option<Vec<EmbedField>>,
}
#[derive(Serialize, Debug, Clone)]
#[cfg_attr(feature = "c_compatible", repr(C))]
pub struct EmbedFooter {
pub text: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub icon_url: Option<String>,
}
#[derive(Serialize, Debug, Clone)]
#[cfg_attr(feature = "c_compatible", repr(C))]
pub struct EmbedImage {
pub url: String,
}
#[derive(Serialize, Debug, Clone)]
#[cfg_attr(feature = "c_compatible", repr(C))]
pub struct EmbedThumbnail {
pub url: String,
}
#[derive(Serialize, Debug, Clone)]
#[cfg_attr(feature = "c_compatible", repr(C))]
pub struct EmbedAuthor {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icon_url: Option<String>,
}
#[derive(Serialize, Debug, Clone)]
#[cfg_attr(feature = "c_compatible", repr(C))]
pub struct EmbedField {
pub name: String,
pub value: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub inline: Option<bool>,
}
#[derive(Serialize, Default, Debug, Clone)]
#[cfg_attr(feature = "c_compatible", repr(C))]
pub struct AllowedMentions {
#[serde(skip_serializing_if = "Option::is_none")]
pub parse: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub roles: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub users: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub replied_user: Option<bool>,
}
#[allow(clippy::doc_markdown)]
#[derive(Serialize, Debug, Clone)]
#[cfg_attr(feature = "c_compatible", repr(C))]
pub struct Component {
#[serde(rename = "type")]
pub component_type: u8,
#[serde(skip_serializing_if = "Option::is_none")]
pub components: Option<Vec<Self>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub style: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub emoji: Option<Emoji>,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub disabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub options: Option<Vec<SelectOption>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub placeholder: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_values: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_values: Option<u8>,
}
#[derive(Serialize, Debug, Clone)]
#[cfg_attr(feature = "c_compatible", repr(C))]
pub struct Emoji {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub animated: Option<bool>,
}
#[derive(Serialize, Debug, Clone)]
#[cfg_attr(feature = "c_compatible", repr(C))]
pub struct SelectOption {
pub label: String,
pub value: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub emoji: Option<Emoji>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<bool>,
}
#[derive(Serialize, Debug, Clone)]
#[cfg_attr(feature = "c_compatible", repr(C))]
pub struct Attachment {
pub id: String,
pub filename: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
impl WebhookPayload {
pub fn new_text(content: impl Into<String>) -> Self {
Self {
content: Some(content.into()),
..Default::default()
}
}
#[must_use]
pub fn new_embed(embed: Embed) -> Self {
Self {
embeds: Some(vec![embed]),
..Default::default()
}
}
#[must_use]
pub fn with_content(mut self, content: impl Into<String>) -> Self {
self.content = Some(content.into());
self
}
#[must_use]
pub fn with_username(mut self, username: impl Into<String>) -> Self {
self.username = Some(username.into());
self
}
#[must_use]
pub fn with_avatar_url(mut self, url: impl Into<String>) -> Self {
self.avatar_url = Some(url.into());
self
}
#[must_use]
pub fn add_embed(mut self, embed: Embed) -> Self {
self.embeds.get_or_insert_with(Vec::new).push(embed);
self
}
#[must_use]
pub const fn with_tts(mut self, tts: bool) -> Self {
self.tts = Some(tts);
self
}
}
impl Embed {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
#[must_use]
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
#[must_use]
pub const fn with_color(mut self, color: u32) -> Self {
self.color = Some(color);
self
}
#[must_use]
pub fn with_url(mut self, url: impl Into<String>) -> Self {
self.url = Some(url.into());
self
}
#[must_use]
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]
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]
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]
pub fn with_thumbnail(mut self, url: impl Into<String>) -> Self {
self.thumbnail = Some(EmbedThumbnail {
url: url.into(),
});
self
}
#[must_use]
pub fn with_image(mut self, url: impl Into<String>) -> Self {
self.image = Some(EmbedImage {
url: url.into(),
});
self
}
}
impl AllowedMentions {
#[must_use]
pub fn none() -> Self {
Self {
parse: Some(vec![]),
..Default::default()
}
}
#[must_use]
pub fn all() -> Self {
Self {
parse: Some(vec![
"roles".to_string(),
"users".to_string(),
"everyone".to_string(),
]),
..Default::default()
}
}
#[must_use]
pub fn users_only() -> Self {
Self {
parse: Some(vec!["users".to_string()]),
..Default::default()
}
}
}
use reqwest::blocking::Client;
pub fn send_discord_message(
webhook_url: &str,
payload: &WebhookPayload,
) -> Result<(), Box<dyn core::error::Error>> {
const MAX_CONTENT_LENGTH: usize = 2000;
if let Some(content) = &payload.content {
if content.len() > MAX_CONTENT_LENGTH {
let chunks: Vec<String> = content
.chars()
.collect::<Vec<char>>()
.chunks(MAX_CONTENT_LENGTH)
.map(|chunk| chunk.iter().collect())
.collect();
for chunk in chunks {
let mut chunk_payload = payload.clone();
chunk_payload.content = Some(chunk);
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)
}
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(())
}