Merge branch 'develop' into dr/move-app-store-to-ssd

This commit is contained in:
dr-frmr 2024-10-25 19:49:11 -04:00
commit 7b59e50e33
No known key found for this signature in database
17 changed files with 4090 additions and 29 deletions

91
Cargo.lock generated
View File

@ -78,7 +78,7 @@ name = "alias"
version = "0.1.0"
dependencies = [
"anyhow",
"kinode_process_lib 0.9.4",
"kinode_process_lib 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
"serde",
"serde_json",
"wit-bindgen",
@ -1012,7 +1012,7 @@ dependencies = [
"alloy-sol-types",
"anyhow",
"bincode",
"kinode_process_lib 0.9.4",
"kinode_process_lib 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
"process_macros",
"rand 0.8.5",
"serde",
@ -1383,7 +1383,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"bincode",
"kinode_process_lib 0.9.4",
"kinode_process_lib 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
"serde",
"serde_json",
"url",
@ -1592,7 +1592,7 @@ name = "cat"
version = "0.1.0"
dependencies = [
"anyhow",
"kinode_process_lib 0.9.4",
"kinode_process_lib 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
"serde",
"serde_json",
"wit-bindgen",
@ -1656,7 +1656,7 @@ dependencies = [
"alloy-sol-types",
"anyhow",
"bincode",
"kinode_process_lib 0.9.4",
"kinode_process_lib 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
"process_macros",
"rand 0.8.5",
"serde",
@ -1675,7 +1675,7 @@ version = "0.2.1"
dependencies = [
"anyhow",
"bincode",
"kinode_process_lib 0.9.4",
"kinode_process_lib 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
"pleco",
"serde",
"serde_json",
@ -1863,6 +1863,17 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2"
[[package]]
name = "contacts"
version = "0.1.0"
dependencies = [
"kinode_process_lib 0.9.4 (git+https://github.com/kinode-dao/process_lib?rev=088a549)",
"process_macros",
"serde",
"serde_json",
"wit-bindgen",
]
[[package]]
name = "convert_case"
version = "0.4.0"
@ -2433,7 +2444,7 @@ name = "download"
version = "0.1.0"
dependencies = [
"anyhow",
"kinode_process_lib 0.9.4",
"kinode_process_lib 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
"process_macros",
"serde",
"serde_json",
@ -2445,7 +2456,7 @@ name = "downloads"
version = "0.5.0"
dependencies = [
"anyhow",
"kinode_process_lib 0.9.4",
"kinode_process_lib 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
"process_macros",
"rand 0.8.5",
"serde",
@ -2482,7 +2493,7 @@ dependencies = [
name = "echo"
version = "0.1.0"
dependencies = [
"kinode_process_lib 0.9.4",
"kinode_process_lib 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
"wit-bindgen",
]
@ -2721,7 +2732,7 @@ version = "0.2.0"
dependencies = [
"anyhow",
"bincode",
"kinode_process_lib 0.9.4",
"kinode_process_lib 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
"process_macros",
"rand 0.8.5",
"serde",
@ -2875,7 +2886,7 @@ dependencies = [
name = "get_block"
version = "0.1.0"
dependencies = [
"kinode_process_lib 0.9.4",
"kinode_process_lib 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
"serde",
"serde_json",
"wit-bindgen",
@ -2940,7 +2951,7 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
name = "globe"
version = "0.1.0"
dependencies = [
"kinode_process_lib 0.9.4",
"kinode_process_lib 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
"serde",
"serde_json",
"url",
@ -3067,7 +3078,7 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
name = "help"
version = "0.1.0"
dependencies = [
"kinode_process_lib 0.9.4",
"kinode_process_lib 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
"wit-bindgen",
]
@ -3096,7 +3107,7 @@ checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46"
name = "hi"
version = "0.1.0"
dependencies = [
"kinode_process_lib 0.9.4",
"kinode_process_lib 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
"serde",
"serde_json",
"wit-bindgen",
@ -3129,7 +3140,7 @@ version = "0.1.1"
dependencies = [
"anyhow",
"bincode",
"kinode_process_lib 0.9.4",
"kinode_process_lib 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
"serde",
"serde_json",
"wit-bindgen",
@ -3444,7 +3455,7 @@ name = "install"
version = "0.1.0"
dependencies = [
"anyhow",
"kinode_process_lib 0.9.4",
"kinode_process_lib 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
"process_macros",
"serde",
"serde_json",
@ -3621,7 +3632,7 @@ name = "kfetch"
version = "0.1.0"
dependencies = [
"anyhow",
"kinode_process_lib 0.9.4",
"kinode_process_lib 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
"rmp-serde",
"serde",
"serde_json",
@ -3633,7 +3644,7 @@ name = "kill"
version = "0.1.0"
dependencies = [
"anyhow",
"kinode_process_lib 0.9.4",
"kinode_process_lib 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
"serde",
"serde_json",
"wit-bindgen",
@ -3749,6 +3760,28 @@ dependencies = [
"wit-bindgen",
]
[[package]]
name = "kinode_process_lib"
version = "0.9.4"
source = "git+https://github.com/kinode-dao/process_lib?rev=088a549#088a5497257eada697e0869d6a8d7e9ef5e620f6"
dependencies = [
"alloy 0.1.4",
"alloy-primitives",
"alloy-sol-macro",
"alloy-sol-types",
"anyhow",
"bincode",
"http 1.1.0",
"mime_guess",
"rand 0.8.5",
"rmp-serde",
"serde",
"serde_json",
"thiserror",
"url",
"wit-bindgen",
]
[[package]]
name = "kit"
version = "0.7.7"
@ -3794,7 +3827,7 @@ dependencies = [
"alloy-sol-types",
"anyhow",
"hex",
"kinode_process_lib 0.9.4",
"kinode_process_lib 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
"rmp-serde",
"serde",
"serde_json",
@ -4023,7 +4056,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"kinode_process_lib 0.9.4",
"kinode_process_lib 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
"regex",
"serde",
"serde_json",
@ -4193,7 +4226,7 @@ dependencies = [
name = "net_diagnostics"
version = "0.1.0"
dependencies = [
"kinode_process_lib 0.9.4",
"kinode_process_lib 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
"rmp-serde",
"serde",
"wit-bindgen",
@ -4519,7 +4552,7 @@ dependencies = [
name = "peer"
version = "0.1.0"
dependencies = [
"kinode_process_lib 0.9.4",
"kinode_process_lib 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
"rmp-serde",
"serde",
"wit-bindgen",
@ -4529,7 +4562,7 @@ dependencies = [
name = "peers"
version = "0.1.0"
dependencies = [
"kinode_process_lib 0.9.4",
"kinode_process_lib 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
"rmp-serde",
"serde",
"wit-bindgen",
@ -5570,7 +5603,7 @@ dependencies = [
"anyhow",
"base64 0.22.1",
"bincode",
"kinode_process_lib 0.9.4",
"kinode_process_lib 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
"rmp-serde",
"serde",
"serde_json",
@ -5788,7 +5821,7 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
name = "state"
version = "0.1.0"
dependencies = [
"kinode_process_lib 0.9.4",
"kinode_process_lib 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
"serde",
"serde_json",
"wit-bindgen",
@ -5965,7 +5998,7 @@ version = "0.1.1"
dependencies = [
"anyhow",
"bincode",
"kinode_process_lib 0.9.4",
"kinode_process_lib 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.8.5",
"regex",
"serde",
@ -5979,7 +6012,7 @@ version = "0.1.1"
dependencies = [
"anyhow",
"bincode",
"kinode_process_lib 0.9.4",
"kinode_process_lib 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
"process_macros",
"serde",
"serde_json",
@ -6236,7 +6269,7 @@ version = "0.2.0"
dependencies = [
"anyhow",
"clap",
"kinode_process_lib 0.9.4",
"kinode_process_lib 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
"serde",
"serde_json",
"wit-bindgen",
@ -6567,7 +6600,7 @@ name = "uninstall"
version = "0.1.0"
dependencies = [
"anyhow",
"kinode_process_lib 0.9.4",
"kinode_process_lib 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
"process_macros",
"serde",
"serde_json",

View File

@ -17,6 +17,7 @@ members = [
"kinode/packages/app_store/app_store", "kinode/packages/app_store/ft_worker",
"kinode/packages/app_store/download", "kinode/packages/app_store/install", "kinode/packages/app_store/uninstall", "kinode/packages/app_store/downloads", "kinode/packages/app_store/chain",
"kinode/packages/chess/chess",
"kinode/packages/contacts/contacts",
"kinode/packages/homepage/homepage",
"kinode/packages/kino_updates/blog", "kinode/packages/kino_updates/globe",
"kinode/packages/kns_indexer/kns_indexer", "kinode/packages/kns_indexer/get_block", "kinode/packages/kns_indexer/state",

View File

@ -129,6 +129,7 @@ The distro userspace packages are:
- `app_store:sys`
- `chess:sys`
- `contacts:sys`
- `homepage:sys`
- `kino_updates:sys`
- `kns_indexer:sys`

3218
kinode/packages/contacts/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
[workspace]
resolver = "2"
members = [
"contacts",
"get_names",
]
[profile.release]
panic = "abort"
opt-level = "s"
lto = true

View File

@ -0,0 +1,36 @@
interface contacts {
enum capability {
read-name-only,
read,
add,
remove,
}
variant request {
get-names, // requires read-names-only
get-all-contacts, // requires read
get-contact(string), // requires read
add-contact(string), // requires add
// tuple<node, field, value>
add-field(tuple<string, string, string>), // requires add
remove-contact(string), // requires remove
// tuple<node, field>
remove-field(tuple<string, string>), // requires remove
}
variant response {
get-names(list<string>),
get-all-contacts, // JSON all-contacts dict in blob
get-contact, // JSON contact dict in blob
add-contact,
add-field,
remove-contact,
remove-field,
err(string), // any failed request will receive this response
}
}
world contacts-sys-v0 {
import contacts;
include process-v0;
}

View File

@ -0,0 +1,20 @@
[package]
name = "contacts"
version = "0.1.0"
edition = "2021"
[features]
simulation-mode = []
[dependencies]
kinode_process_lib = { git = "https://github.com/kinode-dao/process_lib", rev = "088a549" }
process_macros = { git = "https://github.com/kinode-dao/process_macros", rev = "626e501" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
wit-bindgen = "0.24.0"
[lib]
crate-type = ["cdylib"]
[package.metadata.component]
package = "kinode:process"

View File

@ -0,0 +1 @@
data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwMCIgaGVpZ2h0PSIxMDAwIiB2aWV3Qm94PSIwIDAgMTAwMCAxMDAwIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cmVjdCB4PSIyMCIgeT0iMjAiIHdpZHRoPSI5NjAiIGhlaWdodD0iOTYwIiByeD0iNDgwIiBmaWxsPSJ1cmwoI3BhaW50MF9saW5lYXJfMjA5MV8xMDI4MikiIGZpbGwtb3BhY2l0eT0iMC40Ii8+CjxyZWN0IHg9IjIwIiB5PSIyMCIgd2lkdGg9Ijk2MCIgaGVpZ2h0PSI5NjAiIHJ4PSI0ODAiIHN0cm9rZT0idXJsKCNwYWludDFfbGluZWFyXzIwOTFfMTAyODIpIiBzdHJva2Utd2lkdGg9IjQwIi8+CjxwYXRoIGQ9Ik01ODEuMTE2IDU4NS43ODNMNjY0LjUzOCA2NjkuMTJDNjE3LjY5OSA3MTUuNzU4IDU1Ni42MjUgNzM5IDQ5NS4yNDUgNzM5QzQzMy44NjUgNzM5IDM3Mi42MzggNzE1LjYwNSAzMjYuMTA1IDY2OS4xMkMyODAuNzk3IDYyMy44NTggMjU2IDU2My45MTcgMjU2IDUwMEMyNTYgNDM2LjA4MyAyODAuOTUgMzc2LjE0MiAzMjYuMTA1IDMzMC44OEMzNzEuMjYgMjg1LjYxOSA0MzEuMjYyIDI2MSA0OTUuMjQ1IDI2MUM1NTkuMjI3IDI2MSA2MTkuMjMgMjg1LjkyNCA2NjQuNTM4IDMzMC44OEw1ODAuOTYzIDQxNC41MjNDNTQ1LjYwNCAzODMuOTQgNTA1LjE5NCAzNzQuNzY2IDQ2MS4yNjQgMzkxLjEyN0MzOTQuOTg1IDQxNS44OTkgMzY2LjgyMSA0ODkuMjk2IDM5Ny40MzUgNTUyLjc1NEMzOTguNjU5IDU1NS4yMDEgMzk4Ljk2NSA1NTguNzE4IDM5OC4zNTMgNTYxLjMxN0MzOTUuNzUxIDU3Mi40OCAzOTIuNjg5IDU4My40OSAzODkuNzgxIDU5NC4zNDZDMzg2LjEwNyA2MDcuOTU1IDM5My45MTQgNjE1Ljc1NCA0MDcuNTM3IDYxMi4yMzdDNDE4LjU1OCA2MDkuMzMxIDQyOS41NzkgNjA2LjEyIDQ0MC42IDYwMy42NzRDNDQzLjM1NSA2MDIuOTA5IDQ0Ny4wMjkgNjAzLjIxNSA0NDkuNjMxIDYwNC40MzhDNDkwLjUgNjIzLjM5OSA1MjkuOTkxIDYyMC44IDU2Ny43OTkgNTk2LjE4MUM1NzIuNTQ0IDU5My4xMjMgNTc2LjY3NyA1ODkuNDUzIDU4MS4xMTYgNTg1Ljc4M1oiIGZpbGw9InVybCgjcGFpbnQyX2xpbmVhcl8yMDkxXzEwMjgyKSIvPgo8ZGVmcz4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDBfbGluZWFyXzIwOTFfMTAyODIiIHgxPSI1MDAiIHkxPSIwIiB4Mj0iNTAwIiB5Mj0iMTAwMCIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBzdG9wLWNvbG9yPSIjRjM1NDIyIi8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1vcGFjaXR5PSIwIi8+CjwvbGluZWFyR3JhZGllbnQ+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQxX2xpbmVhcl8yMDkxXzEwMjgyIiB4MT0iNzgyLjUiIHkxPSI3My41IiB4Mj0iMTg1LjUiIHkyPSI4OTQuNSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBzdG9wLWNvbG9yPSIjRjM1NDIyIi8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI0E1MzEwQyIvPgo8L2xpbmVhckdyYWRpZW50Pgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50Ml9saW5lYXJfMjA5MV8xMDI4MiIgeDE9Ijc1NCIgeTE9Ijg5LjUiIHgyPSIyNTYiIHkyPSIxMDE1LjUiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agc3RvcC1jb2xvcj0iI0YzNTQyMiIvPgo8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNBNzMyMEQiLz4KPC9saW5lYXJHcmFkaWVudD4KPC9kZWZzPgo8L3N2Zz4K

View File

@ -0,0 +1,350 @@
use crate::kinode::process::contacts;
use kinode_process_lib::{
await_message, call_init, eth, get_blob, get_typed_state, homepage, http, kimap, kiprintln,
set_state, Address, Capability, LazyLoadBlob, Message, NodeId, Response,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::str::FromStr;
wit_bindgen::generate!({
path: "target/wit",
world: "contacts-sys-v0",
generate_unused_types: true,
additional_derives: [PartialEq, serde::Deserialize, serde::Serialize, process_macros::SerdeJsonInto],
});
const ICON: &str = include_str!("icon");
#[cfg(not(feature = "simulation-mode"))]
const CHAIN_ID: u64 = kimap::KIMAP_CHAIN_ID;
#[cfg(feature = "simulation-mode")]
const CHAIN_ID: u64 = 31337; // local
const CHAIN_TIMEOUT: u64 = 60; // 60s
#[cfg(not(feature = "simulation-mode"))]
const KIMAP_ADDRESS: &'static str = kimap::KIMAP_ADDRESS; // optimism
#[cfg(feature = "simulation-mode")]
const KIMAP_ADDRESS: &str = "0xEce71a05B36CA55B895427cD9a440eEF7Cf3669D";
#[derive(Debug, Serialize, Deserialize)]
struct Contact(HashMap<String, serde_json::Value>);
#[derive(Debug, Serialize, Deserialize)]
struct Contacts(HashMap<NodeId, Contact>);
#[derive(Debug, Serialize, Deserialize)]
struct ContactsState {
our: Address,
contacts: Contacts,
}
impl ContactsState {
fn new(our: Address) -> Self {
get_typed_state(|bytes| serde_json::from_slice(bytes)).unwrap_or(Self {
our,
contacts: Contacts(HashMap::new()),
})
}
fn save(&self) {
set_state(&serde_json::to_vec(&self).expect("Failed to serialize contacts state!"));
}
fn contacts(&self) -> &Contacts {
&self.contacts
}
fn get_contact(&self, node: NodeId) -> Option<&Contact> {
self.contacts.0.get(&node)
}
fn add_contact(&mut self, node: NodeId) {
self.contacts.0.insert(node, Contact(HashMap::new()));
self.save();
}
fn remove_contact(&mut self, node: NodeId) {
self.contacts.0.remove(&node);
self.save();
}
fn add_field(&mut self, node: NodeId, field: String, value: serde_json::Value) {
self.contacts
.0
.entry(node)
.or_insert_with(|| Contact(HashMap::new()))
.0
.insert(field, value);
self.save();
}
fn remove_field(&mut self, node: NodeId, field: String) {
if let Some(contact) = self.contacts.0.get_mut(&node) {
contact.0.remove(&field);
}
self.save();
}
fn ws_update(&self, http_server: &mut http::server::HttpServer) {
http_server.ws_push_all_channels(
"/",
http::server::WsMessageType::Text,
LazyLoadBlob::new(
Some("application/json"),
serde_json::to_vec(self.contacts()).unwrap(),
),
);
}
}
call_init!(initialize);
fn initialize(our: Address) {
kiprintln!("started");
homepage::add_to_homepage("Contacts", Some(ICON), Some("/"), None);
let mut state: ContactsState = ContactsState::new(our);
let kimap = kimap::Kimap::new(
eth::Provider::new(CHAIN_ID, CHAIN_TIMEOUT),
eth::Address::from_str(KIMAP_ADDRESS).unwrap(),
);
let mut http_server = http::server::HttpServer::new(5);
// serve the frontend on a secure subdomain
http_server
.serve_ui(
&state.our,
"ui",
vec!["/"],
http::server::HttpBindingConfig::default().secure_subdomain(true),
)
.unwrap();
http_server.secure_bind_http_path("/ask").unwrap();
http_server.secure_bind_ws_path("/").unwrap();
main_loop(&mut state, &kimap, &mut http_server);
}
fn main_loop(
state: &mut ContactsState,
kimap: &kimap::Kimap,
http_server: &mut http::server::HttpServer,
) {
loop {
match await_message() {
Err(_send_error) => {
// ignore send errors, local-only process
continue;
}
Ok(Message::Request {
source,
body,
capabilities,
..
}) => {
// ignore messages from other nodes -- technically superfluous check
// since manifest does not acquire networking capability
if source.node() != state.our.node {
continue;
}
handle_request(&source, &body, capabilities, state, kimap, http_server);
}
_ => continue, // ignore responses
}
}
}
fn handle_request(
source: &Address,
body: &[u8],
capabilities: Vec<Capability>,
state: &mut ContactsState,
kimap: &kimap::Kimap,
http_server: &mut http::server::HttpServer,
) {
// source node is ALWAYS ourselves since networking is disabled
if source.process == "http_server:distro:sys" {
// receive HTTP requests and websocket connection messages from our server
let server_request = http_server.parse_request(body).unwrap();
http_server.handle_request(
server_request,
|req| handle_http_request(state, kimap, &req),
|_channel_id, _message_type, _blob| {
// we don't expect websocket messages
},
);
} else {
// if request is not from frontend, check that it has the required capabilities
let (response, blob) = handle_contacts_request(state, kimap, body, Some(capabilities));
let mut response = Response::new().body(response);
if let Some(blob) = blob {
response = response.blob(blob);
}
response.send().unwrap();
}
state.ws_update(http_server);
}
/// Handle HTTP requests from our own frontend.
fn handle_http_request(
state: &mut ContactsState,
kimap: &kimap::Kimap,
http_request: &http::server::IncomingHttpRequest,
) -> (http::server::HttpResponse, Option<LazyLoadBlob>) {
match http_request.method().unwrap().as_str() {
"GET" => (
http::server::HttpResponse::new(http::StatusCode::OK)
.header("Content-Type", "application/json"),
Some(LazyLoadBlob::new(
Some("application/json"),
serde_json::to_vec(state.contacts()).unwrap(),
)),
),
"POST" => {
let blob = get_blob().unwrap();
let (response, blob) = handle_contacts_request(state, kimap, blob.bytes(), None);
if let contacts::Response::Err(e) = response {
return (
http::server::HttpResponse::new(http::StatusCode::BAD_REQUEST)
.header("Content-Type", "application/json"),
Some(LazyLoadBlob::new(
Some("application/json"),
serde_json::to_vec(&e).unwrap(),
)),
);
}
(
http::server::HttpResponse::new(http::StatusCode::OK)
.header("Content-Type", "application/json"),
match blob {
Some(blob) => Some(LazyLoadBlob::new(
Some("application/json"),
serde_json::to_vec(&blob.bytes).unwrap(),
)),
None => None,
},
)
}
// Any other method will be rejected.
_ => (
http::server::HttpResponse::new(http::StatusCode::METHOD_NOT_ALLOWED),
None,
),
}
}
fn handle_contacts_request(
state: &mut ContactsState,
kimap: &kimap::Kimap,
request_bytes: &[u8],
capabilities: Option<Vec<Capability>>,
) -> (contacts::Response, Option<LazyLoadBlob>) {
let Ok(request) = serde_json::from_slice::<contacts::Request>(request_bytes) else {
return (
contacts::Response::Err("Malformed request".to_string()),
None,
);
};
// if request is not from frontend, check capabilities:
// each request requires one of read-name-only, read, add, or remove
if let Some(capabilities) = capabilities {
let required_capability = Capability::new(
&state.our,
serde_json::to_string(&match request {
contacts::Request::GetNames => contacts::Capability::ReadNameOnly,
contacts::Request::GetAllContacts | contacts::Request::GetContact(_) => {
contacts::Capability::Read
}
contacts::Request::AddContact(_) | contacts::Request::AddField(_) => {
contacts::Capability::Add
}
contacts::Request::RemoveContact(_) | contacts::Request::RemoveField(_) => {
contacts::Capability::Remove
}
})
.unwrap(),
);
if !capabilities.contains(&required_capability) {
return (
contacts::Response::Err("Missing capability".to_string()),
None,
);
}
}
match request {
contacts::Request::GetNames => (
contacts::Response::GetNames(
state
.contacts()
.0
.keys()
.map(|node| node.to_string())
.collect(),
),
None,
),
contacts::Request::GetAllContacts => (
contacts::Response::GetAllContacts,
Some(LazyLoadBlob::new(
Some("application/json"),
serde_json::to_vec(state.contacts()).unwrap(),
)),
),
contacts::Request::GetContact(node) => (
contacts::Response::GetContact,
Some(LazyLoadBlob::new(
Some("application/json"),
serde_json::to_vec(&state.get_contact(node)).unwrap(),
)),
),
contacts::Request::AddContact(node) => {
if let Some((response, blob)) = invalid_node(kimap, &node) {
return (response, blob);
}
state.add_contact(node);
(contacts::Response::AddContact, None)
}
contacts::Request::AddField((node, field, value)) => {
if let Some((response, blob)) = invalid_node(kimap, &node) {
return (response, blob);
}
let Ok(value) = serde_json::from_str::<serde_json::Value>(&value) else {
return (contacts::Response::Err("Malformed value".to_string()), None);
};
state.add_field(node, field, value);
(contacts::Response::AddField, None)
}
contacts::Request::RemoveContact(node) => {
state.remove_contact(node);
(contacts::Response::RemoveContact, None)
}
contacts::Request::RemoveField((node, field)) => {
state.remove_field(node, field);
(contacts::Response::RemoveField, None)
}
}
}
fn invalid_node(
kimap: &kimap::Kimap,
node: &str,
) -> Option<(contacts::Response, Option<LazyLoadBlob>)> {
if kimap
.get(&node)
.map(|(tba, _, _)| tba != eth::Address::ZERO)
.unwrap_or(false)
{
None
} else {
Some((
contacts::Response::Err("Node name invalid or does not exist".to_string()),
None,
))
}
}

View File

@ -0,0 +1,19 @@
[package]
name = "get-names"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
anyhow = "1.0"
kinode_process_lib = "0.9.2"
process_macros = { git = "https://github.com/kinode-dao/process_macros", rev = "626e501" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
wit-bindgen = "0.24.0"
[lib]
crate-type = ["cdylib"]
[package.metadata.component]
package = "kinode:process"

View File

@ -0,0 +1,35 @@
use crate::kinode::process::contacts;
use kinode_process_lib::{call_init, println, Address, Capability, Request};
wit_bindgen::generate!({
path: "target/wit",
world: "contacts-sys-v0",
generate_unused_types: true,
additional_derives: [serde::Deserialize, serde::Serialize, process_macros::SerdeJsonInto],
});
call_init!(init);
fn init(our: Address) {
let contacts_process = Address::from((our.node(), ("contacts", "contacts", "sys")));
let read_names_cap = Capability::new(
&contacts_process,
serde_json::to_string(&contacts::Capability::ReadNameOnly).unwrap(),
);
let Ok(Ok(response)) = Request::to(&contacts_process)
.body(contacts::Request::GetNames)
.capabilities(vec![read_names_cap])
.send_and_await_response(5)
else {
println!("did not receive expected response from contacts:contacts:sys");
return;
};
let Ok(contacts::Response::GetNames(names)) = response.body().try_into() else {
println!("did not receive GetNames response from contacts:contacts:sys");
return;
};
println!("{names:?}");
}

View File

@ -0,0 +1,18 @@
{
"name": "Contacts",
"description": "Save and manage your Kinode OS contacts.",
"image": "",
"properties": {
"package_name": "contacts",
"current_version": "0.1.0",
"publisher": "sys",
"mirrors": [],
"code_hashes": {
"0.1.0": ""
},
"wit_version": 0,
"dependencies": []
},
"external_url": "https://kinode.org",
"animation_url": ""
}

View File

@ -0,0 +1,20 @@
[
{
"process_name": "contacts",
"process_wasm_path": "/contacts.wasm",
"on_exit": "Restart",
"request_networking": false,
"request_capabilities": [
"eth:distro:sys",
"homepage:homepage:sys",
"http_server:distro:sys",
"vfs:distro:sys"
],
"grant_capabilities": [
"eth:distro:sys",
"http_server:distro:sys",
"vfs:distro:sys"
],
"public": false
}
]

View File

@ -0,0 +1,18 @@
{
"get_names.wasm": {
"root": false,
"public": false,
"request_networking": false,
"request_capabilities": [
"contacts:contacts:sys",
{
"process": "contacts:contacts:sys",
"params": "ReadNameOnly"
}
],
"grant_capabilities": [
"contacts:contacts:sys"
],
"wit_version": 0
}
}

View File

@ -0,0 +1,146 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="/kinode.css">
<link href="https://api.fontshare.com/v2/css?f[]=clash-display@400,700,500,600,300&display=swap" rel="stylesheet" />
<script src="/our.js"></script>
<script>
document.title = "contacts - " + window.our.node;
</script>
<style>
h1,
h2,
h3,
h4,
h5,
h6,
p,
a,
li {
font-family: 'Kode Mono', monospace;
}
#title {
display: flex;
align-items: center;
padding: 20px;
max-width: 720px;
min-width: 300px;
margin: 0 auto;
}
#title h1 {
margin: 0;
flex-grow: 1;
text-align: right;
}
#title button {
margin-right: 20px;
}
#edit {
max-width: 720px;
margin: 0 auto;
}
#contacts-article {
margin: 20px;
}
main {
margin: 0 auto;
padding: 20px;
max-width: 960px;
min-width: 300px;
}
form.add-contact {
max-width: 400px;
}
#contacts {
list-style: none;
}
.contact:first-of-type {
margin-top: 10px;
}
.contact {
padding: 10px;
border: 1px solid var(--tasteful-dark);
border-radius: 5px;
margin-bottom: 10px;
display: grid;
gap: 10px;
grid-auto-flow: row;
grid-template-areas:
"name name delete"
"fields fields fields"
"add-field add-field add-field";
}
.contact h3 {
grid-area: name;
}
.contact ul {
grid-area: fields;
list-style: none;
max-width: 100%;
min-width: 0;
}
.contact ul li {
font-size: 1em;
}
form.delete-contact {
grid-area: delete;
}
form.add-field {
grid-area: add-field;
max-width: 400px;
}
.remove-field {
background-color: var(--tasteful-red);
font-size: 0.8em;
padding: 3px 10px;
}
</style>
</head>
<body>
<span id="title">
<button id="back-button"><svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 576 512"
height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
<path
d="M280.37 148.26L96 300.11V464a16 16 0 0 0 16 16l112.06-.29a16 16 0 0 0 15.92-16V368a16 16 0 0 1 16-16h64a16 16 0 0 1 16 16v95.64a16 16 0 0 0 16 16.05L464 480a16 16 0 0 0 16-16V300L295.67 148.26a12.19 12.19 0 0 0-15.3 0zM571.6 251.47L488 182.56V44.05a12 12 0 0 0-12-12h-56a12 12 0 0 0-12 12v72.61L318.47 43a48 48 0 0 0-61 0L4.34 251.47a12 12 0 0 0-1.6 16.9l25.5 31A12 12 0 0 0 45.15 301l235.22-193.74a12.19 12.19 0 0 1 15.3 0L530.9 301a12 12 0 0 0 16.9-1.6l25.5-31a12 12 0 0 0-1.7-16.93z">
</path>
</svg></button>
<h1>contacts</h1>
</span>
<main>
<article id="edit">
<form id="add-contact">
<input type="text" name="node" placeholder="node name (e.g. my-friend.os)">
<button type="submit">add new contact</button>
</form>
</article>
<article id="contacts-article">
<ul id="contacts"></ul>
</article>
<script src="/contacts:contacts:sys/script.js"></script>
</main>
</body>
</html>

View File

@ -0,0 +1,132 @@
const APP_PATH = '/contacts:contacts:sys/ask';
function api_call(body) {
fetch(APP_PATH, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
}
function populate(data) {
console.log(data);
populate_contacts(data);
}
function populate_contacts(contacts) {
const ul = document.getElementById('contacts');
ul.innerHTML = '';
// sort contacts alphabetically by node
Object.entries(contacts).sort((a, b) => a[0].localeCompare(b[0])).forEach(([node, contact]) => {
const li = document.createElement('li');
const div = document.createElement('div');
div.classList.add('contact');
div.innerHTML = `<h3>${node}</h3>
<ul>
${Object.entries(contact).sort((a, b) => a[0].localeCompare(b[0])).map(([field, value]) => `
<li>
${field}: ${JSON.stringify(value)}
<button class="remove-field" onclick="removeField('${node}', '${field}')">X</button>
</li>
`).join('')}
</ul>
<form class="delete-contact" id="${node}">
<button type="submit">delete</button>
</form>
<form class="add-field" id="${node}">
<input type="text" name="field" placeholder="field (e.g. name)">
<input type="text" name="value" placeholder="value (e.g. John Doe)" title="Enter any valid JSON value (e.g. &quot;John Doe&quot;, 42, true, [1,2,3], {&quot;key&quot;:&quot;value&quot;})">
<button type="submit">add</button>
</form>
`;
li.appendChild(div);
ul.appendChild(li);
});
ul.querySelectorAll('.delete-contact').forEach(form => {
form.addEventListener('submit', function (e) {
e.preventDefault();
const node = this.getAttribute('id');
api_call({
"RemoveContact": node
});
});
});
ul.querySelectorAll('.add-field').forEach(form => {
form.addEventListener('submit', function (e) {
e.preventDefault();
const node = this.getAttribute('id');
const data = new FormData(e.target);
let value = data.get('value');
// if value is not valid JSON, wrap it in quotes
try {
JSON.parse(value);
} catch (e) {
// If parsing fails, assume it's a string and wrap it in quotes
value = `"${value}"`;
}
api_call({
"AddField": [node, data.get('field'), value]
});
});
});
}
document.getElementById('back-button').addEventListener('click', () => {
// set page to `/` while also removing the subdomain
const url = new URL(window.location.href);
if (url.hostname.split('.')[0] === 'contacts-sys') {
url.hostname = url.hostname.split('.').slice(1).join('.');
}
url.pathname = '/';
window.location.href = url.toString();
});
document.getElementById('add-contact').addEventListener('submit', (e) => {
e.preventDefault();
const data = new FormData(e.target);
const node = data.get('node');
const body = {
"AddContact": node
};
fetch(APP_PATH, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
}).then(response => {
e.target.reset();
if (response.status === 200) {
return null;
} else {
return response.json();
}
}).then(data => {
if (data === null) {
return;
} else {
alert(JSON.stringify(data));
}
}).catch(error => {
console.error('Error:', error);
});
})
function removeField(node, field) {
api_call({
"RemoveField": [node, field]
});
}
// Setup WebSocket connection
const wsProtocol = location.protocol === 'https:' ? 'wss://' : 'ws://';
const ws = new WebSocket(wsProtocol + location.host + "/contacts:contacts:sys/");
ws.onmessage = event => {
const data = JSON.parse(event.data);
populate(data);
};

View File

@ -5,6 +5,8 @@
"on_exit": "Restart",
"request_networking": true,
"request_capabilities": [
"app_store:app_store:sys",
"contacts:contacts:sys",
"chess:chess:sys",
"eth:distro:sys",
{