smol bot
add html page
bendn 2024-08-24
parent 1f5b971 · commit 9a4de61
-rw-r--r--.gitignore1
-rw-r--r--Cargo.toml7
-rw-r--r--build.rs41
-rw-r--r--html-src/bg.pngbin0 -> 116347 bytes
-rw-r--r--html-src/border.pngbin0 -> 383 bytes
-rw-r--r--html-src/border_active.pngbin0 -> 379 bytes
-rw-r--r--html-src/border_hover.pngbin0 -> 396 bytes
-rw-r--r--html-src/default.woffbin0 -> 3444104 bytes
-rw-r--r--html-src/fail.pngbin0 -> 28579 bytes
-rw-r--r--html-src/favicon.pngbin0 -> 288164 bytes
-rw-r--r--html-src/index.html228
-rw-r--r--html-src/index.js102
-rw-r--r--html-src/schematic.html13
-rw-r--r--src/bot/mod.rs34
-rw-r--r--src/expose.rs164
-rw-r--r--src/main.rs5
16 files changed, 591 insertions, 4 deletions
diff --git a/.gitignore b/.gitignore
index c9dd672..f6da125 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@
token
Cargo.lock
repo
+html \ No newline at end of file
diff --git a/Cargo.toml b/Cargo.toml
index c0d6f18..85d7424 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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
new file mode 100644
index 0000000..119d708
--- /dev/null
+++ b/html-src/bg.png
Binary files differ
diff --git a/html-src/border.png b/html-src/border.png
new file mode 100644
index 0000000..0dd9761
--- /dev/null
+++ b/html-src/border.png
Binary files differ
diff --git a/html-src/border_active.png b/html-src/border_active.png
new file mode 100644
index 0000000..541a407
--- /dev/null
+++ b/html-src/border_active.png
Binary files differ
diff --git a/html-src/border_hover.png b/html-src/border_hover.png
new file mode 100644
index 0000000..cdce442
--- /dev/null
+++ b/html-src/border_hover.png
Binary files differ
diff --git a/html-src/default.woff b/html-src/default.woff
new file mode 100644
index 0000000..92cf31f
--- /dev/null
+++ b/html-src/default.woff
Binary files differ
diff --git a/html-src/fail.png b/html-src/fail.png
new file mode 100644
index 0000000..bed589a
--- /dev/null
+++ b/html-src/fail.png
Binary files differ
diff --git a/html-src/favicon.png b/html-src/favicon.png
new file mode 100644
index 0000000..b393c59
--- /dev/null
+++ b/html-src/favicon.png
Binary files differ
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;
}