idk. but added AGENTS.md
This commit is contained in:
@@ -5,18 +5,24 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.59", optional = true }
|
||||
deno_core = { version = "0.385.0", optional = true }
|
||||
clap = { version = "4.5.59", optional = true, features = ["string"] }
|
||||
flume = "0.12.0"
|
||||
futures = "0.3.32"
|
||||
mize = { path = "../mize/", default-features = false }
|
||||
rustyscript = "0.12.3"
|
||||
tokio = "1.49.0"
|
||||
# https://github.com/rscarson/rustyscript/pull/415
|
||||
rustyscript = { git = 'https://github.com/rscarson/rustyscript', rev = 'e39529eac2c5d37cfcbb7d7d63da7264d7a5afdb', optional = true }
|
||||
deno_core = { version = "0.380.1", optional = true }
|
||||
chrono = "0.4.44"
|
||||
reqwest = { version = "0.13.2", features = ["blocking", "json"] }
|
||||
serde = "1.0.228"
|
||||
serde_json = "1.0.149"
|
||||
|
||||
[features]
|
||||
default = ["target-os"]
|
||||
target-os = [ "clap", "mize/target-os", "deno_core" ]
|
||||
target-os = [ "clap", "mize/target-os", "deno_core", "rustyscript" ]
|
||||
target-wasm = [ "mize/target-wasm" ]
|
||||
clap = ["dep:clap"]
|
||||
|
||||
[lib]
|
||||
path = "lib.rs"
|
||||
|
||||
225
packages/marts/c2vi/mod.rs
Normal file
225
packages/marts/c2vi/mod.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
use chrono::{Datelike, Timelike, Utc};
|
||||
use clap::{Arg, Command};
|
||||
use rusqlite::{params, Connection, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
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.
|
||||
// This might need adjustment based on the actual definitions in other files.
|
||||
use crate::habitica::Habitica; // Assuming this exists
|
||||
|
||||
pub struct C2vi {
|
||||
pub db: Connection,
|
||||
}
|
||||
|
||||
impl C2vi {
|
||||
pub fn new(local_storage_path: &str) -> Result<Self> {
|
||||
let db_path = PathBuf::from(local_storage_path).join("data.db");
|
||||
let conn = Connection::open(db_path)?;
|
||||
Ok(C2vi { db: conn })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extend_cli_c2vi(mut cli: Command) -> Command {
|
||||
cli.subcommand(
|
||||
Command::new("bed")
|
||||
.about("Command to run when c2vi goes to bed")
|
||||
.action(async || {
|
||||
// Placeholder for actual PPC context
|
||||
// await save_habitica_dailies(ppc);
|
||||
// await save_habitica_logs(ppc);
|
||||
}),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("clearTodos")
|
||||
.about("Clear all todos in Habitica")
|
||||
.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 {
|
||||
date.format("%Y-%m-%d").to_string()
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
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(
|
||||
"CREATE TABLE IF NOT EXISTS habitica_dailies (
|
||||
id TEXT PRIMARY KEY,
|
||||
text TEXT NOT NULL,
|
||||
notes TEXT,
|
||||
priority TEXT,
|
||||
frequency TEXT
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS habitica_log (
|
||||
date TEXT PRIMARY KEY,
|
||||
data TEXT NOT NULL
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// In-progress translation. Many functions will need the PPC context passed in.
|
||||
|
||||
/*
|
||||
async fn save_habitica_dailies(ppc: &PPC) -> Result<()> {
|
||||
println!("######### cli bed cmd #########");
|
||||
setup_tables(&ppc.c2vi.db)?;
|
||||
|
||||
// Assuming ppc.habitica.get_tasks returns a Result
|
||||
let dailies = ppc.habitica.get_tasks("dailys").await?;
|
||||
write_dailies_to_database(&dailies, &ppc.c2vi.db).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn write_dailies_to_database(dailies: &[HabiticaTask], db: &Connection) -> Result<()> {
|
||||
println!("Syncing {} dailies to database...", dailies.len());
|
||||
let tx = db.transaction()?;
|
||||
for daily in dailies {
|
||||
tx.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![
|
||||
daily.id,
|
||||
daily.text,
|
||||
daily.notes.as_deref().unwrap_or(""),
|
||||
daily.priority.map(|p| p.to_string()),
|
||||
daily.frequency.as_deref().unwrap_or("daily"),
|
||||
],
|
||||
)?;
|
||||
}
|
||||
tx.commit()
|
||||
}
|
||||
|
||||
async fn add_action_items(ppc: &PPC) -> Result<()> {
|
||||
let initial_content = "
|
||||
gu 10min TIMER
|
||||
uw 15min FROG
|
||||
b coffee/tee/kakau
|
||||
uw FROG
|
||||
b breakfast, brush teeth, lüften
|
||||
uw FROG
|
||||
|
||||
lunch
|
||||
|
||||
uw 15min TIMER of next days FROG
|
||||
fw
|
||||
dt 30min TIMER
|
||||
|
||||
fun 20:00 (smth that can be stopped easily)
|
||||
se 21:40
|
||||
read
|
||||
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 list_str = fs::read_to_string(&tmp_file)?;
|
||||
fs::remove_file(&tmp_file)?;
|
||||
|
||||
let tasks: Vec<&str> = list_str.lines().map(|t| t.trim()).filter(|t| !t.is_empty()).collect();
|
||||
let mut task_ids = vec![];
|
||||
|
||||
for todo_text in tasks {
|
||||
// Assuming api_request exists and works like this
|
||||
let data = ppc.habitica.api_request("POST", "tasks/user", Some(serde_json::json!({ "text": todo_text, "type": "todo" }))).await?;
|
||||
let task_id = data["id"].as_str().unwrap().to_string();
|
||||
println!("Added to-do: '{}' with id '{}'", todo_text, task_id);
|
||||
task_ids.push(task_id);
|
||||
}
|
||||
|
||||
for task_id in task_ids {
|
||||
ppc.habitica.api_request("POST", &format!("tasks/{}/move/to/-1", task_id), None).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
*/
|
||||
@@ -8,8 +8,12 @@ use mize::{mize_part, Mize, MizePart, MizeResult};
|
||||
pub struct CliPart {
|
||||
mize: Mize,
|
||||
cmd: Option<Command>,
|
||||
actions: HashMap<String, fn(&ArgMatches) -> MizeResult<()>>,
|
||||
parsers: Option<Vec<Box<dyn (FnOnce(Mize, Vec<String>) -> MizeResult<()>) + Send + Sync>>>,
|
||||
actions: HashMap<
|
||||
String,
|
||||
Box<dyn FnOnce(&ArgMatches, Mize) -> MizeResult<()> + Send + Sync + 'static>,
|
||||
>,
|
||||
parsers:
|
||||
Option<Vec<Box<dyn FnOnce(Mize, Vec<String>) -> MizeResult<()> + Send + Sync + 'static>>>,
|
||||
}
|
||||
|
||||
pub fn cli(mize: &mut Mize) -> MizeResult<()> {
|
||||
@@ -37,7 +41,7 @@ impl MizePart for CliPart {
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let action = match self.actions.get(sub_cmd) {
|
||||
let action = match self.actions.remove(sub_cmd) {
|
||||
Some(action) => action,
|
||||
None => {
|
||||
// external parsers
|
||||
@@ -56,7 +60,7 @@ impl MizePart for CliPart {
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
action(matches.subcommand_matches(sub_cmd).unwrap())?;
|
||||
action(matches.subcommand_matches(sub_cmd).unwrap(), mize.clone())?;
|
||||
Ok(())
|
||||
}
|
||||
fn opts(&self, mize: &mut Mize) {
|
||||
@@ -65,14 +69,14 @@ impl MizePart for CliPart {
|
||||
}
|
||||
|
||||
impl CliPart {
|
||||
pub fn subcommand(
|
||||
pub fn subcommand<T: FnOnce(&ArgMatches, Mize) -> MizeResult<()> + Send + Sync + 'static>(
|
||||
&mut self,
|
||||
subcmd: Command,
|
||||
action: fn(sub_matches: &ArgMatches) -> MizeResult<()>,
|
||||
action: T,
|
||||
) -> &mut Command {
|
||||
let name = subcmd.get_name().to_string();
|
||||
let cmd = self.cmd.take().unwrap().subcommand(subcmd);
|
||||
self.actions.insert(name, action);
|
||||
self.actions.insert(name, Box::new(action));
|
||||
self.cmd = Some(cmd);
|
||||
self.cmd.as_mut().unwrap()
|
||||
}
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"stub_deno": "./js/stub_deno.ts"
|
||||
},
|
||||
"tasks": {
|
||||
"build": "deno bundle -o ./deno_dist/habitica.js ./habitica.ts"
|
||||
"build": "deno bundle -o ./deno_dist/habitica.js ./habitica.ts && deno -A ./esbuild.config.mjs"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,75 +1,76 @@
|
||||
// habitica.ts
|
||||
var opts = {
|
||||
"habitica.api_url": {
|
||||
env_var_name: "PPC_HABITICA_API_URL",
|
||||
default_val: "https://habitica.com/api/v3"
|
||||
},
|
||||
"habitica.user_id": {
|
||||
env_var_name: "PPC_HABITICA_USER_ID"
|
||||
},
|
||||
"habitica.client_name": {
|
||||
default_val: "3544a0b8-d71a-46e0-9bb1-6ddbb2abcddb-PPC-Software"
|
||||
},
|
||||
"habitica.api_token": {
|
||||
env_var_name: "PPC_HABITICA_API_TOKEN"
|
||||
(() => {
|
||||
// habitica.ts
|
||||
var opts = {
|
||||
"habitica.api_url": {
|
||||
env_var_name: "PPC_HABITICA_API_URL",
|
||||
default_val: "https://habitica.com/api/v3"
|
||||
},
|
||||
"habitica.user_id": {
|
||||
env_var_name: "PPC_HABITICA_USER_ID"
|
||||
},
|
||||
"habitica.client_name": {
|
||||
default_val: "3544a0b8-d71a-46e0-9bb1-6ddbb2abcddb-PPC-Software"
|
||||
},
|
||||
"habitica.api_token": {
|
||||
env_var_name: "PPC_HABITICA_API_TOKEN"
|
||||
}
|
||||
};
|
||||
async function habitica(mize) {
|
||||
Deno.core.print("hiiiiiiiiiiiiiiiiiiii from habitica");
|
||||
log("hiiiiiiiiiiiiiiiiiiii");
|
||||
mize.add_opts(opts);
|
||||
mize.add_part(new Habitica(mize));
|
||||
}
|
||||
};
|
||||
async function habitica(mize) {
|
||||
console.log("hiiiiiiiiiiiiiiiiiiii");
|
||||
mize.add_opts(opts);
|
||||
mize.add_part(new Habitica(mize));
|
||||
}
|
||||
async function handleRateLimit(response) {
|
||||
const limit = response.headers.get("X-RateLimit-Limit") || "NONE";
|
||||
const remaining = parseInt(response.headers.get("X-RateLimit-Remaining") || "10", 10);
|
||||
const reset = response.headers.get("X-RateLimit-Reset");
|
||||
console.log(`RateLimit: ${limit} | Remaining: ${remaining} | Reset: ${reset}`);
|
||||
if (remaining < 2 && reset) {
|
||||
const resetDate = new Date(reset);
|
||||
const now = /* @__PURE__ */ new Date();
|
||||
const waitMs = resetDate.getTime() - now.getTime() + 1e3;
|
||||
if (waitMs > 0) {
|
||||
console.log(`Waiting ${Math.round(waitMs / 1e3)} secs for next rate limit window...`);
|
||||
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
||||
async function handleRateLimit(response) {
|
||||
const limit = response.headers.get("X-RateLimit-Limit") || "NONE";
|
||||
const remaining = parseInt(response.headers.get("X-RateLimit-Remaining") || "10", 10);
|
||||
const reset = response.headers.get("X-RateLimit-Reset");
|
||||
console.log(`RateLimit: ${limit} | Remaining: ${remaining} | Reset: ${reset}`);
|
||||
if (remaining < 2 && reset) {
|
||||
const resetDate = new Date(reset);
|
||||
const now = /* @__PURE__ */ new Date();
|
||||
const waitMs = resetDate.getTime() - now.getTime() + 1e3;
|
||||
if (waitMs > 0) {
|
||||
console.log(`Waiting ${Math.round(waitMs / 1e3)} secs for next rate limit window...`);
|
||||
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var Habitica = class {
|
||||
mize;
|
||||
constructor(mize) {
|
||||
this.mize = mize;
|
||||
}
|
||||
async get_tasks(type = "todos") {
|
||||
return await this.api_request("GET", `tasks/user?type=${type}`);
|
||||
}
|
||||
async delete_task(id) {
|
||||
await this.api_request("DELETE", `tasks/${id}`);
|
||||
}
|
||||
async api_request(method, path, extraHeaders = {}, data = {}) {
|
||||
const mize = this.mize;
|
||||
if (!path.startsWith("/")) {
|
||||
path = "/" + path;
|
||||
var Habitica = class {
|
||||
mize;
|
||||
constructor(mize) {
|
||||
this.mize = mize;
|
||||
}
|
||||
const headers = Object.assign({
|
||||
"Content-Type": "application/json",
|
||||
"x-api-user": mize.get_config("habitica.user_id"),
|
||||
"x-api-key": mize.get_config("habitica.api_token"),
|
||||
"x-client": mize.get_config("habitica.client_name")
|
||||
}, extraHeaders);
|
||||
const response = await fetch(mize.get_config("habitica.api_url") + path, {
|
||||
method,
|
||||
headers,
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
await handleRateLimit(response);
|
||||
if (!response.ok) {
|
||||
console.log("request_url:", mize.get_config("habitica.api_url") + path);
|
||||
throw new Error(`Habitica API error: ${response.status} ${response.statusText}`);
|
||||
async get_tasks(type = "todos") {
|
||||
return await this.api_request("GET", `tasks/user?type=${type}`);
|
||||
}
|
||||
return (await response.json()).data;
|
||||
}
|
||||
};
|
||||
export {
|
||||
habitica,
|
||||
opts
|
||||
};
|
||||
async delete_task(id) {
|
||||
await this.api_request("DELETE", `tasks/${id}`);
|
||||
}
|
||||
async api_request(method, path, extraHeaders = {}, data = {}) {
|
||||
const mize = this.mize;
|
||||
if (!path.startsWith("/")) {
|
||||
path = "/" + path;
|
||||
}
|
||||
const headers = Object.assign({
|
||||
"Content-Type": "application/json",
|
||||
"x-api-user": mize.get_config("habitica.user_id"),
|
||||
"x-api-key": mize.get_config("habitica.api_token"),
|
||||
"x-client": mize.get_config("habitica.client_name")
|
||||
}, extraHeaders);
|
||||
const response = await fetch(mize.get_config("habitica.api_url") + path, {
|
||||
method,
|
||||
headers,
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
await handleRateLimit(response);
|
||||
if (!response.ok) {
|
||||
console.log("request_url:", mize.get_config("habitica.api_url") + path);
|
||||
throw new Error(`Habitica API error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
return (await response.json()).data;
|
||||
}
|
||||
};
|
||||
habitica(globalThis.mize);
|
||||
Deno.core.print("hiiiiiiiiiiiiiiiiiiii from habitica one");
|
||||
})();
|
||||
|
||||
12
packages/marts/esbuild.config.mjs
Normal file
12
packages/marts/esbuild.config.mjs
Normal file
@@ -0,0 +1,12 @@
|
||||
import * as esbuild from "npm:esbuild";
|
||||
|
||||
import { denoPlugin } from "jsr:@deno/esbuild-plugin";
|
||||
|
||||
await esbuild.build({
|
||||
plugins: [denoPlugin()],
|
||||
entryPoints: ["./habitica.ts"],
|
||||
outfile: "./deno_dist/habitica.js",
|
||||
bundle: true,
|
||||
format: "iife",
|
||||
});
|
||||
esbuild.stop();
|
||||
108
packages/marts/habitica.rs
Normal file
108
packages/marts/habitica.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use mize::{mize_err, mize_part, Mize, MizeError, MizePart, MizeResult};
|
||||
use reqwest::{blocking::Client, blocking::RequestBuilder, blocking::Response, header, Method};
|
||||
use serde_json::{json, Value};
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
#[mize_part]
|
||||
#[derive(Default)]
|
||||
pub struct Habitica {
|
||||
mize: Mize,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
pub fn habitica(mize: &mut Mize) -> MizeResult<()> {
|
||||
let client = Client::new();
|
||||
mize.register_part(Box::new(Habitica {
|
||||
mize: mize.clone(),
|
||||
client,
|
||||
}))
|
||||
}
|
||||
|
||||
impl MizePart for Habitica {}
|
||||
|
||||
impl Habitica {
|
||||
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 user_id = self.mize.get_config("habitica.user_id")?.to_string();
|
||||
let api_token = self.mize.get_config("habitica.api_token")?.to_string();
|
||||
let client_name = self.mize.get_config("habitica.client_name")?.to_string();
|
||||
|
||||
let mut headers = header::HeaderMap::new();
|
||||
headers.insert(
|
||||
"Content-Type",
|
||||
header::HeaderValue::from_static("application/json"),
|
||||
);
|
||||
headers.insert("x-api-user", header::HeaderValue::from_str(&user_id)?);
|
||||
headers.insert("x-api-key", header::HeaderValue::from_str(&api_token)?);
|
||||
headers.insert("x-client", header::HeaderValue::from_str(&client_name)?);
|
||||
|
||||
let url = format!("{}/{}", api_url, path.trim_start_matches('/'));
|
||||
|
||||
let mut request_builder = self.client.request(method, &url).headers(headers);
|
||||
|
||||
if data != json!({}) {
|
||||
request_builder = request_builder.json(&data);
|
||||
}
|
||||
|
||||
let response = request_builder.send()?;
|
||||
|
||||
handle_rate_limit(&response);
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let text = response.text().unwrap_or_default();
|
||||
println!("request_url: {}", url);
|
||||
return Err(mize_err!(
|
||||
"Habitica API error: {} {} - {}",
|
||||
status.as_u16(),
|
||||
status.canonical_reason().unwrap_or(""),
|
||||
text
|
||||
));
|
||||
}
|
||||
|
||||
let json_response: Value = response.json()?;
|
||||
Ok(json_response.get("data").cloned().unwrap_or(Value::Null))
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_rate_limit(response: &Response) {
|
||||
let headers = response.headers();
|
||||
let limit = headers
|
||||
.get("X-RateLimit-Limit")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("NONE");
|
||||
let remaining = headers
|
||||
.get("X-RateLimit-Remaining")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|s| s.parse::<i32>().ok())
|
||||
.unwrap_or(10);
|
||||
let reset = headers
|
||||
.get("X-RateLimit-Reset")
|
||||
.and_then(|v| v.to_str().ok());
|
||||
|
||||
println!(
|
||||
"RateLimit: {} | Remaining: {} | Reset: {}",
|
||||
limit,
|
||||
remaining,
|
||||
reset.unwrap_or("N/A")
|
||||
);
|
||||
|
||||
if remaining < 2 {
|
||||
if let Some(reset_str) = reset {
|
||||
if let Ok(reset_date) = DateTime::parse_from_rfc2822(reset_str) {
|
||||
let now = Utc::now();
|
||||
let wait_duration = reset_date.signed_duration_since(now).to_std();
|
||||
if let Ok(wait_duration) = wait_duration {
|
||||
let wait_ms = wait_duration.as_millis() as u64 + 1000;
|
||||
println!(
|
||||
"Waiting {} secs for next rate limit window...",
|
||||
(wait_ms / 1000)
|
||||
);
|
||||
sleep(Duration::from_millis(wait_ms));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
import { Mize } from "@ppc/marts/stub_deno";
|
||||
|
||||
export const opts = {
|
||||
"habitica.api_url": {
|
||||
env_var_name: "PPC_HABITICA_API_URL",
|
||||
default_val: "https://habitica.com/api/v3",
|
||||
},
|
||||
"habitica.user_id": {
|
||||
env_var_name: "PPC_HABITICA_USER_ID",
|
||||
},
|
||||
"habitica.client_name": {
|
||||
default_val: "3544a0b8-d71a-46e0-9bb1-6ddbb2abcddb-PPC-Software",
|
||||
},
|
||||
"habitica.api_token": {
|
||||
env_var_name: "PPC_HABITICA_API_TOKEN",
|
||||
},
|
||||
};
|
||||
|
||||
export async function habitica(mize: Mize) {
|
||||
console.log("hiiiiiiiiiiiiiiiiiiii");
|
||||
mize.add_opts(opts);
|
||||
|
||||
mize.add_part(new Habitica(mize));
|
||||
}
|
||||
|
||||
async function handleRateLimit(response) {
|
||||
const limit = response.headers.get("X-RateLimit-Limit") || "NONE";
|
||||
const remaining = parseInt(
|
||||
response.headers.get("X-RateLimit-Remaining") || "10",
|
||||
10,
|
||||
);
|
||||
const reset = response.headers.get("X-RateLimit-Reset");
|
||||
|
||||
console.log(
|
||||
`RateLimit: ${limit} | Remaining: ${remaining} | Reset: ${reset}`,
|
||||
);
|
||||
|
||||
if (remaining < 2 && reset) {
|
||||
const resetDate = new Date(reset);
|
||||
const now = new Date();
|
||||
const waitMs = resetDate.getTime() - now.getTime() + 1000;
|
||||
|
||||
if (waitMs > 0) {
|
||||
console.log(
|
||||
`Waiting ${Math.round(waitMs / 1000)} secs for next rate limit window...`,
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Habitica {
|
||||
mize: Mize;
|
||||
|
||||
constructor(mize: Mize) {
|
||||
this.mize = mize;
|
||||
}
|
||||
|
||||
async get_tasks(type: string = "todos") {
|
||||
return await this.api_request("GET", `tasks/user?type=${type}`);
|
||||
}
|
||||
|
||||
async delete_task(id: string) {
|
||||
await this.api_request("DELETE", `tasks/${id}`);
|
||||
}
|
||||
|
||||
async api_request(
|
||||
method: string,
|
||||
path: string,
|
||||
extraHeaders: object = {},
|
||||
data: object = {},
|
||||
) {
|
||||
const mize = this.mize;
|
||||
|
||||
if (!path.startsWith("/")) {
|
||||
path = "/" + path;
|
||||
}
|
||||
const headers = Object.assign(
|
||||
{
|
||||
"Content-Type": "application/json",
|
||||
"x-api-user": mize.get_config("habitica.user_id"),
|
||||
"x-api-key": mize.get_config("habitica.api_token"),
|
||||
"x-client": mize.get_config("habitica.client_name"),
|
||||
},
|
||||
extraHeaders,
|
||||
);
|
||||
|
||||
const response = await fetch(mize.get_config("habitica.api_url") + path, {
|
||||
method,
|
||||
headers,
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
await handleRateLimit(response);
|
||||
|
||||
if (!response.ok) {
|
||||
console.log("request_url:", mize.get_config("habitica.api_url") + path);
|
||||
throw new Error(
|
||||
`Habitica API error: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
return (await response.json()).data;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,9 @@ use deno_core::OpState;
|
||||
use mize::{Mize, MizeError};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::CliPart;
|
||||
use crate::JsPart;
|
||||
|
||||
#[op2(fast)]
|
||||
fn op_mize_get_part(#[string] name: &str) {
|
||||
println!("js wants the part {}", name);
|
||||
@@ -22,6 +25,21 @@ fn op_mize_add_part(state: &mut OpState, #[string] key: &str) {
|
||||
println!("adding part {}", key);
|
||||
}
|
||||
|
||||
#[op2(fast)]
|
||||
fn op_cli_subcommand(state: &mut OpState, #[string] name: String) {
|
||||
let mut mize: Mize = state.borrow::<Mize>().clone();
|
||||
let mut cli = mize.get_part_native::<CliPart>("cli").unwrap();
|
||||
cli.subcommand(
|
||||
clap::Command::new(name.clone()),
|
||||
move |_sub_matches, mut mize| {
|
||||
let mut js = mize.get_part_native::<JsPart>("js").unwrap();
|
||||
js.eval("log(\"cli command being called\")".to_string())?;
|
||||
js.eval(format!("mize.vars.cli.subcommand({name})"))?;
|
||||
Ok(())
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Create an extension with your custom ops
|
||||
extension!(
|
||||
my_extension,
|
||||
|
||||
11
packages/marts/js/glue_deno.ts
Normal file
11
packages/marts/js/glue_deno.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Mize } from "./stub_deno.ts";
|
||||
|
||||
function argsToMessage(...args) {
|
||||
return args.map((arg) => JSON.stringify(arg)).join(" ");
|
||||
}
|
||||
|
||||
globalThis.log = (...args) => {
|
||||
Deno.core.print(`[out]: ${argsToMessage(...args)}\n`, false);
|
||||
};
|
||||
|
||||
globalThis.mize = new Mize();
|
||||
@@ -1,6 +1,10 @@
|
||||
use deno_core::error::AnyError;
|
||||
use deno_core::url::Url;
|
||||
use deno_core::{extension, op2, JsRuntime, ModuleSpecifier, PollEventLoopOptions, RuntimeOptions};
|
||||
use deno_core::v8::OneByteConst;
|
||||
use deno_core::{
|
||||
ascii_str_include, extension, op2, JsRuntime, ModuleSpecifier, PollEventLoopOptions,
|
||||
RuntimeOptions,
|
||||
};
|
||||
use deno_core::{FastStaticString, FsModuleLoader};
|
||||
use mize::async_trait;
|
||||
use mize::instance::MizePartCreate;
|
||||
@@ -19,35 +23,36 @@ mod glue_deno;
|
||||
#[derive(Default)]
|
||||
pub struct JsPart {
|
||||
mize: Mize,
|
||||
closure_sender: Option<flume::Sender<Box<dyn FnOnce(&mut JsRuntime) -> MizeResult<()> + Send>>>,
|
||||
async_closure_sender: Option<flume::Sender<BoxClosure>>,
|
||||
sender: Option<flume::Sender<JsRuntimeThreadMessage>>,
|
||||
}
|
||||
|
||||
pub type BoxFuture<'a> = Pin<Box<dyn Future<Output = MizeResult<()>> + 'a>>;
|
||||
pub type BoxClosure = Box<dyn for<'a> FnOnce(&'a mut JsRuntime) -> BoxFuture<'a> + Send + 'static>;
|
||||
|
||||
impl MizePart for JsPart {
|
||||
fn init(&mut self, mize: &mut Mize) -> MizeResult<()> {
|
||||
println!("js part init");
|
||||
fn run(&mut self, mize: &mut Mize) -> MizeResult<()> {
|
||||
println!("js part run");
|
||||
self.sender
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.send(JsRuntimeThreadMessage::DoRunPhase)
|
||||
.unwrap();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn js(mize: &mut Mize) -> MizeResult<()> {
|
||||
let (closure_sender, closure_receiver) =
|
||||
flume::unbounded::<Box<dyn FnOnce(&mut JsRuntime) -> MizeResult<()> + Send>>();
|
||||
let (async_closure_sender, async_closure_receiver) = flume::unbounded::<BoxClosure>();
|
||||
let (sender, receiver) = flume::unbounded::<JsRuntimeThreadMessage>();
|
||||
let mize_clone = mize.clone();
|
||||
|
||||
// the thread which will run any js
|
||||
mize.spawn("js_runtime_thread", || {
|
||||
js_runtime_thread(mize_clone, closure_receiver, async_closure_receiver)
|
||||
mize.spawn_and_wait("js_runtime_thread", || {
|
||||
js_runtime_thread(mize_clone, receiver)
|
||||
})?;
|
||||
|
||||
mize.add_part(Box::new(JsPart {
|
||||
mize: mize.clone(),
|
||||
closure_sender: Some(closure_sender),
|
||||
async_closure_sender: Some(async_closure_sender),
|
||||
sender: Some(sender),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -57,169 +62,75 @@ pub fn part_from_file(
|
||||
code: FastStaticString,
|
||||
) -> MizeResult<()> {
|
||||
let mut js = mize.get_part_native::<JsPart>("js")?;
|
||||
js.with_runtime_async(move |runtime: &mut JsRuntime| {
|
||||
let module_spec = ModuleSpecifier::from_str(name).unwrap();
|
||||
return Box::pin(async move {
|
||||
runtime
|
||||
.load_side_es_module_from_code(&module_spec, code)
|
||||
.await;
|
||||
Ok(())
|
||||
});
|
||||
});
|
||||
js.execute_init_js(code)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl JsPart {
|
||||
/*
|
||||
pub fn part_from_js_file(
|
||||
&mut self,
|
||||
name: &'static str,
|
||||
path: &'static str,
|
||||
) -> Box<dyn MizePart + Send + Sync> {
|
||||
Box::new(PartFromJsFileAdapter {
|
||||
mize: self.mize.clone(),
|
||||
name,
|
||||
js_file: path,
|
||||
})
|
||||
}
|
||||
*/
|
||||
pub fn with_runtime<T: FnOnce(&mut JsRuntime) -> MizeResult<()> + Send + 'static>(
|
||||
&mut self,
|
||||
func: T,
|
||||
) {
|
||||
self.closure_sender
|
||||
.as_mut()
|
||||
pub fn execute_init_js(&mut self, code: FastStaticString) -> MizeResult<()> {
|
||||
self.sender
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.send(Box::new(func))
|
||||
.send(JsRuntimeThreadMessage::RunInitJs(code))
|
||||
.unwrap();
|
||||
}
|
||||
pub fn with_runtime_async<F>(&mut self, func: F)
|
||||
where
|
||||
F: for<'a> FnOnce(&'a mut JsRuntime) -> BoxFuture<'a> + Send + 'static,
|
||||
{
|
||||
self.async_closure_sender
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.send(Box::new(func))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
#[mize_part]
|
||||
#[derive(Default)]
|
||||
struct PartFromJsFileAdapter {
|
||||
mize: Mize,
|
||||
name: &'static str,
|
||||
js_file: &'static str,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl MizePart for PartFromJsFileAdapter {
|
||||
fn deps(&self) -> &'static [&'static str] {
|
||||
&["js"]
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
self.name
|
||||
}
|
||||
async fn async_init(&mut self, mize: &mut Mize) -> MizeResult<()> {
|
||||
let js = mize.get_part_native::<JsPart>("js")?;
|
||||
let path = self.js_file;
|
||||
let js_code = format!(
|
||||
r#"
|
||||
import {{ opts, deps, create }} from "{path}";
|
||||
|
||||
const isPromise = (value) => {{
|
||||
return !!value && (typeof value === 'object' || typeof value === 'function') && typeof value.then === 'function';
|
||||
}};
|
||||
|
||||
// Call the async function
|
||||
let create_result = create(mize);
|
||||
if (isPromise(create_result)) {{
|
||||
create_result = await create_result;
|
||||
}}
|
||||
const result = {{
|
||||
opts: opts(mize),
|
||||
deps: deps(mize),
|
||||
create: create_result,
|
||||
}};
|
||||
|
||||
result;
|
||||
"#
|
||||
);
|
||||
|
||||
// Execute the script
|
||||
let result = js.runtime().execute_script("[main]", js_code)?;
|
||||
|
||||
// Resolve the promise and run event loop
|
||||
let resolved_value = js.runtime().resolve_value(result).await?;
|
||||
|
||||
// Run the event loop to completion
|
||||
js.runtime()
|
||||
.run_event_loop(PollEventLoopOptions::default())
|
||||
.await?;
|
||||
|
||||
println!("Execution of part {} completed successfully", self.name());
|
||||
Ok(())
|
||||
}
|
||||
async fn async_run(&mut self, mize: &mut Mize) -> MizeResult<()> {
|
||||
let js = mize.get_part_native::<JsPart>("js")?;
|
||||
let path = self.js_file;
|
||||
let js_code = format!(
|
||||
r#"
|
||||
import {{ run }} from "{path}";
|
||||
|
||||
const result = run()
|
||||
|
||||
result;
|
||||
"#
|
||||
);
|
||||
// Execute the script
|
||||
let result = js.runtime().execute_script("[main]", js_code)?;
|
||||
|
||||
// Resolve the promise and run event loop
|
||||
let resolved_value = js.runtime().resolve_value(result).await?;
|
||||
|
||||
// Run the event loop to completion
|
||||
js.runtime()
|
||||
.run_event_loop(PollEventLoopOptions::default())
|
||||
.await?;
|
||||
|
||||
println!("Running of part {} completed successfully", self.name());
|
||||
pub fn eval(&mut self, code: String) -> MizeResult<()> {
|
||||
self.sender
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.send(JsRuntimeThreadMessage::RunJs(code))
|
||||
.unwrap();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
fn js_runtime_thread(
|
||||
mize: Mize,
|
||||
closure_receiver: flume::Receiver<Box<dyn FnOnce(&mut JsRuntime) -> MizeResult<()> + Send>>,
|
||||
async_closure_receiver: flume::Receiver<BoxClosure>,
|
||||
receiver: flume::Receiver<JsRuntimeThreadMessage>,
|
||||
) -> MizeResult<()> {
|
||||
let mut js_runtime = JsRuntime::new(RuntimeOptions {
|
||||
module_loader: Some(Rc::new(FsModuleLoader)),
|
||||
extensions: vec![glue_deno::my_extension::init(mize.clone())],
|
||||
//module_loader: Some(Rc::new(FsModuleLoader)),
|
||||
//extensions: vec![glue_deno::my_extension::init(mize.clone())],
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let poll_opts = PollEventLoopOptions::default();
|
||||
js_runtime
|
||||
.execute_script("[stub]", ascii_str_include!("../deno_dist/glue_deno.js"))
|
||||
.unwrap();
|
||||
|
||||
let tokio_runtime = Builder::new_current_thread().enable_all().build().unwrap();
|
||||
|
||||
let local = LocalSet::new();
|
||||
local.block_on(&tokio_runtime, async move {
|
||||
loop {
|
||||
// Drain queued closures quickly (no await here if possible)
|
||||
if let Ok(func) = closure_receiver.recv_async().await {
|
||||
if let Err(e) = func(&mut js_runtime) {
|
||||
mize.report_err(e);
|
||||
loop {
|
||||
println!("js thread waiting for smth");
|
||||
let msg = receiver.recv().unwrap();
|
||||
println!("js thread got smth");
|
||||
match msg {
|
||||
JsRuntimeThreadMessage::RunInitJs(js_code) => {
|
||||
if let Err(err) = js_runtime.execute_script("[init]", js_code) {
|
||||
println!("err: {}", err);
|
||||
}
|
||||
println!("done running js");
|
||||
}
|
||||
JsRuntimeThreadMessage::RunJs(js_code) => {
|
||||
if let Err(err) = js_runtime.execute_script("[idk]", js_code) {
|
||||
println!("err: {}", err);
|
||||
}
|
||||
println!("done running js");
|
||||
}
|
||||
JsRuntimeThreadMessage::DoRunPhase => {
|
||||
if let Err(err) = js_runtime.execute_script("[runPhase]", "mize.runPhase()") {
|
||||
println!("err: {}", err);
|
||||
}
|
||||
println!("done with runPhase");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Drive JS one tick
|
||||
let _ = futures::future::poll_fn(|cx| js_runtime.poll_event_loop(cx, poll_opts)).await;
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
enum JsRuntimeThreadMessage {
|
||||
//Closure(Box<dyn FnOnce(&mut JsRuntime) -> MizeResult<()> + Send>),
|
||||
//AsyncClosure(BoxClosure),
|
||||
RunInitJs(FastStaticString),
|
||||
RunJs(String),
|
||||
DoRunPhase,
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ export class Mize {
|
||||
async add_opts(opts: any): Promise<void> {}
|
||||
async add_part(part: any): Promise<void> {}
|
||||
async report_err(err: MizeError) {}
|
||||
runPhase() {
|
||||
Deno.core.print("runPhase in js \n");
|
||||
}
|
||||
}
|
||||
|
||||
type MizeError = string;
|
||||
|
||||
@@ -5,18 +5,10 @@ mod cli;
|
||||
#[cfg(feature = "target-os")]
|
||||
pub use cli::*;
|
||||
|
||||
#[cfg(feature = "target-os")]
|
||||
pub mod habitica;
|
||||
#[cfg(feature = "target-os")]
|
||||
pub use habitica::*;
|
||||
|
||||
pub mod js;
|
||||
pub use js::*;
|
||||
|
||||
#[cfg(feature = "target-os")]
|
||||
pub fn habitica(mize: &mut mize::Mize) -> MizeResult<()> {
|
||||
use deno_core::ascii_str_include;
|
||||
|
||||
js::part_from_file(
|
||||
mize,
|
||||
"habitica",
|
||||
ascii_str_include!("./deno_dist/habitica.js"),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user