mirror of
https://github.com/uqbar-dao/nectar.git
synced 2024-11-22 03:04:35 +03:00
contacts system prim
This commit is contained in:
parent
dba98d1884
commit
5eb9492e6c
10
Cargo.lock
generated
10
Cargo.lock
generated
@ -1849,6 +1849,16 @@ 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.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.4.0"
|
||||
|
@ -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",
|
||||
|
3174
kinode/packages/contacts/Cargo.lock
generated
Normal file
3174
kinode/packages/contacts/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
10
kinode/packages/contacts/Cargo.toml
Normal file
10
kinode/packages/contacts/Cargo.toml
Normal file
@ -0,0 +1,10 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"contacts",
|
||||
]
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
opt-level = "s"
|
||||
lto = true
|
19
kinode/packages/contacts/contacts/Cargo.toml
Normal file
19
kinode/packages/contacts/contacts/Cargo.toml
Normal file
@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "contacts"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
simulation-mode = []
|
||||
|
||||
[dependencies]
|
||||
kinode_process_lib = "0.9.1"
|
||||
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"
|
1
kinode/packages/contacts/contacts/src/icon
Normal file
1
kinode/packages/contacts/contacts/src/icon
Normal file
@ -0,0 +1 @@
|
||||
data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTM2IiBoZWlnaHQ9IjEzNiIgdmlld0JveD0iMCAwIDEzNiAxMzYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxMzYiIGhlaWdodD0iMTM2IiByeD0iNjgiIGZpbGw9IiNGRkY1RDkiIGZpbGwtb3BhY2l0eT0iMC40Ii8+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF8xODIxXzkwOTMpIj4KPHBhdGggZD0iTTY3LjQ4NTUgOTQuOTg4MUM2NS44MTYxIDk0Ljk4ODEgNjQuMTQ2NyA5NC45NjQzIDYyLjQ3NzIgOTQuOTg4MUM2MS41NjAyIDk1LjAxMTkgNjEuMDQyOSA5NC40MTc0IDYwLjkzNzEgOTMuNjQ0N0M2MC42NjY3IDkxLjU1MjMgNjAuMzQ5MyA4OS40NTk4IDYwLjA2NzEgODcuMzY3NEM2MC4wMjAxIDg3LjAxMDggNTkuODc5IDg2Ljc5NjggNTkuNTM4MSA4Ni42NTQxQzU4LjIzMzEgODYuMDgzNCA1Ny4wMjIyIDg1LjM1ODIgNTUuODgxOCA4NC40OTA0QzU1LjYyMzEgODQuMjg4MiA1NS4zOTk4IDg0LjI2NDUgNTUuMDk0MSA4NC4zOTUyQzUzLjE1NDMgODUuMjAzNyA1MS4yMTQ0IDg1Ljk4ODMgNDkuMjc0NiA4Ni43NzNDNDguMjc1MyA4Ny4xNzcyIDQ3Ljc5MzMgODcuMDIyNyA0Ny4yNTI1IDg2LjA5NTNDNDUuNTcxMyA4My4xNTg4IDQzLjg5MDEgODAuMjEwNCA0Mi4yMDg5IDc3LjI2MkM0MS44MDkyIDc2LjU3MjUgNDEuOTI2NyA3NS45NDIzIDQyLjU0OTggNzUuNDQzQzQ0LjIzMSA3NC4wOTk2IDQ1LjkxMjIgNzIuNzU2MiA0Ny42MDUyIDcxLjQzNjVDNDcuODk5MSA3MS4yMTA2IDQ4LjAwNDkgNzAuOTYxIDQ3Ljk1NzkgNzAuNTkyNEM0Ny43ODE1IDY5LjIwMTQgNDcuNzkzMyA2Ny43OTg2IDQ3Ljk1NzkgNjYuNDA3NkM0OC4wMDQ5IDY2LjA1MDkgNDcuOTEwOCA2NS44MTMxIDQ3LjYyODcgNjUuNTg3M0M0NS45OTQ1IDY0LjMwMzMgNDQuMzYwNCA2My4wMDc0IDQyLjczNzkgNjEuNzExNUM0MS44Nzk3IDYxLjAzMzkgNDEuNzczOSA2MC41MTA4IDQyLjMxNDcgNTkuNTcxNkM0NC4wMDc3IDU2LjYxMTMgNDUuNjg4OCA1My42MzkxIDQ3LjM5MzYgNTAuNjc4OEM0Ny43NDYyIDUwLjA3MjUgNDguMzgxMSA0OS44NTg1IDQ5LjA3NDcgNTAuMTQzOEM1MC40MjY3IDUwLjY5MDcgNTEuNzc4OCA1MS4yMzc1IDUzLjEzMDggNTEuNzk2M0M1My44MjQ0IDUyLjA4MTcgNTQuNTE4IDUyLjM1NTEgNTUuMjExNyA1Mi42NTIzQzU1LjQyMzMgNTIuNzQ3NCA1NS41ODc5IDUyLjczNTUgNTUuNzc2IDUyLjU5MjlDNTYuOTg2OSA1MS42NjU1IDU4LjMwMzYgNTAuODkyOCA1OS43MDI3IDUwLjI4NjVDNTkuOTQ5NiA1MC4xNzk1IDYwLjAyMDEgNDkuOTg5MiA2MC4wNDM2IDQ5Ljc1MTVDNjAuMzE0IDQ3LjgxMzYgNjAuNTg0NCA0NS44NzU3IDYwLjg1NDggNDMuOTM3OUM2MC44OTAxIDQzLjcxMiA2MC45MTM2IDQzLjQ4NjEgNjAuOTQ4OSA0My4yNjAyQzYxLjA5IDQyLjQxNjEgNjEuNTYwMiA0MiA2Mi4zNzE0IDQyQzY0LjQ0MDYgNDIgNjYuNTA5NyA0MiA2OC41Nzg5IDQyQzY5LjkwNzQgNDIgNzEuMjQ3NiA0MiA3Mi41NzYxIDQyQzczLjQ0NjEgNDIgNzMuOTI4MSA0Mi40NTE4IDc0LjA1NzUgNDMuMzMxNUM3NC4zNTE0IDQ1LjQzNTggNzQuNjQ1MyA0Ny41NDAyIDc0LjkyNzQgNDkuNjQ0NUM3NC45NzQ1IDQ5Ljk4OTIgNzUuMTE1NSA1MC4xNzk1IDc1LjQzMyA1MC4zMzRDNzYuNzM4IDUwLjkxNjYgNzcuOTcyNCA1MS42Mjk5IDc5LjExMjggNTIuNTA5NkM3OS4zNTk3IDUyLjY5OTkgNzkuNTU5NSA1Mi43MjM2IDc5Ljg0MTcgNTIuNjA0OEM4MS44MDUgNTEuNzk2MyA4My43Njg0IDUwLjk4NzkgODUuNzQzNSA1MC4xOTEzQzg2LjY3MjMgNDkuODEwOSA4Ny4yMTMxIDUwLjAxMyA4Ny43MTg2IDUwLjg5MjhDODkuMzc2MyA1My44MDU1IDkxLjAzMzkgNTYuNzE4MyA5Mi42OTE2IDU5LjYxOTFDOTMuMTczNiA2MC40NTEzIDkzLjA2NzggNjEuMDIyIDkyLjMxNTQgNjEuNjI4M0M5MC42NDYgNjIuOTU5OCA4OC45NzY1IDY0LjI3OTUgODcuMjk1NCA2NS41ODczQzg3LjAyNDkgNjUuODAxMyA4Ni45NDI3IDY2LjAyNzEgODYuOTc3OSA2Ni4zNkM4Ny4xNTQzIDY3Ljc4NjcgODcuMTU0MyA2OS4yMTMzIDg2Ljk3NzkgNzAuNjRDODYuOTQyNyA3MC45NjEgODcuMDI1IDcxLjE4NjkgODcuMjgzNiA3MS4zODlDODguOTI5NSA3Mi42ODQ4IDkwLjU3NTQgNzMuOTkyNiA5Mi4yMjE0IDc1LjMwMDRDOTMuMDU2MSA3NS45NjYxIDkzLjE2MTkgNzYuNDg5MiA5Mi42MzI4IDc3LjQxNjZDOTAuOTc1MiA4MC4zNDEyIDg5LjMwNTcgODMuMjUzOSA4Ny42MzYzIDg2LjE2NjdDODcuMTg5NSA4Ni45NTEzIDg2LjYwMTcgODcuMTUzNCA4NS43NjcgODYuODA4N0M4My44MDM3IDg2LjAxMjEgODEuODUyMSA4NS4yMTU2IDc5LjkwMDUgODQuNDA3MUM3OS41NzEzIDg0LjI2NDUgNzkuMzI0NCA4NC4yODgyIDc5LjAzMDUgODQuNTE0MUM3Ny45MDE5IDg1LjM5MzkgNzYuNjc5MiA4Ni4wOTUzIDc1LjM3NDIgODYuNjY2Qzc1LjA2ODUgODYuNzk2OCA3NC45MzkyIDg2Ljk4NyA3NC44OTIyIDg3LjMwOEM3NC42MSA4OS40MDA0IDc0LjMxNjEgOTEuNDkyOCA3NC4wMTA0IDkzLjU4NTJDNzMuOTYzNCA5My45MDYyIDczLjgzNDEgOTQuMjI3MiA3My42Njk1IDk0LjUwMDdDNzMuNDM0NCA5NC44NTczIDczLjA1ODEgOTQuOTg4MSA3Mi42MjMxIDk0Ljk4ODFDNzAuOTA2NyA5NC45ODgxIDY5LjE3ODUgOTQuOTg4MSA2Ny40NjIgOTQuOTg4MUg2Ny40ODU1Wk02Ny40NzM4IDU5LjE1NTVDNjIuNDMwMiA1OS4xNTU1IDU4LjI2ODQgNjMuMzUyMiA1OC4yNTY2IDY4LjQ2NDNDNTguMjQ0OSA3My42MDAzIDYyLjQwNjcgNzcuODIwOCA2Ny40ODU1IDc3LjgyMDhDNzIuNTUyNiA3Ny44MjA4IDc2LjcxNDQgNzMuNjEyMiA3Ni43MTQ0IDY4LjQ4ODFDNzYuNzE0NCA2My4zNDAzIDcyLjU2NDQgNTkuMTU1NSA2Ny40NjIgNTkuMTQzNkw2Ny40NzM4IDU5LjE1NTVaIiBmaWxsPSIjRkZGNUQ5Ii8+CjwvZz4KPGRlZnM+CjxjbGlwUGF0aCBpZD0iY2xpcDBfMTgyMV85MDkzIj4KPHJlY3Qgd2lkdGg9IjUxIiBoZWlnaHQ9IjUzIiBmaWxsPSJ3aGl0ZSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNDIgNDIpIi8+CjwvY2xpcFBhdGg+CjwvZGVmcz4KPC9zdmc+Cg==
|
291
kinode/packages/contacts/contacts/src/lib.rs
Normal file
291
kinode/packages/contacts/contacts/src/lib.rs
Normal file
@ -0,0 +1,291 @@
|
||||
use kinode_process_lib::{
|
||||
await_message, call_init, eth, get_blob, homepage, http, kernel_types, net, println, Address,
|
||||
LazyLoadBlob, Message, NodeId, ProcessId, Request, Response, SendError, SendErrorKind,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
const ICON: &str = include_str!("icon");
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct ContactsState {
|
||||
our: Address,
|
||||
}
|
||||
|
||||
impl ContactsState {
|
||||
fn new(our: Address) -> Self {
|
||||
Self { our }
|
||||
}
|
||||
}
|
||||
|
||||
wit_bindgen::generate!({
|
||||
path: "target/wit",
|
||||
world: "process-v0",
|
||||
});
|
||||
|
||||
call_init!(initialize);
|
||||
fn initialize(our: Address) {
|
||||
// add ourselves to the homepage
|
||||
homepage::add_to_homepage("Contacts", Some(ICON), Some("/"), None);
|
||||
|
||||
// Grab our state, then enter the main event loop.
|
||||
let mut state: ContactsState = ContactsState::new(our);
|
||||
|
||||
let mut http_server = http::server::HttpServer::new(5);
|
||||
|
||||
// Serve the index.html and other UI files found in pkg/ui at the root path.
|
||||
// Serving securely at `settings-sys` 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, &mut http_server);
|
||||
}
|
||||
|
||||
fn main_loop(state: &mut ContactsState, http_server: &mut http::server::HttpServer) {
|
||||
loop {
|
||||
match await_message() {
|
||||
Err(send_error) => {
|
||||
println!("got send error: {send_error:?}");
|
||||
continue;
|
||||
}
|
||||
Ok(Message::Request {
|
||||
source,
|
||||
body,
|
||||
expects_response,
|
||||
..
|
||||
}) => {
|
||||
if source.node() != state.our.node {
|
||||
continue; // ignore messages from other nodes
|
||||
}
|
||||
let response = handle_request(&source, &body, state, http_server);
|
||||
// state.ws_update(http_server);
|
||||
if expects_response.is_some() {
|
||||
Response::new()
|
||||
.body(serde_json::to_vec(&response).unwrap())
|
||||
.send()
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
_ => continue, // ignore responses
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_request(
|
||||
source: &Address,
|
||||
body: &[u8],
|
||||
state: &mut ContactsState,
|
||||
http_server: &mut http::server::HttpServer,
|
||||
) -> SettingsResponse {
|
||||
// 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)
|
||||
.map_err(|_| SettingsError::MalformedRequest)?;
|
||||
|
||||
http_server.handle_request(
|
||||
server_request,
|
||||
|req| {
|
||||
let result = handle_http_request(state, &req);
|
||||
match result {
|
||||
Ok((resp, blob)) => (resp, blob),
|
||||
Err(e) => {
|
||||
println!("error handling HTTP request: {e}");
|
||||
(
|
||||
http::server::HttpResponse {
|
||||
status: 500,
|
||||
headers: HashMap::new(),
|
||||
},
|
||||
Some(LazyLoadBlob {
|
||||
mime: Some("application/text".to_string()),
|
||||
bytes: e.to_string().as_bytes().to_vec(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
|_channel_id, _message_type, _blob| {
|
||||
// we don't expect websocket messages
|
||||
},
|
||||
);
|
||||
Ok(None)
|
||||
} else {
|
||||
let settings_request = serde_json::from_slice::<SettingsRequest>(body)
|
||||
.map_err(|_| SettingsError::MalformedRequest)?;
|
||||
handle_settings_request(state, settings_request)
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle HTTP requests from our own frontend.
|
||||
fn handle_http_request(
|
||||
state: &mut ContactsState,
|
||||
http_request: &http::server::IncomingHttpRequest,
|
||||
) -> anyhow::Result<(http::server::HttpResponse, Option<LazyLoadBlob>)> {
|
||||
match http_request.method()?.as_str() {
|
||||
"GET" => {
|
||||
state.fetch()?;
|
||||
Ok((
|
||||
http::server::HttpResponse::new(http::StatusCode::OK)
|
||||
.header("Content-Type", "application/json"),
|
||||
Some(LazyLoadBlob::new(
|
||||
Some("application/json"),
|
||||
serde_json::to_vec(&state)?,
|
||||
)),
|
||||
))
|
||||
}
|
||||
"POST" => {
|
||||
let Some(blob) = get_blob() else {
|
||||
return Err(anyhow::anyhow!("malformed request"));
|
||||
};
|
||||
let request = serde_json::from_slice::<SettingsRequest>(&blob.bytes)?;
|
||||
let response = handle_settings_request(state, request);
|
||||
Ok((
|
||||
http::server::HttpResponse::new(http::StatusCode::OK)
|
||||
.header("Content-Type", "application/json"),
|
||||
match response {
|
||||
Ok(Some(data)) => Some(LazyLoadBlob::new(
|
||||
Some("application/json"),
|
||||
serde_json::to_vec(&data)?,
|
||||
)),
|
||||
Ok(None) => None,
|
||||
Err(e) => Some(LazyLoadBlob::new(
|
||||
Some("application/json"),
|
||||
serde_json::to_vec(&e)?,
|
||||
)),
|
||||
},
|
||||
))
|
||||
}
|
||||
// Any other method will be rejected.
|
||||
_ => Ok((
|
||||
http::server::HttpResponse::new(http::StatusCode::METHOD_NOT_ALLOWED),
|
||||
None,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_settings_request(
|
||||
state: &mut SettingsState,
|
||||
request: SettingsRequest,
|
||||
) -> SettingsResponse {
|
||||
match request {
|
||||
SettingsRequest::Hi {
|
||||
node,
|
||||
content,
|
||||
timeout,
|
||||
} => {
|
||||
if let Err(SendError { kind, .. }) = Request::to((&node, "net", "distro", "sys"))
|
||||
.body(content.into_bytes())
|
||||
.send_and_await_response(timeout)
|
||||
.unwrap()
|
||||
{
|
||||
match kind {
|
||||
SendErrorKind::Timeout => {
|
||||
println!("message to {node} timed out");
|
||||
return Err(SettingsError::HiTimeout);
|
||||
}
|
||||
SendErrorKind::Offline => {
|
||||
println!("{node} is offline or does not exist");
|
||||
return Err(SettingsError::HiOffline);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
SettingsRequest::PeerId(node) => {
|
||||
// get peer info
|
||||
match Request::to(("our", "net", "distro", "sys"))
|
||||
.body(rmp_serde::to_vec(&net::NetAction::GetPeer(node)).unwrap())
|
||||
.send_and_await_response(30)
|
||||
.unwrap()
|
||||
{
|
||||
Ok(msg) => match rmp_serde::from_slice::<net::NetResponse>(msg.body()) {
|
||||
Ok(net::NetResponse::Peer(Some(peer))) => {
|
||||
println!("got peer info: {peer:?}");
|
||||
return Ok(Some(SettingsData::PeerId(peer)));
|
||||
}
|
||||
Ok(net::NetResponse::Peer(None)) => {
|
||||
println!("peer not found");
|
||||
return Ok(None);
|
||||
}
|
||||
_ => {
|
||||
return Err(SettingsError::KernelNonresponsive);
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
return Err(SettingsError::KernelNonresponsive);
|
||||
}
|
||||
}
|
||||
}
|
||||
SettingsRequest::EthConfig(action) => {
|
||||
match Request::to(("our", "eth", "distro", "sys"))
|
||||
.body(serde_json::to_vec(&action).unwrap())
|
||||
.send_and_await_response(30)
|
||||
.unwrap()
|
||||
{
|
||||
Ok(msg) => match serde_json::from_slice::<eth::EthConfigResponse>(msg.body()) {
|
||||
Ok(eth::EthConfigResponse::PermissionDenied) => {
|
||||
return Err(SettingsError::KernelNonresponsive);
|
||||
}
|
||||
Ok(other) => {
|
||||
println!("eth config action succeeded: {other:?}");
|
||||
}
|
||||
Err(_) => {
|
||||
return Err(SettingsError::KernelNonresponsive);
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
return Err(SettingsError::KernelNonresponsive);
|
||||
}
|
||||
}
|
||||
}
|
||||
SettingsRequest::Shutdown => {
|
||||
// shutdown the node IMMEDIATELY!
|
||||
Request::to(("our", "kernel", "distro", "sys"))
|
||||
.body(serde_json::to_vec(&kernel_types::KernelCommand::Shutdown).unwrap())
|
||||
.send()
|
||||
.unwrap();
|
||||
}
|
||||
SettingsRequest::KillProcess(pid) => {
|
||||
// kill a process
|
||||
if let Err(_) = Request::to(("our", "kernel", "distro", "sys"))
|
||||
.body(serde_json::to_vec(&kernel_types::KernelCommand::KillProcess(pid)).unwrap())
|
||||
.send_and_await_response(30)
|
||||
.unwrap()
|
||||
{
|
||||
return SettingsResponse::Err(SettingsError::KernelNonresponsive);
|
||||
}
|
||||
}
|
||||
SettingsRequest::SetStylesheet(stylesheet) => {
|
||||
let Ok(()) = kinode_process_lib::vfs::File {
|
||||
path: "/homepage:sys/pkg/kinode.css".to_string(),
|
||||
timeout: 5,
|
||||
}
|
||||
.write(stylesheet.as_bytes()) else {
|
||||
return SettingsResponse::Err(SettingsError::KernelNonresponsive);
|
||||
};
|
||||
Request::to(("our", "homepage", "homepage", "sys"))
|
||||
.body(
|
||||
serde_json::json!({ "SetStylesheet": stylesheet })
|
||||
.to_string()
|
||||
.as_bytes(),
|
||||
)
|
||||
.send()
|
||||
.unwrap();
|
||||
state.stylesheet = Some(stylesheet);
|
||||
return SettingsResponse::Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
state.fetch().map_err(|_| SettingsError::StateFetchFailed)?;
|
||||
SettingsResponse::Ok(None)
|
||||
}
|
18
kinode/packages/contacts/metadata.json
Normal file
18
kinode/packages/contacts/metadata.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "Contacts",
|
||||
"description": "Store and manage your 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": ""
|
||||
}
|
18
kinode/packages/contacts/pkg/manifest.json
Normal file
18
kinode/packages/contacts/pkg/manifest.json
Normal file
@ -0,0 +1,18 @@
|
||||
[
|
||||
{
|
||||
"process_name": "contacts",
|
||||
"process_wasm_path": "/contacts.wasm",
|
||||
"on_exit": "Restart",
|
||||
"request_networking": false,
|
||||
"request_capabilities": [
|
||||
"homepage:homepage:sys",
|
||||
"http_server:distro:sys",
|
||||
"vfs:distro:sys"
|
||||
],
|
||||
"grant_capabilities": [
|
||||
"http_server:distro:sys",
|
||||
"vfs:distro:sys"
|
||||
],
|
||||
"public": false
|
||||
}
|
||||
]
|
235
kinode/packages/contacts/pkg/ui/index.html
Normal file
235
kinode/packages/contacts/pkg/ui/index.html
Normal file
@ -0,0 +1,235 @@
|
||||
<!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 = window.our.node + " - settings";
|
||||
</script>
|
||||
<style>
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p,
|
||||
a,
|
||||
li {
|
||||
font-family: 'Kode Mono', monospace;
|
||||
}
|
||||
|
||||
h1 {
|
||||
padding: 20px;
|
||||
max-width: 960px;
|
||||
min-width: 300px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
main {
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 20px 20px;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas:
|
||||
"diagnostics diagnostics diagnostics"
|
||||
"node-info pings pings"
|
||||
"eth-rpc-providers eth-rpc-providers eth-rpc-settings"
|
||||
"kernel kernel kernel"
|
||||
"kinode-css kinode-css kinode-css";
|
||||
padding: 20px;
|
||||
max-width: 960px;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
article#net-diagnostics {
|
||||
grid-area: diagnostics;
|
||||
}
|
||||
|
||||
p#diagnostics,
|
||||
p#peer-pki-response,
|
||||
p#peer-ping-response {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
article#node-info {
|
||||
grid-area: node-info;
|
||||
word-wrap: break-word;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
#shutdown {
|
||||
background-color: var(--ansi-red)
|
||||
}
|
||||
|
||||
#shutdown:hover {
|
||||
background-color: var(--maroon);
|
||||
}
|
||||
|
||||
article#pings {
|
||||
grid-area: pings;
|
||||
}
|
||||
|
||||
article#eth-rpc-providers {
|
||||
grid-area: eth-rpc-providers;
|
||||
}
|
||||
|
||||
article#eth-rpc-settings {
|
||||
grid-area: eth-rpc-settings;
|
||||
}
|
||||
|
||||
article#kernel {
|
||||
grid-area: kernel;
|
||||
}
|
||||
|
||||
article#kinode-css {
|
||||
grid-area: kinode-css;
|
||||
}
|
||||
|
||||
textarea#stylesheet-editor {
|
||||
width: 100%;
|
||||
min-width: 300px;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
div#provider-edits {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px 20px;
|
||||
grid-auto-flow: row;
|
||||
}
|
||||
|
||||
article {
|
||||
border: 1px solid #444;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
padding: 8px;
|
||||
margin-bottom: 6px;
|
||||
border-radius: 4px;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
#process-map li p:first-child {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#process-map li ul {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
#process-map li ul li {
|
||||
margin-bottom: 1px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button.kill-process {
|
||||
padding: 3px 6px;
|
||||
margin: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>system diagnostics & settings</h1>
|
||||
<main>
|
||||
<article id="net-diagnostics">
|
||||
<h2>networking diagnostics</h2>
|
||||
<p id="diagnostics"></p>
|
||||
</article>
|
||||
|
||||
<article id="node-info">
|
||||
<h2>node info</h2>
|
||||
<p id="node-name"></p>
|
||||
<p id="net-key"></p>
|
||||
<p id="ip-ports"></p>
|
||||
<p id="routers"></p>
|
||||
<button id="shutdown">shut down node(!)</button>
|
||||
</article>
|
||||
|
||||
<article id="pings">
|
||||
<h2>fetch PKI data</h2>
|
||||
<form id="get-peer-pki">
|
||||
<input type="text" name="peer" placeholder="peer-name.os">
|
||||
<button type="submit">get peer info</button>
|
||||
</form>
|
||||
<p id="peer-pki-response"></p>
|
||||
<h2>ping a node</h2>
|
||||
<form id="ping-peer">
|
||||
<input type="text" name="peer" placeholder="peer-name.os">
|
||||
<input type="text" name="content" placeholder="message">
|
||||
<input type="number" name="timeout" placeholder="timeout (seconds)">
|
||||
<button type="submit">ping</button>
|
||||
</form>
|
||||
<p id="peer-ping-response"></p>
|
||||
</article>
|
||||
|
||||
<article id="eth-rpc-providers">
|
||||
<h2>ETH RPC providers</h2>
|
||||
<div id="provider-edits">
|
||||
<form id="add-eth-provider">
|
||||
<input type="number" name="chain-id" placeholder="1">
|
||||
<input type="text" name="rpc-url" placeholder="wss://rpc-url.com">
|
||||
<button type="submit">add provider</button>
|
||||
</form>
|
||||
<form id="remove-eth-provider">
|
||||
<input type="number" name="chain-id" placeholder="1">
|
||||
<input type="text" name="rpc-url" placeholder="wss://rpc-url.com">
|
||||
<button type="submit">remove provider</button>
|
||||
</form>
|
||||
</div>
|
||||
<ul id="providers"></ul>
|
||||
</article>
|
||||
|
||||
<article id="eth-rpc-settings">
|
||||
<h2>ETH RPC settings</h2>
|
||||
<p id="public"></p>
|
||||
<div>
|
||||
<p>nodes allowed to connect:</p>
|
||||
<ul id="allowed-nodes"></ul>
|
||||
</div>
|
||||
<div>
|
||||
<p>nodes banned from connecting:</p>
|
||||
<ul id="denied-nodes"></ul>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article id="kernel">
|
||||
<h2>running processes</h2>
|
||||
<ul id="process-map"></ul>
|
||||
</article>
|
||||
|
||||
<article id="kinode-css">
|
||||
<h2>stylesheet editor</h2>
|
||||
<textarea id="stylesheet-editor"></textarea>
|
||||
<button id="save-stylesheet">update kinode.css</button>
|
||||
</article>
|
||||
|
||||
|
||||
<script src="/settings:settings:sys/script.js"></script>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
</html>
|
320
kinode/packages/contacts/pkg/ui/script.js
Normal file
320
kinode/packages/contacts/pkg/ui/script.js
Normal file
@ -0,0 +1,320 @@
|
||||
const APP_PATH = '/settings:settings:sys/ask';
|
||||
|
||||
// Fetch initial data and populate the UI
|
||||
function init() {
|
||||
fetch(APP_PATH)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
populate(data);
|
||||
});
|
||||
}
|
||||
|
||||
function api_call(body) {
|
||||
fetch(APP_PATH, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
function shutdown() {
|
||||
api_call("Shutdown");
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function populate(data) {
|
||||
populate_node_info(data.identity);
|
||||
populate_net_diagnostics(data.diagnostics);
|
||||
populate_eth_rpc_providers(data.eth_rpc_providers);
|
||||
populate_eth_rpc_settings(data.eth_rpc_access_settings);
|
||||
populate_process_map(data.process_map);
|
||||
populate_stylesheet_editor(data.stylesheet);
|
||||
}
|
||||
|
||||
function populate_node_info(identity) {
|
||||
document.getElementById('node-name').innerText = identity.name;
|
||||
document.getElementById('net-key').innerText = identity.networking_key;
|
||||
if (identity.ws_routing) {
|
||||
document.getElementById('ip-ports').innerText = identity.ws_routing;
|
||||
} else {
|
||||
document.getElementById('ip-ports').style.display = 'none';
|
||||
}
|
||||
if (identity.routers) {
|
||||
document.getElementById('routers').innerText = identity.routers;
|
||||
} else {
|
||||
document.getElementById('routers').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function populate_net_diagnostics(diagnostics) {
|
||||
document.getElementById('diagnostics').innerText = diagnostics;
|
||||
}
|
||||
|
||||
function populate_eth_rpc_providers(providers) {
|
||||
const ul = document.getElementById('providers');
|
||||
ul.innerHTML = '';
|
||||
providers.forEach(provider => {
|
||||
const li = document.createElement('li');
|
||||
li.innerHTML = `${JSON.stringify(provider, undefined, 2)}`;
|
||||
ul.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
function populate_eth_rpc_settings(settings) {
|
||||
if (settings.public) {
|
||||
document.getElementById('public').innerText = 'status: public';
|
||||
document.getElementById('allowed-nodes').style.display = 'none';
|
||||
} else {
|
||||
document.getElementById('public').innerText = 'status: private';
|
||||
const ul = document.getElementById('allowed-nodes');
|
||||
ul.innerHTML = '';
|
||||
if (settings.allow.length === 0) {
|
||||
const li = document.createElement('li');
|
||||
li.innerHTML = `<li>(none)</li>`;
|
||||
ul.appendChild(li);
|
||||
} else {
|
||||
settings.allow.forEach(allowed_node => {
|
||||
const li = document.createElement('li');
|
||||
li.innerHTML = `<li>${allowed_node}</li>`;
|
||||
ul.appendChild(li);
|
||||
});
|
||||
}
|
||||
}
|
||||
const ul = document.getElementById('denied-nodes');
|
||||
ul.innerHTML = '';
|
||||
if (settings.deny.length === 0) {
|
||||
const li = document.createElement('li');
|
||||
li.innerHTML = `<li>(none)</li>`;
|
||||
ul.appendChild(li);
|
||||
} else {
|
||||
settings.deny.forEach(denied_node => {
|
||||
const li = document.createElement('li');
|
||||
li.innerHTML = `<li>${denied_node}</li>`;
|
||||
ul.appendChild(li);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function populate_process_map(process_map) {
|
||||
// apps we don't want user to kill, also runtime modules that cannot be killed
|
||||
const do_not_kill = [
|
||||
'settings:setting:sys',
|
||||
'main:app_store:sys',
|
||||
'net:distro:sys',
|
||||
'kernel:distro:sys',
|
||||
'kv:distro:sys',
|
||||
'sqlite:distro:sys',
|
||||
'eth:distro:sys',
|
||||
'vfs:distro:sys',
|
||||
'state:distro:sys',
|
||||
'kns_indexer:kns_indexer:sys',
|
||||
'http_client:distro:sys',
|
||||
'http_server:distro:sys',
|
||||
'terminal:terminal:sys',
|
||||
'timer:distro:sys',
|
||||
];
|
||||
|
||||
const ul = document.getElementById('process-map');
|
||||
ul.innerHTML = '';
|
||||
Object.entries(process_map).forEach(([id, process]) => {
|
||||
const li = document.createElement('li');
|
||||
|
||||
const toggleButton = document.createElement('button');
|
||||
toggleButton.textContent = `${id}`;
|
||||
toggleButton.onclick = function () {
|
||||
const details = this.nextElementSibling;
|
||||
details.style.display = details.style.display === 'none' ? 'block' : 'none';
|
||||
};
|
||||
li.appendChild(toggleButton);
|
||||
|
||||
const detailsDiv = document.createElement('div');
|
||||
detailsDiv.style.display = 'none';
|
||||
|
||||
if (!do_not_kill.includes(id)) {
|
||||
const killButton = document.createElement('button');
|
||||
killButton.className = 'kill-process';
|
||||
killButton.setAttribute('data-id', id);
|
||||
killButton.textContent = 'kill';
|
||||
detailsDiv.appendChild(killButton);
|
||||
}
|
||||
|
||||
const publicInfo = document.createElement('p');
|
||||
publicInfo.textContent = `public: ${process.public}`;
|
||||
detailsDiv.appendChild(publicInfo);
|
||||
|
||||
const onExit = document.createElement('p');
|
||||
onExit.textContent = `on_exit: ${process.on_exit}`;
|
||||
detailsDiv.appendChild(onExit);
|
||||
|
||||
if (process.wit_version) {
|
||||
const witVersion = document.createElement('p');
|
||||
witVersion.textContent = `wit_version: ${process.wit_version}`;
|
||||
detailsDiv.appendChild(witVersion);
|
||||
}
|
||||
|
||||
if (process.wasm_bytes_handle) {
|
||||
const wasmBytesHandle = document.createElement('p');
|
||||
wasmBytesHandle.textContent = `wasm_bytes_handle: ${process.wasm_bytes_handle}`;
|
||||
detailsDiv.appendChild(wasmBytesHandle);
|
||||
}
|
||||
|
||||
const capsList = document.createElement('ul');
|
||||
process.capabilities.forEach(cap => {
|
||||
const capLi = document.createElement('li');
|
||||
capLi.textContent = `${cap.issuer}(${JSON.stringify(JSON.parse(cap.params), null, 2)})`;
|
||||
capsList.appendChild(capLi);
|
||||
});
|
||||
detailsDiv.appendChild(capsList);
|
||||
|
||||
li.appendChild(detailsDiv);
|
||||
ul.appendChild(li);
|
||||
});
|
||||
document.querySelectorAll('.kill-process').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
api_call({ "KillProcess": button.getAttribute('data-id') });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function populate_stylesheet_editor(stylesheet) {
|
||||
document.getElementById('stylesheet-editor').value = stylesheet;
|
||||
}
|
||||
|
||||
function save_stylesheet() {
|
||||
const stylesheet = document.getElementById('stylesheet-editor').value;
|
||||
api_call({ "SetStylesheet": stylesheet });
|
||||
}
|
||||
|
||||
// Call init to start the application
|
||||
init();
|
||||
|
||||
// Setup event listeners
|
||||
document.getElementById('shutdown').addEventListener('click', shutdown);
|
||||
|
||||
document.getElementById('save-stylesheet').addEventListener('click', save_stylesheet);
|
||||
|
||||
document.getElementById('get-peer-pki').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const data = new FormData(e.target);
|
||||
const body = {
|
||||
"PeerId": data.get('peer'),
|
||||
};
|
||||
fetch(APP_PATH, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
}).then(response => response.json())
|
||||
.then(data => {
|
||||
if (data === null) {
|
||||
document.getElementById('peer-pki-response').innerText = "no pki data for peer";
|
||||
} else {
|
||||
e.target.reset();
|
||||
document.getElementById('peer-pki-response').innerText = JSON.stringify(data, undefined, 2);
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
document.getElementById('ping-peer').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const data = new FormData(e.target);
|
||||
const body = {
|
||||
"Hi": {
|
||||
node: data.get('peer'),
|
||||
content: data.get('content'),
|
||||
timeout: Number(data.get('timeout')),
|
||||
}
|
||||
};
|
||||
fetch(APP_PATH, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
}).then(response => response.json())
|
||||
.then(data => {
|
||||
if (data === null) {
|
||||
e.target.reset();
|
||||
document.getElementById('peer-ping-response').innerText = "ping successful!";
|
||||
} else if (data === "HiTimeout") {
|
||||
document.getElementById('peer-ping-response').innerText = "node timed out";
|
||||
} else if (data === "HiOffline") {
|
||||
document.getElementById('peer-ping-response').innerText = "node is offline";
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
document.getElementById('add-eth-provider').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const data = new FormData(e.target);
|
||||
const rpc_url = data.get('rpc-url');
|
||||
// validate rpc url
|
||||
if (!rpc_url.startsWith('wss://') && !rpc_url.startsWith('ws://')) {
|
||||
alert('Invalid RPC URL');
|
||||
return;
|
||||
}
|
||||
const body = {
|
||||
"EthConfig": {
|
||||
"AddProvider": {
|
||||
chain_id: Number(data.get('chain-id')),
|
||||
trusted: false,
|
||||
provider: { "RpcUrl": rpc_url },
|
||||
}
|
||||
}
|
||||
};
|
||||
fetch(APP_PATH, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
}).then(response => response.json())
|
||||
.then(data => {
|
||||
if (data === null) {
|
||||
e.target.reset();
|
||||
return;
|
||||
} else {
|
||||
alert(data);
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
document.getElementById('remove-eth-provider').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const data = new FormData(e.target);
|
||||
const body = {
|
||||
"EthConfig": {
|
||||
"RemoveProvider": [Number(data.get('chain-id')), data.get('rpc-url')]
|
||||
}
|
||||
};
|
||||
fetch(APP_PATH, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
}).then(response => response.json())
|
||||
.then(data => {
|
||||
if (data === null) {
|
||||
e.target.reset();
|
||||
return;
|
||||
} else {
|
||||
alert(data);
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
// Setup WebSocket connection
|
||||
const wsProtocol = location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||
const ws = new WebSocket(wsProtocol + location.host + "/settings:settings:sys/");
|
||||
ws.onmessage = event => {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log(data);
|
||||
populate(data);
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user