smol bot
add html page
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | Cargo.toml | 7 | ||||
| -rw-r--r-- | build.rs | 41 | ||||
| -rw-r--r-- | html-src/bg.png | bin | 0 -> 116347 bytes | |||
| -rw-r--r-- | html-src/border.png | bin | 0 -> 383 bytes | |||
| -rw-r--r-- | html-src/border_active.png | bin | 0 -> 379 bytes | |||
| -rw-r--r-- | html-src/border_hover.png | bin | 0 -> 396 bytes | |||
| -rw-r--r-- | html-src/default.woff | bin | 0 -> 3444104 bytes | |||
| -rw-r--r-- | html-src/fail.png | bin | 0 -> 28579 bytes | |||
| -rw-r--r-- | html-src/favicon.png | bin | 0 -> 288164 bytes | |||
| -rw-r--r-- | html-src/index.html | 228 | ||||
| -rw-r--r-- | html-src/index.js | 102 | ||||
| -rw-r--r-- | html-src/schematic.html | 13 | ||||
| -rw-r--r-- | src/bot/mod.rs | 34 | ||||
| -rw-r--r-- | src/expose.rs | 164 | ||||
| -rw-r--r-- | src/main.rs | 5 |
16 files changed, 591 insertions, 4 deletions
@@ -2,3 +2,4 @@ token Cargo.lock repo +html
\ No newline at end of file @@ -42,6 +42,13 @@ 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 } +serde_json = "1.0.122" +serde = "1.0.204" +atools = "0.1.5" +edg = { path = "../edg" } +httpdate = "1.0.3" + [profile.release] strip = true diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..88304fa --- /dev/null +++ b/build.rs @@ -0,0 +1,41 @@ +#![feature(let_chains)] +use std::fs; +use std::io::prelude::*; +use std::path::Path; + +pub fn process(input: impl AsRef<Path>) -> std::io::Result<()> { + let mut f = fs::File::create(dbg!(Path::new("html").join(input.as_ref()))).unwrap(); + if !matches!( + input.as_ref().extension().unwrap().to_str().unwrap(), + "html" | "css" + ) { + return f.write_all(&std::fs::read(Path::new("html-src").join(input.as_ref()))?); + } + let mut c = std::process::Command::new("minify") + .arg(Path::new("html-src").join(input.as_ref())) + .stdout(std::process::Stdio::piped()) + .spawn() + .unwrap(); + let mut o = c.stdout.take().unwrap(); + let mut buf = [0; 1024]; + while let Ok(x) = o.read(&mut buf) + && x != 0 + { + f.write_all(&buf[..x])?; + } + c.wait()?; + Ok(()) +} + +fn main() -> std::io::Result<()> { + if !Path::new("html").exists() { + fs::create_dir("html")?; + } + + for path in fs::read_dir("html-src")? { + process(path.unwrap().path().file_name().unwrap())?; + } + println!("cargo:rerun-if-changed=html-src/"); + println!("cargo:rerun-if-changed=build.rs"); + Ok(()) +} diff --git a/html-src/bg.png b/html-src/bg.png Binary files differnew file mode 100644 index 0000000..119d708 --- /dev/null +++ b/html-src/bg.png diff --git a/html-src/border.png b/html-src/border.png Binary files differnew file mode 100644 index 0000000..0dd9761 --- /dev/null +++ b/html-src/border.png diff --git a/html-src/border_active.png b/html-src/border_active.png Binary files differnew file mode 100644 index 0000000..541a407 --- /dev/null +++ b/html-src/border_active.png diff --git a/html-src/border_hover.png b/html-src/border_hover.png Binary files differnew file mode 100644 index 0000000..cdce442 --- /dev/null +++ b/html-src/border_hover.png diff --git a/html-src/default.woff b/html-src/default.woff Binary files differnew file mode 100644 index 0000000..92cf31f --- /dev/null +++ b/html-src/default.woff diff --git a/html-src/fail.png b/html-src/fail.png Binary files differnew file mode 100644 index 0000000..bed589a --- /dev/null +++ b/html-src/fail.png diff --git a/html-src/favicon.png b/html-src/favicon.png Binary files differnew file mode 100644 index 0000000..b393c59 --- /dev/null +++ b/html-src/favicon.png diff --git a/html-src/index.html b/html-src/index.html new file mode 100644 index 0000000..4befc53 --- /dev/null +++ b/html-src/index.html @@ -0,0 +1,228 @@ +<!DOCTYPE html> +<html lang="en"> + +<head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <link rel="shortcut icon" href="https://apricotalliance.org/schems/favicon.ico" /> + <title>Curated schematic viewer</title> + <style> + @font-face { + font-family: "default"; + src: url("/schems/default.woff") format('woff'); + } + + body { + font-family: "default"; + } + + .schem { + min-width: 130px; + min-height: 170px; + max-width: 130px; + max-height: 170px; + } + + .bar { + display: flex; + align-items: center; + } + + .rondbutton { + /* border: 5px solid #454545; */ + /* border-radius: 4px; */ + background-color: transparent; + color: white; + padding: 10px; + margin: 2px; + font-size: 1.5em; + border-image: url("border.png") 30 / 19px round; + } + + .rondbutton:hover { + border-image: url("border-hover.png") 30 / 19px round; + } + + .rondbutton:active { + border-image: url("border-active.png") 30 / 19px round; + } + + .rondbutton>span { + background-color: black; + } + + .squareb { + font-size: 1.2em; + background-color: transparent; + color: #fff; + width: 40px; + padding: 5px; + padding-bottom: 3px; + height: 40px; + text-align: center; + border-color: transparent; + } + + .squareb>svg { + pointer-events: auto; + } + + .squareb:hover { + color: #bfbfbf; + } + + .squareb:active { + color: #ffd37f; + } + + .background { + border: 5px solid #454545; + position: absolute; + background-color: #020202; + width: 120px; + height: 40px; + z-index: -1; + } + + .preview { + border: 5px solid #454545; + user-select: none; + image-rendering: crisp-edges; + } + + .preview:hover { + border-color: #ffd37f; + } + + button { + font-family: inherit; + font-size: 1em; + } + + + #grid { + width: 100%; + display: flex; + flex-wrap: wrap; + align-items: center; + } + + #grid>* { + margin: 5px 3px; + } + + body { + margin: 0; + background-color: #1F1E30; + } + + #bg { + position: absolute; + width: 100%; + min-height: 100%; + overflow: hidden; + } + + #holder { + z-index: -1; + width: 100%; + min-height: 100%; + position: absolute; + overflow: hidden; + } + + #modal { + font-size: 0.7em; + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(3px); + z-index: 2; + } + + body.modal-open { + height: 100vh; + overflow-y: hidden; + } + + .hide { + visibility: hidden; + } + + .title { + position: absolute; + height: 17px; + margin-left: 5px; + padding: 0px 2px; + background-color: rgba(0, 0, 0, 0.2); + color: white; + width: 116px; + text-shadow: 0 0 5px #3f3f33; + margin-top: 5px; + font-size: 0.6em; + text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + </style> +</head> + +<body id="body"> + <div id="holder"><img id="bg" src="/schems/bg.png"></div> + + <div id="modal" class="hide"> + <div style="width: 100%;display: flex;justify-content: center; position: absolute; height: 10px; margin-top: 5px"> + <span style="color:#ffd37f">[Schematic] + <span id="modal-name"></span> + (by <span id="modal-author" style="color: #B8C5E8">unknown</span>) + </div> + <div style="display:flex; justify-content: center; position: absolute; top: 24px; width: 100%"> + <div style="background-color: #ffd37f; height: 3px; width: 95%"></div> + </div> + <div + style="display:flex; align-items: center; align-content: center; justify-content: center; position: absolute; top: 50px; bottom: 50px; width: 100%; padding: 10px;"> + <p id="modal-desc" style="color: white; margin-right: 5px; max-width: 20%; height: 70%"></p> + <img id="modal-pic" class="preview" style="height:70%" draggable="false" /> + </div> + <div style="width: 100%;display: flex;justify-content: center; bottom: 0; position: absolute; height: 50px"> + <button class="rondbutton" onclick="window.close()"> + <span> back</span> + </button> + <button class="rondbutton" id="modal-copy"> + <span> copy</span> + </button> + <button class="rondbutton" id="modal-download"> + <span> download</span> + </button> + </div> + </div> + + <div id="grid"> + <script type="module" src="/schems/index.js"></script> + <script> + "use strict" + let update = () => document.getElementById("holder").style.height = document.getElementById("body").clientHeight + "px"; + addEventListener('DOMContentLoaded', update, false); + addEventListener('load', update, false); + addEventListener('scroll', update, false); + addEventListener('resize', update, false); + </script> + <script> + function close() { + document.getElementById('modal').className = "hide"; + document.getElementById('modal-author').innerText = "unknown"; + document.getElementById("body").className = ""; + } + document.onkeydown = (c) => { + if (c.key == "Escape") close() + } + </script> +</body> + +</html>
\ No newline at end of file diff --git a/html-src/index.js b/html-src/index.js new file mode 100644 index 0000000..3fc871c --- /dev/null +++ b/html-src/index.js @@ -0,0 +1,102 @@ +"use strict"; +import init, { render_schem, tags } from "/masm.js"; +// import init, { render_map } from "https://apricotalliance.org/masm.js"; + +let tasks = []; +init().then(() => { + window.init = true; + tasks.forEach((t) => t()); + tasks = []; +}); + +const template = `<div class=schem><div class=bar><div class=background id={ID}></div><span class="typcn typcn-arrow-left"></span> <button class=squareb title=info id={ID}-info></button> <button class=squareb title=copy id={ID}-copy></button> <button class=squareb title=download id={ID}-download></button></div><span class=title id={ID}-title></span> <img id={ID}-picture onmouseenter='document.getElementById("{ID}").style.backgroundColor="#454545"' onmouseleave='document.getElementById("{ID}").style.backgroundColor="#020202"' draggable=false class=preview width=120px height=120px src=fail.png></div>`; +function b64(buf) { + const a = new Uint8Array(buf); + let b = ""; + for (let i = 0; i < a.byteLength; i++) { + b += String.fromCharCode(a[i]); + } + return btoa(b); +} + +function vis(el) { + var rect = el.getBoundingClientRect(); + + return ( + rect.top >= -100 && + rect.bottom <= + (window.innerHeight || document.documentElement.clientHeight) + 200 + ); +} + +async function build(schems) { + let jobs = 0; + for (const schem of schems) { + document + .getElementById("grid") + .insertAdjacentHTML("beforeend", template.replaceAll("{ID}", schem)); + let p = async function () { + jobs += 1; + let data = await (await fetch(`/schems/files/${schem}`)).arrayBuffer(); + + let tagz; + if (window.init) { + tagz = tags(data); + document.getElementById(`${schem}-title`).innerText = tagz["name"]; + } else + tasks.push(() => { + tagz = tags(data); + document.getElementById(`${schem}-title`).innerText = tagz["name"]; + }); + + let flag = 0; + let pic = document.getElementById(`${schem}-picture`); + let f = () => { + setTimeout(() => { + if (vis(pic) && flag == 0) { + flag = 1; + // SLOW (~10ms) + if (window.init) pic.src = render_schem(data); + else tasks.push(() => (pic.src = render_schem(data))); + removeEventListener(pic, f); + } + }, 100); + }; + addEventListener("scroll", f, false); + f(); + let download = () => { + Object.assign(document.createElement("a"), { + href: `/schems/files/${schem}`, + download: schem, + }).click(); + }; + let copy = () => navigator.clipboard.writeText(b64(data)); + document.getElementById(`${schem}-info`).onclick = () => { + document.getElementById("modal").className = ""; + document.getElementById("modal-name").innerText = tagz["name"]; + document.getElementById("modal-desc").innerText = tagz["description"]; + document.getElementById("modal-pic").src = pic.src; + document.getElementById("modal-download").onclick = download; + document.getElementById("modal-copy").onclick = copy; + document.getElementById("body").className = "modal-open"; + fetch(`/schems/blame/${schem}`).then((x) => + x.text().then((x) => { + if (x != "plent") + document.getElementById("modal-author").innerText = x; + }) + ); + }; + + document.getElementById(`${schem}-download`).onclick = download; + document.getElementById(`${schem}-copy`).onclick = copy; + jobs -= 1; + }; + if (jobs < 20) p(); + else await p(); + } +} +async function get() { + return await (await fetch("/schems/files")).json(); +} + +get().then(build); diff --git a/html-src/schematic.html b/html-src/schematic.html new file mode 100644 index 0000000..b84d8fd --- /dev/null +++ b/html-src/schematic.html @@ -0,0 +1,13 @@ +<div class="schem"> + <div class="bar"> + <div class="background" id="{ID}"></div> + <span class="typcn typcn-arrow-left"></span> + <button class="squareb" title="info" id="{ID}-info"></button> + <button class="squareb" title="copy" id="{ID}-copy"></button> + <button class="squareb" title="download" id="{ID}-download"></button> + </div> + <span class="title" id="{ID}-title"></span> + <img id="{ID}-picture" onmouseenter="document.getElementById('{ID}').style.backgroundColor = '#454545'" + onmouseleave="document.getElementById('{ID}').style.backgroundColor = '#020202'" draggable="false" class="preview" + width="120px" height="120px" src="fail.png" /> +</div>
\ No newline at end of file diff --git a/src/bot/mod.rs b/src/bot/mod.rs index 8bc11fd..6852011 100644 --- a/src/bot/mod.rs +++ b/src/bot/mod.rs @@ -1,7 +1,7 @@ mod logic; mod map; mod schematic; -mod search; +pub mod search; use anyhow::Result; use dashmap::DashMap; @@ -100,7 +100,7 @@ decl! { 1147887958351945738u64 => "electrolyzer" : [HYDROGEN, OZONE, ""], 1202001032503365673u64 => "nitrogen" : [NITROGEN, ""], 1202001055349477426u64 => "cyanogen" : [CYANOGEN, ""], - 1096157669112418454u64 => "mass-driver" : [""], + 1096157669112418454u64 => "mass-driver" : ["…", PLANET], 973234248054104115u64 => "oxide" : [OXIDE, ""], 973422874734002216u64 => "erekir-phase" : [PHASE_FABRIC, ""], 973369188800413787u64 => "ccc" : ["", POWER], @@ -192,7 +192,7 @@ where } } -mod git { +pub mod git { use mindus::data::DataWrite; use self::schematic::Schem; @@ -520,6 +520,34 @@ impl Bot { .unwrap(); } } +/* +#[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?; + return Ok(()); + } + c.defer().await?; + let tags = tags(SPECIAL[&channel.get()].labels); + for schem in search::dir(channel.get()).unwrap() { + let mut s = search::load(&schem); + let mut v = DataWrite::default(); + s.tags.insert("labels".into(), tags.clone()); + s.serialize(&mut v)?; + std::fs::write(schem, v.consume())?; + } + send(&c, |x| { + x.avatar_url(CAT.to_string()).username("bendn <3").embed( + CreateEmbed::new() + .color(RM) + .description(format!("fixed tags in <#{channel}> :heart:")), + ) + }) + .await; + c.reply("fin").await?; + Ok(()) +} +*/ pub mod emojis { pub const GUILDS: &[u64] = &[1003092764919091282, 925674713429184564]; diff --git a/src/expose.rs b/src/expose.rs new file mode 100644 index 0000000..d6ee7b1 --- /dev/null +++ b/src/expose.rs @@ -0,0 +1,164 @@ +use axum::{ + extract::Path, + http::{header::*, StatusCode}, + response::{AppendHeaders, Html}, + routing::get, + Router, Server as AxumServer, +}; + +use std::{net::SocketAddr, sync::LazyLock, time::SystemTime}; +const COMPILED_AT: LazyLock<SystemTime> = + LazyLock::new(|| edg::r! { || -> std::time::SystemTime { std::time::SystemTime::now() }}); +static COMPILED: LazyLock<String> = LazyLock::new(|| httpdate::fmt_http_date(*COMPILED_AT)); + +fn no_bytes(map: HeaderMap) -> (StatusCode, Option<&'static [u8]>) { + if let Some(x) = map.get("if-modified-since") + && let Ok(x) = x.to_str() + && let Ok(x) = httpdate::parse_http_date(x) + && x < *COMPILED_AT + { + (StatusCode::NOT_MODIFIED, Some(&[])) + } else { + (StatusCode::OK, None) + } +} + +macro_rules! html { + ($file:expr) => { + get(|_map: HeaderMap| async { + println!("a wild visitor approaches"); + #[cfg(debug_assertions)] + return Html(std::fs::read(concat!("html-src/", stringify!($file), ".html")).unwrap()); + #[cfg(not(debug_assertions))] + { + let (code, bytes) = no_bytes(_map); + ( + code, + Html(bytes.unwrap_or(include_bytes!(concat!( + "../html/", + stringify!($file), + ".html" + )))), + ) + } + }) + }; +} + +macro_rules! png { + ($file:expr) => { + get(|map: HeaderMap| async { + let (code, bytes) = no_bytes(map); + let bytes = bytes.unwrap_or(include_bytes!(concat!( + "../html/", + stringify!($file), + ".png" + ))); + + ( + code, + ( + AppendHeaders([(CONTENT_TYPE, "image/png"), (LAST_MODIFIED, &*COMPILED)]), + bytes, + ), + ) + }) + }; +} + +pub struct Server; +impl Server { + pub async fn spawn(addr: SocketAddr) { + let router = Router::new() + .route("/", html!(index)) + .route("/fail.png", png!(fail)) + .route("/bg.png", png!(bg)) + .route("/border.png", png!(border)) + .route("/border-active.png", png!(border_active)) + .route("/border-hover.png", png!(border_hover)) + .route("/favicon.ico", png!(favicon)) + .route( + "/default.woff", + get(|| async { + ( + [(CONTENT_TYPE, "font/woff")], + include_bytes!("../html-src/default.woff"), + ) + }), + ) + .route( + "/index.js", + get(|| async { + ( + [(CONTENT_TYPE, "application/javascript")], + if cfg!(debug_assertions) { + std::fs::read_to_string("html-src/index.js").unwrap().leak() + } else { + include_str!("../html-src/index.js") + }, + ) + }), + ) + .route( + "/files", + get(|| async { + serde_json::to_string( + &crate::bot::search::files() + .map(|(x, _)| { + x.with_extension("") + .file_name() + .unwrap() + .to_string_lossy() + .into_owned() + }) + .collect::<Vec<_>>(), + ) + .unwrap() + }), + ) + .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(), + ), + ), + None => (StatusCode::NOT_FOUND, String::default()), + } + }), + ) + .route( + "/files/: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, std::fs::read(x).unwrap()), + None => (StatusCode::NOT_FOUND, vec![]), + } + }), + ); + tokio::spawn(async move { + AxumServer::bind(&addr) + .serve(router.into_make_service()) + .await + .unwrap(); + }); + } +} diff --git a/src/main.rs b/src/main.rs index ea43871..fed3c1d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,14 @@ #![feature(let_chains, iter_intersperse, if_let_guard, const_mut_refs)] -use std::{sync::OnceLock, time::Instant}; +use std::{net::SocketAddr, sync::OnceLock, time::Instant}; +mod expose; #[macro_use] mod bot; static START: OnceLock<Instant> = OnceLock::new(); #[tokio::main(flavor = "current_thread")] async fn main() { START.get_or_init(|| Instant::now()); + expose::Server::spawn(<SocketAddr as std::str::FromStr>::from_str("0.0.0.0:2000").unwrap()) + .await; bot::Bot::spawn().await; } |