guzzles data
Diffstat (limited to 'src/main.rs')
| -rw-r--r-- | src/main.rs | 186 |
1 files changed, 131 insertions, 55 deletions
diff --git a/src/main.rs b/src/main.rs index e89fb21..0567273 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,8 @@ use serenity::futures::StreamExt; use std::fs::read_to_string; use std::io::Write; use std::ops::{Deref, Sub}; +use std::sync::Arc; +use tokio::sync::Mutex; type Context<'a> = poise::Context<'a, (), anyhow::Error>; #[derive(poise::ChoiceParameter, Default, Copy, Clone, Debug)] @@ -21,6 +23,7 @@ enum Period { LastWeek, } +#[derive(Copy, Clone, Debug)] struct Days(usize); impl Deref for Days { @@ -66,17 +69,121 @@ impl Period { Self::LastWeek => Days::now() - 7, } } - - fn tic(self, t: Days) -> bool { + fn tic(self, t: Days) -> String { match self { - Self::LastYear | Self::AllTime => t.t().day() == 1, - Self::LastMonth => t.t().day() % 7 == 0, - Self::LastWeek => true, + Period::AllTime if t.t().day() == 1 => t.t().format("%m/%y/%d").to_string(), + Period::LastYear if t.t().day() == 1 => t.t().format("%B %d").to_string(), + Period::LastMonth if t.t().day() % 7 == 0 => t.t().format("%B %d").to_string(), + Period::LastWeek => t.t().format("%A").to_string(), + _ => "".to_string(), } } } #[poise::command(slash_command)] +/// graph of messages over the last 4 days +pub async fn msgs(c: Context<'_>) -> Result<()> { + c.defer().await?; + let (min, cs) = { + let Some(g) = c.guild() else { + _ = c.reply(format!("{CANCEL} need guild")); + return Ok(()); + }; + ( + Days::from(g.id.created_at().timestamp() as usize), + g.channels.values().map(|x| x.id).collect::<Vec<_>>(), + ) + }; + + let data: Arc<Mutex<Vec<u16>>> = Arc::new(Mutex::new(vec![0; *Days::now() + 1])); + let cutoff = Days::now() - 3; + futures::stream::iter(cs) + .map(|x| { + let data = data.clone(); + async move { + let mut c = std::pin::pin!(x.messages_iter(c)); + + while let Some(Ok(x)) = c.next().await { + if x.author.bot { + continue; + } + let x = x.timestamp; + let d = *Days::from(x.timestamp() as usize); + if d < *cutoff { + break; + } + data.lock().await[d] += 1; + } + } + }) + .buffer_unordered(32) + .for_each(|_| async {}) + .await; + let min = min.max(*cutoff); + let i = c.id(); + let data = data.lock().await; + let mut f = std::fs::File::create(format!("{i}.dat")).unwrap(); + for (i, &d) in data[min..].iter().enumerate() { + writeln!(&mut f, r"{},{d}", Period::LastWeek.tic(Days(i + min))).unwrap(); + } + let plot = if cfg!(debug_assertions) { + std::fs::read_to_string("y.plot").unwrap() + } else { + include_str!("../y.plot").to_string() + }; + let cleanup = tokio::task::spawn_blocking(move || run(plot, &[], i)) + .await + .unwrap(); + + poise::send_reply( + c, + poise::CreateReply::default() + .attachment(CreateAttachment::path(format!("{i}.png")).await.unwrap()), + ) + .await?; + tokio::task::spawn_blocking(cleanup).await.unwrap(); + Ok(()) +} + +fn run( + mut plot: String, + templates: &[(&'static str, &dyn std::fmt::Display)], + i: u64, +) -> impl FnOnce() -> () { + for (key, value) in templates { + plot = plot.replace(&format!("{{{key}}}"), &value.to_string()) + } + let plot = plot.replace("{id}", &i.to_string()); + let path = format!("{i}.plot"); + std::fs::File::create_new(&path) + .unwrap() + .write_all(plot.as_bytes()) + .unwrap(); + + assert!(std::process::Command::new("gnuplot") + .arg(&path) + .spawn() + .unwrap() + .wait() + .unwrap() + .success()); + assert!(std::process::Command::new("inkscape") + .arg(format!("--export-filename={i}.png")) + .arg(format!("{i}.svg")) + .spawn() + .unwrap() + .wait() + .unwrap() + .success()); + move || { + std::fs::remove_file(format!("{i}.dat")).unwrap(); + std::fs::remove_file(path).unwrap(); + std::fs::remove_file(format!("{i}.svg")).unwrap(); + std::fs::remove_file(format!("{i}.png")).unwrap(); + } +} + +#[poise::command(slash_command)] /// graph of users over time pub async fn users( c: Context<'_>, @@ -92,13 +199,12 @@ pub async fn users( g.id }; let mut s = std::pin::pin!(g.members_iter(c)); - let mut data: Vec<u16> = vec![0; 365 * 12]; - let mut min = 365 * 12; + let mut data: Vec<u16> = vec![0; *Days::now() + 1]; + let min = Days::from(g.created_at().timestamp() as usize); let mut max = 0; while let Some(x) = s.next().await { let x = x?.joined_at.unwrap(); let d = *Days::from(x.timestamp() as usize); - min = min.min(d); max = max.max(d); data[d] += 1; } @@ -110,16 +216,7 @@ pub async fn users( let floor = sum; for (i, &d) in data[min..max].iter().enumerate() { sum += d as u64; - writeln!( - &mut f, - r"{},{sum}", - if p.tic(Days(i + min)) { - Days(i + min).t().format("%m/%d/%y").to_string() - } else { - "".to_string() - } - ) - .unwrap(); + writeln!(&mut f, r"{},{sum}", p.tic(Days(i + min))).unwrap(); } let plot = if cfg!(debug_assertions) { std::fs::read_to_string("x.plot").unwrap() @@ -127,50 +224,29 @@ pub async fn users( include_str!("../x.plot").to_string() }; - let plot = plot - .replace( - "{floor}", - &match p { - Period::AllTime => "".to_string(), - _ => floor.to_string(), - }, + let cleanup = tokio::task::spawn_blocking(move || { + run( + plot, + &[( + "floor", + &match p { + Period::AllTime => "".to_string(), + _ => floor.to_string(), + }, + )], + i, ) - .replace("{id}", &i.to_string()); - let path = format!("{i}.plot"); - std::fs::File::create_new(&path) - .unwrap() - .write_all(plot.as_bytes()) - .unwrap(); + }) + .await + .unwrap(); - assert!(std::process::Command::new("gnuplot") - .arg(&path) - .spawn() - .unwrap() - .wait() - .unwrap() - .success()); - assert!(std::process::Command::new("inkscape") - .arg(format!("--export-filename={i}.png")) - .arg(format!("{i}.svg")) - .spawn() - .unwrap() - .wait() - .unwrap() - .success()); poise::send_reply( c, poise::CreateReply::default() .attachment(CreateAttachment::path(format!("{i}.png")).await.unwrap()), ) .await?; - tokio::task::spawn_blocking(move || { - std::fs::remove_file(format!("{i}.dat")).unwrap(); - std::fs::remove_file(path).unwrap(); - std::fs::remove_file(format!("{i}.svg")).unwrap(); - std::fs::remove_file(format!("{i}.png")).unwrap(); - }) - .await - .unwrap(); + tokio::task::spawn_blocking(cleanup).await.unwrap(); Ok(()) } @@ -180,7 +256,7 @@ async fn main() { std::env::var("TOKEN").unwrap_or_else(|_| read_to_string("token").expect("wher token")); let f = poise::Framework::builder() .options(poise::FrameworkOptions { - commands: vec![users()], + commands: vec![users(), msgs()], ..Default::default() }) .setup(|ctx, _ready, f| { |