updated README and a bunch of other stuff
This commit is contained in:
20
parts/sci/Cargo.toml
Normal file
20
parts/sci/Cargo.toml
Normal 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
2
parts/sci/part
Normal file
@@ -0,0 +1,2 @@
|
||||
id = "sci"
|
||||
gen = [ "cli", "sci", "spacetime" ]
|
||||
1119
parts/sci/spacetime/Cargo.lock
generated
Normal file
1119
parts/sci/spacetime/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
parts/sci/spacetime/Cargo.toml
Normal file
17
parts/sci/spacetime/Cargo.toml
Normal 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"
|
||||
394
parts/sci/spacetime/src/lib.rs
Normal file
394
parts/sci/spacetime/src/lib.rs
Normal 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)
|
||||
}
|
||||
73
parts/sci/src/generated.rs
Normal file
73
parts/sci/src/generated.rs
Normal 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
37
parts/sci/src/lib.rs
Normal 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)"))
|
||||
}
|
||||
Reference in New Issue
Block a user