diff --git a/Cargo.toml b/Cargo.toml index b64758b..aa7330d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ default = ["console_error_panic_hook", "wee_alloc"] [dependencies] cfg-if = "0.1.2" lazy_static = "1.4" +js-sys = "0.3" serde = { version = "1.0", features = [ "derive" ] } serde_json = "1.0" wasm-bindgen = "0.2" diff --git a/src/blog.rs b/src/blog.rs new file mode 100644 index 0000000..9b846f3 --- /dev/null +++ b/src/blog.rs @@ -0,0 +1,45 @@ +// Utility functions and structs for the blogging system +// Due to limitations of the Cloudflare Workers KV, we do not +// store the entire state in one record; instead, different +// parts are stroed in different records. This also increases +// efficiency, since the program won't need to load anything +// unnecessary from KV. +use crate::store; +use crate::utils::*; +use serde::{Serialize, Deserialize}; +use std::vec::Vec; + +// A list of the UUIDs of all published blog posts +// This should be SORTED with the newest posts at lower indices (closer to 0) +// The user may edit this via KV UI to change ordering and such +// by default new posts are always added to the top +#[derive(Serialize, Deserialize)] +pub struct PostsList(pub Vec); + +impl PostsList { + pub async fn load() -> PostsList { + match store::get_obj("posts_list").await { + Ok(v) => PostsList(v), + // Don't panic on empty + // TODO: What if the user messed up when editing? + // That would cause a decode failure and all data will be gone + // if a new post is added or updated (overwriting the KV value) + // under this logic + // (if no new post is added then nothing bad would happen; + // the user would probably notice when trying to visit the blog home page) + Err(_) => PostsList(vec![]) + } + } + + pub fn has_post(&self, uuid: &str) -> bool { + self.0.contains(&uuid.into()) + } + + // Add a post to the list and then update the record in KV + // Also consumes self, as this should normally be the last action + // in an API call + pub async fn add_post(mut self, uuid: &str) -> MyResult<()> { + self.0.insert(0, uuid.into()); + store::put_obj_pretty("posts_list", self.0).await + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index f2e36ef..999e4c9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,8 @@ extern crate lazy_static; #[macro_use] mod utils; mod router; +mod store; +mod blog; mod sn; use cfg_if::cfg_if; diff --git a/src/sn.rs b/src/sn.rs index 1024c1e..f97e806 100644 --- a/src/sn.rs +++ b/src/sn.rs @@ -1,5 +1,5 @@ // Interface for Standard Notes (Actions) -use crate::CONFIG; +use crate::{CONFIG, blog}; use crate::router::Router; use crate::utils::*; use serde::{Serialize, Serializer}; @@ -28,8 +28,17 @@ async fn get_actions(_req: Request, url: Url) -> MyResult { let origin = url.origin(); let mut actions = vec![]; + // Show different options depending on whether the post already exists + let post_exists = match params.get("item_uuid") { + Some(uuid) => { + let posts = blog::PostsList::load().await; + posts.has_post(&uuid) + }, + None => false + }; + actions.push(Action { - label: "Publish".into(), + label: if post_exists { "Update".into() } else { "Publish".into() }, url: format!("{}/post?secret={}", origin, CONFIG.secret.clone()), verb: Verb::Post, context: Context::Item, diff --git a/src/store.rs b/src/store.rs new file mode 100644 index 0000000..ba521ec --- /dev/null +++ b/src/store.rs @@ -0,0 +1,39 @@ +// Bindings to Cloudflare Workers KV +use crate::utils::*; +use js_sys::Promise; +use serde::Serialize; +use serde::de::DeserializeOwned; +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::JsFuture; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_namespace = PAPRIKA, js_name = "get")] + fn kv_get(key: &str) -> Promise; + #[wasm_bindgen(js_namespace = PAPRIKA, js_name = "put")] + fn kv_put_str(key: &str, value: &str) -> Promise; +} + +pub async fn get_str(key: &str) -> MyResult { + Ok(JsFuture::from(kv_get(key)).await.internal_err()?.as_string().unwrap()) +} + +pub async fn get_obj(key: &str) -> MyResult { + let res = get_str(key).await?; + Ok(serde_json::from_str(&res).internal_err()?) +} + +pub async fn put_str(key: &str, value: &str) -> MyResult<()> { + JsFuture::from(kv_put_str(key, value)).await.internal_err()?; + Ok(()) +} + +pub async fn put_obj(key: &str, value: T) -> MyResult<()> { + put_str(key, &serde_json::to_string(&value).internal_err()?).await +} + +// Some objects may be available for manual editing; thus making it pretty may be helpful +// For example, the user may want to manually edit the order in which posts appear +pub async fn put_obj_pretty(key: &str, value: T) -> MyResult<()> { + put_str(key, &serde_json::to_string_pretty(&value).internal_err()?).await +} \ No newline at end of file