updated README and a bunch of other stuff

This commit is contained in:
Sebastian Moser
2026-11-04 20:24:14 +01:00
parent 6eedb1826a
commit 9707286375
46 changed files with 3725 additions and 172 deletions

View File

@@ -202,6 +202,18 @@ pub extern "C" fn part_auth_client() -> MizeResult<()> {
Ok(ItemData::new())
});
method("get_id_token", "() -> String", |_args| {
let data = get_config("self.secrets")?;
let secrets = match data.deserialize::<Option<ClientSecrets>>() {
Ok(Some(secrets)) => secrets,
Ok(None) | Err(_) => {
let mut mize = get_global_mize()?;
load_secrets(&mut mize).map_err(|_| mize_err!("not logged in"))?
}
};
Ok(secrets.id_token.into_item_data())
});
let mut mize = get_global_mize()?;
// start refresh tread if configured

View File

@@ -1,7 +1,7 @@
cargo-features = ["profile-rustflags"]
[package]
name = "mize-part-dev"
name = "mize-part-mize-dev"
version = "0.1.0"
edition = "2024"

View File

@@ -91,7 +91,13 @@ fn generate_part_struct(out: &mut String, meta: &PartMetadata) {
let _ = writeln!(out);
// part methods (skip ones that belong to include_native)
for m in meta.methods.iter().chain(meta.effects.iter()).chain(meta.threads.iter()) {
for m in meta
.methods
.iter()
.chain(meta.effects.iter())
.chain(meta.applies.iter())
.chain(meta.threads.iter())
{
if native_fns.contains(&m.name) {
continue;
}
@@ -466,7 +472,12 @@ pub fn extract_mize_dependency(cargo_toml: &str) -> Result<String, String> {
///
/// `mize_dep` is the full assignment line copied from the part Cargo.toml,
/// e.g. `mize = { workspace = true }` or `mize = { path = "../../core" }`.
pub fn generate_cargo_toml(package_name: &str, mize_dep: &str) -> String {
pub fn generate_cargo_toml(package_name: &str, mize_dep: &str, spacetime_sdk: bool) -> String {
let spacetime_dep = if spacetime_sdk {
"spacetimedb-sdk = \"2.6.0\"\n"
} else {
""
};
format!(
r#"[package]
name = "{package_name}"
@@ -479,9 +490,10 @@ path = "src/lib.rs"
[dependencies]
{mize_dep}
"#,
{spacetime_dep}"#,
package_name = package_name,
lib_name = package_name.replace('-', "_"),
mize_dep = mize_dep.trim(),
spacetime_dep = spacetime_dep,
)
}

View File

@@ -1,8 +1,12 @@
use std::fs::{create_dir_all, write};
use std::path::{Path, PathBuf};
use mize::prelude::*;
use cmd_lib::run_cmd as sh;
use mize::tracing::info;
use mize::tracing::debug;
use mize::platform::os::{
cargo_package_version, get_part_hash, part_dist_artifact_name, parts_dist_cd,
};
// a static version of what would be generated.
// as we need mize.dev to generate anything.
@@ -10,8 +14,6 @@ mod generated;
mod codegen;
use generated::*;
// spacetime gen command: sh!(RUST_LOG=error RUST_BACKTRACE=0 spacetime generate --build-options="--lint-dir=" -p $SRC/mize/marts/spacetime -o $SRC/mize/marts/generated/src/spacetime --lang rust)?;
#[unsafe(no_mangle)]
extern "C" fn part_dev() -> MizeResult<()> {
let cli = generated::CliPart::get("cli")?;
@@ -26,20 +28,245 @@ extern "C" fn part_dev() -> MizeResult<()> {
Ok(())
}
struct PartToPublish {
/// Runtime part id (e.g. `dev`, `auth.client`, `mize`)
id: String,
/// Directory containing Cargo.toml (None for the special `mize` core part)
dir: Option<PathBuf>,
version: String,
}
fn dylib_ext(target: &str) -> &'static str {
if target.contains("windows") {
"dll"
} else if target.contains("apple") || target.contains("darwin") {
"dylib"
} else {
"so"
}
}
fn part_lib_filename(part_id: &str, target: &str) -> String {
let sym = part_id.replace('.', "_");
let ext = dylib_ext(target);
if target.contains("windows") {
format!("mize_part_{sym}.{ext}")
} else {
format!("libmize_part_{sym}.{ext}")
}
}
fn mize_lib_filename(target: &str) -> String {
let ext = dylib_ext(target);
if target.contains("windows") {
format!("mize.{ext}")
} else {
format!("libmize.{ext}")
}
}
fn mize_bin_filename(target: &str) -> String {
if target.contains("windows") {
"mize.exe".to_owned()
} else {
"mize".to_owned()
}
}
fn release_dir(mize_src: &str, target: &str) -> PathBuf {
PathBuf::from(mize_src)
.join("target")
.join(target)
.join("release")
}
fn resolve_part_id(dir: &Path) -> MizeResult<String> {
let part_file = dir.join("part");
if part_file.exists() {
let toml = std::fs::read_to_string(&part_file)
.map_err(|e| mize_err!("could not read {}: {e}", part_file.display()))?;
let data = ItemData::from_toml(&toml)?;
if let Ok(id) = data.get_path("id").and_then(|v| v.value_string()) {
if !id.is_empty() {
return Ok(id);
}
}
}
let cargo_path = dir.join("Cargo.toml");
let cargo = std::fs::read_to_string(&cargo_path)
.map_err(|e| mize_err!("could not read {}: {e}", cargo_path.display()))?;
let data = ItemData::from_toml(&cargo)?;
if let Ok(lib_name) = data.get_path("lib.name").and_then(|v| v.value_string()) {
if let Some(rest) = lib_name.strip_prefix("mize_part_") {
return Ok(rest.replace('_', "."));
}
}
dir.file_name()
.and_then(|s| s.to_str())
.map(|s| s.to_owned())
.ok_or_else(|| mize_err!("could not resolve part id for {}", dir.display()))
}
fn collect_parts_to_publish(mize: &mut Mize, mize_src: &str) -> MizeResult<Vec<PartToPublish>> {
let mut out = Vec::new();
// Special core part "mize"
let mize_version = cargo_package_version(&format!("{mize_src}/core/Cargo.toml"))?;
out.push(PartToPublish {
id: "mize".to_owned(),
dir: None,
version: mize_version.clone(),
});
let workspaces = match mize.get_config("parts.workspaces") {
Ok(data) => data,
Err(_) => {
info!("parts.workspaces not set; only publishing special part 'mize'");
return Ok(out);
}
};
let arr = workspaces
.cbor()
.as_array()
.ok_or_else(|| mize_err!("parts.workspaces must be an array of directory paths"))?;
for src in arr {
let dir_str = src
.as_text()
.ok_or_else(|| mize_err!("parts.workspaces entries must be strings"))?;
let dir = PathBuf::from(dir_str);
if !dir.is_dir() {
info!("parts.workspaces entry is not a directory, skipping: {dir_str}");
continue;
}
let entries = std::fs::read_dir(&dir)
.map_err(|e| mize_err!("could not read parts.workspaces dir {dir_str}: {e}"))?;
for entry in entries {
let entry = entry.map_err(|e| mize_err!("read_dir error in {dir_str}: {e}"))?;
let path = entry.path();
if !path.is_dir() {
continue;
}
let cargo = path.join("Cargo.toml");
if !cargo.exists() {
debug!("skipping {} (no Cargo.toml)", path.display());
continue;
}
let id = resolve_part_id(&path)?;
let version = cargo_package_version(cargo.to_str().unwrap())?;
out.push(PartToPublish {
id,
dir: Some(path),
version,
});
}
}
Ok(out)
}
fn pack_and_upload_part(
mize: &mut Mize,
mize_src: &str,
target: &str,
part: &PartToPublish,
mize_version: &str,
) -> MizeResult<()> {
let rel = release_dir(mize_src, target);
let tmp = std::env::var("TMP").unwrap_or_else(|_| "/tmp".to_owned());
let staging = PathBuf::from(&tmp).join(format!("mize-dist-{}-{}", part.id, target));
if staging.exists() {
std::fs::remove_dir_all(&staging)
.map_err(|e| mize_err!("could not clear staging {}: {e}", staging.display()))?;
}
let lib_dir = staging.join("lib");
create_dir_all(&lib_dir)
.map_err(|e| mize_err!("could not create {}: {e}", lib_dir.display()))?;
if part.id == "mize" {
let lib_name = mize_lib_filename(target);
let bin_name = mize_bin_filename(target);
let lib_src = rel.join(&lib_name);
let bin_src = rel.join(&bin_name);
if !lib_src.exists() {
return Err(mize_err!("missing built artifact {}", lib_src.display()));
}
if !bin_src.exists() {
return Err(mize_err!("missing built artifact {}", bin_src.display()));
}
std::fs::copy(&lib_src, lib_dir.join(&lib_name))
.map_err(|e| mize_err!("copy {}: {e}", lib_src.display()))?;
let bin_dir = staging.join("bin");
create_dir_all(&bin_dir)?;
std::fs::copy(&bin_src, bin_dir.join(&bin_name))
.map_err(|e| mize_err!("copy {}: {e}", bin_src.display()))?;
} else {
let lib_name = part_lib_filename(&part.id, target);
let lib_src = rel.join(&lib_name);
if !lib_src.exists() {
info!(
"skipping part '{}': missing {} (not built for this target?)",
part.id,
lib_src.display()
);
return Ok(());
}
// Archive always uses the fetch_part layout: lib/libmize_part_<sym>.so
// For non-linux targets keep the native filename inside lib/.
let dest_name = if dylib_ext(target) == "so" {
format!("libmize_part_{}.so", part.id.replace('.', "_"))
} else {
lib_name.clone()
};
std::fs::copy(&lib_src, lib_dir.join(&dest_name))
.map_err(|e| mize_err!("copy {}: {e}", lib_src.display()))?;
}
let mut hash_opts = ItemData::new();
hash_opts.set_path("system", target)?;
hash_opts.set_path("mize_version", mize_version)?;
let (hash, selector) = get_part_hash(mize, &part.id, hash_opts)?;
debug!("part {} selector: {}", part.id, selector);
let cd = parts_dist_cd(mize);
let artifact = part_dist_artifact_name(&hash, &part.id, &part.version, cd);
let archive_path = PathBuf::from(&tmp).join(&artifact);
let staging_str = staging.to_string_lossy().to_string();
let archive_str = archive_path.to_string_lossy().to_string();
sh!(tar -C $staging_str -czf $archive_str .)?;
info!("uploading {artifact} -> ocib:host/mize-dist/");
sh!(rsync -av $archive_str ocib:host/mize-dist/)?;
let _ = std::fs::remove_dir_all(&staging);
let _ = std::fs::remove_file(&archive_path);
Ok(())
}
pub fn publish_mize(_ : ArgMatches) -> MizeResult<()> {
let mut mize = get_global_mize()?;
let mize_src = mize.get_config("mize.src")?.value_string()?;
let cargo_dir = std::env::home_dir().unwrap().join(".cargo");
let mize_version = cargo_package_version(&format!("{mize_src}/core/Cargo.toml"))?;
let parts = collect_parts_to_publish(&mut mize, &mize_src)?;
info!(
"publishing {} part(s) (mize_version={mize_version}, cd={})",
parts.len(),
parts_dist_cd(&mut mize)
);
let targets = vec![
"x86_64-pc-windows-gnu",
"aarch64-pc-windows-gnullvm",
"x86_64-unknown-linux-gnu",
"aarch64-unknown-linux-gnu",
"armv7-unknown-linux-gnueabihf",
"arm-unknown-linux-gnueabi",
"x86_64-pc-windows-gnu",
"aarch64-pc-windows-gnullvm",
"aarch64-apple-darwin",
"x86_64-apple-darwin",
];
@@ -50,7 +277,7 @@ pub fn publish_mize(_ : ArgMatches) -> MizeResult<()> {
"x86_64-apple-darwin" => "darwin",
_ => "base",
};
let tmp = std::env::var("TMP").unwrap();
let tmp = std::env::var("TMP").unwrap_or_else(|_| "/tmp".to_owned());
let mut build_config = ItemData::new();
build_config.set_path("namespace", "mize.buildtime.ns")?;
std::fs::create_dir_all(&tmp)?;
@@ -58,10 +285,19 @@ pub fn publish_mize(_ : ArgMatches) -> MizeResult<()> {
std::fs::write(format!("{tmp}/mize-part-dev-build-config.toml"), build_config.to_json()?)?;
sh!(docker build -t mize-build-$docker_image $mize_src/parts/mize.dev/docker/$docker_image)?;
sh!(echo running in container: docker run -it --rm -v "$tmp/mize-part-dev-build-config.toml:/build-config.toml" -e MIZE_BUILD_CONFIG=/build-config.toml -v $cargo_dir/registry:/usr/local/cargo/registry -v $cargo_dir/git:/usr/local/cargo/git -v $mize_src:/src -v $mize_src/target:/src/target mize-build-$docker_image cargo build --target $target_name --release --workspace)?;
let res = sh!(docker run -it --rm -v "$tmp/mize-part-dev-build-config.toml:/build-config.toml" -e MIZE_BUILD_CONFIG=/build-config.toml -v $cargo_dir/registry:/usr/local/cargo/registry -v $cargo_dir/git:/usr/local/cargo/git -v $mize_src:/src -v $mize_src/target:/src/target mize-build-$docker_image cargo build --target $target_name --release);
let res = sh!(docker run -it --rm -v "$tmp/mize-part-dev-build-config.toml:/build-config.toml" -e MIZE_BUILD_CONFIG=/build-config.toml -v $cargo_dir/registry:/usr/local/cargo/registry -v $cargo_dir/git:/usr/local/cargo/git -v $mize_src:/src -v $mize_src/target:/src/target mize-build-$docker_image cargo build --target $target_name --release --workspace);
if res.is_err() {
info!("build for {target_name} failed...");
break;
}
for part in &parts {
if let Err(e) = pack_and_upload_part(&mut mize, &mize_src, target_name, part, &mize_version) {
info!("pack/upload for part '{}' target '{target_name}' failed", part.id);
e.log();
}
}
break;
};
@@ -106,12 +342,16 @@ pub fn gen_bindings(_ : ArgMatches) -> MizeResult<()> {
gen_parts.push(s.to_owned());
}
debug!("parts to generate bindings for: {:?}", gen_parts);
let gen_spacetime = gen_parts.iter().any(|p| p == "spacetime");
let spacetime_module_dir = cwd.join("spacetime");
let gen_spacetime_sdk = gen_spacetime && spacetime_module_dir.is_dir();
// 3. enable generate mode and clear any old metadata
mize.set_generate_mode(true)?;
mize.clear_metadata()?;
// 4. load each part — in generate mode their part_smth funcs only record metadata
// (`"spacetime"` still produces normal SpacetimePart bindings; SDK client codegen is extra)
for p in &gen_parts {
info!("loading part in generate mode: {p}");
mize.load_part(p)?;
@@ -136,11 +376,36 @@ pub fn gen_bindings(_ : ArgMatches) -> MizeResult<()> {
let mize_dep = codegen::extract_mize_dependency(&part_cargo)
.map_err(|e| mize_err!("{e}"))?;
debug!("using mize dependency from part Cargo.toml: {mize_dep}");
let cargo_toml = codegen::generate_cargo_toml(&package_name, &mize_dep);
let cargo_toml = codegen::generate_cargo_toml(&package_name, &mize_dep, gen_spacetime_sdk);
write(gen_dir.join("Cargo.toml"), cargo_toml)
.map_err(|e| mize_err!("could not write generated/Cargo.toml: {e}"))?;
let lib_rs = codegen::generate_lib_rs(&all_metadata);
let mut lib_rs = codegen::generate_lib_rs(&all_metadata);
// 8. if gen includes "spacetime" and {cwd}/spacetime exists, emit SpacetimeDB rust client bindings
if gen_spacetime_sdk {
let module_path = spacetime_module_dir.to_string_lossy().to_string();
let spacetime_out = src_dir.join("spacetime");
create_dir_all(&spacetime_out)
.map_err(|e| mize_err!("could not create {}: {e}", spacetime_out.display()))?;
info!(
"running spacetime generate -p {} -o {}",
module_path,
spacetime_out.display()
);
let spacetime_out_str = spacetime_out.to_string_lossy().to_string();
sh!(
RUST_LOG=error RUST_BACKTRACE=0 spacetime generate
--build-options="--lint-dir="
-p $module_path
-o $spacetime_out_str
--lang rust
)?;
lib_rs.push_str("\npub mod spacetime;\npub use spacetime::*;\n");
}
write(src_dir.join("lib.rs"), lib_rs)
.map_err(|e| mize_err!("could not write generated/src/lib.rs: {e}"))?;

View File

@@ -0,0 +1,19 @@
cargo-features = ["profile-rustflags"]
[package]
name = "mize-part-mos-bash-config-cache"
version = "0.1.0"
edition = "2024"
[lib]
name = "mize_part_mos_bash_config_cache"
crate-type = ["cdylib", "rlib"]
[dependencies]
mize = { workspace = true }
[profile.dev]
rustflags = ["-C", "prefer-dynamic", "-C", "link-args=-Wl,--export-dynamic"]
[profile.release]
rustflags = ["-C", "prefer-dynamic"]

View File

@@ -0,0 +1 @@
id = "mos.bash_config_cache"

View File

@@ -0,0 +1,89 @@
use std::fs::{create_dir_all, write};
use std::path::PathBuf;
use mize::prelude::*;
fn bash_double_quote(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'\\' | '"' | '`' => {
out.push('\\');
out.push(c);
}
'\n' => out.push_str("\\n"),
// leave `$` unescaped so values like $CONFIG_DIR expand when sourced
_ => out.push(c),
}
}
out.push('"');
out
}
fn opt_value_for_bash(mize: &mut Mize, opt_id: &str) -> MizeResult<String> {
println!("hiiii");
let val = mize.get_config(opt_id)?;
if let Some(arr) = val.cbor().as_array() {
println!("as arr!!");
let mut quoted = Vec::with_capacity(arr.len());
for entry in arr {
let s = entry.as_text().ok_or_else(|| {
mize_err!("mos.bash_config_cache: list opt '{opt_id}' has a non-string entry")
})?;
quoted.push(bash_double_quote(s));
}
return Ok(quoted.join(" "));
}
val.value_string()
}
fn write_cache(_args: ItemData) -> MizeResult<ItemData> {
let mut mize = get_global_mize()?;
let store = mize.get_config("store")?.value_string()?;
let opts_data = mize.get_config("mos.bash_config_cache.opts")?;
let arr = opts_data
.cbor()
.as_array()
.ok_or_else(|| mize_err!("mos.bash_config_cache.opts is not a list"))?;
let mut body = String::from("# generated by mos.bash_config_cache.write_cache — do not edit\n");
body.push_str("declare -A myconfig\n");
for entry in arr {
let opt_id = entry
.as_text()
.ok_or_else(|| mize_err!("mos.bash_config_cache.opts entry is not a string"))?;
let val = opt_value_for_bash(&mut mize, opt_id)?;
body.push_str(&format!(
"myconfig[{}]={}\n",
bash_double_quote(opt_id),
bash_double_quote(&val)
));
}
let out_dir = PathBuf::from(&store).join("misc");
create_dir_all(&out_dir)?;
let out_path = out_dir.join("mos.bash_config_cache");
write(&out_path, body)?;
Ok(ItemData::new())
}
#[unsafe(no_mangle)]
pub extern "C" fn part_mos_bash_config_cache() -> MizeResult<()> {
// toml may already have populated opts; opt() inserts empty and would clobber it
let preexisting = get_global_mize()?
.get_config("mos.bash_config_cache.opts")
.ok();
let opts_id = opt("opts");
if let Some(val) = preexisting {
opts_id.default_val(val);
}
apply("write_cache", write_cache);
// until apply hooks run at startup automatically
write_cache(ItemData::new())?;
Ok(())
}

View File

@@ -0,0 +1,29 @@
cargo-features = ["profile-rustflags"]
[package]
name = "mize-part-ppc-server"
version = "0.1.0"
edition = "2024"
[lib]
name = "mize_part_ppc_server"
crate-type = ["cdylib", "rlib"]
[dependencies]
mize = { workspace = true }
axum = "0.8.9"
axum-oidc = "=1.0.0-dev-1"
openidconnect = { version = "4", features = ["reqwest", "rustls-tls"], default-features = false }
tower = "0.5.3"
tower-sessions = "0.15.0"
tower-http = { version = "0.7.1", features = ["cors", "trace"] }
serde_json = "1.0"
tokio = { version = "1", features = ["rt-multi-thread", "net", "macros"] }
tracing = "0.1"
dioxus = { version = "0.7", default-features = false, features = ["ssr", "macro", "html", "signals"] }
[profile.dev]
rustflags = ["-C", "prefer-dynamic", "-C", "link-args=-Wl,--export-dynamic"]
[profile.release]
rustflags = ["-C", "prefer-dynamic"]

176
parts/ppc.server/src/lib.rs Normal file
View File

@@ -0,0 +1,176 @@
mod pages;
use std::str::FromStr;
use std::thread;
use axum::{
Json, Router,
error_handling::HandleErrorLayer,
http::Uri,
response::IntoResponse,
routing::{any, get},
};
use axum_oidc::handle_oidc_redirect;
use axum_oidc::{
EmptyAdditionalClaims, OidcAuthLayer, OidcClaims, OidcClient, OidcLoginLayer,
OidcRpInitiatedLogout, error::MiddlewareError,
};
use mize::prelude::*;
use openidconnect::Scope;
use openidconnect::{ClientId, ClientSecret, IssuerUrl};
use serde_json::{Value, json};
use tokio::runtime::Runtime;
use tower::ServiceBuilder;
use tower_sessions::{
Expiry, MemoryStore, SessionManagerLayer,
cookie::{SameSite, time::Duration},
};
fn ensure_default(rel: &str, default: ItemData) -> MizeResult<()> {
let mut mize = get_global_mize()?;
let cur = mize
.cur_eval_part()?
.ok_or_else(|| mize_err!("no cur_eval_part"))?;
let full = format!("{}.{}", cur.dot(), rel);
if mize.get_config(&full).is_ok() {
return Ok(());
}
let segs: Vec<&str> = rel.split('.').filter(|s| !s.is_empty()).collect();
let last = *segs.last().ok_or_else(|| mize_err!("empty opt path"))?;
let mut parent = cur.clone();
for seg in &segs[..segs.len() - 1] {
parent = parent.join(seg)?;
}
let original = Some(cur);
mize.set_cur_eval_part(Some(parent))?;
opt(last).default_val(default);
mize.set_cur_eval_part(original)?;
Ok(())
}
#[unsafe(no_mangle)]
pub extern "C" fn part_ppc_server() -> MizeResult<()> {
ensure_default("url", "http://localhost:3000".into_item_data())?;
ensure_default("bind", "0.0.0.0:8010".into_item_data())?;
thread::spawn(|| {
let rt = match Runtime::new() {
Ok(rt) => rt,
Err(e) => {
eprintln!("ppc.server: failed to create runtime: {e}");
return;
}
};
if let Err(e) = rt.block_on(start_server()) {
e.log();
}
});
Ok(())
}
async fn start_server() -> MizeResult<()> {
let mut mize = get_global_mize()?;
let issuer = mize.get_config("auth.server.issuer")?.value_string()?;
let client_id = mize.get_config("auth.server.client_id")?.value_string()?;
let client_secret = mize.get_config("auth.server.client_secret")?.value_string()?;
let url = mize.get_config("ppc.server.url")?.value_string()?;
let bind = mize.get_config("ppc.server.bind")?.value_string()?;
let redirect_url = format!("{url}/oidc");
let session_store = MemoryStore::default();
let session_layer = SessionManagerLayer::new(session_store)
.with_secure(false)
.with_same_site(SameSite::Lax)
.with_expiry(Expiry::OnInactivity(Duration::seconds(120)));
let oidc_login_service = ServiceBuilder::new()
.layer(HandleErrorLayer::new(|e: MiddlewareError| async {
dbg!(&e);
e.into_response()
}))
.layer(OidcLoginLayer::<EmptyAdditionalClaims>::new());
let oidc_client = OidcClient::<EmptyAdditionalClaims>::builder()
.with_default_http_client()
.with_redirect_url(Uri::from_str(redirect_url.as_str())?)
.with_client_id(ClientId::new(client_id))
.add_scope(Scope::new("profile".into()))
.add_scope(Scope::new("email".into()))
.with_client_secret(ClientSecret::new(client_secret))
.discover(IssuerUrl::new(issuer).expect("Invalid IssuerUrl"))
.await
.map_err(|e| mize_err!("oidc discover failed: {:?}", e))?
.build();
let oidc_auth_service = ServiceBuilder::new()
.layer(HandleErrorLayer::new(|e: MiddlewareError| async {
dbg!(&e);
e.into_response()
}))
.layer(OidcAuthLayer::new(oidc_client));
let app = Router::new()
.route("/foo", get(authenticated))
.route("/logout", get(logout))
.layer(oidc_login_service)
.route("/bar", get(maybe_authenticated))
.route("/oidc", any(handle_oidc_redirect::<EmptyAdditionalClaims>))
.layer(oidc_auth_service)
.layer(session_layer)
.layer(tower_http::trace::TraceLayer::new_for_http())
.route("/", get(pages::main_page()))
.route("/mc", get(pages::mc_page()))
.route(
"/.well-known/matrix/client",
get(handle_matrix_client).layer(tower_http::cors::CorsLayer::permissive()),
)
.route("/.well-known/matrix/server", get(handle_matrix_server));
tracing::info!("ppc.server listening on http://{bind}");
let listener = tokio::net::TcpListener::bind(&bind)
.await
.map_err(|e| mize_err!("bind {bind}: {e}"))?;
axum::serve(listener, app.into_make_service())
.await
.map_err(|e| mize_err!("axum serve: {e}"))?;
Ok(())
}
async fn authenticated(claims: OidcClaims<EmptyAdditionalClaims>) -> impl IntoResponse {
format!("Hello {}", claims.subject().as_str())
}
async fn maybe_authenticated(
claims: Result<OidcClaims<EmptyAdditionalClaims>, axum_oidc::error::ExtractorError>,
) -> impl IntoResponse {
if let Ok(claims) = claims {
format!(
"Hello {}! You are already logged in from another Handler.",
claims.subject().as_str()
)
} else {
"Hello anon!".to_string()
}
}
async fn logout(logout: OidcRpInitiatedLogout) -> impl IntoResponse {
logout.with_post_logout_redirect(Uri::from_static("https://localhost:3000"))
}
async fn handle_matrix_client() -> Json<Value> {
Json(json!({
"m.homeserver": {
"base_url": "https://matrix.ppc.social"
}
}))
}
async fn handle_matrix_server() -> Json<Value> {
Json(json!({
"m.server": "matrix.ppc.social:443"
}))
}

View File

@@ -0,0 +1,186 @@
use axum::response::Html;
use dioxus::prelude::*;
const SITE_CSS: &str = r#"
*, *::before, *::after { box-sizing: border-box; }
html { color-scheme: light dark; }
body {
margin: 0;
padding: 0 1.25rem 2.5rem;
max-width: 60ch;
margin-inline: auto;
font-family: Georgia, "Times New Roman", serif;
font-size: 1.25rem;
line-height: 1.6;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
background-color: #F7F8F4;
color: #20251C;
word-wrap: break-word;
}
h1, h2 {
line-height: 1.2;
font-weight: 700;
color: #20251C;
}
h1 { margin: 2.5rem 0 1.5rem; font-size: 1.75rem; }
h2 { margin: 2.5rem 0 1rem; font-size: 1.35rem; }
p { margin: 0.85rem 0; }
.tagline { color: #66705E; font-style: italic; }
.meta { color: #66705E; font-size: 0.95rem; }
a {
color: #376777;
text-decoration-thickness: 1px;
text-underline-offset: 3px;
}
a:hover { color: #79500F; }
.accent { color: #79500F; }
.accent-bar {
display: inline-block;
width: 2.5rem;
height: 0.35rem;
background: #FEC251;
border-radius: 2px;
margin-bottom: 0.75rem;
}
@media (prefers-color-scheme: dark) {
body { background-color: #171D18; color: #F2F4EA; }
h1, h2 { color: #F2F4EA; }
.tagline, .meta { color: #AAB5A4; }
a { color: #B9E4EB; }
a:hover { color: #D7E47D; }
.accent { color: #D7E47D; }
.accent-bar { background: #D7E47D; }
}
"#;
const PLAUSIBLE_SRC: &str = "https://plausible.c2vi.dev/js/script.file-downloads.hash.outbound-links.pageview-props.revenue.tagged-events.js";
const PLAUSIBLE_BOOT: &str =
"window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }";
fn render_page(title: &str, body: Element) -> Html<String> {
let markup = dioxus::ssr::render_element(rsx! {
head {
meta { charset: "UTF-8" }
meta { name: "viewport", content: "width=device-width, initial-scale=1.0" }
title { "{title}" }
script {
defer: true,
"data-domain": "ppc.social",
src: PLAUSIBLE_SRC,
}
script { dangerous_inner_html: PLAUSIBLE_BOOT }
style { {SITE_CSS} }
}
body { {body} }
});
Html(format!("<!DOCTYPE html>\n<html lang=\"en\">\n{markup}\n</html>"))
}
#[component]
fn MainPageBody() -> Element {
rsx! {
div { class: "accent-bar" }
h1 { "People Corner" }
p { class: "tagline", "\"Cool People doing Cool things together\"" }
p {
"Read what this is all about: "
a { href: "https://wiki.ppc.social/u/c2vi/The-Idea-of-PPC", "About" }
}
p {
"There is a Discord Server: "
a { href: "https://discord.gg/cMKQNJMkRe", "https://discord.gg/cMKQNJMkRe" }
}
p {
"There is a "
a { href: "https://ppc.social/mc", "Minecraft server" }
"."
}
p {
"There is "
a { href: "https://wiki.ppc.social", "The PPC Wiki Projekt" }
"."
}
p {
"There is an "
a { href: "https://www.instagram.com/ppc.social/", "Instagram" }
" and a "
a { href: "https://www.youtube.com/@ppc.social", "Youtube Channel" }
}
p { class: "meta", "and that's about all for now..." }
}
}
#[component]
fn McPageBody() -> Element {
rsx! {
div { class: "accent-bar" }
h1 { "Welcome to the PPC Minecraft Cluster" }
h2 { "Address" }
p { class: "accent", "first.ppc.social" }
p { class: "meta", "Java 1.21.11 and Bedrock (bedrock not working rn)" }
h2 { "Discord" }
p {
a {
href: "https://discord.gg/cMKQNJMkRe",
target: "_blank",
"https://discord.gg/cMKQNJMkRe"
}
}
p { class: "meta", "come and say hi!" }
p { "See you ingame and invite all your friends to play as well!!" }
h2 { "SimpleVoicechat" }
p {
"This is "
span { class: "accent", "not required" }
", but makes playing soooo much more fun, so it's "
span { class: "accent", "highly encouraged" }
"."
}
p {
"Instructions can be found here:"
}
p {
a { href: "https://fabricmc.net/use/installer/", "Fabric" }
}
p {
a {
href: "https://modrepo.de/minecraft/voicechat/wiki/installation#fabric",
"SimpleVoiceChat"
}
}
}
}
pub fn main_page() -> Html<String> {
render_page("People Corner", rsx! { MainPageBody {} })
}
pub fn mc_page() -> Html<String> {
render_page("PPC Minecraft Server", rsx! { McPageBody {} })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn main_page_renders() {
let html = main_page().0;
assert!(html.contains("<!DOCTYPE html>"));
assert!(html.contains("People Corner"));
assert!(html.contains("Cool People doing Cool things together"));
assert!(html.contains("#F7F8F4"));
assert!(html.contains("#376777"));
assert!(html.contains("https://wiki.ppc.social/u/c2vi/The-Idea-of-PPC"));
}
#[test]
fn mc_page_renders() {
let html = mc_page().0;
assert!(html.contains("PPC Minecraft Cluster"));
assert!(html.contains("first.ppc.social"));
assert!(html.contains("SimpleVoicechat"));
assert!(html.contains("https://fabricmc.net/use/installer/"));
}
}

19
parts/ppc/Cargo.toml Normal file
View File

@@ -0,0 +1,19 @@
cargo-features = ["profile-rustflags"]
[package]
name = "mize-part-ppc"
version = "0.1.0"
edition = "2024"
[lib]
name = "mize_part_ppc"
crate-type = ["cdylib", "rlib"]
[dependencies]
mize = { workspace = true }
[profile.dev]
rustflags = ["-C", "prefer-dynamic", "-C", "link-args=-Wl,--export-dynamic"]
[profile.release]
rustflags = ["-C", "prefer-dynamic"]

2
parts/ppc/part Normal file
View File

@@ -0,0 +1,2 @@
id = "ppc"
gen = [ "spacetime" ]

7
parts/ppc/src/lib.rs Normal file
View File

@@ -0,0 +1,7 @@
use mize::prelude::*;
#[unsafe(no_mangle)]
pub extern "C" fn part_ppc() -> MizeResult<()> {
load_part_at("ppc.spacetime", "spacetime")?;
Ok(())
}

View File

@@ -0,0 +1,19 @@
cargo-features = ["profile-rustflags"]
[package]
name = "mize-part-sci-inbox"
version = "0.1.0"
edition = "2024"
[lib]
name = "mize_part_sci_inbox"
crate-type = ["cdylib", "rlib"]
[dependencies]
mize = { workspace = true }
[profile.dev]
rustflags = ["-C", "prefer-dynamic", "-C", "link-args=-Wl,--export-dynamic"]
[profile.release]
rustflags = ["-C", "prefer-dynamic"]

View File

@@ -0,0 +1,6 @@
use mize::prelude::*;
#[unsafe(no_mangle)]
pub extern "C" fn part_sci_inbox() -> MizeResult<()> {
Ok(())
}

View File

@@ -0,0 +1,19 @@
cargo-features = ["profile-rustflags"]
[package]
name = "mize-part-sci-timetrack"
version = "0.1.0"
edition = "2024"
[lib]
name = "mize_part_sci_timetrack"
crate-type = ["cdylib", "rlib"]
[dependencies]
mize = { workspace = true }
[profile.dev]
rustflags = ["-C", "prefer-dynamic", "-C", "link-args=-Wl,--export-dynamic"]
[profile.release]
rustflags = ["-C", "prefer-dynamic"]

View File

@@ -0,0 +1,73 @@
use mize::prelude::*;
pub struct CliPart {
id: MizeId,
}
impl MizePart for CliPart {
fn id(&self) -> MizeResult<MizeId> {
Ok(self.id.clone())
}
}
impl CliPart {
pub fn get(name: &str) -> MizeResult<CliPart> {
let mut mize = get_global_mize()?;
let part = mize.get_part(name)?;
Ok(CliPart { id: part.id()? })
}
pub fn set_command(&self, name: &str) -> MizeResult<()> {
let mut args = ItemData::new();
args.set_path("0", name.into_item_data())?;
dyn_call(self.id.clone(), "set_command", args)?;
Ok(())
}
pub fn subcommand<T: Fn(ArgMatches) -> MizeResult<()> + Send + Sync + 'static>(
&self,
subcmd: Command,
action: T,
) -> MizeResult<()> {
let mut subcmd_arg = ItemData::new();
subcmd_arg.set_path("_native", subcmd.native_id)?;
let action_arg = ItemData::fun(Box::new(move |args: ItemData| {
let matches = args.clone().get_path("0")?.native_take_from::<ArgMatches>()?;
action(matches)?;
Ok(ItemData::new())
}))?;
let mut args = ItemData::new();
args.set_path("0", subcmd_arg)?;
args.set_path("1", action_arg)?;
dyn_call(self.id.clone(), "subcommand", args)?;
Ok(())
}
}
pub struct Command {
native_id: u32,
}
pub struct ArgMatches {
native_id: u32,
}
impl FromItemData for ArgMatches {
fn from_itemdata(data: ItemData) -> MizeResult<Self> {
Ok(ArgMatches {
native_id: data.get_path("_native")?.value_u32()?,
})
}
}
impl Command {
pub fn new(name: &str) -> MizeResult<Command> {
let mut args = ItemData::new();
args.set_path("0", name.into_item_data())?;
let mut mize = get_global_mize()?;
let id = mize.id_new_dots("cli.Command")?;
let data = dyn_call(id, "new", args)?;
let native_id: u32 = data.get_path("_native")?.native_take()?;
Ok(Command { native_id })
}
}

View File

@@ -0,0 +1,65 @@
mod generated;
use generated::{ArgMatches, CliPart, Command};
use mize::prelude::*;
use mize::proto::MizeMessage;
fn ensure_default(rel: &str, default: ItemData) -> MizeResult<()> {
let mut mize = get_global_mize()?;
let cur = mize
.cur_eval_part()?
.ok_or_else(|| mize_err!("no cur_eval_part"))?;
let full = format!("{}.{}", cur.dot(), rel);
if mize.get_config(&full).is_ok() {
return Ok(());
}
let segs: Vec<&str> = rel.split('.').filter(|s| !s.is_empty()).collect();
let last = *segs.last().ok_or_else(|| mize_err!("empty opt path"))?;
let mut parent = cur.clone();
for seg in &segs[..segs.len() - 1] {
parent = parent.join(seg)?;
}
let original = Some(cur);
mize.set_cur_eval_part(Some(parent))?;
opt(last).default_val(default);
mize.set_cur_eval_part(original)?;
Ok(())
}
fn spacetime_unwired(_: ArgMatches) -> MizeResult<()> {
Err(mize_err!("spacetime SDK not wired yet"))
}
#[unsafe(no_mangle)]
pub extern "C" fn part_sci_timetrack() -> MizeResult<()> {
ensure_default("daily_notes_path", "".into_item_data())?;
ensure_default("profile", "c2vi".into_item_data())?;
let cli = CliPart::get("cli")?;
cli.set_command("sci")?;
for (name, alias) in [
("editActionList", "aal"),
("nextAction", "ne"),
("printCurrentAction", "cu"),
("listActions", "la"),
("listActivities", "lac"),
("editActivities", "eac"),
("timetrackExport", "te"),
] {
cli.subcommand(Command::new(name)?, spacetime_unwired)?;
cli.subcommand(Command::new(alias)?, spacetime_unwired)?;
}
let mut mize = get_global_mize()?;
mize.add_listener(
"timetrack.sub_current",
Box::new(|_msg: MizeMessage, _mize: &mut Mize| {
Err(mize_err!(
"spacetime SDK not wired yet (timetrack.sub_current)"
))
}),
)?;
Ok(())
}

20
parts/sci/Cargo.toml Normal file
View File

@@ -0,0 +1,20 @@
cargo-features = ["profile-rustflags"]
[package]
name = "mize-part-sci"
version = "0.1.0"
edition = "2024"
[lib]
name = "mize_part_sci"
crate-type = ["cdylib", "rlib"]
[dependencies]
mize = { workspace = true }
chrono = { version = "0.4", default-features = false, features = ["clock"] }
[profile.dev]
rustflags = ["-C", "prefer-dynamic", "-C", "link-args=-Wl,--export-dynamic"]
[profile.release]
rustflags = ["-C", "prefer-dynamic"]

2
parts/sci/part Normal file
View File

@@ -0,0 +1,2 @@
id = "sci"
gen = [ "cli", "sci", "spacetime" ]

1119
parts/sci/spacetime/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,17 @@
[package]
name = "mize-spacetime"
version = "0.1.0"
edition = "2021"
# Standalone SpacetimeDB module crate (not a mize workspace member / not a dep of sci).
[workspace]
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb = { version = "=2.6.0", features = ["unstable"] }
log = "0.4"
chrono = { version = "0.4.45", features = [], default-features = false }
chrono-tz = { version = "0.10.4", default-features = false }
url = "2.5.8"

View File

@@ -0,0 +1,394 @@
use std::collections::HashMap;
use std::time::Duration;
use chrono::{DateTime, TimeZone};
use chrono_tz::Europe;
use log::*;
use spacetimedb::SpacetimeType;
use spacetimedb::sys::raw::index_id_from_name;
use spacetimedb::{Identity, ReducerContext, ProcedureContext, Table, Timestamp, reducer, table, procedure};
use spacetimedb::http::*;
#[table(accessor = profiles, public)]
pub struct Profile {
#[primary_key]
identity: Identity,
#[unique]
username: String,
}
#[table(accessor = task_part, public)]
pub struct TaskPart {
#[primary_key]
identity: Identity,
start_of_current: Timestamp,
current: Action,
upcoming: Vec<Action>,
}
#[table(accessor = activities, public)]
pub struct Activity {
#[primary_key]
#[auto_inc]
id: u64,
name: String,
task: Option<u64>,
parents_ids: Vec<u64>,
}
#[table(accessor = tasks, public)]
pub struct Task {
#[primary_key]
#[auto_inc]
id: u64,
done: bool,
name: String,
parent: Option<u64>,
}
#[table(accessor = thoughts, public)]
pub struct Thought {
#[primary_key]
#[auto_inc]
id: u64,
text: String,
user: Identity,
#[index(btree)]
time: Timestamp,
}
#[table(accessor = action)]
#[derive(Clone)]
pub struct Action {
name: String,
activity: Option<u64>,
task: Option<u64>,
start: Option<Timestamp>,
end: Option<Timestamp>,
duration: Option<u64>,
}
impl Action {
pub fn name_only(name: String) -> Self {
Action {
name,
activity: None,
task: None,
start: None,
end: None,
duration: None,
}
}
}
#[reducer]
pub fn add_thought(ctx: &ReducerContext, thought: String) -> Result<(), String> {
ctx.db.thoughts().try_insert(Thought {
id: 0,
text: thought,
user: ctx.sender(),
time: ctx.timestamp,
})?;
Ok(())
}
#[table(accessor = log_events, public, index(accessor = log_events_index, btree(columns = [user, end])))]
pub struct LogEvent {
#[primary_key]
#[auto_inc]
id: u64,
start: Timestamp,
end: Timestamp,
action: Action,
user: Identity,
}
#[reducer(init)]
pub fn init(_ctx: &ReducerContext) {
// Called when the module is initially published
info!("hiiiiiii from ppc module");
}
// the one from ppc, hardcoded rn, as we for now only support config at run time.
// this needs to be loaded at build time. (this code runs as a wasm module on the spacetimedb server)
const PPC_AUTH_CLIENT_ID: &str = "363331597203734531";
#[reducer(client_connected)]
pub fn connect(ctx: &ReducerContext) -> Result<(), String> {
check_auth(ctx)?;
info!("hooooo");
// add user
let _ = ctx.db.profiles().try_insert(Profile {
identity: ctx.sender(),
username: ctx.sender().to_string(),
});
Ok(())
}
fn check_auth(ctx: &ReducerContext) -> Result<(), String> {
let jwt = ctx
.sender_auth()
.jwt()
.ok_or("Authentication required".to_string())?;
if jwt.issuer() != "https://auth.ppc.social" {
return Err("Invalid issuer".to_string());
}
if !jwt.audience().iter().any(|a| a == PPC_AUTH_CLIENT_ID) {
return Err("Invalid audience".to_string());
}
Ok(())
}
#[reducer(client_disconnected)]
pub fn identity_disconnected(_ctx: &ReducerContext) {
// Called everytime a client disconnects
}
#[reducer]
pub fn add_activity(ctx: &ReducerContext, name: String) {
ctx.db.activities().insert(Activity {
name,
task: None,
id: 0, // auto-increments
parents_ids: Vec::new(),
});
}
#[reducer]
pub fn set_activity_name(ctx: &ReducerContext, id: u64, name: String) {
if let Some(old_ac) = ctx.db.activities().id().find(id) {
ctx.db.activities().id().update(Activity { name, ..old_ac });
} else {
return ();
}
}
#[reducer]
pub fn update_upcoming(ctx: &ReducerContext, upcoming: Vec<Action>) {
if ctx.db.task_part().identity().find(ctx.sender()).is_none() {
ctx.db.task_part().insert(TaskPart {
upcoming,
identity: ctx.sender(),
current: Action {
name: "filler".to_string(),
activity: None,
task: None,
start: None,
end: None,
duration: None,
},
start_of_current: ctx.timestamp,
});
} else {
ctx.db.task_part().identity().update(TaskPart {
upcoming,
..ctx.db.task_part().identity().find(&ctx.sender()).unwrap()
});
}
}
#[reducer]
pub fn next_action(ctx: &ReducerContext, break_flag: bool, action: Option<Action>) -> Result<(), String> {
if let Some(mut task_part) = ctx.db.task_part().identity().find(&ctx.sender()) {
if task_part.upcoming.is_empty() {
return Err("No upcoming actions".to_string());
}
let new_action = match action {
Some(action) => action,
// when we do "ppc nextAction -b" ... we want to do a generic break "b" action.
None => if break_flag { Action::name_only("idk".to_string()) } else { task_part.upcoming.remove(0) },
};
// when break flag, push current_action again to the front of upcoming
if break_flag {
// insert generic "b" action when no one is specified
task_part.upcoming.insert(0, task_part.current.clone());
}
let start_of_old_action = task_part.start_of_current;
ctx.db.task_part().identity().update(TaskPart {
upcoming: task_part.upcoming,
identity: task_part.identity,
current: new_action.clone(),
start_of_current: ctx.timestamp,
});
// create log event
ctx.db.log_events().insert(LogEvent {
start: start_of_old_action,
end: ctx.timestamp,
action: task_part.current,
id: 0,
user: ctx.sender(),
});
Ok(())
} else {
Err("Task part data not found for this user".to_string())
}
}
#[reducer]
pub fn update_activity(ctx: &ReducerContext, activity: Activity) {
if activity.id == 0 {
ctx.db.activities().insert(activity);
} else {
ctx.db.activities().id().update(activity);
}
}
#[reducer]
pub fn remove_activity(ctx: &ReducerContext, activity: Activity) {
ctx.db.activities().delete(activity);
}
//////////////////// daily metric ///////////////////
#[table(accessor = daily_task, public)]
pub struct DailyTask {
id: i64,
name: String,
owner: Identity,
}
#[table(accessor = daily_task_log, public)]
pub struct DailyTaskLog {
daily_task_id: i64,
done: Timestamp,
}
#[procedure]
fn first_timetrack_date(ctx: &mut ProcedureContext, username: String) -> Result<String, String> {
let timestamp = ctx.try_with_tx(|tx| {
let profile = tx.db.profiles().username().find(username.clone()).ok_or("No such Profile".to_string())?;
let event = tx.db.log_events()
.log_events_index()
.filter(&profile.identity)
.nth(0)
.ok_or("Profile has no row in log_events")?;
let time = event.start
.to_chrono_date_time().map_err(|e| format!("chrono error: {e}"))?
.with_timezone(&chrono_tz::Europe::Vienna);
Ok::<String, String>(time.format("%Y-%m-%d").to_string())
})?;
Ok(timestamp)
}
#[procedure]
fn export_timetrack(ctx: &mut ProcedureContext, username: String, date: String) -> Result<String, String> {
// CEST only rn
let start = chrono::NaiveDate::parse_from_str(&date, "%Y-%m-%d").unwrap().and_hms(5, 0, 0);
let start_local = chrono_tz::Europe::Vienna.from_local_datetime(&start).unwrap();
let start_utc = start_local.with_timezone(&chrono::Utc);
let start_timestamp = Timestamp::from_micros_since_unix_epoch(start_utc.timestamp_micros());
let end = chrono::NaiveDate::parse_from_str(&date, "%Y-%m-%d").unwrap().and_hms(4, 0, 0) + Duration::from_hours(24);
let end_local = chrono_tz::Europe::Vienna.from_local_datetime(&end).unwrap();
let end_utc = end_local.with_timezone(&chrono::Utc);
let end_timestamp = Timestamp::from_micros_since_unix_epoch(end_utc.timestamp_micros());
info!("start_of_day: {}", start_timestamp);
info!("end_of_day: {}", end_timestamp);
let mut export = String::new();
//export.push_str(&format!("### actions\n"));
let profile = ctx.with_tx(|tx| tx.db.profiles().username().find(&username)).ok_or(format!("Profile with username {} not found", username))?;
let events = ctx.with_tx(|tx|
tx.db.log_events()
.log_events_index()
.filter((profile.identity, start_timestamp..end_timestamp))
);
for event in events {
let start = event.start
.to_chrono_date_time()
.map_err(|err| err.to_string())?
.with_timezone(&Europe::Vienna)
.format("%H:%M");
let end = event.end
.to_chrono_date_time()
.map_err(|err| err.to_string())?
.with_timezone(&Europe::Vienna)
.format("%H:%M");
let line = format!("{} to {} ... {}\n", start, end, event.action.name);
export.push_str(line.as_str());
}
return Ok(export); // don't do thoughts right now
export.push_str("\n");
export.push_str("## thoughts\n");
let thoughts = ctx.with_tx(|tx| {
tx.db.thoughts().time().filter(start_timestamp..end_timestamp)
});
for thought in thoughts {
let line = format!("- {}\n\n", thought.text);
export.push_str(line.as_str());
}
Ok(export)
}
#[procedure]
pub fn migrate(ctx: &mut ProcedureContext) -> String {
info!("migrate procedure called");
//let msg = "Nothing to migrate".to_string();
let msg: Result<String, String> = ctx.try_with_tx(|tx| {
let mut out = String::new();
let mut count = 0;
//for event in tx.db.log_events_tmp().iter() {
//count += 1;
//}
out.push_str(format!("Migrated {} rows................", count).as_str());
Ok(out)
});
return match msg {
Ok(msg) => msg,
Err(err) => err,
};
}
#[procedure]
pub fn test(ctx: &mut ProcedureContext) -> String {
let mut s = String::from("hoooooooooooooooooooooo\n");
let a: String = ctx.with_tx(|_tx| {
let s = String::new();
return s;
});
s.push_str(&format!("{}\n\n", a));
s.push_str("\n");
s.push_str(&format!("sender: {}", ctx.sender()));
s
}
#[handler]
pub fn test_handler(ctx: &mut HandlerContext, _req: Request<Body>) -> Response<Body> {
let res = ctx.try_with_tx(|tx| {
let mut s = String::new();
for a in tx.db.profiles().iter() {
s.push_str(&format!("{} ... {}\n", a.identity, a.username));
}
s.push_str(&format!("sender: {}", tx.sender()));
return Ok(s);
});
return Response::new(Body::from_bytes(match res {
Ok(body) => body,
Err(e) => e,
}));
}
#[spacetimedb::http::router]
fn router() -> Router {
Router::new()
.get("/test", test_handler)
}

View File

@@ -0,0 +1,73 @@
use mize::prelude::*;
pub struct CliPart {
id: MizeId,
}
impl MizePart for CliPart {
fn id(&self) -> MizeResult<MizeId> {
Ok(self.id.clone())
}
}
impl CliPart {
pub fn get(name: &str) -> MizeResult<CliPart> {
let mut mize = get_global_mize()?;
let part = mize.get_part(name)?;
Ok(CliPart { id: part.id()? })
}
pub fn set_command(&self, name: &str) -> MizeResult<()> {
let mut args = ItemData::new();
args.set_path("0", name.into_item_data())?;
dyn_call(self.id.clone(), "set_command", args)?;
Ok(())
}
pub fn subcommand<T: Fn(ArgMatches) -> MizeResult<()> + Send + Sync + 'static>(
&self,
subcmd: Command,
action: T,
) -> MizeResult<()> {
let mut subcmd_arg = ItemData::new();
subcmd_arg.set_path("_native", subcmd.native_id)?;
let action_arg = ItemData::fun(Box::new(move |args: ItemData| {
let matches = args.clone().get_path("0")?.native_take_from::<ArgMatches>()?;
action(matches)?;
Ok(ItemData::new())
}))?;
let mut args = ItemData::new();
args.set_path("0", subcmd_arg)?;
args.set_path("1", action_arg)?;
dyn_call(self.id.clone(), "subcommand", args)?;
Ok(())
}
}
pub struct Command {
native_id: u32,
}
pub struct ArgMatches {
native_id: u32,
}
impl FromItemData for ArgMatches {
fn from_itemdata(data: ItemData) -> MizeResult<Self> {
Ok(ArgMatches {
native_id: data.get_path("_native")?.value_u32()?,
})
}
}
impl Command {
pub fn new(name: &str) -> MizeResult<Command> {
let mut args = ItemData::new();
args.set_path("0", name.into_item_data())?;
let mut mize = get_global_mize()?;
let id = mize.id_new_dots("cli.Command")?;
let data = dyn_call(id, "new", args)?;
let native_id: u32 = data.get_path("_native")?.native_take()?;
Ok(Command { native_id })
}
}

37
parts/sci/src/lib.rs Normal file
View File

@@ -0,0 +1,37 @@
mod generated;
use generated::{ArgMatches, CliPart, Command};
use mize::prelude::*;
#[unsafe(no_mangle)]
pub extern "C" fn part_sci() -> MizeResult<()> {
load_part_at("priv.spacetime", "spacetime")?;
let cli = CliPart::get("cli")?;
cli.set_command("sci")?;
cli.subcommand(Command::new("inputThought")?, input_thought)?;
cli.subcommand(Command::new("ti")?, input_thought)?;
Ok(())
}
fn input_thought(_matches: ArgMatches) -> MizeResult<()> {
let tmp_file = std::env::temp_dir().join(format!(
"sc2-input-thought-{}.txt",
chrono::Utc::now().timestamp()
));
let status = std::process::Command::new("nvim")
.arg(&tmp_file)
.status()
.map_err(|e| mize_err!("failed to spawn nvim: {}", e))?;
if !status.success() {
return Err(mize_err!("nvim exited with {}", status));
}
let _content = std::fs::read_to_string(&tmp_file)
.map_err(|e| mize_err!("Failed to read temp file: {}", e))?;
let _ = std::fs::remove_file(&tmp_file);
// spacetime SDK not wired yet — reducer add_thought comes later
Err(mize_err!("spacetime SDK not wired yet (add_thought)"))
}

View File

@@ -0,0 +1,19 @@
cargo-features = ["profile-rustflags"]
[package]
name = "mize-part-spacetime"
version = "0.1.0"
edition = "2024"
[lib]
name = "mize_part_spacetime"
crate-type = ["cdylib", "rlib"]
[dependencies]
mize = { workspace = true }
[profile.dev]
rustflags = ["-C", "prefer-dynamic", "-C", "link-args=-Wl,--export-dynamic"]
[profile.release]
rustflags = ["-C", "prefer-dynamic"]

View File

@@ -0,0 +1,9 @@
use mize::prelude::*;
/// Stub SpacetimeDB client part.
/// SDK wiring (connect with auth.client.get_id_token) comes later; dependents may fail until then.
#[unsafe(no_mangle)]
pub extern "C" fn part_spacetime() -> MizeResult<()> {
opt("url");
Ok(())
}