use serde::Deserialize; use std::{ffi::OsString, fs, path::PathBuf}; pub mod get_config_icons { use super::*; use crate::Config; #[derive(Deserialize)] pub struct ConfigToml { pub source: ConfigTomlSource, pub festivals: Vec, } #[derive(Deserialize)] pub struct ConfigTomlLocal { pub source: ConfigTomlSource, } #[derive(Deserialize)] pub struct ConfigTomlRemote { pub festivals: Vec, } #[derive(Deserialize, Debug)] pub struct ConfigTomlSource { pub repo: String, pub directory: String, pub file: String, } #[derive(Deserialize, Debug)] pub struct ConfigTomlFestivals { pub name: String, pub all_year: bool, pub start: ConfigTomlFestivalsTime, pub end: ConfigTomlFestivalsTime, } #[derive(Deserialize, Debug)] pub struct ConfigTomlFestivalsTime { pub day: u32, pub month: u32, pub year: i32, } pub fn minimal() -> ConfigTomlLocal { let toml_raw_min = include_str!("../../.server-icons.toml"); toml::from_str::(toml_raw_min).unwrap_or_else(|e| { dbg!(e); ConfigTomlLocal { source: ConfigTomlSource { repo: "".to_string(), directory: "".to_string(), file: "".to_string(), }, } }) } // since a copy of the festival file is in the repo we just need to get to it pub fn full(config: &Config) -> ConfigToml { let config_source = minimal(); let file_path = format!("{}/open-governance/{}/{}", &config.home, &config_source.source.directory, &config_source.source.file); let contents = fs::read_to_string(file_path).unwrap_or_else(|e| { dbg!(e); "".to_string() }); let festivals = match toml::from_str::(&contents) { Ok(config_festivals) => config_festivals.festivals, Err(e) => { dbg!(e); vec![] } }; ConfigToml { source: config_source.source, festivals, } } } #[derive(Debug)] pub struct LogoData { pub name: OsString, pub path: PathBuf, } #[derive(Debug, Clone, sqlx::FromRow)] pub struct ServerIcons { pub id: i64, pub name: String, pub path: String, pub date: String, } pub mod update_icon { use super::*; use crate::{ common::{ renderer::{Args, Renderer}, server_icon::get_config_icons::{self, ConfigToml, ConfigTomlLocal}, }, get_now_iso, Config, }; use chrono::{Datelike, Utc}; use rand::{rngs::SmallRng, seq::IndexedRandom, SeedableRng}; use serenity::{ all::GuildId, builder::{CreateAttachment, EditGuild}, client::Context, }; use sqlx::{Pool, Sqlite}; use std::process::Command; /// Update the server icon, pulling from open governance. pub async fn update_icon_main(ctx: &Context, db: &Pool, config_global: &Config, config_toml_local: &ConfigTomlLocal) { let server = config_global.compsoc_server; // clone repo into local folder clone_repo(config_global, config_toml_local); // now the repo has been downloaded/updated we can now access the festivals let config_toml = get_config_icons::full(config_global); // see if there is a current festival let festival_data = get_festival(&config_toml); // get a list of all the graphics files let logos = get_logos(config_global, &config_toml); // filter them so only the current season (if any) are active let logos_filtered = logos_filter(&festival_data, logos); let mut rng = SmallRng::from_os_rng(); let logo_selected = logos_filtered.choose(&mut rng).unwrap(); logo_set(ctx, db, &server, logo_selected).await; } #[derive(Debug)] pub struct FestivalData { pub current: Vec, exclusions: Vec, } pub fn get_festival(config_toml: &ConfigToml) -> FestivalData { let today = Utc::now(); let day = today.day(); let month = today.month(); let year = today.year(); let mut result = FestivalData { current: vec![], exclusions: vec![], }; for festival in &config_toml.festivals { if (day >= festival.start.day && day <= festival.end.day) && (month >= festival.start.month && month <= festival.end.month) { if festival.start.year == 0 || festival.end.year == 0 || (year >= festival.start.year && year <= festival.end.year) { result.current.push(festival.name.to_owned()); } } else if !festival.all_year { result.exclusions.push(festival.name.to_owned()); } } result } fn clone_repo(config: &Config, config_toml: &ConfigTomlLocal) { let url = &config_toml.source.repo; let folder = format!("{}/open-governance", &config.home); if let Err(e) = Command::new("git") // clone the repo, gracefully "fails" .arg("clone") .arg(url) .arg(&folder) .output() { dbg!(e); } if let Err(e) = Command::new("git") // Update the repo .arg("pull") .arg("origin") .arg("main") .current_dir(&folder) .output() { dbg!(e); } } fn get_logos(config: &Config, config_toml: &ConfigToml) -> Vec { let folder = format!("{}/open-governance/{}", &config.home, &config_toml.source.directory); let folder_path = PathBuf::from(&folder); let mut folder_output = folder_path.clone(); folder_output.push("converted"); let paths = match fs::read_dir(folder) { Ok(x) => x, Err(e) => { dbg!(e); return vec![]; } }; let args = Args { input: folder_path.clone(), output: folder_output, colors: String::from(""), width: 1024, height: 1024, }; let mut r = match Renderer::new(&args) { Ok(x) => x, Err(e) => { let _ = dbg!(e); return vec![]; } }; let mut logos = vec![]; for tmp in paths.flatten() { let path_local = tmp.path().to_owned(); let path_local2 = tmp.path().to_owned(); let name = match path_local2.file_name() { None => { dbg!(path_local2); continue; } Some(x) => x.to_owned(), }; let mut path = tmp.path(); if path.is_dir() { continue; } match tmp.path().extension() { None => {} Some(ext) => { if ext == "svg" { let mut path_new = path_local.clone(); path_new.set_extension("png"); let filename_tmp = path_new.clone(); let filename = match filename_tmp.file_name() { None => { dbg!(filename_tmp); continue; } Some(x) => x, }; path_new.pop(); path_new.push("converted"); path_new.push(filename); // check if exists if !path_new.exists() { // convert if it hasnt been converted already match r.render(&path_local, &args) { Ok(_) => {} Err(_e) => { dbg!("Failed to render {path_local:?}: {}"); } } } path = path_new; } } }; logos.push(LogoData { name, path, }); // println!("Name: {}", &tmp.path().display()); } logos } fn logos_filter(festival_data: &FestivalData, existing: Vec) -> Vec { let mut filtered: Vec = vec![]; let allowed_files = vec![".png", ".jpeg", ".gif", ".svg"]; 'outer: for logo in existing { let name_lowercase0 = logo.name.to_ascii_lowercase(); let name_lowercase = name_lowercase0.to_str().unwrap_or_default(); let mut allowed = false; for allowed_type in &allowed_files { if name_lowercase.ends_with(allowed_type) { allowed = true; } } if !allowed { continue; } if !festival_data.current.is_empty() { // if its a current festival filter based on it for festival in &festival_data.current { if name_lowercase.contains(festival) { filtered.push(logo); continue 'outer; } } } else { // else filter using the excluded ones let mut excluded = false; for festival in &festival_data.exclusions { if name_lowercase.contains(festival) { excluded = true; } } if !excluded { filtered.push(logo); } } } filtered } async fn logo_set(ctx: &Context, db: &Pool, server: &GuildId, logo_selected: &LogoData) { // add to teh database if !logo_set_db(db, logo_selected).await { // something went wrong return; } if let Some(logo_path) = logo_selected.path.to_str() { match CreateAttachment::path(logo_path).await { Ok(icon) => { // assuming a `guild` has already been bound let builder = EditGuild::new().icon(Some(&icon)); if let Err(e) = server.edit(ctx, builder).await { dbg!(e); } } Err(e) => { dbg!(e); } } } } async fn logo_set_db(db: &Pool, logo_selected: &LogoData) -> bool { let name = match logo_selected.name.to_str() { None => return false, Some(x) => x, }; let path = match logo_selected.path.to_str() { None => return false, Some(x) => x, }; match sqlx::query_as::<_, ServerIcons>( " INSERT OR REPLACE INTO server_icons (name, date, path) VALUES (?1, ?2, ?3) ", ) .bind(name) .bind(get_now_iso(false)) .bind(path) .fetch_optional(db) .await { Ok(_) => {} Err(e) => { dbg!(e); return false; } } true } }