smol bot
ownership and leaderboard
| -rw-r--r-- | Cargo.toml | 6 | ||||
| -rw-r--r-- | src/bot/mod.rs | 183 | ||||
| -rw-r--r-- | src/bot/ownership.rs | 21 | ||||
| -rw-r--r-- | src/bot/search.rs | 2 | ||||
| -rw-r--r-- | src/expose.rs | 30 |
5 files changed, 198 insertions, 44 deletions
@@ -42,7 +42,11 @@ logos = "0.14.0" base64 = "0.21.7" humantime = "2.1.0" memory-stats = { version = "1.1.0", features = ["always_use_statm"] } -axum = { version = "0.6.18", features = ["tokio", "http1", "macros"], default-features = false } +axum = { version = "0.6.18", features = [ + "tokio", + "http1", + "macros", +], default-features = false } serde_json = "1.0.122" serde = "1.0.204" atools = "0.1.5" diff --git a/src/bot/mod.rs b/src/bot/mod.rs index a61fd34..b504c6b 100644 --- a/src/bot/mod.rs +++ b/src/bot/mod.rs @@ -1,5 +1,6 @@ mod logic; mod map; +mod ownership; mod schematic; pub mod search; @@ -9,12 +10,11 @@ use mindus::data::DataWrite; use mindus::Serializable; use poise::{serenity_prelude::*, CreateReply}; use serenity::futures::StreamExt; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::fmt::Write; use std::fs::read_to_string; use std::ops::ControlFlow; use std::path::Path; -use std::process::Stdio; use std::sync::{Arc, LazyLock, OnceLock}; use std::time::{Duration, Instant}; use tokio::sync::Mutex; @@ -154,6 +154,7 @@ pub async fn scour(c: Context<'_>, ch: ChannelId) -> Result<()> { if let Ok(Some(mut x)) = schematic::from((&msg.content, &msg.attachments)).await { x.schem.tags.insert("labels".into(), tags); let who = msg.author_nick(c).await.unwrap_or(msg.author.name.clone()); + ownership::insert(msg.id.get(), (msg.author.name.clone(), msg.author.id.get())); git::write(d, msg.id, x); git::commit(&who, &format!("add {:x}.msch", msg.id.get())); msg.react(c, emojis::get!(MERGE)).await?; @@ -224,21 +225,8 @@ pub mod git { path(dir, x).exists() } - pub fn whos(dir: &str, x: MessageId) -> String { - let mut dat = std::process::Command::new("git") - .current_dir("repo") - .arg("blame") - .arg("--porcelain") - .arg(gpath(dir, x)) - .stdout(Stdio::piped()) - .spawn() - .unwrap() - .wait_with_output() - .unwrap() - .stdout; - dat.drain(0..=dat.iter().position(|&x| x == b'\n').unwrap() + "author ".len()); - dat.truncate(dat.iter().position(|&x| x == b'\n').unwrap()); - String::from_utf8(dat).unwrap() + pub fn whos(x: MessageId) -> String { + ownership::get(x.get()).0 } pub fn remove(dir: &str, x: MessageId) { @@ -307,7 +295,7 @@ impl Bot { std::env::var("TOKEN").unwrap_or_else(|_| read_to_string("token").expect("wher token")); let f = poise::Framework::builder() .options(poise::FrameworkOptions { - commands: vec![logic::run(), ping(), help(), scour(), retag(), search::search(), search::file(), render(), render_file(), render_message()], + commands: vec![logic::run(), lb(), lb_no_vds(), ping(), help(), search::search(), search::file(), render(), render_file(), render_message()], event_handler: |c, e, _, d| { Box::pin(async move { match e { @@ -320,8 +308,9 @@ impl Bot { FullEvent::ReactionAdd { add_reaction: Reaction { message_id, emoji: ReactionType::Custom { id,.. } ,channel_id,member: Some(Member{roles,nick,user,..}),..}} if *id == 1192388789952319499 && let Some(Ch {d:dir,..}) = SPECIAL.get(&channel_id.get()) && roles.contains(&RoleId::new(925676016708489227)) => { let m = c.http().get_message(*channel_id,* message_id).await?; if let Ok(s) = git::schem(dir,*message_id) { + ownership::erase(message_id.get()); let who = nick.as_deref().unwrap_or(&user.name); - let own = git::whos(dir,*message_id); + let own = git::whos(*message_id); git::remove(dir, *message_id); git::commit(who, &format!("remove {:x}.msch", message_id.get())); git::push(); @@ -389,6 +378,7 @@ impl Bot { } if let Some(dir) = dir { // add :) + ownership::insert(m.id.get(), (m.author.name.clone(), m.author.id.get())); send(c,|x| x .avatar_url(new_message.author.avatar_url().unwrap_or(CAT.to_string())) .username(&who) @@ -461,7 +451,8 @@ impl Bot { } => { if let Some(Ch{ d:dir,..}) = SPECIAL.get(&channel_id.get()) { if let Ok(s) = git::schem(dir, *deleted_message_id) { - let own = git::whos(dir,*deleted_message_id); + ownership::erase(deleted_message_id.get()); + let own = git::whos(*deleted_message_id); git::remove(dir, *deleted_message_id); git::commit("plent", &format!("remove {:x}", deleted_message_id.get())); git::push(); @@ -497,7 +488,7 @@ impl Bot { .setup(|ctx, _ready, _| { Box::pin(async move { poise::builtins::register_globally(ctx, &[logic::run(), help(), ping(), render(), render_file(), render_message()]).await?; - poise::builtins::register_in_guild(ctx, &[search::search(), retag(), scour(), search::file()], 925674713429184564.into()).await?; + poise::builtins::register_in_guild(ctx, &[search::search(), lb(), lb_no_vds(), search::file()], 925674713429184564.into()).await?; println!("registered"); let tracker = Arc::new(DashMap::new()); let tc = Arc::clone(&tracker); @@ -529,6 +520,37 @@ impl Bot { } #[poise::command(slash_command)] +pub async fn own(c: Context<'_>) -> Result<()> { + let h = c.reply(emoji::named::LOCK_OPEN).await?; + let mut n = 0; + let mut map = HashMap::<u64, (String, u64)>::new(); + for &id in SPECIAL.keys() { + let ch = c.guild().unwrap().channels[&id.into()].clone(); + let Some(i) = search::dir(id.into()) else { + continue; + }; + for f in i { + let f = search::flake(f.file_name().unwrap().to_str().unwrap()); + let User { id, name, .. } = ch.message(c, f).await?.author; + map.insert(f, (name, id.get())); + n += 1; + if n % 10 == 0 { + h.edit( + c, + poise::CreateReply::default() + .content(format!("{}: {n}", emoji::named::LOCK_OPEN)), + ) + .await?; + } + } + } + std::fs::write("repo/ownership.json", serde_json::to_string(&map).unwrap()).unwrap(); + h.edit(c, poise::CreateReply::default().content(emoji::named::LOCK)) + .await?; + Ok(()) +} + +#[poise::command(slash_command)] pub async fn retag(c: Context<'_>, channel: ChannelId) -> Result<()> { if c.author().id != OWNER { poise::say_reply(c, "access denied. this incident will be reported").await?; @@ -547,6 +569,125 @@ pub async fn retag(c: Context<'_>, channel: ChannelId) -> Result<()> { Ok(()) } +// dbg!(m +// .iter() +// .filter(|x| x.roles.contains(&925676016708489227.into())) +// .map(|x| x.user.id.get()) +// .collect::<Vec<_>>()); + +const VDS: &[u64] = &[ + 126381304857100288, + 175218107084832768, + 221780012372721664, + 231505175246798851, + 291255752729821185, + 301919226078298114, + 315827169395998720, + 324736330418487317, + 325570201837895680, + 330298929331699713, + 332054403160735765, + 343939197738024961, + 360488990974935040, + 384188568270274581, + 387018214103842818, + 391302959444656128, + 399346439349600256, + 404682730190798858, + 417607639938236427, + 461517080856887297, + 464033296012017674, + 488243005283631106, + 490271325126918154, + 514981385660792852, + 527626094744961053, + 586994631879819266, + 595625721129336868, + 618507912511225876, + 665938033987682350, + 696196765564534825, + 705503407179431937, + 724657758280089701, + 729281676441550898, + 797211831894016012, + 845191508033667096, +]; +pub async fn leaderboard(c: Context<'_>, channel: Option<ChannelId>, vds: bool) -> Result<()> { + use emoji::named::*; + c.defer().await?; + let process = |map: HashMap<u64, u16>| { + let mut v = map.into_iter().collect::<Vec<_>>(); + v.sort_by_key(|(_, x)| *x); + use std::fmt::Write; + let mut out = String::new(); + v.iter() + .rev() + .zip(1..) + .take(5) + .for_each(|((y, z), x)| writeln!(out, "{x}. **<@{y}>**: {z}").unwrap()); + + out + }; + match channel { + Some(ch) => { + let Some(x) = SPECIAL.get(&ch.get()) else { + poise::say_reply(c, format!("{CANCEL} not a schem channel")).await?; + return Ok(()); + }; + let mut map = HashMap::new(); + search::dir(ch.get()) + .unwrap() + .map(|y| { + ownership::get(search::flake(y.file_name().unwrap().to_str().unwrap()).into()).1 + }) + .filter(|x| vds || !VDS.contains(x)) + .for_each(|x| *map.entry(x).or_default() += 1); + poise::say_reply( + c, + format!( + "## Leaderboard of {}\n{}", + x.labels + .join("") + .chars() + .map(|x| emoji::mindustry::TO_DISCORD[&x]) + .collect::<String>(), + process(map) + ), + ) + } + None => { + let mut map = std::collections::HashMap::new(); + search::files() + .map(|(y, _)| { + ownership::get(search::flake(y.file_name().unwrap().to_str().unwrap()).into()).1 + }) + .filter(|x| vds || !VDS.contains(x)) + .for_each(|x| *map.entry(x).or_default() += 1); + poise::say_reply(c, format!("## Leaderboard\n{}", process(map))) + } + } + .await?; + Ok(()) +} + +#[poise::command(slash_command)] +/// Show the leaderboard for players with the most contributed schems, optionally in a certain channel. +pub async fn lb( + c: Context<'_>, + #[description = "optional channel filter"] channel: Option<ChannelId>, +) -> Result<()> { + leaderboard(c, channel, true).await +} + +#[poise::command(slash_command)] +/// Show the leaderboard for players, excepting verified designers, with the most schemes. +pub async fn lb_no_vds( + c: Context<'_>, + #[description = "optional channel filter"] channel: Option<ChannelId>, +) -> Result<()> { + leaderboard(c, channel, false).await +} + pub mod emojis { pub const GUILDS: &[u64] = &[1003092764919091282, 925674713429184564]; use poise::serenity_prelude::*; diff --git a/src/bot/ownership.rs b/src/bot/ownership.rs new file mode 100644 index 0000000..a226bdc --- /dev/null +++ b/src/bot/ownership.rs @@ -0,0 +1,21 @@ +use std::{ + collections::HashMap, + sync::{LazyLock, Mutex}, +}; + +static MAP: LazyLock<Mutex<HashMap<u64, (String, u64)>>> = LazyLock::new(|| { + Mutex::new(serde_json::from_slice(&std::fs::read("repo/ownership.json").unwrap()).unwrap()) +}); + +pub fn insert(k: u64, v: (String, u64)) { + MAP.lock().unwrap().insert(k, v); + std::fs::write("repo/ownership.json", serde_json::to_string(&*MAP).unwrap()).unwrap(); +} +pub fn get(k: u64) -> (String, u64) { + MAP.lock().unwrap()[&k].clone() +} +pub fn erase(k: u64) -> Option<(String, u64)> { + let x = MAP.lock().unwrap().remove(&k); + std::fs::write("repo/ownership.json", serde_json::to_string(&*MAP).unwrap()).unwrap(); + x +} diff --git a/src/bot/search.rs b/src/bot/search.rs index 4d0582d..28d6cd3 100644 --- a/src/bot/search.rs +++ b/src/bot/search.rs @@ -6,7 +6,7 @@ use poise::serenity_prelude::*; use std::mem::MaybeUninit; use std::path::{Path, PathBuf}; -struct Dq<T, const N: usize> { +pub struct Dq<T, const N: usize> { arr: [MaybeUninit<T>; N], front: u8, len: u8, diff --git a/src/expose.rs b/src/expose.rs index 9a13eb5..c06d23f 100644 --- a/src/expose.rs +++ b/src/expose.rs @@ -119,28 +119,16 @@ impl Server { .route( "/blame/:file", get(|Path(file): Path<String>| async move { - match crate::bot::search::files().map(|(x, _)| x).find(|x| { - x.with_extension("").file_name().unwrap().to_string_lossy() == file - }) { - Some(x) => ( - StatusCode::OK, - crate::bot::git::whos( - &x.components().nth(1).unwrap().as_os_str().to_str().unwrap(), - u64::from_str_radix( - &x.with_extension("") - .file_name() - .unwrap() - .to_os_string() - .to_str() - .unwrap(), - 16, - ) - .unwrap() - .into(), - ), + ( + StatusCode::OK, + crate::bot::git::whos( + match u64::from_str_radix(file.trim_end_matches(".msch"), 16) { + Ok(x) => x, + Err(_) => return (StatusCode::NOT_FOUND, "".into()), + } + .into(), ), - None => (StatusCode::NOT_FOUND, String::default()), - } + ) }), ) .route( |