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, } #[table(accessor = activities, public)] pub struct Activity { #[primary_key] #[auto_inc] id: u64, name: String, task: Option, parents_ids: Vec, } #[table(accessor = tasks, public)] pub struct Task { #[primary_key] #[auto_inc] id: u64, done: bool, name: String, parent: Option, } #[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, task: Option, start: Option, end: Option, duration: Option, } 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) { 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) -> 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 { 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::(time.format("%Y-%m-%d").to_string()) })?; Ok(timestamp) } #[procedure] fn export_timetrack(ctx: &mut ProcedureContext, username: String, date: String) -> Result { // 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 = 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) -> Response { 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) }