port habitica integration from JS to Rust

The old JS ppc repo had a working habitica integration. This ports all
that functionality into the new Rust-based marts package so it can run
natively without the Deno/JS runtime dependency.

Changes:
- habitica.rs: make api_request public, add get_tasks() and delete_task()
  convenience methods, register config opts
- c2vi/mod.rs: full rewrite implementing all 9 CLI commands (bed,
  clearTodos, listTodos, buyHealthPotion, addActionItems, skip, dumpLog,
  listDailies, printTask) with SQLite persistence via rusqlite
- lib.rs: wire c2vi module into marts exports
- ppc main.rs: register c2vi part at startup
- Cargo.toml: add rusqlite dependency with bundled feature

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Raphael 'kena' Poss
2026-03-09 14:22:08 +01:00
parent 0dc09e7618
commit d7a0780095
6 changed files with 610 additions and 168 deletions

47
Cargo.lock generated
View File

@@ -2728,6 +2728,18 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "fallible-iterator"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
[[package]]
name = "fallible-streaming-iterator"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "2.3.0" version = "2.3.0"
@@ -3483,6 +3495,15 @@ dependencies = [
"foldhash", "foldhash",
] ]
[[package]]
name = "hashlink"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
dependencies = [
"hashbrown 0.14.5",
]
[[package]] [[package]]
name = "headers" name = "headers"
version = "0.3.9" version = "0.3.9"
@@ -4260,6 +4281,17 @@ dependencies = [
"redox_syscall 0.7.0", "redox_syscall 0.7.0",
] ]
[[package]]
name = "libsqlite3-sys"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f"
dependencies = [
"cc",
"pkg-config",
"vcpkg",
]
[[package]] [[package]]
name = "libxdo" name = "libxdo"
version = "0.6.0" version = "0.6.0"
@@ -4430,6 +4462,7 @@ dependencies = [
"futures", "futures",
"mize", "mize",
"reqwest 0.13.2", "reqwest 0.13.2",
"rusqlite",
"rustyscript", "rustyscript",
"serde", "serde",
"serde_json", "serde_json",
@@ -6036,6 +6069,20 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "rusqlite"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae"
dependencies = [
"bitflags 2.11.0",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
"libsqlite3-sys",
"smallvec",
]
[[package]] [[package]]
name = "rustc-hash" name = "rustc-hash"
version = "1.1.0" version = "1.1.0"

View File

@@ -17,10 +17,11 @@ chrono = "0.4.44"
reqwest = { version = "0.13.2", features = ["blocking", "json"] } reqwest = { version = "0.13.2", features = ["blocking", "json"] }
serde = "1.0.228" serde = "1.0.228"
serde_json = "1.0.149" serde_json = "1.0.149"
rusqlite = { version = "0.31", features = ["bundled"], optional = true }
[features] [features]
default = ["target-os"] default = ["target-os"]
target-os = [ "clap", "mize/target-os", "deno_core", "rustyscript" ] target-os = [ "clap", "mize/target-os", "deno_core", "rustyscript", "rusqlite" ]
target-wasm = [ "mize/target-wasm" ] target-wasm = [ "mize/target-wasm" ]
clap = ["dep:clap"] clap = ["dep:clap"]

View File

@@ -1,125 +1,66 @@
use chrono::{Datelike, Timelike, Utc}; use std::sync::Mutex;
use chrono::{Timelike, Utc};
use clap::{Arg, Command}; use clap::{Arg, Command};
use rusqlite::{params, Connection, Result}; use rusqlite::{params, Connection};
use serde::{Deserialize, Serialize}; use serde_json::{json, Value};
use std::env;
use std::fs;
use std::path::PathBuf;
use std::process::Command as StdCommand;
// Assuming PPC and other modules are structured like this. use mize::{mize_err, mize_part, Mize, MizeError, MizePart, MizeResult};
// This might need adjustment based on the actual definitions in other files.
use crate::habitica::Habitica; // Assuming this exists
use crate::cli::CliPart;
use crate::habitica::Habitica;
#[mize_part]
#[derive(Default)]
pub struct C2vi { pub struct C2vi {
pub db: Connection, mize: Mize,
db: Mutex<Option<Connection>>,
} }
impl C2vi { impl MizePart for C2vi {
pub fn new(local_storage_path: &str) -> Result<Self> { fn opts(&self, mize: &mut Mize) {
let db_path = PathBuf::from(local_storage_path).join("data.db"); mize.new_opt("c2vi.local_storage_path");
let conn = Connection::open(db_path)?;
Ok(C2vi { db: conn })
} }
} }
pub fn extend_cli_c2vi(mut cli: Command) -> Command { impl C2vi {
cli.subcommand( fn with_db<F, R>(&self, f: F) -> MizeResult<R>
Command::new("bed") where
.about("Command to run when c2vi goes to bed") F: FnOnce(&Connection) -> MizeResult<R>,
.action(async || { {
// Placeholder for actual PPC context let guard = self
// await save_habitica_dailies(ppc); .db
// await save_habitica_logs(ppc); .lock()
}), .map_err(|e| mize_err!("DB lock poisoned: {}", e))?;
) let conn = guard
.subcommand( .as_ref()
Command::new("clearTodos") .ok_or_else(|| mize_err!("DB not initialized"))?;
.about("Clear all todos in Habitica") f(conn)
.alias("clt") }
.action(async || {
// Placeholder
}),
)
.subcommand(
Command::new("listTodos")
.about("List all todos in Habitica")
.alias("dut")
.action(async || {
// Placeholder
}),
)
.subcommand(
Command::new("buyHealthPotion")
.about("Buy a health potion in Habitica")
.alias("buyh")
.action(async || {
// Placeholder
}),
)
.subcommand(
Command::new("addActionItems")
.about("Add action items from a temp file")
.alias("aal")
.action(async || {
// add_action_items();
}),
)
.subcommand(
Command::new("skip")
.about("Skip a Habitica todo")
.alias("sk")
.arg(Arg::new("num").default_value("1"))
.action(async || {
// let num = matches.value_of("num").unwrap().parse::<i32>().unwrap();
// skip_habitica_todo(ppc, num);
}),
)
.subcommand(
Command::new("dumpLog")
.about("Dump the complete Habitica log")
.action(async || {
// dump_habitica_logs(ppc);
}),
)
.subcommand(
Command::new("listDailies")
.about("List all dailies from the local db")
.action(async || {
// Placeholder
}),
)
.subcommand(
Command::new("printTask")
.about("Print a specific todo from Habitica")
.arg(Arg::new("id").required(true))
.action(async || {
// let id = matches.value_of("id").unwrap();
// Placeholder
}),
)
} }
fn format_date(date: chrono::DateTime<Utc>) -> String { pub fn c2vi(mize: &mut Mize) -> MizeResult<()> {
date.format("%Y-%m-%d").to_string() let storage_path = mize.get_config("c2vi.local_storage_path")?.to_string();
let db_path = std::path::PathBuf::from(&storage_path).join("data.db");
let conn = Connection::open(&db_path)
.map_err(|e| mize_err!("Failed to open C2vi database at {:?}: {}", db_path, e))?;
setup_tables(&conn)?;
let c2vi_part = C2vi {
mize: mize.clone(),
db: Mutex::new(Some(conn)),
};
mize.register_part(Box::new(c2vi_part))?;
// Register CLI subcommands
let mut cli = mize.get_part_native::<CliPart>("cli")?;
register_subcommands(&mut cli);
Ok(())
} }
#[derive(Serialize, Deserialize)] fn setup_tables(conn: &Connection) -> MizeResult<()> {
struct HabiticaDaily {
id: String,
text: String,
notes: String,
priority: Option<String>,
frequency: String,
}
#[derive(Serialize, Deserialize)]
struct HabiticaLog {
date: String,
data: String, // JSON string
}
fn setup_tables(conn: &Connection) -> Result<()> {
conn.execute( conn.execute(
"CREATE TABLE IF NOT EXISTS habitica_dailies ( "CREATE TABLE IF NOT EXISTS habitica_dailies (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
@@ -129,97 +70,525 @@ fn setup_tables(conn: &Connection) -> Result<()> {
frequency TEXT frequency TEXT
)", )",
[], [],
)?; )
.map_err(|e| mize_err!("DB error creating habitica_dailies: {}", e))?;
conn.execute( conn.execute(
"CREATE TABLE IF NOT EXISTS habitica_log ( "CREATE TABLE IF NOT EXISTS habitica_log (
date TEXT PRIMARY KEY, date TEXT PRIMARY KEY,
data TEXT NOT NULL data TEXT NOT NULL
)", )",
[], [],
)?; )
.map_err(|e| mize_err!("DB error creating habitica_log: {}", e))?;
Ok(()) Ok(())
} }
// In-progress translation. Many functions will need the PPC context passed in. fn register_subcommands(cli: &mut CliPart) {
cli.subcommand(
Command::new("bed").about("Command to run when c2vi goes to bed"),
|_matches, mut mize| {
save_habitica_dailies(&mut mize)?;
save_habitica_logs(&mut mize)?;
Ok(())
},
);
/* cli.subcommand(
async fn save_habitica_dailies(ppc: &PPC) -> Result<()> { Command::new("clearTodos")
println!("######### cli bed cmd #########"); .about("Clear all Habitica todos")
setup_tables(&ppc.c2vi.db)?; .alias("clt"),
|_matches, mut mize| {
let todos = {
let mut hab = mize.get_part_native::<Habitica>("habitica")?;
hab.get_tasks("todos")?
};
let mut hab = mize.get_part_native::<Habitica>("habitica")?;
if let Some(arr) = todos.as_array() {
for todo in arr {
if let Some(id) = todo["id"].as_str() {
hab.delete_task(id)?;
}
}
}
Ok(())
},
);
// Assuming ppc.habitica.get_tasks returns a Result cli.subcommand(
let dailies = ppc.habitica.get_tasks("dailys").await?; Command::new("listTodos")
write_dailies_to_database(&dailies, &ppc.c2vi.db).await?; .about("List all Habitica todos")
Ok(()) .alias("dut"),
|_matches, mut mize| {
let mut hab = mize.get_part_native::<Habitica>("habitica")?;
let todos = hab.get_tasks("todos")?;
if let Some(arr) = todos.as_array() {
for todo in arr {
if let Some(text) = todo["text"].as_str() {
println!("{}", text);
}
}
}
Ok(())
},
);
cli.subcommand(
Command::new("buyHealthPotion")
.about("Buy a health potion")
.alias("buyh"),
|_matches, mut mize| {
let mut hab = mize.get_part_native::<Habitica>("habitica")?;
hab.api_request(
reqwest::Method::POST,
"user/buy-health-potion".to_string(),
json!({}),
)?;
Ok(())
},
);
cli.subcommand(
Command::new("addActionItems")
.about("Add action items from temp file")
.alias("aal"),
|_matches, mut mize| add_action_items(&mut mize),
);
cli.subcommand(
Command::new("skip")
.about("Skip a Habitica todo")
.alias("sk")
.arg(Arg::new("num").default_value("1")),
|matches, mut mize| {
let num: usize = matches
.get_one::<String>("num")
.unwrap()
.parse()
.unwrap_or(1);
skip_habitica_todo(&mut mize, num)
},
);
cli.subcommand(
Command::new("dumpLog").about("Dump the complete Habitica log"),
|_matches, mut mize| dump_habitica_logs(&mut mize),
);
cli.subcommand(
Command::new("listDailies").about("List all dailies from local DB"),
|_matches, mut mize| {
let c2vi = mize.get_part_native::<C2vi>("c2vi")?;
c2vi.with_db(|conn| {
let mut stmt = conn
.prepare("SELECT id, text FROM habitica_dailies")
.map_err(|e| mize_err!("DB error: {}", e))?;
let rows = stmt
.query_map([], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
))
})
.map_err(|e| mize_err!("DB error: {}", e))?;
for row in rows {
let (id, text) = row.map_err(|e| mize_err!("DB error: {}", e))?;
println!("{}: {}", id, text);
}
Ok(())
})
},
);
cli.subcommand(
Command::new("printTask")
.about("Print a specific task from Habitica")
.arg(Arg::new("id").required(true)),
|matches, mut mize| {
let id = matches.get_one::<String>("id").unwrap();
let mut hab = mize.get_part_native::<Habitica>("habitica")?;
let data = hab.api_request(
reqwest::Method::GET,
format!("tasks/{}", id),
json!({}),
)?;
if let Some(history) = data.get("history") {
println!("{}", serde_json::to_string_pretty(history).unwrap_or_default());
} else {
println!("{}", serde_json::to_string_pretty(&data).unwrap_or_default());
}
Ok(())
},
);
} }
async fn write_dailies_to_database(dailies: &[HabiticaTask], db: &Connection) -> Result<()> { // ============= Business Logic =============
println!("Syncing {} dailies to database...", dailies.len());
let tx = db.transaction()?; fn format_date(dt: chrono::DateTime<Utc>) -> String {
for daily in dailies { dt.format("%Y-%m-%d").to_string()
tx.execute( }
"INSERT INTO habitica_dailies (id, text, notes, priority, frequency)
VALUES (?1, ?2, ?3, ?4, ?5) /// Get the target date, adjusted for "save for yesterday" logic (before noon = yesterday)
ON CONFLICT(id) DO UPDATE SET fn get_target_date() -> (String, bool) {
text=excluded.text, let now = Utc::now();
notes=excluded.notes, let save_for_yesterday = now.hour() < 12;
priority=excluded.priority, let date = if save_for_yesterday {
frequency=excluded.frequency", now - chrono::Duration::days(1)
params![ } else {
daily.id, now
daily.text, };
daily.notes.as_deref().unwrap_or(""), if save_for_yesterday {
daily.priority.map(|p| p.to_string()), println!(
daily.frequency.as_deref().unwrap_or("daily"), "Saving logs for yesterday because it is {} hours of the day",
], now.hour()
)?; );
} }
tx.commit() (format_date(date), save_for_yesterday)
} }
async fn add_action_items(ppc: &PPC) -> Result<()> { /// Load existing log entry from DB, or return a default empty one
let initial_content = " fn get_or_create_log(conn: &Connection, date: &str) -> MizeResult<Value> {
let mut stmt = conn
.prepare("SELECT data FROM habitica_log WHERE date = ?1")
.map_err(|e| mize_err!("DB error: {}", e))?;
let result: Option<String> = stmt
.query_row(params![date], |row| row.get(0))
.ok();
match result {
Some(data_str) => {
serde_json::from_str(&data_str).map_err(|e| mize_err!("JSON parse error: {}", e))
}
None => Ok(json!({
"dailies_done": [],
"dailies_skipped": [],
"todos_done": [],
"todos_skipped": []
})),
}
}
/// Upsert a log entry into the database
fn upsert_log(conn: &Connection, date: &str, data: &Value) -> MizeResult<()> {
let data_str =
serde_json::to_string(data).map_err(|e| mize_err!("JSON serialize error: {}", e))?;
conn.execute(
"INSERT INTO habitica_log (date, data) VALUES (?1, ?2)
ON CONFLICT(date) DO UPDATE SET data=excluded.data",
params![date, data_str],
)
.map_err(|e| mize_err!("DB error upserting log: {}", e))?;
Ok(())
}
fn save_habitica_dailies(mize: &mut Mize) -> MizeResult<()> {
println!("######### cli bed cmd #########");
// Fetch dailies from Habitica
let dailies = {
let mut hab = mize.get_part_native::<Habitica>("habitica")?;
hab.get_tasks("dailys")?
};
// Write to database
let c2vi = mize.get_part_native::<C2vi>("c2vi")?;
c2vi.with_db(|conn| {
if let Some(arr) = dailies.as_array() {
println!("Syncing {} dailies to database...", arr.len());
for daily in arr {
let id = daily["id"].as_str().unwrap_or("");
let text = daily["text"].as_str().unwrap_or("");
let notes = daily["notes"].as_str().unwrap_or("");
let priority = daily["priority"].to_string();
let frequency = daily["frequency"].as_str().unwrap_or("daily");
conn.execute(
"INSERT INTO habitica_dailies (id, text, notes, priority, frequency)
VALUES (?1, ?2, ?3, ?4, ?5)
ON CONFLICT(id) DO UPDATE SET
text=excluded.text,
notes=excluded.notes,
priority=excluded.priority,
frequency=excluded.frequency",
params![id, text, notes, priority, frequency],
)
.map_err(|e| mize_err!("DB error writing daily: {}", e))?;
}
}
Ok(())
})
}
fn save_habitica_logs(mize: &mut Mize) -> MizeResult<()> {
let (date_str, save_for_yesterday) = get_target_date();
// Fetch dailies and completed todos from Habitica
let (dailies, completed_todos) = {
let mut hab = mize.get_part_native::<Habitica>("habitica")?;
let dailies = hab.get_tasks("dailys")?;
let completed_todos = hab.get_tasks("completedTodos")?;
(dailies, completed_todos)
};
let c2vi = mize.get_part_native::<C2vi>("c2vi")?;
c2vi.with_db(|conn| {
let mut data = get_or_create_log(conn, &date_str)?;
// Process dailies
if let Some(arr) = dailies.as_array() {
for daily in arr {
let is_due = if save_for_yesterday {
// Look at history for yesterday's status
daily["history"]
.as_array()
.and_then(|h| h.iter().max_by_key(|e| e["date"].as_i64().unwrap_or(0)))
.map(|e| e["isDue"].as_bool().unwrap_or(false))
.unwrap_or(false)
} else {
daily["isDue"].as_bool().unwrap_or(false)
};
if !is_due {
continue;
}
let completed = if save_for_yesterday {
daily["history"]
.as_array()
.and_then(|h| h.iter().max_by_key(|e| e["date"].as_i64().unwrap_or(0)))
.map(|e| e["completed"].as_bool().unwrap_or(false))
.unwrap_or(false)
} else {
daily["completed"].as_bool().unwrap_or(false)
};
let daily_id = daily["id"].as_str().unwrap_or("");
let daily_text = daily["text"].as_str().unwrap_or("");
if completed {
data["dailies_done"]
.as_array_mut()
.unwrap()
.push(json!(daily_id));
println!("daily-done: {}", daily_text);
} else {
data["dailies_skipped"]
.as_array_mut()
.unwrap()
.push(json!(daily_id));
println!("daily-skipped: {}", daily_text);
}
}
}
// Process completed todos
if let Some(arr) = completed_todos.as_array() {
let today_str = format_date(Utc::now());
for todo in arr {
let date_completed_str = todo["dateCompleted"].as_str().unwrap_or("");
// Parse the completion date (ISO 8601 format from Habitica)
let date_completed = chrono::DateTime::parse_from_rfc3339(date_completed_str)
.map(|d| format_date(d.with_timezone(&Utc)))
.unwrap_or_default();
// Check if already in the log for this date
if save_for_yesterday {
let existing_todos = data["todos_done"].as_array().unwrap();
let todo_id = todo["id"].as_str().unwrap_or("");
if existing_todos.iter().any(|t| t["id"].as_str() == Some(todo_id)) {
continue;
}
}
let todo_entry = json!({
"id": todo["id"],
"text": todo["text"],
"notes": todo.get("notes").unwrap_or(&json!("")),
"checklist": todo.get("checklist").unwrap_or(&json!([])),
"tags": todo.get("tags").unwrap_or(&json!([])),
});
// Save if completed on the target date
if date_completed == date_str {
println!("todo-done: {}", todo["text"].as_str().unwrap_or(""));
data["todos_done"]
.as_array_mut()
.unwrap()
.push(todo_entry.clone());
}
// If saving for yesterday but todo was completed today, also record it
if save_for_yesterday && date_completed == today_str {
println!("todo-done: {}", todo["text"].as_str().unwrap_or(""));
data["todos_done"]
.as_array_mut()
.unwrap()
.push(todo_entry);
}
}
}
upsert_log(conn, &date_str, &data)?;
Ok(())
})
}
fn skip_habitica_todo(mize: &mut Mize, num: usize) -> MizeResult<()> {
let (date_str, _) = get_target_date();
// Fetch todos from Habitica
let todos = {
let mut hab = mize.get_part_native::<Habitica>("habitica")?;
hab.get_tasks("todos")?
};
let arr = todos.as_array().ok_or_else(|| mize_err!("Expected array of todos"))?;
// Delete and log each skipped todo
for i in 0..num {
let todo = match arr.get(i) {
Some(t) => t,
None => continue,
};
let todo_id = todo["id"].as_str().unwrap_or("");
// Delete the todo and score the skip habit
{
let mut hab = mize.get_part_native::<Habitica>("habitica")?;
hab.delete_task(todo_id)?;
}
increment_habit(mize)?;
// Record in log
let c2vi = mize.get_part_native::<C2vi>("c2vi")?;
c2vi.with_db(|conn| {
let mut data = get_or_create_log(conn, &date_str)?;
data["todos_skipped"].as_array_mut().unwrap().push(json!({
"id": todo["id"],
"text": todo["text"],
"notes": todo.get("notes").unwrap_or(&json!("")),
"checklist": todo.get("checklist").unwrap_or(&json!([])),
"tags": todo.get("tags").unwrap_or(&json!([])),
}));
upsert_log(conn, &date_str, &data)?;
Ok(())
})?;
}
Ok(())
}
fn increment_habit(mize: &mut Mize) -> MizeResult<()> {
let mut hab = mize.get_part_native::<Habitica>("habitica")?;
let habits = hab.get_tasks("habits")?;
if let Some(arr) = habits.as_array() {
for habit in arr {
if habit["text"].as_str() == Some("skipped a planned task") {
let id = habit["id"].as_str().unwrap_or("");
hab.api_request(
reqwest::Method::POST,
format!("tasks/{}/score/down", id),
json!({}),
)?;
return Ok(());
}
}
}
Ok(())
}
fn add_action_items(mize: &mut Mize) -> MizeResult<()> {
let initial_content = "\
gu 10min TIMER gu 10min TIMER
uw 15min FROG uw 15min FROG
b coffee/tee/kakau b coffee/tee/kakau
uw FROG uw FROG
b breakfast, brush teeth, lüften b breakfast, brush teeth, lüften
uw FROG uw FROG
lunch lunch
uw 15min TIMER of next days FROG uw 15min TIMER of next days FROG
fw fw
dt 30min TIMER dt 30min TIMER
fun 20:00 (smth that can be stopped easily) fun 20:00 (smth that can be stopped easily)
se 21:40 se 21:40
read read
bed 22:10 bed 22:10";
";
let tmp_file = env::temp_dir().join(format!("habitica-tasks-{}.md", Utc::now().timestamp()));
fs::write(&tmp_file, initial_content.trim())?;
StdCommand::new("nvim").arg(&tmp_file).status()?; let tmp_file = std::env::temp_dir().join(format!(
"habitica-tasks-{}.md",
Utc::now().timestamp()
));
std::fs::write(&tmp_file, initial_content)
.map_err(|e| mize_err!("Failed to write temp file: {}", e))?;
let list_str = fs::read_to_string(&tmp_file)?; // Open in nvim for editing
fs::remove_file(&tmp_file)?; std::process::Command::new("nvim")
.arg(&tmp_file)
.status()
.map_err(|e| mize_err!("Failed to run nvim: {}", e))?;
let tasks: Vec<&str> = list_str.lines().map(|t| t.trim()).filter(|t| !t.is_empty()).collect(); // Read back and parse
let mut task_ids = vec![]; let list_str =
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);
for todo_text in tasks { let tasks: Vec<&str> = list_str
// Assuming api_request exists and works like this .lines()
let data = ppc.habitica.api_request("POST", "tasks/user", Some(serde_json::json!({ "text": todo_text, "type": "todo" }))).await?; .map(|l| l.trim())
let task_id = data["id"].as_str().unwrap().to_string(); .filter(|l| !l.is_empty())
println!("Added to-do: '{}' with id '{}'", todo_text, task_id); .collect();
task_ids.push(task_id);
}
for task_id in task_ids { // Add tasks to Habitica
ppc.habitica.api_request("POST", &format!("tasks/{}/move/to/-1", task_id), None).await?; let mut task_ids = Vec::new();
{
let mut hab = mize.get_part_native::<Habitica>("habitica")?;
for todo_text in &tasks {
let data = hab.api_request(
reqwest::Method::POST,
"tasks/user".to_string(),
json!({ "text": todo_text, "type": "todo" }),
)?;
let id = data["id"].as_str().unwrap_or("").to_string();
println!("Added to-do: '{}' with id '{}'", todo_text, id);
task_ids.push(id);
}
// Move all tasks to bottom
for task_id in &task_ids {
hab.api_request(
reqwest::Method::POST,
format!("tasks/{}/move/to/-1", task_id),
json!({}),
)?;
}
} }
Ok(()) Ok(())
} }
*/
fn dump_habitica_logs(mize: &mut Mize) -> MizeResult<()> {
let c2vi = mize.get_part_native::<C2vi>("c2vi")?;
c2vi.with_db(|conn| {
let mut stmt = conn
.prepare("SELECT date, data FROM habitica_log ORDER BY date")
.map_err(|e| mize_err!("DB error: {}", e))?;
let rows = stmt
.query_map([], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
))
})
.map_err(|e| mize_err!("DB error: {}", e))?;
for row in rows {
let (date, data_str) = row.map_err(|e| mize_err!("DB error: {}", e))?;
let data: Value =
serde_json::from_str(&data_str).unwrap_or(json!({"parse_error": data_str}));
println!("{}: {}", date, serde_json::to_string_pretty(&data).unwrap_or_default());
}
Ok(())
})
}

View File

@@ -20,10 +20,17 @@ pub fn habitica(mize: &mut Mize) -> MizeResult<()> {
})) }))
} }
impl MizePart for Habitica {} impl MizePart for Habitica {
fn opts(&self, mize: &mut Mize) {
mize.new_opt("habitica.api_url");
mize.new_opt("habitica.user_id");
mize.new_opt("habitica.api_token");
mize.new_opt("habitica.client_name");
}
}
impl Habitica { impl Habitica {
fn api_request(&mut self, method: Method, path: String, data: Value) -> MizeResult<Value> { pub fn api_request(&mut self, method: Method, path: String, data: Value) -> MizeResult<Value> {
let api_url = self.mize.get_config("habitica.api_url")?.to_string(); let api_url = self.mize.get_config("habitica.api_url")?.to_string();
let user_id = self.mize.get_config("habitica.user_id")?.to_string(); let user_id = self.mize.get_config("habitica.user_id")?.to_string();
let api_token = self.mize.get_config("habitica.api_token")?.to_string(); let api_token = self.mize.get_config("habitica.api_token")?.to_string();
@@ -65,6 +72,18 @@ impl Habitica {
let json_response: Value = response.json()?; let json_response: Value = response.json()?;
Ok(json_response.get("data").cloned().unwrap_or(Value::Null)) Ok(json_response.get("data").cloned().unwrap_or(Value::Null))
} }
pub fn get_tasks(&mut self, task_type: &str) -> MizeResult<Value> {
self.api_request(
Method::GET,
format!("tasks/user?type={}", task_type),
json!({}),
)
}
pub fn delete_task(&mut self, id: &str) -> MizeResult<Value> {
self.api_request(Method::DELETE, format!("tasks/{}", id), json!({}))
}
} }
fn handle_rate_limit(response: &Response) { fn handle_rate_limit(response: &Response) {

View File

@@ -10,5 +10,10 @@ pub mod habitica;
#[cfg(feature = "target-os")] #[cfg(feature = "target-os")]
pub use habitica::*; pub use habitica::*;
#[cfg(feature = "target-os")]
pub mod c2vi;
#[cfg(feature = "target-os")]
pub use c2vi::*;
pub mod js; pub mod js;
pub use js::*; pub use js::*;

View File

@@ -18,6 +18,7 @@ fn os_main(mize: &mut Mize) -> MizeResult<()> {
marts::cli(mize)?; marts::cli(mize)?;
//marts::js(mize)?; //marts::js(mize)?;
marts::habitica(mize)?; marts::habitica(mize)?;
marts::c2vi(mize)?;
let mut cli = mize.get_part_native::<marts::CliPart>("cli")?; let mut cli = mize.get_part_native::<marts::CliPart>("cli")?;