diff --git a/Cargo.lock b/Cargo.lock index 5d1de9f..5205c13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1070,6 +1070,7 @@ checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", "hashbrown", + "serde", ] [[package]] @@ -1834,6 +1835,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2214,6 +2224,40 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tower-service" version = "0.3.2" @@ -2412,6 +2456,7 @@ dependencies = [ "dotenvy", "env_logger", "humantime", + "indexmap", "log", "nanoid", "num-traits", @@ -2428,6 +2473,7 @@ dependencies = [ "serde_json", "sysinfo", "tokio", + "toml", ] [[package]] @@ -2744,6 +2790,15 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +[[package]] +name = "winnow" +version = "0.5.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7cf47b659b318dccbd69cc4797a39ae128f533dce7902a1096044d1967b9c16" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.50.0" diff --git a/Cargo.toml b/Cargo.toml index 66778a6..fdb4e86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ color-eyre = "0.6.2" dotenvy = "0.15.7" env_logger = "0.10.1" humantime = "2.1.0" +indexmap = { version = "2.1.0", features = ["serde"] } log = "0.4.20" nanoid = "0.4.0" num-traits = "0.2.17" @@ -32,6 +33,7 @@ serde = { version = "1.0.193", features = ["derive"] } serde_json = "1.0.109" sysinfo = "0.30.5" tokio = { version = "1.35.0", features = ["full"] } +toml = "0.8.8" [profile.release] opt-level = "z" diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 480edf5..cf60532 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -9,6 +9,7 @@ mod say; mod self_timeout; mod shiggy; mod sysinfo; +mod template_channel; mod translate; mod version; @@ -31,6 +32,7 @@ pub fn to_vec() -> Vec< self_timeout::transparency(), shiggy::shiggy(), sysinfo::sysinfo(), + template_channel::template_channel(), translate::translate(), version::version(), ] diff --git a/src/commands/template_channel.rs b/src/commands/template_channel.rs new file mode 100644 index 0000000..edfac43 --- /dev/null +++ b/src/commands/template_channel.rs @@ -0,0 +1,50 @@ +use color_eyre::eyre::Result; +use poise::{ + serenity_prelude::{futures::StreamExt, ChannelId, CreateEmbed}, + CreateReply, +}; + +use crate::{reqwest_client::HTTP, template_channel::Config as TemplateChannelConfig, Context}; + +/// Apply a channel template from a URL to a channel +#[poise::command(rename = "template-channel", slash_command, guild_only, ephemeral)] +pub async fn template_channel( + ctx: Context<'_>, + #[description = "The URL to fetch the template from"] url: String, + #[description = "The channel to apply the template to"] channel: ChannelId, + #[description = "Whether or not to clear the channel (default true)"] clear: Option, +) -> Result<()> { + let clear = clear.unwrap_or(true); + ctx.defer_ephemeral().await?; + + let source = HTTP.get(&url).send().await?.text().await?; + let data = TemplateChannelConfig::parse(&source)?; + let messages = data.to_messages(); + + if clear { + let mut message_iter = channel.messages_iter(&ctx).boxed(); + while let Some(message) = message_iter.next().await { + if let Ok(message) = message { + message.delete(&ctx).await?; + } + } + } + + for m in messages { + channel.send_message(&ctx, m).await?; + } + + ctx.send( + CreateReply::default().embed( + CreateEmbed::default() + .title("Applied channel template!") + .field("URL", format!("`{url}`"), false) + .field("Channel", format!("<#{channel}>"), false) + .field("Components", format!("{}", data.components.len()), false) + .color(0x22d3ee), + ), + ) + .await?; + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 572b7b7..6c3ef77 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,6 +29,7 @@ mod handlers; mod reqwest_client; mod starboard; mod storage; +mod template_channel; mod utils; #[allow(clippy::too_many_lines)] diff --git a/src/template_channel.rs b/src/template_channel.rs new file mode 100644 index 0000000..16bd268 --- /dev/null +++ b/src/template_channel.rs @@ -0,0 +1,143 @@ +use color_eyre::eyre::Result; + +use indexmap::IndexMap; +use poise::serenity_prelude::{CreateEmbed, CreateMessage}; +use serde::{Deserialize, Serialize}; + +fn default_to_false() -> bool { + false +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub struct Config { + #[serde(default)] + pub components: Vec, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +#[serde(rename_all = "snake_case", tag = "type")] +pub enum Component { + Embed(EmbedComponent), + Rules(RulesComponent), + Links(LinksComponent), + Text(TextComponent), +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub struct EmbedComponent { + #[serde(default)] + embeds: Vec, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub struct EmbedComponentEmbed { + title: Option, + description: Option, + color: Option, + fields: Option>, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub struct EmbedComponentEmbedField { + name: String, + value: String, + #[serde(default = "default_to_false")] + inline: bool, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub struct RulesComponent { + #[serde(default)] + rules: IndexMap, + #[serde(default)] + colors: Vec, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub struct LinksComponent { + title: String, + color: Option, + links: IndexMap, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub struct TextComponent { + text: String, +} + +impl Config { + pub fn parse(source: &str) -> Result { + Ok(toml::from_str(source)?) + } +} + +impl Config { + pub fn to_messages(&self) -> Vec { + self.components + .iter() + .map(|component| match component { + Component::Embed(data) => { + let mut message = CreateMessage::default(); + + for embed_data in &data.embeds { + let mut embed = CreateEmbed::default(); + + if let Some(title) = &embed_data.title { + embed = embed.title(title); + } + if let Some(description) = &embed_data.description { + embed = embed.description(description); + } + if let Some(color) = &embed_data.color { + embed = embed.color(*color); + } + if let Some(fields) = &embed_data.fields { + embed = + embed.fields(fields.iter().map(|f| (&f.name, &f.value, f.inline))); + } + + message = message.embed(embed); + } + + message + } + + Component::Links(data) => { + let mut embed = CreateEmbed::default().title(&data.title).description( + data.links + .iter() + .map(|(title, href)| format!("ยป [{title}]({href})")) + .collect::>() + .join("\n"), + ); + + if let Some(color) = data.color { + embed = embed.color(color); + } + + CreateMessage::default().embed(embed) + } + + Component::Rules(data) => { + let mut message = CreateMessage::default(); + + for (idx, (title, desc)) in data.rules.iter().enumerate() { + let mut embed = CreateEmbed::default() + .title(format!("{}. {}", idx + 1, title)) + .description(desc); + + if let Some(color) = data.colors.get(idx % data.colors.len()) { + embed = embed.color(*color); + } + + message = message.add_embed(embed); + } + + message + } + + Component::Text(data) => CreateMessage::default().content(&data.text), + }) + .collect::>() + } +}