smol bot
add addable repos
| -rw-r--r-- | .gitignore | 4 | ||||
| -rw-r--r-- | src/bot/mod.rs | 475 | ||||
| -rw-r--r-- | src/bot/ownership.rs | 65 | ||||
| -rw-r--r-- | src/bot/repo.md | 14 | ||||
| -rw-r--r-- | src/bot/repos.rs | 254 | ||||
| -rw-r--r-- | src/expose.rs | 1 |
6 files changed, 526 insertions, 287 deletions
@@ -2,4 +2,6 @@ token Cargo.lock repo -html
\ No newline at end of file +html +*.auth +repos/
\ No newline at end of file diff --git a/src/bot/mod.rs b/src/bot/mod.rs index ed47c01..d76718a 100644 --- a/src/bot/mod.rs +++ b/src/bot/mod.rs @@ -1,6 +1,7 @@ mod logic; mod map; pub mod ownership; +mod repos; mod schematic; pub mod search; @@ -9,16 +10,40 @@ use dashmap::DashMap; use mindus::data::DataWrite; use mindus::Serializable; use poise::{serenity_prelude::*, CreateReply}; +use repos::{Repo, REPOS, SPECIAL, THREADED}; use serenity::futures::StreamExt; 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; +fn clone() { + for (k, repos::Repo { auth, .. }) in repos::REPOS.entries() { + if !Path::new(&format!("repos/{k:x}")).exists() { + assert_eq!( + std::process::Command::new("git") + .current_dir("repos") + .arg("clone") + .args(["--depth", "5"]) + .arg(auth) + .arg(format!("{k:x}")) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .status() + .unwrap() + .code() + .unwrap(), + 0 + ); + } + } +} + #[derive(Debug)] pub struct Data { // message -> resp @@ -44,16 +69,6 @@ const SUCCESS: (u8, u8, u8) = (34, 139, 34); const PFX: char = '}'; -macro_rules! decl { - ($($ch:literal $( => $item:literal : [$($labels: expr),* $(,)?])?),+ $(,)?) => { - use emoji::to_mindustry::named::*; - const THREADED: phf::Set<u64> = phf::phf_set! { $($ch,)+ }; - const SPECIAL: phf::Map<u64, Ch> = phf::phf_map! { - $($($ch => Ch { d: $item, labels: &[$($labels,)+] })?),+ - }; - }; -} - fn tags(t: &[&str]) -> String { if let [x, rest @ ..] = t { let mut s = format!("[\"{x}\""); @@ -67,101 +82,56 @@ fn tags(t: &[&str]) -> String { } } -decl! { - 925721957209636914u64 => "cryofluid" : [CRYOFLUID, CRYOFLUID_MIXER], - 925721791475904533u64 => "graphite" : [GRAPHITE, GRAPHITE_PRESS], - 925721824556359720u64 => "metaglass" : [METAGLASS, KILN], - 925721863525646356u64 => "phase-fabric" : [PHASE_FABRIC, PHASE_WEAVER], - 927036346869104693u64 => "plastanium" : [PLASTANIUM, PLASTANIUM_COMPRESSOR], - 925736419983515688u64 => "pyratite" : [PYRATITE, PYRATITE_MIXER], - 925736573037838397u64 => "blast-compound" : [BLAST_COMPOUND, BLAST_MIXER], - 927793648417009676u64 => "scrap" : [DISASSEMBLER, SCRAP], - 1198556531281637506u64 => "spore-press" : [OIL, SPORE_PRESS], - 1200308146460180520u64 => "oil-extractor" : [OIL, OIL_EXTRACTOR], - 1200301847387316317u64 => "rtg-gen" : [POWER, RTG_GENERATOR], - 1200308292744921088u64 => "cultivator" : [SPORE_POD, CULTIVATOR], - 1200305956689547324u64 => "graphite-multipress" : [GRAPHITE, MULTI_PRESS], - 1200306409036857384u64 => "silicon-crucible" : [SILICON, SILICON_CRUCIBLE], - 1198555991667646464u64 => "coal" : [COAL, COAL_CENTRIFUGE], - 925721763856404520u64 => "silicon" : [SILICON, SILICON_SMELTER], - 925721930814869524u64 => "surge-alloy" : [SURGE_ALLOY, SURGE_SMELTER], - 1141034314163826879u64 => "defensive-outpost" : [""], - 949529149800865862u64 => "drills" : [PRODUCTION], - 925729855574794311u64 => "logic-schems" : [MICRO_PROCESSOR], - 1185702384194818048u64 => "miscellaneous" : ["…"], - 1018541701431836803u64 => "combustion-gen" : [POWER, COMBUSTION_GENERATOR], - 927480650859184171u64 => "differential-gen" : [POWER, DIFFERENTIAL_GENERATOR], - 925719985987403776u64 => "impact-reactor" : [POWER, IMPACT_REACTOR], - 949740875817287771u64 => "steam-gen" : [POWER, STEAM_GENERATOR], - 926163105694752811u64 => "thorium-reactor" : [POWER, THORIUM_REACTOR], - 973234467357458463u64 => "carbide" : [CARBIDE, ""], - 1198527267933007893u64 => "erekir-defensive-outpost" : [""], - 973236445567410186u64 => "fissile-matter" : [FISSILE_MATTER, ""], - 1147887958351945738u64 => "electrolyzer" : [HYDROGEN, OZONE, ""], - 1202001032503365673u64 => "nitrogen" : [NITROGEN, ""], - 1202001055349477426u64 => "cyanogen" : [CYANOGEN, ""], - 1096157669112418454u64 => "mass-driver" : ["…", PLANET], - 973234248054104115u64 => "oxide" : [OXIDE, ""], - 973422874734002216u64 => "erekir-phase" : [PHASE_FABRIC, ""], - 973369188800413787u64 => "ccc" : ["", POWER], - 1218453338396430406u64 => "neoplasia-reactor": ["", POWER], - 1218453292045172817u64 => "flux-reactor": ["", POWER], - 1218452986788053012u64 => "pyrolisis-gen": ["", POWER], - 1147722735305367572u64 => "silicon-arc" : [SILICON, ""], - 974450769967341568u64 => "erekir-surge" : [SURGE_ALLOY, ""], - 973241041685737532u64 => "erekir-units" : ["[#ff9266][]"], - 1158818171139133490u64 => "unit-core" : [UNITS, CORE_NUCLEUS], - 1158818324210274365u64 => "unit-delivery" : [UNITS, FLARE], - 1158818598568075365u64 => "unit-raw" : [UNITS, PRODUCTION], - 1142181013779398676u64 => "unit-sand" : [UNITS, SAND], - 1222270513045438464u64 => "bore": [PRODUCTION], - 1226407271978766356u64 => "pulveriser": [PULVERIZER, SAND], - 1277138620863742003u64 => "melter": [MELTER, SLAG], - 1277138532355543070u64 => "separator": [SEPARATOR, SCRAP], - - 1129391545418797147u64, -} - #[derive(Copy, Clone, Debug)] -struct Ch { +pub struct Ch { + repo: u64, d: &'static str, labels: &'static [&'static str], } -fn sep(x: Option<&Ch>) -> (Option<&'static str>, Option<String>) { - (x.map(|x| x.d), x.map(|x| tags(x.labels))) +fn sep(x: Option<&Ch>) -> (Option<&'static str>, Option<String>, Option<&Repo>) { + ( + x.map(|x| x.d), + x.map(|x| tags(x.labels)), + x.map(|x| &REPOS[&x.repo]), + ) } const OWNER: u64 = 696196765564534825; #[poise::command(slash_command)] -pub async fn scour(c: Context<'_>, ch: ChannelId) -> Result<()> { - if c.author().id != OWNER { - poise::say_reply(c, "access denied. this incident will be reported").await?; - return Ok(()); - } +/// This command reads all messages to find the schems. +/// This command will possibly add denied schems. +pub async fn scour( + c: Context<'_>, + #[description = "the channel in question"] ch: ChannelId, +) -> Result<()> { + let g = c.guild_id().unwrap(); + let repo = repos::chief!(c); let mut n = 0; let d = SPECIAL[&ch.get()].d; let h = c.say(format!("scouring {d}...")).await?; - _ = std::fs::create_dir(format!("repo/{d}")); + _ = std::fs::create_dir(format!("repos/{:x}/{d}", repo.id)); let mut msgs = ch.messages_iter(c).boxed(); while let Some(msg) = msgs.next().await { let Ok(msg) = msg else { continue; }; - let (_, Some(tags)) = sep(SPECIAL.get(&ch.get())) else { + let (_, Some(tags), _) = sep(SPECIAL.get(&ch.get())) else { unreachable!() }; 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())).await; - git::write(d, msg.id, x); - git::commit(&who, &format!("add {:x}.msch", msg.id.get())); + ownership::get(g) + .await + .insert(msg.id.get(), (msg.author.name.clone(), msg.author.id.get())); + repo.write(d, msg.id, x); + repo.commit(&who, &format!("add {:x}.msch", msg.id.get())); msg.react(c, emojis::get!(MERGE)).await?; n += 1; } } - git::push(); + repo.push(); h.edit( c, poise::CreateReply::default().content(format!( @@ -200,98 +170,97 @@ where } } -pub mod git { - use mindus::data::DataWrite; - - use self::schematic::Schem; - - use super::*; - pub fn schem(dir: &str, x: MessageId) -> std::io::Result<mindus::Schematic> { - std::fs::read(path(dir, x)) - .map(|x| mindus::Schematic::deserialize(&mut mindus::data::DataRead::new(&x)).unwrap()) - } - - pub fn path(dir: &str, x: MessageId) -> std::path::PathBuf { - Path::new("repo") - .join(dir) - .join(format!("{:x}.msch", x.get())) - } - - pub fn gpath(dir: &str, x: MessageId) -> std::path::PathBuf { - Path::new(dir).join(format!("{:x}.msch", x.get())) - } - - pub fn has(dir: &str, x: MessageId) -> bool { - path(dir, x).exists() - } - - pub fn remove(dir: &str, x: MessageId) { - assert!(std::process::Command::new("git") - .current_dir("repo") - .arg("rm") - .arg("-q") - .arg(gpath(dir, x)) - .status() - .unwrap() - .success()); - } - - pub fn commit(by: &str, msg: &str) { - assert!(std::process::Command::new("git") - .current_dir("repo") - .args(["commit", "-q", "--author"]) - .arg(format!("{by} <@designit>",)) - .arg("-m") - .arg(msg) - .status() - .unwrap() - .success()); - } - - pub fn push() { - assert!(std::process::Command::new("git") - .current_dir("repo") - .arg("push") - .arg("-q") - .status() - .unwrap() - .success()) - } - - pub fn write(dir: &str, x: MessageId, s: Schem) { - _ = std::fs::create_dir(format!("repo/{dir}")); - let mut v = DataWrite::default(); - s.serialize(&mut v).unwrap(); - std::fs::write(path(dir, x), v.consume()).unwrap(); - add(); - } - - pub fn add() { - assert!(std::process::Command::new("git") - .current_dir("repo") - .arg("add") - .arg(".") - .status() - .unwrap() - .success()); - } -} - const RM: (u8, u8, u8) = (242, 121, 131); const AD: (u8, u8, u8) = (128, 191, 255); const CAT: &str = "https://cdn.discordapp.com/avatars/696196765564534825/6f3c605329ffb5cfb790343f59ed355d.webp"; +async fn handle_message( + c: &poise::serenity_prelude::Context, + new_message: &Message, + d: &Data, +) -> Result<()> { + let who = new_message + .author_nick(c) + .await + .unwrap_or(new_message.author.name.clone()); + let (dir, l, repo) = sep(SPECIAL.get(&new_message.channel_id.get())); + let m = Msg { + author: who.clone(), + avatar: new_message.author.avatar_url().unwrap_or(CAT.to_string()), + attachments: new_message.attachments.clone(), + content: new_message.content.clone(), + channel: new_message.channel_id, + }; + let x = schematic::with(m, c, l).await?; + match x { + ControlFlow::Continue(()) + if THREADED.contains(&new_message.channel_id.get()) + || SPECIAL.contains_key(&new_message.channel_id.get()) => + { + new_message.delete(c).await?; + return Ok(()); + } + ControlFlow::Break((m, n, s)) => { + if THREADED.contains(&m.channel_id.get()) { + m.channel_id + .create_thread_from_message( + c, + m.id, + CreateThread::new(n) + .audit_log_reason("because yes") + .auto_archive_duration(AutoArchiveDuration::OneDay), + ) + .await + .unwrap(); + } + if let Some(dir) = dir + && let Some(repo) = repo + { + println!("adding {dir}"); + // add :) + repo.own().await.insert( + new_message.id.get(), + (m.author.name.clone(), m.author.id.get()), + ); + use emoji::named::*; + if repo.id == 925674713429184564 && !cfg!(debug_assertions) { + send(c,|x| x + .avatar_url(new_message.author.avatar_url().unwrap_or(CAT.to_string())) + .username(&who) + .embed(CreateEmbed::new().color(AD) + .description(format!("https://discord.com/channels/925674713429184564/{}/{} {ADD} add {} (`{:x}.msch`)", m.channel_id,m.id, emoji::mindustry::to_discord(&strip_colors(s.tags.get("name").unwrap())), new_message.id.get()))) + ).await; + } + repo.write(dir, new_message.id, s); + repo.add(); + repo.commit(&who, &format!("add {:x}.msch", new_message.id.get())); + repo.push(); + new_message.react(c, emojis::get!(MERGE)).await?; + } + d.tracker.insert(new_message.id, m); + return Ok(()); + } + _ => (), + }; + + // not tracked, as you cant add a attachment afterwwards. + map::with(new_message, c).await?; + Ok(()) +} + pub struct Bot; impl Bot { pub async fn spawn() { use emoji::named::*; + println!("check clones"); + clone(); println!("bot startup"); let tok = 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(), lb(), bust_ghosts(), lb_no_vds(), ping(), help(), search::search(), search::file(), render(), render_file(), render_message()], + commands: vec![logic::run(), lb(), schembrowser_instructions(), lb_no_vds(), ping(), help(), scour(), search::search(), search::file(), render(), render_file(), render_message()], event_handler: |c, e, _, d| { Box::pin(async move { match e { @@ -301,23 +270,32 @@ impl Bot { hookup(c.http()).await; } // :deny:, @vd - 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)) => { + FullEvent::ReactionAdd { + add_reaction: Reaction { message_id, guild_id: Some(guild_id), emoji: ReactionType::Custom { id,.. } ,channel_id,member: Some( m @ Member{ nick,user,..}),..}} + if let Some(git) = REPOS.get(&guild_id.get()) + && *id == git.deny_emoji + && git.auth(m) + && let Some(Ch {d:dir,..}) = SPECIAL.get(&channel_id.get()) + => { let m = c.http().get_message(*channel_id,* message_id).await?; - if let Ok(s) = git::schem(dir,*message_id) { + if let Some(git) = REPOS.get(&guild_id.get()) && let Ok(s) = git.schem(dir,*message_id) { let who = nick.as_deref().unwrap_or(&user.name); - let own = ownership::erase(message_id.get()).await.unwrap(); - git::remove(dir, *message_id); - git::commit(who, &format!("remove {:x}.msch", message_id.get())); - git::push(); + let own = ownership::get(*guild_id).await.erase(*message_id).unwrap(); + git.remove(dir, *message_id); + git.commit(who, &format!("remove {:x}.msch", message_id.get())); + git.push(); _ = m.delete_reaction(c,Some(1174262682573082644.into()), emojis::get!(MERGE)).await; _ = m.delete_reaction(c,Some(1174262682573082644.into()), ReactionType::Custom { animated: false, id: 1192316518395039864.into(), name: Some("merge".into()) }).await.unwrap(); m.react(c,emojis::get!(DENY)).await?; + // only design-it has a webhook (possibly subject to future change) + if *guild_id == 925674713429184564 && !cfg!(debug_assertions) { send(c,|x| x .avatar_url(user.avatar_url().unwrap_or(CAT.to_string())) .username(who) .embed(CreateEmbed::new().color(RM) .description(format!("https://discord.com/channels/925674713429184564/{channel_id}/{message_id} {} {} (added by {own}) (`{:x}`)", emojis::get!(DENY), emoji::mindustry::to_discord(&strip_colors(s.tags.get("name").unwrap())), message_id.get()))) ).await; + } }; } FullEvent::GuildCreate { guild ,..} => { @@ -344,56 +322,12 @@ impl Bot { } FullEvent::Message { new_message } => { if new_message.content.starts_with('!') - || new_message.content.starts_with(PFX) - || new_message.author.bot + || new_message.content.starts_with(PFX) + || new_message.author.bot { return Ok(()); } - let who = new_message - .author_nick(c) - .await - .unwrap_or(new_message.author.name.clone()); - let m = Msg { - author: who.clone(), - avatar: new_message. author.avatar_url().unwrap_or(CAT.to_string()), - attachments: new_message.attachments.clone(), - content: new_message.content.clone(), - channel: new_message.channel_id, - }; - let (dir, l) = sep(SPECIAL.get(&new_message.channel_id.get())); - let x = schematic::with(m, c, l).await?; - match x { - ControlFlow::Continue(()) if THREADED.contains(&new_message.channel_id.get()) => { - new_message.delete(c).await?; - return Ok(()); - }, - ControlFlow::Break((m, n, s)) => { - if THREADED.contains(&m.channel_id.get()) { - m.channel_id.create_thread_from_message(c, m.id,CreateThread::new(n).audit_log_reason("because yes").auto_archive_duration(AutoArchiveDuration::OneDay)).await.unwrap(); - } - if let Some(dir) = dir { - // add :) - ownership::insert(new_message.id.get(), (m.author.name.clone(), m.author.id.get())).await; - send(c,|x| x - .avatar_url(new_message.author.avatar_url().unwrap_or(CAT.to_string())) - .username(&who) - .embed(CreateEmbed::new().color(AD) - .description(format!("https://discord.com/channels/925674713429184564/{}/{} {ADD} add {} (`{:x}.msch`)", m.channel_id,m.id, emoji::mindustry::to_discord(&strip_colors(s.tags.get("name").unwrap())), new_message.id.get()))) - ).await; - git::write(dir, new_message.id, s); - git::add(); - git::commit(&who, &format!("add {:x}.msch", new_message.id.get())); - git::push(); - new_message.react(c, emojis::get!(MERGE)).await?; - } - d.tracker.insert(new_message.id, m); - return Ok(()); - }, - _ => (), - }; - - // not tracked, as you cant add a attachment afterwwards. - map::with(new_message, c).await?; + handle_message(c, new_message, d).await?; } FullEvent::MessageUpdate {event: MessageUpdateEvent { author: Some(author), @@ -411,7 +345,7 @@ impl Bot { .nick_in(c, guild_id) .await .unwrap_or(author.name.clone()); - let (dir, l) = sep(SPECIAL.get(&r.channel_id.get())); + let (dir, l, repo) = sep(SPECIAL.get(&r.channel_id.get())); if let ControlFlow::Break((m,_,s)) = schematic::with( Msg { avatar: author.avatar_url().unwrap_or(CAT.to_string()), @@ -426,17 +360,19 @@ impl Bot { .await? { d.tracker.insert(*id, m); - if let Some(dir) = dir && git::has(dir, *id) { + if let Some(dir) = dir && let Some(git) = repo && git.has(dir, *id) { // update :) + if *guild_id == 925674713429184564 && !cfg!(debug_assertions) { send(c,|x| x .avatar_url(author.avatar_url().unwrap_or(CAT.to_string())) .username(&who) .embed(CreateEmbed::new().color(AD) .description(format!("https://discord.com/channels/925674713429184564/{channel_id}/{id} {ROTATE} update {} (`{:x}.msch`)", emoji::mindustry::to_discord(&strip_colors(s.tags.get("name").unwrap())), id.get()))) ).await; - git::write(dir, *id, s); - git::commit(&who,&format!("update {:x}.msch", id.get())); - git::push(); + } + git.write(dir, *id, s); + git.commit(&who,&format!("update {:x}.msch", id.get())); + git.push(); } } } @@ -444,12 +380,14 @@ impl Bot { FullEvent::MessageDelete { deleted_message_id, channel_id, .. } => { - if let Some(Ch{ d:dir,..}) = SPECIAL.get(&channel_id.get()) { - if let Ok(s) = git::schem(dir, *deleted_message_id) { - let own = ownership::erase(deleted_message_id.get()).await.unwrap(); - git::remove(dir, *deleted_message_id); - git::commit("plent", &format!("remove {:x}", deleted_message_id.get())); - git::push(); + if let Some(&Ch{ d:dir, repo, ..}) = SPECIAL.get(&channel_id.get()) { + let git = &REPOS[&repo]; + if let Ok(s) = git.schem(dir, *deleted_message_id) { + let own = git.own().await.erase(deleted_message_id.get()).unwrap(); + git.remove(dir, *deleted_message_id); + git.commit("plent", &format!("remove {:x}", deleted_message_id.get())); + git.push(); + if repo == 925674713429184564 && !cfg!(debug_assertions) { send(c,|x| x .username("plent") .embed(CreateEmbed::new().color(RM) @@ -457,6 +395,7 @@ impl Bot { .footer(CreateEmbedFooter::new("message was deleted.") )) ).await; + } }; } @@ -481,8 +420,9 @@ 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(), bust_ghosts(), lb(), lb_no_vds(), search::file()], 925674713429184564.into()).await?; + poise::builtins::register_globally(ctx, &[logic::run(), help(), ping(), render(), schembrowser_instructions(), render_file(), render_message()]).await?; + poise::builtins::register_in_guild(ctx, &[scour()], 1110086242177142854.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); @@ -513,39 +453,39 @@ impl Bot { } } -pub async fn missing() -> impl Iterator<Item = (MessageId, ChannelId)> { - let lock = ownership::MAP.lock().await; - search::files() - .map(move |(x, ch)| { - let f = search::flake(x.file_name().unwrap().to_str().unwrap()); - (lock.contains_key(&f), f, ch) - }) - .filter_map(|(x, m, c)| (!x).then(|| (m.into(), c.into()))) -} - -#[poise::command(slash_command)] -pub async fn bust_ghosts(c: Context<'_>) -> Result<()> { - if c.author().id != OWNER { - poise::say_reply(c, "access denied. this incident will be reported").await?; - return Ok(()); - } - let h = c.reply(emoji::named::LOCK_OPEN).await?; - for (m, ch) in missing().await.collect::<Vec<_>>() { - let ch = c.guild().unwrap().channels[&ch].clone(); - let User { id, name, .. } = match ch.message(c, m).await { - Ok(x) => x.author, - Err(_) => { - // removes ghosts - std::fs::remove_file(git::path(&SPECIAL[&ch.id.get()].d, m)).unwrap(); - continue; - } - }; - ownership::insert(m.into(), (name, id.get())).await; - } - h.edit(c, poise::CreateReply::default().content(emoji::named::LOCK)) - .await?; - Ok(()) -} +// pub async fn missing(r: &'static Repo) -> impl Iterator<Item = (MessageId, ChannelId)> { +// let lock = r.own().await; +// search::files() +// .map(move |(x, ch)| { +// let f = search::flake(x.file_name().unwrap().to_str().unwrap()); +// (lock.map.contains_key(&f), f, ch) +// }) +// .filter_map(|(x, m, c)| (!x).then(|| (m.into(), c.into()))) +// } + +// #[poise::command(slash_command)] +// pub async fn bust_ghosts(c: Context<'_>) -> Result<()> { +// if c.author().id != OWNER { +// poise::say_reply(c, "access denied. this incident will be reported").await?; +// return Ok(()); +// } +// let h = c.reply(emoji::named::LOCK_OPEN).await?; +// for (m, ch) in missing().await.collect::<Vec<_>>() { +// let ch = c.guild().unwrap().channels[&ch].clone(); +// let User { id, name, .. } = match ch.message(c, m).await { +// Ok(x) => x.author, +// Err(_) => { +// // removes ghosts +// std::fs::remove_file(git::path(&SPECIAL[&ch.id.get()].d, m)).unwrap(); +// continue; +// } +// }; +// ownership::insert(m.into(), (name, id.get())).await; +// } +// 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<()> { @@ -613,7 +553,7 @@ const VDS: &[u64] = &[ pub async fn leaderboard(c: Context<'_>, channel: Option<ChannelId>, vds: bool) -> Result<()> { use emoji::named::*; c.defer().await?; - let lock = ownership::MAP.lock().await; + let lock = REPOS[&925674713429184564].own().await; let process = |map: HashMap<u64, u16>| { let mut v = map.into_iter().collect::<Vec<_>>(); v.sort_by_key(|(_, x)| *x); @@ -636,7 +576,7 @@ pub async fn leaderboard(c: Context<'_>, channel: Option<ChannelId>, vds: bool) let mut map = HashMap::new(); search::dir(ch.get()) .unwrap() - .map(|y| lock[&search::flake(y.file_name().unwrap().to_str().unwrap()).into()].1) + .map(|y| lock.map[&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( @@ -656,7 +596,7 @@ pub async fn leaderboard(c: Context<'_>, channel: Option<ChannelId>, vds: bool) let mut map = std::collections::HashMap::new(); search::files() .map(|(y, _)| { - lock[&search::flake(y.file_name().unwrap().to_str().unwrap()).into()].1 + lock.map[&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); @@ -969,3 +909,20 @@ pub async fn render_message(c: Context<'_>, m: Message) -> Result<()> { .await?; Ok(()) } + +#[poise::command( + slash_command, + install_context = "Guild", + interaction_context = "Guild|PrivateChannel" +)] +/// Instructions on adding a schematic repository to YOUR server! +pub async fn schembrowser_instructions(c: Context<'_>) -> Result<()> { + poise::send_reply( + c, + poise::CreateReply::default() + .content(include_str!("repo.md")) + .allowed_mentions(CreateAllowedMentions::default().empty_users().empty_roles()), + ) + .await?; + Ok(()) +} diff --git a/src/bot/ownership.rs b/src/bot/ownership.rs index ab8639a..12a3fc3 100644 --- a/src/bot/ownership.rs +++ b/src/bot/ownership.rs @@ -1,33 +1,44 @@ use serenity::all::MessageId; -use std::{collections::HashMap, sync::LazyLock}; -use tokio::sync::Mutex; +use std::collections::HashMap; +use tokio::sync::MutexGuard; -pub 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 async fn insert(k: u64, v: (String, u64)) { - let mut lock = MAP.lock().await; - lock.insert(k, v); - std::fs::write( - "repo/ownership.json", - serde_json::to_string_pretty(&*lock).unwrap(), - ) - .unwrap(); +pub struct Ownership { + pub map: HashMap<u64, (String, u64)>, + path: &'static str, } -pub async fn get(k: u64) -> (String, u64) { - MAP.lock().await[&k].clone() +impl Ownership { + pub fn new(id: u64) -> Self { + let path = format!("repos/{id:x}/ownership.json").leak(); + Self { + map: serde_json::from_slice(&std::fs::read(&path).unwrap_or_default()) + .unwrap_or_default(), + path, + } + } + + pub fn insert(&mut self, k: u64, v: (String, u64)) { + self.map.insert(k, v); + self.flush(); + } + fn flush(&self) { + std::fs::write(&self.path, serde_json::to_string_pretty(&self.map).unwrap()).unwrap(); + } + pub fn get(&self, k: u64) -> &(String, u64) { + self.map.get(&k).unwrap() + } + pub fn erase(&mut self, k: impl Into<u64>) -> Option<String> { + let x = self.map.remove(&k.into()).map(|(x, _)| x); + self.flush(); + x + } + + pub fn whos(&self, x: impl Into<MessageId>) -> &str { + &self.get(x.into().get()).0 + } } -pub async fn erase(k: u64) -> Option<String> { - let mut lock = MAP.lock().await; - let x = lock.remove(&k).map(|(x, _)| x); - std::fs::write( - "repo/ownership.json", - serde_json::to_string_pretty(&*lock).unwrap(), - ) - .unwrap(); - x +pub async fn whos(g: u64, x: impl Into<u64>) -> String { + super::repos::REPOS[&g].own().await.get(x.into()).0.clone() } -pub async fn whos(x: impl Into<MessageId>) -> String { - get(x.into().get()).await.0 +pub async fn get(g: impl Into<u64>) -> MutexGuard<'static, Ownership> { + super::repos::REPOS[&g.into()].own().await } diff --git a/src/bot/repo.md b/src/bot/repo.md new file mode 100644 index 0000000..f5bb680 --- /dev/null +++ b/src/bot/repo.md @@ -0,0 +1,14 @@ +# you want your own schematic repository? look no further. + +if you dont know what a schematic repository is, look at [design-it](<https://github.com/bend-n/design-it>). +its technology that lets your community upload schematics to a repository, which can then be pulled down by the [schembrowser](<https://github.com/sbxte/mindustry-schematic-browser>) mod. + +now, if you want this, or somebody in your community is bugging you to add this, here are the steps. + +- <:right:1182119750155915264> invite bendn (<:discord:1182124785371729990> `bendn_` <@696196765564534825>) to your server. +- create a <:github_square:1182121451655004221> github repository and account for your server, or ask bendn or somebody in your community to do it for you. +- elect <:admin:1182128872435749005> administrators of the schematic repository, certain trusted users. +- create a discord channel, or three, for your schematics to go in, along with tags for each channel if you need them. +- add an emote <:deny:1192388789952319499> for the administrators to react with to bad schems to delete them (or dont, if you dont want the schematics to ever go away). +- create a [github PAT](<https://github.com/settings/tokens/new?description=Schematic%20Repo&scopes=repo>) and send it to bendn (<:warning:1182119952048726066> privately!) +- wait for bendn to add your repo to me! diff --git a/src/bot/repos.rs b/src/bot/repos.rs new file mode 100644 index 0000000..33c1a92 --- /dev/null +++ b/src/bot/repos.rs @@ -0,0 +1,254 @@ +use super::{ownership::Ownership, *}; +use tokio::sync::Mutex; + +#[derive(Copy, Clone)] +pub enum Person { + Role(u64), + User(u64), +} + +#[derive(Copy, Clone)] +pub struct Repo { + pub id: u64, + // delete power + pub admins: &'static [Person], + /// power to `scour` and `retag` etc. + pub chief: u64, + pub deny_emoji: u64, + /// clone url: https://bend-n:github_pat_…@github.com/… + pub auth: &'static str, + ownership: &'static LazyLock<Mutex<Ownership>>, + // possibly posters? +} + +use super::schematic::Schem; +use mindus::data::DataWrite; +impl Repo { + pub fn auth(&self, member: &Member) -> bool { + self.chief == member.user.id.get() + || (self.admins.iter().any(|&x| match x { + Person::Role(x) => member.roles.contains(&RoleId::new(x)), + Person::User(x) => member.user.id.get() == x, + })) + } + + pub async fn own(&self) -> tokio::sync::MutexGuard<Ownership> { + self.ownership.lock().await + } + + pub fn schem(&self, dir: &str, x: MessageId) -> std::io::Result<mindus::Schematic> { + std::fs::read(self.path(dir, x)) + .map(|x| mindus::Schematic::deserialize(&mut mindus::data::DataRead::new(&x)).unwrap()) + } + + pub fn path(&self, dir: &str, x: MessageId) -> std::path::PathBuf { + self.repopath() + .join(dir) + .join(format!("{:x}.msch", x.get())) + } + + pub fn gpath(&self, dir: &str, x: MessageId) -> std::path::PathBuf { + Path::new(dir).join(format!("{:x}.msch", x.get())) + } + + pub fn has(&self, dir: &str, x: MessageId) -> bool { + self.path(dir, x).exists() + } + + pub fn repopath(&self) -> std::path::PathBuf { + Path::new("repos").join(format!("{:x}", self.id)) + } + + pub fn remove(&self, dir: &str, x: MessageId) { + assert!(std::process::Command::new("git") + .current_dir(self.repopath()) + .arg("rm") + .arg("-q") + .arg("-f") + .arg(self.gpath(dir, x)) + .status() + .unwrap() + .success()); + } + + pub fn commit(&self, by: &str, msg: &str) { + assert!(std::process::Command::new("git") + .current_dir(self.repopath()) + .args(["commit", "-q", "--author"]) + .arg(format!("{by} <@designit>")) + .arg("-m") + .arg(msg) + .status() + .unwrap() + .success()); + } + + pub fn push(&self) { + assert!(std::process::Command::new("git") + .current_dir(self.repopath()) + .arg("push") + .arg("-q") + .status() + .unwrap() + .success()) + } + + pub fn write(&self, dir: &str, x: MessageId, s: Schem) { + _ = std::fs::create_dir(self.repopath().join(dir)); + let mut v = DataWrite::default(); + s.serialize(&mut v).unwrap(); + std::fs::write(self.path(dir, x), v.consume()).unwrap(); + self.add(); + } + + pub fn add(&self) { + assert!(std::process::Command::new("git") + .current_dir(self.repopath()) + .arg("add") + .arg(".") + .status() + .unwrap() + .success()); + } +} + +macro_rules! decl { + ( + [$($threaded:literal)+ $(,)?]; + $( + $repo:literal => [ + $( + // xu64 => "dirname" : [label, label] + $ch:literal => $item:literal : [$($labels: expr),* $(,)?] + ),+ $(,)? + ]; + )+ + ) => { + use emoji::to_mindustry::named::*; + pub const THREADED: phf::Set<u64> = phf::phf_set! { $($threaded,)+ }; + pub const SPECIAL: phf::Map<u64, Ch> = phf::phf_map! { + $($($ch => Ch { d: $item, labels: &[$($labels,)+], repo: $repo },)?)+ + }; + }; +} +macro_rules! person { + (&$x:literal) => { + Person::Role($x) + }; + ($x:literal) => { + Person::User($x) + }; +} + +macro_rules! repos { + ( + $($repo_ident:ident $repo:literal => { admins: $admins:expr, chief: $chief:expr, deny_emoji: $deny:expr,} $(,)?),+ + ) => { + + $(static $repo_ident: LazyLock<Mutex<Ownership>> = LazyLock::new(|| Mutex::new(super::ownership::Ownership::new($repo)));)+ + pub static REPOS: phf::Map<u64, Repo> = phf::phf_map! { + $($repo => Repo { + id: $repo, + admins: $admins, + chief: $chief, + deny_emoji: $deny, + auth: include_str!(concat!("../../", $repo, ".auth")), + ownership: &$repo_ident, + },)+ + }; + }; +} + +repos! { + DESIGN_IT 925674713429184564u64 => { + admins: &[person!(&925676016708489227)], + chief: 696196765564534825, + deny_emoji: 1192388789952319499u64, + }, + ACP 1110086242177142854u64 => { + admins: &[person!(&1110439183190863913)], + chief: 696196765564534825, + deny_emoji: 1182469319641272370u64, + } +} + +decl! { + [1129391545418797147u64]; + 925674713429184564 => [ +925721957209636914u64 => "cryofluid" : [CRYOFLUID, CRYOFLUID_MIXER], +925721791475904533u64 => "graphite" : [GRAPHITE, GRAPHITE_PRESS], +925721824556359720u64 => "metaglass" : [METAGLASS, KILN], +925721863525646356u64 => "phase-fabric" : [PHASE_FABRIC, PHASE_WEAVER], +927036346869104693u64 => "plastanium" : [PLASTANIUM, PLASTANIUM_COMPRESSOR], +925736419983515688u64 => "pyratite" : [PYRATITE, PYRATITE_MIXER], +925736573037838397u64 => "blast-compound" : [BLAST_COMPOUND, BLAST_MIXER], +927793648417009676u64 => "scrap" : [DISASSEMBLER, SCRAP], +1198556531281637506u64 => "spore-press" : [OIL, SPORE_PRESS], +1200308146460180520u64 => "oil-extractor" : [OIL, OIL_EXTRACTOR], +1200301847387316317u64 => "rtg-gen" : [POWER, RTG_GENERATOR], +1200308292744921088u64 => "cultivator" : [SPORE_POD, CULTIVATOR], +1200305956689547324u64 => "graphite-multipress" : [GRAPHITE, MULTI_PRESS], +1200306409036857384u64 => "silicon-crucible" : [SILICON, SILICON_CRUCIBLE], +1198555991667646464u64 => "coal" : [COAL, COAL_CENTRIFUGE], +925721763856404520u64 => "silicon" : [SILICON, SILICON_SMELTER], +925721930814869524u64 => "surge-alloy" : [SURGE_ALLOY, SURGE_SMELTER], +1141034314163826879u64 => "defensive-outpost" : [""], +949529149800865862u64 => "drills" : [PRODUCTION], +925729855574794311u64 => "logic-schems" : [MICRO_PROCESSOR], +1185702384194818048u64 => "miscellaneous" : ["…"], +1018541701431836803u64 => "combustion-gen" : [POWER, COMBUSTION_GENERATOR], +927480650859184171u64 => "differential-gen" : [POWER, DIFFERENTIAL_GENERATOR], +925719985987403776u64 => "impact-reactor" : [POWER, IMPACT_REACTOR], +949740875817287771u64 => "steam-gen" : [POWER, STEAM_GENERATOR], +926163105694752811u64 => "thorium-reactor" : [POWER, THORIUM_REACTOR], +973234467357458463u64 => "carbide" : [CARBIDE, ""], +1198527267933007893u64 => "erekir-defensive-outpost" : [""], +973236445567410186u64 => "fissile-matter" : [FISSILE_MATTER, ""], +1147887958351945738u64 => "electrolyzer" : [HYDROGEN, OZONE, ""], +1202001032503365673u64 => "nitrogen" : [NITROGEN, ""], +1202001055349477426u64 => "cyanogen" : [CYANOGEN, ""], +1096157669112418454u64 => "mass-driver" : ["…", PLANET], +973234248054104115u64 => "oxide" : [OXIDE, ""], +973422874734002216u64 => "erekir-phase" : [PHASE_FABRIC, ""], +973369188800413787u64 => "ccc" : ["", POWER], +1218453338396430406u64 => "neoplasia-reactor": ["", POWER], +1218453292045172817u64 => "flux-reactor": ["", POWER], +1218452986788053012u64 => "pyrolisis-gen": ["", POWER], +1147722735305367572u64 => "silicon-arc" : [SILICON, ""], +974450769967341568u64 => "erekir-surge" : [SURGE_ALLOY, ""], +973241041685737532u64 => "erekir-units" : ["[#ff9266][]"], +1158818171139133490u64 => "unit-core" : [UNITS, CORE_NUCLEUS], +1158818324210274365u64 => "unit-delivery" : [UNITS, FLARE], +1158818598568075365u64 => "unit-raw" : [UNITS, PRODUCTION], +1142181013779398676u64 => "unit-sand" : [UNITS, SAND], +1222270513045438464u64 => "bore": [PRODUCTION], +1226407271978766356u64 => "pulveriser": [PULVERIZER, SAND], +1277138620863742003u64 => "melter": [MELTER, SLAG], +1277138532355543070u64 => "separator": [SEPARATOR, SCRAP], + ]; +1110086242177142854u64 => [ + 1276759410722738186u64 => "schems": ["plague"] + ]; +} + +macro_rules! chief { + ($c:ident) => {{ + let repo = repos::REPOS[&$c.guild_id().unwrap().get()]; + if repo.chief != $c.author().id.get() { + poise::send_reply( + $c, + poise::CreateReply::default() + .content(format!( + "access denied. only the chief <@{}> can use this command.", + repo.chief, + )) + .allowed_mentions(CreateAllowedMentions::default().empty_users().empty_roles()), + ) + .await?; + return Ok(()); + } + repo + }}; +} + +pub(crate) use chief; diff --git a/src/expose.rs b/src/expose.rs index a79d0e5..28d25e4 100644 --- a/src/expose.rs +++ b/src/expose.rs @@ -122,6 +122,7 @@ impl Server { ( StatusCode::OK, crate::bot::ownership::whos( + 925674713429184564, match u64::from_str_radix(file.trim_end_matches(".msch"), 16) { Ok(x) => x, Err(_) => return (StatusCode::NOT_FOUND, "".into()), |