diff --git a/Cargo.lock b/Cargo.lock index c532719..ac3048c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -37,6 +46,8 @@ name = "arche" version = "0.1.0" dependencies = [ "axum", + "chrono", + "chrono-tz", "dotenvy", "rand 0.9.1", "serenity", @@ -245,17 +256,40 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-link", ] +[[package]] +name = "chrono-tz" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efdce149c370f133a071ca8ef6ea340b7b88748ab0810097a9e2976eaa34b4f3" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f10f8c9340e31fc120ff885fcdb54a0b48e474bbd77cab557f0c30a3e569402" +dependencies = [ + "parse-zoneinfo", + "phf_codegen", +] + [[package]] name = "command_attr" version = "0.5.3" @@ -1124,12 +1158,59 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1260,6 +1341,35 @@ dependencies = [ "bitflags 2.9.0", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "reqwest" version = "0.11.27" @@ -1590,6 +1700,12 @@ dependencies = [ "libc", ] +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "skeptic" version = "0.13.7" diff --git a/Cargo.toml b/Cargo.toml index fb4897a..776d84f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,8 @@ edition = "2024" [dependencies] axum = "0.8.3" +chrono = "0.4.41" +chrono-tz = "0.10.3" dotenvy = "0.15.7" rand = "0.9.1" serenity = "0.12.4" diff --git a/assets/logo-x512-lgbtflag.png b/assets/logo-x512-lgbtflag.png new file mode 100644 index 0000000..1ff2293 Binary files /dev/null and b/assets/logo-x512-lgbtflag.png differ diff --git a/assets/logo-x512-polish.png b/assets/logo-x512-polish.png new file mode 100644 index 0000000..5441868 Binary files /dev/null and b/assets/logo-x512-polish.png differ diff --git a/assets/logo-x512-starwars.png b/assets/logo-x512-starwars.png new file mode 100644 index 0000000..c9a7786 Binary files /dev/null and b/assets/logo-x512-starwars.png differ diff --git a/assets/logo-x512.png b/assets/logo-x512.png new file mode 100644 index 0000000..634f4a2 Binary files /dev/null and b/assets/logo-x512.png differ diff --git a/src/discordbot/guild_icon.rs b/src/discordbot/guild_icon.rs new file mode 100644 index 0000000..9c70e1a --- /dev/null +++ b/src/discordbot/guild_icon.rs @@ -0,0 +1,142 @@ +use chrono::{Datelike, Duration, Local, TimeZone, Utc}; +use chrono_tz::Europe::Warsaw; +use serenity::all::{Context, CreateAttachment, EditGuild, Guild, GuildId}; +use std::time::Duration as StdDuration; +use tracing::{error, info, warn}; + +const MAIN_GUILD_ID: GuildId = GuildId::new(447075692664979466); + +enum Event { + Normal, + PolskaGórą, + PrideMonth, + ValentineDay, + Rogaliki, + Halloween, + Christmas, + NewYears, + AnniversaryGractwo, + AnniversaryTF2, + AnniversaryMinecraft, + StarWarsDay, +} + +impl Event { + fn icon(&self) -> &str { + use Event as E; + match self { + E::PolskaGórą => "./assets/logo-x512-polish.png", + E::PrideMonth => "./assets/logo-x512-lgbtflag.png", + E::StarWarsDay => "./assets/logo-x512-starwars.png", + _ => "./assets/logo-x512.png", + } + } +} + +fn get_current_event() -> Event { + let today = Local::now(); + let (month, day) = (today.month(), today.day()); + + match (day, month) { + (1, 1) => Event::NewYears, + (2..=6, 1) => Event::Christmas, + (14, 2) => Event::ValentineDay, + (1..=3, 5) => Event::PolskaGórą, + (4, 5) => Event::StarWarsDay, + (17, 5) => Event::AnniversaryMinecraft, + (7, 6) => Event::AnniversaryGractwo, + (_, 6) => Event::PrideMonth, + (24, 8) => Event::AnniversaryTF2, + (10, 10) => Event::AnniversaryTF2, + (31, 10) => Event::Halloween, + (11, 11) => Event::Rogaliki, + (24..=30, 12) => Event::Christmas, + (31, 12) => Event::NewYears, + _ => Event::Normal, + } +} + +pub fn init_service(ctx: &Context, guild_id: &GuildId) { + let (ctx, guild_id) = (ctx.clone(), guild_id.clone()); + info!("Initialising guild name/icon automation service..."); + + if guild_id != MAIN_GUILD_ID { + info!("Guild name/icon automation service not initialised; Bot not running on main guild."); + return; + } + + tokio::spawn(async move { + run_icon_service(ctx, guild_id).await; + }); +} + +pub async fn run_icon_service(ctx: Context, guild_id: GuildId) { + loop { + update_icon(&ctx, guild_id).await.ok(); + sleep_until_next_midnight().await; + } +} + +async fn update_icon(ctx: &Context, guild_id: GuildId) -> Result<(), String> { + let mut guild = match Guild::get(&ctx.http, guild_id).await { + Ok(g) => g, + Err(e) => { + error!("Could not get guild info..."); + return Err("Could not get guild info: {e}".to_string()); + } + }; + let event = get_current_event(); + + let icon = match CreateAttachment::path(event.icon()).await { + Ok(i) => i, + Err(e) => { + error!("Could not create icon attachment..."); + return Err("Could not create icon attachment: {e}".to_string()); + } + }; + match guild + .edit(&ctx.http, EditGuild::new().icon(Some(&icon))) + .await + { + Ok(_) => info!("Guild icon updated."), + Err(e) => { + error!("Could not update guild icon..."); + return Err("Could not update guild icon: {e}".to_string()); + } + }; + + Ok(()) +} + +async fn sleep_until_next_midnight() { + let now = Utc::now().with_timezone(&Warsaw); + let tomorrow = now.date_naive() + chrono::Duration::days(1); + + let next_midnight = match Warsaw + .with_ymd_and_hms( + tomorrow.year_ce().1 as i32, + tomorrow.month(), + tomorrow.day(), + 0, + 0, + 0, + ) + .earliest() + { + Some(nm) => nm, + None => Utc::now().with_timezone(&Warsaw) + Duration::minutes(61), + // e.g. jumps in time occur at 2AM/3AM, Midnight shouldn't be affected + // but even if it was, returning None, a gap shouldn't last more than an hour. + }; + + let sleeptime = match next_midnight.signed_duration_since(now).to_std() { + Ok(t) => t, + Err(_) => { + warn!("The next midnight is, supposedly, not in the future."); + warn!("Sleeping an hour instead..."); + StdDuration::from_secs(1 * 60 * 60) + } + }; + + tokio::time::sleep(sleeptime).await; +} diff --git a/src/discordbot/mod.rs b/src/discordbot/mod.rs index 438081a..b3df5b3 100644 --- a/src/discordbot/mod.rs +++ b/src/discordbot/mod.rs @@ -11,6 +11,7 @@ const GUILD_ID: &str = "DISCORD_SERVER_ID"; struct Handler; mod commands; +mod guild_icon; #[async_trait] impl EventHandler for Handler { @@ -31,6 +32,8 @@ impl EventHandler for Handler { Ok(_) => info!("Successfully registered commands on the guild.",), Err(why) => info!("Failed to register commands on the guild: {why:?}"), }; + + guild_icon::init_service(&ctx, &guild_id); } async fn interaction_create(&self, ctx: Context, interaction: Interaction) {