init
This commit is contained in:
commit
f1791b8699
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
*/target/
|
||||||
|
/target
|
||||||
|
pkg/*.wasm
|
||||||
|
pkg/ui
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*/wasi_snapshot_preview1.wasm
|
||||||
|
*/wit/
|
||||||
|
*/process_env
|
3473
Cargo.lock
generated
Normal file
3473
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
12
Cargo.toml
Normal file
12
Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[workspace]
|
||||||
|
resolver = "2"
|
||||||
|
members = [
|
||||||
|
"twittok",
|
||||||
|
"cookies",
|
||||||
|
"proxy"
|
||||||
|
]
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
panic = "abort"
|
||||||
|
opt-level = "s"
|
||||||
|
lto = true
|
6
api/tok:sortugdev.os.wit
Normal file
6
api/tok:sortugdev.os.wit
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
world tok-sortugdev-dot-os-v0{
|
||||||
|
import all;
|
||||||
|
include process-v0;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface all {}
|
18
cookies/Cargo.toml
Normal file
18
cookies/Cargo.toml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
[package]
|
||||||
|
name = "cookies"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0"
|
||||||
|
kinode_process_lib = { version = "0.9.2", features = ["logging"] }
|
||||||
|
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"
|
316
cookies/src/lib.rs
Normal file
316
cookies/src/lib.rs
Normal file
@ -0,0 +1,316 @@
|
|||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use kinode_process_lib::{
|
||||||
|
await_message, await_next_message_body, call_init, get_blob, get_typed_state,
|
||||||
|
http::{
|
||||||
|
server::{
|
||||||
|
send_response, HttpBindingConfig, HttpServer, HttpServerRequest, IncomingHttpRequest,
|
||||||
|
WsBindingConfig,
|
||||||
|
},
|
||||||
|
StatusCode,
|
||||||
|
},
|
||||||
|
println, set_state, Address, Message, Request,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
wit_bindgen::generate!({
|
||||||
|
path: "target/wit",
|
||||||
|
world: "tok-sortugdev-dot-os-v0",
|
||||||
|
generate_unused_types: true,
|
||||||
|
additional_derives: [serde::Deserialize, serde::Serialize],
|
||||||
|
});
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
enum UIReq {
|
||||||
|
SetCookie { app: String, cookie: CookieMap },
|
||||||
|
SetAPIKey(APIRes),
|
||||||
|
}
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
enum UIRes {
|
||||||
|
All(State),
|
||||||
|
App(AppRes),
|
||||||
|
NotFound,
|
||||||
|
Ack, // ErrorRes,
|
||||||
|
// Ack(Ack)
|
||||||
|
}
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
struct ErrorRes {
|
||||||
|
error: ErrorType,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
enum ErrorType {
|
||||||
|
#[serde(rename = "not found")]
|
||||||
|
NotFound,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
struct Ack {
|
||||||
|
ack: String,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
enum AppRes {
|
||||||
|
API(APIRes),
|
||||||
|
Cookie(CookieRes),
|
||||||
|
}
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||||
|
struct APIRes {
|
||||||
|
app: String,
|
||||||
|
api_key: String,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||||
|
struct CookieRes {
|
||||||
|
app: String,
|
||||||
|
cookie: CookieMap,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct Cookie {
|
||||||
|
domain: String,
|
||||||
|
path: String,
|
||||||
|
name: String,
|
||||||
|
value: String,
|
||||||
|
#[serde(rename = "sameSite")]
|
||||||
|
same_site: SameSite,
|
||||||
|
#[serde(rename = "httpOnly")]
|
||||||
|
http_only: bool,
|
||||||
|
host_only: bool,
|
||||||
|
secure: bool,
|
||||||
|
session: bool,
|
||||||
|
#[serde(rename = "expirationDate", default = "default_expiration")]
|
||||||
|
expiration: f64,
|
||||||
|
#[serde(rename = "storeId")]
|
||||||
|
store_id: Option<String>,
|
||||||
|
}
|
||||||
|
fn default_expiration() -> f64 {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
type CookieMap = HashMap<String, Cookie>;
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
enum SameSite {
|
||||||
|
Strict,
|
||||||
|
Lax,
|
||||||
|
None,
|
||||||
|
#[default]
|
||||||
|
#[serde(alias = "no_restrictions")]
|
||||||
|
#[serde(other)]
|
||||||
|
Unspecified,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||||
|
pub struct State {
|
||||||
|
cookies: HashMap<String, CookieMap>,
|
||||||
|
#[serde(rename = "apiKeys")]
|
||||||
|
api_keys: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
impl State {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
cookies: HashMap::new(),
|
||||||
|
api_keys: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn save(&self) -> Result<()> {
|
||||||
|
let bytes = serde_json::to_vec(self)?;
|
||||||
|
set_state(&bytes);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn load_state() -> State {
|
||||||
|
get_typed_state(|bytes| serde_json::from_slice::<State>(bytes).map_err(Box::new))
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_message(our: &Address) -> anyhow::Result<()> {
|
||||||
|
let message = await_message()?;
|
||||||
|
|
||||||
|
if !message.is_request() {
|
||||||
|
return Err(anyhow::anyhow!("unexpected Response: {:?}", message));
|
||||||
|
}
|
||||||
|
// println!("kinode message received {:?}", message);
|
||||||
|
match message {
|
||||||
|
Message::Request {
|
||||||
|
ref source,
|
||||||
|
ref body,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
return handle_ui(our, body);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_ui(our: &Address, body: &Vec<u8>) -> Result<()> {
|
||||||
|
let Ok(server_request) = serde_json::from_slice::<HttpServerRequest>(body) else {
|
||||||
|
// Fail silently if we can't parse the request
|
||||||
|
// return Err("parsing error");
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
match server_request {
|
||||||
|
HttpServerRequest::WebSocketOpen { channel_id, .. } => Ok(()),
|
||||||
|
HttpServerRequest::WebSocketPush { .. } => Ok(()),
|
||||||
|
HttpServerRequest::WebSocketClose(_channel_id) => Ok(()),
|
||||||
|
HttpServerRequest::Http(request) => handle_http(our, request),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_http(our: &Address, request: IncomingHttpRequest) -> Result<()> {
|
||||||
|
let mut state = load_state();
|
||||||
|
match request.method()?.as_str() {
|
||||||
|
"GET" => handle_get(our, request, &mut state),
|
||||||
|
"POST" => handle_post(our, request, &mut state),
|
||||||
|
_ => {
|
||||||
|
println!("cookies - got weird req - {:?}", request);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn handle_get(our: &Address, req: IncomingHttpRequest, state: &mut State) -> anyhow::Result<()> {
|
||||||
|
// println!("got GET req - {:?}", req);
|
||||||
|
let conc = format!("{}:{}{}", our.process(), our.package_id(), "/api");
|
||||||
|
let pat = req.bound_path(Some(conc.as_str()));
|
||||||
|
// let pats = req.path()?;
|
||||||
|
// let pat = pats.as_str();
|
||||||
|
let uparams = req.url_params();
|
||||||
|
let qparams = req.query_params();
|
||||||
|
|
||||||
|
// println!("request path {}", pat);
|
||||||
|
// println!("request up {:?}", uparams);
|
||||||
|
// println!("request qp {:?}", qparams);
|
||||||
|
match pat {
|
||||||
|
"/debug" => {
|
||||||
|
println!("current state {:?}", state);
|
||||||
|
}
|
||||||
|
"/all" => {
|
||||||
|
let res = UIRes::All(state.to_owned());
|
||||||
|
send_json(res)?;
|
||||||
|
}
|
||||||
|
"/app" => {
|
||||||
|
let app_name = qparams.get("name").unwrap().as_str();
|
||||||
|
let api_param = qparams.get("api").unwrap().as_str();
|
||||||
|
let is_api = api_param == "true";
|
||||||
|
if is_api {
|
||||||
|
let value = state.api_keys.get(app_name);
|
||||||
|
match value {
|
||||||
|
None => {
|
||||||
|
send_json(UIRes::NotFound)?;
|
||||||
|
}
|
||||||
|
Some(v) => {
|
||||||
|
let res = APIRes {
|
||||||
|
app: app_name.to_owned(),
|
||||||
|
api_key: v.to_string(),
|
||||||
|
};
|
||||||
|
send_json(UIRes::App(AppRes::API(res)))?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
let value = state.cookies.get(app_name);
|
||||||
|
match value {
|
||||||
|
None => {
|
||||||
|
send_json(UIRes::NotFound)?;
|
||||||
|
}
|
||||||
|
Some(v) => {
|
||||||
|
let res = CookieRes {
|
||||||
|
app: app_name.to_owned(),
|
||||||
|
cookie: v.to_owned(),
|
||||||
|
};
|
||||||
|
send_json(UIRes::App(AppRes::Cookie(res)))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
send_response(StatusCode::CREATED, None, vec![]);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_json(res: UIRes) -> Result<()> {
|
||||||
|
let body = serde_json::to_vec(&res)?;
|
||||||
|
let mut headers = HashMap::new();
|
||||||
|
headers.insert("Content-Type".to_string(), "application/json".to_string());
|
||||||
|
send_response(StatusCode::OK, Some(headers), body);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_test() {
|
||||||
|
let str = r#"
|
||||||
|
{"auth_multi":{"domain":".x.com","expirationDate":1736167622.15024,"hostOnly":false,"httpOnly":true,"name":"auth_multi","path":"/","sameSite":"lax","secure":true,"session":false,"storeId":"0","value":"\"1710606417324015616:604ed700cfb5c8a5d2665086503f1ec8b6032ef4\"","id":1},"auth_token":{"domain":".x.com","expirationDate":1736097942.573061,"hostOnly":false,"httpOnly":true,"name":"auth_token","path":"/","sameSite":"no_restriction","secure":true,"session":false,"storeId":"0","value":"2527de3d3719d900cd5658525e559d0966d86662","id":2},"ct0":{"domain":".x.com","expirationDate":1736097942.861119,"hostOnly":false,"httpOnly":false,"name":"ct0","path":"/","sameSite":"lax","secure":true,"session":false,"storeId":"0","value":"1461eb581cb824ea00b652d27a735f1abebe2b47de834cddd624afdbee20c4f033972cb63fd298d0db8cded9a75429df3907ced5bb375f12cf02100825b16f7c02a00253ca1e8883b60ace17ccea1622","id":3},"dnt":{"domain":".x.com","expirationDate":1736097942.572255,"hostOnly":false,"httpOnly":false,"name":"dnt","path":"/","sameSite":"no_restriction","secure":true,"session":false,"storeId":"0","value":"1","id":4},"external_referer":{"domain":".x.com","expirationDate":1721160063.509523,"hostOnly":false,"httpOnly":false,"name":"external_referer","path":"/","sameSite":"unspecified","secure":true,"session":false,"storeId":"0","value":"padhuUp37zjgzgv1mFWxJ12Ozwit7owX|0|8e8t2xd8A2w%3D","id":5},"guest_id":{"domain":".x.com","expirationDate":1736097942.861021,"hostOnly":false,"httpOnly":false,"name":"guest_id","path":"/","sameSite":"no_restriction","secure":true,"session":false,"storeId":"0","value":"v1%3A172054594269674803","id":6},"guest_id_ads":{"domain":".x.com","expirationDate":1736167616.625858,"hostOnly":false,"httpOnly":false,"name":"guest_id_ads","path":"/","sameSite":"no_restriction","secure":true,"session":false,"storeId":"0","value":"v1%3A172054594269674803","id":7},"guest_id_marketing":{"domain":".x.com","expirationDate":1736167616.625932,"hostOnly":false,"httpOnly":false,"name":"guest_id_marketing","path":"/","sameSite":"no_restriction","secure":true,"session":false,"storeId":"0","value":"v1%3A172054594269674803","id":8},"kdt":{"domain":".x.com","expirationDate":1736097942.572385,"hostOnly":false,"httpOnly":true,"name":"kdt","path":"/","sameSite":"unspecified","secure":true,"session":false,"storeId":"0","value":"iWdBqeAH3UcgpwzPxi6CZ2lRTk4Fqia3OR5VbiSo","id":9},"personalization_id":{"domain":".x.com","expirationDate":1736167616.625994,"hostOnly":false,"httpOnly":false,"name":"personalization_id","path":"/","sameSite":"no_restriction","secure":true,"session":false,"storeId":"0","value":"\"v1_O5kcdlrANZbUakXhbmlSTw==\"","id":10},"twid":{"domain":".x.com","expirationDate":1736167616.773335,"hostOnly":false,"httpOnly":false,"name":"twid","path":"/","sameSite":"no_restriction","secure":true,"session":false,"storeId":"0","value":"u%3D1809740330922831872","id":11},"d_prefs":{"domain":"x.com","hostOnly":true,"httpOnly":false,"name":"d_prefs","path":"/","sameSite":"unspecified","secure":true,"session":true,"storeId":"0","value":"MjoxLGNvbnNlbnRfdmVyc2lvbjoyLHRleHRfdmVyc2lvbjoxMDAw","id":12},"lang":{"domain":"x.com","hostOnly":true,"httpOnly":false,"name":"lang","path":"/","sameSite":"unspecified","secure":false,"session":true,"storeId":"0","value":"en","id":13}}
|
||||||
|
"#;
|
||||||
|
let pj = serde_json::from_str::<CookieMap>(str);
|
||||||
|
println!("json parsing test {:?}", pj);
|
||||||
|
// let jason = r#"
|
||||||
|
// {
|
||||||
|
// "domain": ".x.com",
|
||||||
|
// "expirationDate": 1736167622.15024,
|
||||||
|
// "hostOnly": false,
|
||||||
|
// "httpOnly": true,
|
||||||
|
// "name": "auth_multi",
|
||||||
|
// "path": "/",
|
||||||
|
// "sameSite": "lax",
|
||||||
|
// "secure": true,
|
||||||
|
// "session": false,
|
||||||
|
// "storeId": "0",
|
||||||
|
// "value": "\"1710606417324015616:604ed700cfb5c8a5d2665086503f1ec8b6032ef4\"",
|
||||||
|
// "id": 1
|
||||||
|
// }
|
||||||
|
// "#;
|
||||||
|
// let pj = serde_json::from_str::<Cookie>(jason);
|
||||||
|
// println!("json parsing test {:?}", pj);
|
||||||
|
// // let example: UIReq = UIReq::SetAPIKey(APIRes {
|
||||||
|
// // app: "foo".to_string(),
|
||||||
|
// // api_key: "bar".to_string(),
|
||||||
|
// // });
|
||||||
|
// // let value = serde_json::to_string(&example)?;
|
||||||
|
// // println!("expected json {}", value);
|
||||||
|
}
|
||||||
|
fn handle_post(our: &Address, req: IncomingHttpRequest, state: &mut State) -> anyhow::Result<()> {
|
||||||
|
println!("cokies post request!");
|
||||||
|
// parse_test();
|
||||||
|
|
||||||
|
// let conc = format!("{}:{}{}", our.process(), our.package_id(), "/api");
|
||||||
|
let Some(blob) = get_blob() else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let Ok(post_request) = serde_json::from_slice::<UIReq>(&blob.bytes) else {
|
||||||
|
// Fail silently if we can't parse the request
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
println!("post body {:?}", post_request);
|
||||||
|
|
||||||
|
match post_request {
|
||||||
|
UIReq::SetCookie { app, cookie } => {
|
||||||
|
state.cookies.insert(app, cookie);
|
||||||
|
state.save()?;
|
||||||
|
send_json(UIRes::Ack)?;
|
||||||
|
}
|
||||||
|
UIReq::SetAPIKey(ar) => {
|
||||||
|
state.api_keys.insert(ar.app, ar.api_key);
|
||||||
|
state.save()?;
|
||||||
|
send_json(UIRes::Ack)?;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
call_init!(init);
|
||||||
|
fn init(our: Address) {
|
||||||
|
let mut http_server = HttpServer::new(5);
|
||||||
|
let http_config = HttpBindingConfig::default();
|
||||||
|
|
||||||
|
let ws_config = WsBindingConfig::default();
|
||||||
|
http_server.bind_ws_path("/", ws_config.clone());
|
||||||
|
// REST API
|
||||||
|
http_server.bind_http_path("/", http_config.clone());
|
||||||
|
http_server.bind_http_path("/api", http_config.clone());
|
||||||
|
http_server.bind_http_path("/api/all", http_config.clone());
|
||||||
|
http_server.bind_http_path("/api/app", http_config.clone());
|
||||||
|
loop {
|
||||||
|
match handle_message(&our) {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(e) => {
|
||||||
|
println!("error: {:?}", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
18
metadata.json
Normal file
18
metadata.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "tok",
|
||||||
|
"description": "Tweet Tok",
|
||||||
|
"image": "",
|
||||||
|
"properties": {
|
||||||
|
"package_name": "tok",
|
||||||
|
"current_version": "0.1.0",
|
||||||
|
"publisher": "sortugdev.os",
|
||||||
|
"mirrors": [],
|
||||||
|
"code_hashes": {
|
||||||
|
"0.1.0": ""
|
||||||
|
},
|
||||||
|
"wit_version": 0,
|
||||||
|
"dependencies": []
|
||||||
|
},
|
||||||
|
"external_url": "",
|
||||||
|
"animation_url": ""
|
||||||
|
}
|
BIN
pkg/api.zip
Normal file
BIN
pkg/api.zip
Normal file
Binary file not shown.
44
pkg/manifest.json
Normal file
44
pkg/manifest.json
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"process_name": "cookies",
|
||||||
|
"process_wasm_path": "/cookies.wasm",
|
||||||
|
"on_exit": "Restart",
|
||||||
|
"request_networking": true,
|
||||||
|
"request_capabilities": [
|
||||||
|
"http_server:distro:sys"
|
||||||
|
],
|
||||||
|
"grant_capabilities": [],
|
||||||
|
"public": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"process_name": "proxy",
|
||||||
|
"process_wasm_path": "/proxy.wasm",
|
||||||
|
"on_exit": "Restart",
|
||||||
|
"request_networking": true,
|
||||||
|
"request_capabilities": [
|
||||||
|
"http_server:distro:sys",
|
||||||
|
"http_client:distro:sys"
|
||||||
|
],
|
||||||
|
"grant_capabilities": [
|
||||||
|
"http_server:distro:sys",
|
||||||
|
"http_client:distro:sys"
|
||||||
|
],
|
||||||
|
"public": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"process_name": "twittok",
|
||||||
|
"process_wasm_path": "/twittok.wasm",
|
||||||
|
"on_exit": "Restart",
|
||||||
|
"request_networking": true,
|
||||||
|
"request_capabilities": [
|
||||||
|
"homepage:homepage:sys",
|
||||||
|
"http_server:distro:sys",
|
||||||
|
"http_client:distro:sys"
|
||||||
|
],
|
||||||
|
"grant_capabilities": [
|
||||||
|
"http_server:distro:sys",
|
||||||
|
"http_client:distro:sys"
|
||||||
|
],
|
||||||
|
"public": true
|
||||||
|
}
|
||||||
|
]
|
21
proxy/Cargo.toml
Normal file
21
proxy/Cargo.toml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
[package]
|
||||||
|
name = "proxy"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0"
|
||||||
|
kinode_process_lib = { version = "0.9.2", features = ["logging"] }
|
||||||
|
process_macros = { git = "https://github.com/kinode-dao/process_macros", rev = "626e501" }
|
||||||
|
mime = "0.3.17"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
strsim = "0.10.0"
|
||||||
|
wit-bindgen = "0.24.0"
|
||||||
|
url = "2.5.2"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[package.metadata.component]
|
||||||
|
package = "kinode:process"
|
141
proxy/src/lib.rs
Normal file
141
proxy/src/lib.rs
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use kinode_process_lib::{
|
||||||
|
await_message, call_init, get_blob,
|
||||||
|
http::{
|
||||||
|
server::{
|
||||||
|
send_response, HttpBindingConfig, HttpServer, HttpServerRequest, IncomingHttpRequest,
|
||||||
|
WsBindingConfig,
|
||||||
|
},
|
||||||
|
StatusCode,
|
||||||
|
},
|
||||||
|
println, Address, Message, ProcessId, Request, Response,
|
||||||
|
};
|
||||||
|
|
||||||
|
mod types;
|
||||||
|
use types::*;
|
||||||
|
mod proxy;
|
||||||
|
|
||||||
|
wit_bindgen::generate!({
|
||||||
|
path: "target/wit",
|
||||||
|
world: "tok-sortugdev-dot-os-v0",
|
||||||
|
generate_unused_types: true,
|
||||||
|
additional_derives: [serde::Deserialize, serde::Serialize],
|
||||||
|
});
|
||||||
|
|
||||||
|
fn handle_message(our: &Address) -> anyhow::Result<()> {
|
||||||
|
let message = await_message()?;
|
||||||
|
if !message.is_request() {
|
||||||
|
return Err(anyhow::anyhow!("unexpected Response: {:?}", message));
|
||||||
|
}
|
||||||
|
match message {
|
||||||
|
Message::Request {
|
||||||
|
ref source,
|
||||||
|
ref body,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
return handle_ui(our, body);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn handle_ui(our: &Address, body: &Vec<u8>) -> Result<()> {
|
||||||
|
let Ok(server_request) = serde_json::from_slice::<HttpServerRequest>(body) else {
|
||||||
|
// Fail silently if we can't parse the request
|
||||||
|
// return Err("parsing error");
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
match server_request {
|
||||||
|
HttpServerRequest::WebSocketOpen { channel_id, .. } => Ok(()),
|
||||||
|
HttpServerRequest::WebSocketPush { .. } => Ok(()),
|
||||||
|
HttpServerRequest::WebSocketClose(_channel_id) => Ok(()),
|
||||||
|
HttpServerRequest::Http(request) => handle_http(our, request),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_http(our: &Address, request: IncomingHttpRequest) -> Result<()> {
|
||||||
|
match request.method()?.as_str() {
|
||||||
|
"POST" => handle_post(our, request),
|
||||||
|
_ => {
|
||||||
|
println!("got weird req - {:?}", request);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn handle_post(our: &Address, req: IncomingHttpRequest) -> anyhow::Result<()> {
|
||||||
|
println!("post request!");
|
||||||
|
// parse_test();
|
||||||
|
|
||||||
|
// let conc = format!("{}:{}{}", our.process(), our.package_id(), "/api");
|
||||||
|
let Some(blob) = get_blob() else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let Ok(post_request) = serde_json::from_slice::<UIReq>(&blob.bytes) else {
|
||||||
|
// Fail silently if we can't parse the request
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let res = proxy::run(post_request);
|
||||||
|
match res {
|
||||||
|
Ok((mime, body)) => {
|
||||||
|
handle_body(mime, body);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let s = e.to_string();
|
||||||
|
send_json(UIRes::Err { error: s })?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn handle_body(mt: mime::Mime, body: Vec<u8>) -> Result<()> {
|
||||||
|
match (mt.type_(), mt.subtype()) {
|
||||||
|
(mime::APPLICATION, mime::JSON) => {
|
||||||
|
let s = String::from_utf8(body)?;
|
||||||
|
send_json(UIRes::Ok { result: s })?;
|
||||||
|
}
|
||||||
|
(mime::AUDIO, mime::MPEG) => {
|
||||||
|
send_mime(mt, body)?;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
println!("unhandled mime type {:?}", mt);
|
||||||
|
// Err(anyhow!(""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn send_mime(mt: mime::Mime, body: Vec<u8>) -> Result<()> {
|
||||||
|
let mut headers = HashMap::new();
|
||||||
|
headers.insert("Content-Type".to_string(), mt.to_string());
|
||||||
|
send_response(StatusCode::OK, Some(headers), body);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn send_json(res: UIRes) -> Result<()> {
|
||||||
|
let body = serde_json::to_vec(&res)?;
|
||||||
|
let mut headers = HashMap::new();
|
||||||
|
headers.insert("Content-Type".to_string(), "application/json".to_string());
|
||||||
|
send_response(StatusCode::OK, Some(headers), body);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
call_init!(init);
|
||||||
|
fn init(our: Address) {
|
||||||
|
println!("begin proxy");
|
||||||
|
println!("our {:?}", our);
|
||||||
|
println!("our {:?}", our.process());
|
||||||
|
let mut http_server = HttpServer::new(5);
|
||||||
|
let http_config = HttpBindingConfig::default();
|
||||||
|
|
||||||
|
let ws_config = WsBindingConfig::default();
|
||||||
|
http_server.bind_ws_path("/", ws_config.clone());
|
||||||
|
// REST API
|
||||||
|
http_server.bind_http_path("/api", http_config.clone());
|
||||||
|
loop {
|
||||||
|
match handle_message(&our) {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(e) => {
|
||||||
|
println!("error: {:?}", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
92
proxy/src/proxy.rs
Normal file
92
proxy/src/proxy.rs
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
use std::io::BufRead;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use kinode_process_lib::{
|
||||||
|
http::{client::send_request_await_response, Method},
|
||||||
|
println,
|
||||||
|
};
|
||||||
|
use mime::Mime;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::UIReq;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
|
pub enum ScrapeRes {
|
||||||
|
Image(String),
|
||||||
|
HTML(String),
|
||||||
|
}
|
||||||
|
pub fn scrape(url: &str) -> Result<ScrapeRes> {
|
||||||
|
let url = Url::parse(url)?;
|
||||||
|
let mut headers = std::collections::HashMap::new();
|
||||||
|
headers.insert(
|
||||||
|
"User-Agent".to_string(),
|
||||||
|
"facebookexternalhit/1.1".to_string(),
|
||||||
|
// "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let res = send_request_await_response(Method::GET, url.clone(), Some(headers), 5000, vec![])?;
|
||||||
|
let h = res.headers().get("content-type");
|
||||||
|
match h {
|
||||||
|
None => {
|
||||||
|
let b = res.body().to_vec();
|
||||||
|
let text = String::from_utf8(b)?;
|
||||||
|
Ok(ScrapeRes::HTML(text))
|
||||||
|
}
|
||||||
|
Some(val) => {
|
||||||
|
let str = val.to_str()?;
|
||||||
|
if str.starts_with("image") {
|
||||||
|
Ok(ScrapeRes::Image(url.to_string()))
|
||||||
|
} else {
|
||||||
|
let b = res.body().to_vec();
|
||||||
|
let text = String::from_utf8(b)?;
|
||||||
|
Ok(ScrapeRes::HTML(text))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// let body = get_blob().ok_or(anyhow::anyhow!("no blob"))?;
|
||||||
|
}
|
||||||
|
pub fn proxy(url: &str) -> Result<Vec<u8>> {
|
||||||
|
let url = Url::parse(url)?;
|
||||||
|
let mut headers = std::collections::HashMap::new();
|
||||||
|
headers.insert(
|
||||||
|
"User-Agent".to_string(),
|
||||||
|
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let res = send_request_await_response(Method::GET, url.clone(), Some(headers), 5000, vec![])?;
|
||||||
|
let b = res.body().to_vec();
|
||||||
|
Ok(b)
|
||||||
|
// let body = get_blob().ok_or(anyhow::anyhow!("no blob"))?;
|
||||||
|
}
|
||||||
|
pub fn run(req: UIReq) -> Result<(Mime, Vec<u8>)> {
|
||||||
|
let url = Url::parse(&req.url)?;
|
||||||
|
let body: Vec<u8> = match req.body {
|
||||||
|
None => vec![],
|
||||||
|
Some(s) => s.as_bytes().to_vec(),
|
||||||
|
};
|
||||||
|
let mut headers = req.headers.clone();
|
||||||
|
headers.insert(
|
||||||
|
"User-Agent".to_string(),
|
||||||
|
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36".to_string(),
|
||||||
|
);
|
||||||
|
// println!("running req {:?} {:?}", url, headers);
|
||||||
|
|
||||||
|
let res = send_request_await_response(req.method, url.clone(), Some(req.headers), 5000, body)?;
|
||||||
|
let h = res.headers();
|
||||||
|
println!("res headers {:?}", h);
|
||||||
|
let content_type: Mime = res
|
||||||
|
.headers()
|
||||||
|
.get("content-type")
|
||||||
|
.and_then(|ct| ct.to_str().ok())
|
||||||
|
.and_then(|ct| ct.parse::<Mime>().ok())
|
||||||
|
.ok_or_else(|| anyhow!("invalid content type"))?;
|
||||||
|
println!(
|
||||||
|
"fucking mime {:?} {:?}",
|
||||||
|
content_type.type_(),
|
||||||
|
content_type.subtype()
|
||||||
|
);
|
||||||
|
let b = res.body().to_vec();
|
||||||
|
Ok((content_type, b))
|
||||||
|
// let body = get_blob().ok_or(anyhow::anyhow!("no blob"))?;
|
||||||
|
}
|
39
proxy/src/types.rs
Normal file
39
proxy/src/types.rs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
use kinode_process_lib::http::Method;
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct UIReq {
|
||||||
|
pub url: String,
|
||||||
|
#[serde(deserialize_with = "deserialize_method")]
|
||||||
|
pub method: Method,
|
||||||
|
pub headers: HashMap<String, String>,
|
||||||
|
pub body: Option<String>,
|
||||||
|
}
|
||||||
|
fn deserialize_method<'de, D>(deserializer: D) -> Result<Method, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s: String = String::deserialize(deserializer)?;
|
||||||
|
match s.to_uppercase().as_str() {
|
||||||
|
"GET" => Ok(Method::GET),
|
||||||
|
"POST" => Ok(Method::POST),
|
||||||
|
"PUT" => Ok(Method::PUT),
|
||||||
|
"DELETE" => Ok(Method::DELETE),
|
||||||
|
"HEAD" => Ok(Method::HEAD),
|
||||||
|
"OPTIONS" => Ok(Method::OPTIONS),
|
||||||
|
"CONNECT" => Ok(Method::CONNECT),
|
||||||
|
"PATCH" => Ok(Method::PATCH),
|
||||||
|
"TRACE" => Ok(Method::TRACE),
|
||||||
|
_ => Err(serde::de::Error::custom(format!(
|
||||||
|
"Unknown HTTP method: {}",
|
||||||
|
s
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub enum UIRes {
|
||||||
|
Ok { result: String },
|
||||||
|
Err { error: String },
|
||||||
|
}
|
18
twittok/Cargo.toml
Normal file
18
twittok/Cargo.toml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
[package]
|
||||||
|
name = "twittok"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0"
|
||||||
|
kinode_process_lib = { version = "0.9.2", features = ["logging"] }
|
||||||
|
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"
|
288
twittok/src/lib.rs
Normal file
288
twittok/src/lib.rs
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use kinode_process_lib::{
|
||||||
|
await_message, call_init, get_blob,
|
||||||
|
http::{
|
||||||
|
server::{
|
||||||
|
send_response, send_ws_push, HttpBindingConfig, HttpServer, HttpServerRequest,
|
||||||
|
WsBindingConfig,
|
||||||
|
},
|
||||||
|
StatusCode,
|
||||||
|
},
|
||||||
|
println, Address, LazyLoadBlob, Message, ProcessId, Request, Response,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
wit_bindgen::generate!({
|
||||||
|
path: "target/wit",
|
||||||
|
world: "tok-sortugdev-dot-os-v0",
|
||||||
|
generate_unused_types: true,
|
||||||
|
additional_derives: [serde::Deserialize, serde::Serialize]
|
||||||
|
});
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
enum TwittokRequest {
|
||||||
|
Send { target: String, message: String },
|
||||||
|
History,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
enum TwittokResponse {
|
||||||
|
Ack,
|
||||||
|
History { messages: MessageArchive },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
struct TwittokMessage {
|
||||||
|
author: String,
|
||||||
|
content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct NewMessage {
|
||||||
|
twittok: String,
|
||||||
|
author: String,
|
||||||
|
content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageArchive = HashMap<String, Vec<TwittokMessage>>;
|
||||||
|
|
||||||
|
fn handle_http_server_request(
|
||||||
|
our: &Address,
|
||||||
|
message_archive: &mut MessageArchive,
|
||||||
|
our_channel_id: &mut u32,
|
||||||
|
source: &Address,
|
||||||
|
body: &[u8],
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let Ok(server_request) = serde_json::from_slice::<HttpServerRequest>(body) else {
|
||||||
|
// Fail silently if we can't parse the request
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
match server_request {
|
||||||
|
HttpServerRequest::WebSocketOpen { channel_id, .. } => {
|
||||||
|
// Set our channel_id to the newly opened channel
|
||||||
|
// Note: this code could be improved to support multiple channels
|
||||||
|
*our_channel_id = channel_id;
|
||||||
|
}
|
||||||
|
HttpServerRequest::WebSocketPush { .. } => {
|
||||||
|
let Some(blob) = get_blob() else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
handle_twittok_request(
|
||||||
|
our,
|
||||||
|
message_archive,
|
||||||
|
our_channel_id,
|
||||||
|
source,
|
||||||
|
&blob.bytes,
|
||||||
|
false,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
HttpServerRequest::WebSocketClose(_channel_id) => {}
|
||||||
|
HttpServerRequest::Http(request) => {
|
||||||
|
match request.method()?.as_str() {
|
||||||
|
// Get all messages
|
||||||
|
"GET" => {
|
||||||
|
let mut headers = HashMap::new();
|
||||||
|
headers.insert("Content-Type".to_string(), "application/json".to_string());
|
||||||
|
|
||||||
|
send_response(
|
||||||
|
StatusCode::OK,
|
||||||
|
Some(headers),
|
||||||
|
serde_json::to_vec(&TwittokResponse::History {
|
||||||
|
messages: message_archive.clone(),
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Send a message
|
||||||
|
"POST" => {
|
||||||
|
let Some(blob) = get_blob() else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
handle_twittok_request(
|
||||||
|
our,
|
||||||
|
message_archive,
|
||||||
|
our_channel_id,
|
||||||
|
source,
|
||||||
|
&blob.bytes,
|
||||||
|
true,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Send an http response via the http server
|
||||||
|
send_response(StatusCode::CREATED, None, vec![]);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Method not allowed
|
||||||
|
send_response(StatusCode::METHOD_NOT_ALLOWED, None, vec![]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_twittok_request(
|
||||||
|
our: &Address,
|
||||||
|
message_archive: &mut MessageArchive,
|
||||||
|
channel_id: &mut u32,
|
||||||
|
source: &Address,
|
||||||
|
body: &[u8],
|
||||||
|
is_http: bool,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let Ok(twittok_request) = serde_json::from_slice::<TwittokRequest>(body) else {
|
||||||
|
// Fail silently if we can't parse the request
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
match twittok_request {
|
||||||
|
TwittokRequest::Send {
|
||||||
|
ref target,
|
||||||
|
ref message,
|
||||||
|
} => {
|
||||||
|
// counterparty will be the other node in the twittok with us
|
||||||
|
let (counterparty, author) = if target == &our.node {
|
||||||
|
(&source.node, source.node.clone())
|
||||||
|
} else {
|
||||||
|
(target, our.node.clone())
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the target is not us, send a request to the target
|
||||||
|
if target == &our.node {
|
||||||
|
println!("{}: {}", source.node, message);
|
||||||
|
} else {
|
||||||
|
Request::new()
|
||||||
|
.target(Address {
|
||||||
|
node: target.clone(),
|
||||||
|
process: ProcessId::from_str("twittok:twittok:template.os")?,
|
||||||
|
})
|
||||||
|
.body(body)
|
||||||
|
.send_and_await_response(5)??;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retreive the message archive for the counterparty, or create a new one if it doesn't exist
|
||||||
|
let messages = match message_archive.get_mut(counterparty) {
|
||||||
|
Some(messages) => messages,
|
||||||
|
None => {
|
||||||
|
message_archive.insert(counterparty.clone(), Vec::new());
|
||||||
|
message_archive.get_mut(counterparty).unwrap()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_message = TwittokMessage {
|
||||||
|
author: author.clone(),
|
||||||
|
content: message.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// If this is an HTTP request, handle the response in the calling function
|
||||||
|
if is_http {
|
||||||
|
// Add the new message to the archive
|
||||||
|
messages.push(new_message);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is not an HTTP request, send a response to the other node
|
||||||
|
Response::new()
|
||||||
|
.body(serde_json::to_vec(&TwittokResponse::Ack).unwrap())
|
||||||
|
.send()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Add the new message to the archive
|
||||||
|
messages.push(new_message);
|
||||||
|
|
||||||
|
// Generate a blob for the new message
|
||||||
|
let blob = LazyLoadBlob {
|
||||||
|
mime: Some("application/json".to_string()),
|
||||||
|
bytes: serde_json::json!({
|
||||||
|
"NewMessage": NewMessage {
|
||||||
|
twittok: counterparty.clone(),
|
||||||
|
author,
|
||||||
|
content: message.clone(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.to_string()
|
||||||
|
.as_bytes()
|
||||||
|
.to_vec(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send a WebSocket message to the http server in order to update the UI
|
||||||
|
send_ws_push(
|
||||||
|
channel_id.clone(),
|
||||||
|
kinode_process_lib::http::server::WsMessageType::Text,
|
||||||
|
blob,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
TwittokRequest::History => {
|
||||||
|
// If this is an HTTP request, send a response to the http server
|
||||||
|
|
||||||
|
Response::new()
|
||||||
|
.body(
|
||||||
|
serde_json::to_vec(&TwittokResponse::History {
|
||||||
|
messages: message_archive.clone(),
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.send()
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_message(
|
||||||
|
our: &Address,
|
||||||
|
message_archive: &mut MessageArchive,
|
||||||
|
channel_id: &mut u32,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let message = await_message().unwrap();
|
||||||
|
|
||||||
|
match message {
|
||||||
|
Message::Response { .. } => {
|
||||||
|
println!("got response - {:?}", message);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Message::Request {
|
||||||
|
ref source,
|
||||||
|
ref body,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
// Requests that come from other nodes running this app
|
||||||
|
handle_twittok_request(our, message_archive, channel_id, source, body, false)?;
|
||||||
|
// Requests that come from our http server
|
||||||
|
handle_http_server_request(our, message_archive, channel_id, source, body)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
call_init!(init);
|
||||||
|
fn init(our: Address) {
|
||||||
|
println!("begin");
|
||||||
|
|
||||||
|
let mut message_archive: MessageArchive = HashMap::new();
|
||||||
|
let mut channel_id = 0;
|
||||||
|
|
||||||
|
let mut http_server = HttpServer::new(5);
|
||||||
|
let http_config = HttpBindingConfig::default();
|
||||||
|
|
||||||
|
let ws_config = WsBindingConfig::default();
|
||||||
|
|
||||||
|
http_server.bind_ws_path("/", ws_config.clone());
|
||||||
|
// REST API
|
||||||
|
http_server.bind_http_path("/api", http_config.clone());
|
||||||
|
http_server.serve_ui(&our, "ui", vec!["/"], http_config.clone());
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match handle_message(&our, &mut message_archive, &mut channel_id) {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(e) => {
|
||||||
|
println!("error: {:?}", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
18
ui/.eslintrc.cjs
Normal file
18
ui/.eslintrc.cjs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: { browser: true, es2020: true },
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
],
|
||||||
|
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: ['react-refresh'],
|
||||||
|
rules: {
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
24
ui/.gitignore
vendored
Normal file
24
ui/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
56
ui/README.md
Normal file
56
ui/README.md
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# Kinode UI Template
|
||||||
|
|
||||||
|
Based on the Vite React Typescript template.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
When using `kit new`, the `BASE_URL` on line 9 of `vite.config.ts` will be set automatically.
|
||||||
|
The `BASE_URL` will be the first process in `manifest.json`, the `package` from `metadata.json`, and `publisher` from `metadata.json`.
|
||||||
|
If you have multiple processes in `manifest.json`, make sure the first process will be the one serving the UI.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Run `npm i` and then `npm run dev` to start working on the UI.
|
||||||
|
|
||||||
|
You may see an error:
|
||||||
|
|
||||||
|
```
|
||||||
|
[vite] Pre-transform error: Failed to load url /our.js (resolved id: /our.js). Does the file exist?
|
||||||
|
```
|
||||||
|
|
||||||
|
You can safely ignore this error. The file will be served by the node via the proxy.
|
||||||
|
|
||||||
|
## public vs assets
|
||||||
|
|
||||||
|
The `public/assets` folder contains files that are referenced in `index.html`, `src/assets` is for asset files that are only referenced in `src` code.
|
||||||
|
|
||||||
|
## About Vite + React
|
||||||
|
|
||||||
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
|
|
||||||
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
|
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
||||||
|
|
||||||
|
- Configure the top-level `parserOptions` property like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default {
|
||||||
|
// other rules...
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module',
|
||||||
|
project: ['./tsconfig.json', './tsconfig.node.json'],
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
|
||||||
|
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
|
||||||
|
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
|
16
ui/index.html
Normal file
16
ui/index.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<!-- This sets window.our.node -->
|
||||||
|
<script src="/our.js"></script>
|
||||||
|
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>抖推</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
3482
ui/package-lock.json
generated
Normal file
3482
ui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
37
ui/package.json
Normal file
37
ui/package.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "ui-template",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"copy": "mkdir -p ../pkg/ui && rm -rf ../pkg/ui/* && cp -r dist/* ../pkg/ui/",
|
||||||
|
"build:copy": "npm run build && npm run copy",
|
||||||
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@kinode/client-api": "^0.1.0",
|
||||||
|
"@tanstack/react-query": "^5.50.1",
|
||||||
|
"hls.js": "^1.5.13",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-swipeable": "^7.0.1",
|
||||||
|
"zustand": "^5.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.10.4",
|
||||||
|
"@types/react": "^18.2.43",
|
||||||
|
"@types/react-dom": "^18.2.17",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||||
|
"@typescript-eslint/parser": "^6.14.0",
|
||||||
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"eslint": "^8.55.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.5",
|
||||||
|
"http-proxy-middleware": "^2.0.6",
|
||||||
|
"typescript": "^5.6.3",
|
||||||
|
"vite": "^5.3.1"
|
||||||
|
}
|
||||||
|
}
|
1
ui/public/assets/vite.svg
Normal file
1
ui/public/assets/vite.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
BIN
ui/public/fonts/Charm-Bold.ttf
Executable file
BIN
ui/public/fonts/Charm-Bold.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Charm-Regular.ttf
Executable file
BIN
ui/public/fonts/Charm-Regular.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Chonburi-Regular.ttf
Executable file
BIN
ui/public/fonts/Chonburi-Regular.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/IBMPlexSansThai-Bold.ttf
Executable file
BIN
ui/public/fonts/IBMPlexSansThai-Bold.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/IBMPlexSansThai-ExtraLight.ttf
Executable file
BIN
ui/public/fonts/IBMPlexSansThai-ExtraLight.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/IBMPlexSansThai-Light.ttf
Executable file
BIN
ui/public/fonts/IBMPlexSansThai-Light.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/IBMPlexSansThai-Medium.ttf
Executable file
BIN
ui/public/fonts/IBMPlexSansThai-Medium.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/IBMPlexSansThai-Regular.ttf
Executable file
BIN
ui/public/fonts/IBMPlexSansThai-Regular.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/IBMPlexSansThai-SemiBold.ttf
Executable file
BIN
ui/public/fonts/IBMPlexSansThai-SemiBold.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/IBMPlexSansThai-Thin.ttf
Executable file
BIN
ui/public/fonts/IBMPlexSansThai-Thin.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Judson-Bold.ttf
Executable file
BIN
ui/public/fonts/Judson-Bold.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Judson-Italic.ttf
Executable file
BIN
ui/public/fonts/Judson-Italic.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Judson-Regular.ttf
Executable file
BIN
ui/public/fonts/Judson-Regular.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Kanit-Black.ttf
Executable file
BIN
ui/public/fonts/Kanit-Black.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Kanit-BlackItalic.ttf
Executable file
BIN
ui/public/fonts/Kanit-BlackItalic.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Kanit-Bold.ttf
Executable file
BIN
ui/public/fonts/Kanit-Bold.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Kanit-BoldItalic.ttf
Executable file
BIN
ui/public/fonts/Kanit-BoldItalic.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Kanit-ExtraBold.ttf
Executable file
BIN
ui/public/fonts/Kanit-ExtraBold.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Kanit-ExtraBoldItalic.ttf
Executable file
BIN
ui/public/fonts/Kanit-ExtraBoldItalic.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Kanit-ExtraLight.ttf
Executable file
BIN
ui/public/fonts/Kanit-ExtraLight.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Kanit-ExtraLightItalic.ttf
Executable file
BIN
ui/public/fonts/Kanit-ExtraLightItalic.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Kanit-Italic.ttf
Executable file
BIN
ui/public/fonts/Kanit-Italic.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Kanit-Light.ttf
Executable file
BIN
ui/public/fonts/Kanit-Light.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Kanit-LightItalic.ttf
Executable file
BIN
ui/public/fonts/Kanit-LightItalic.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Kanit-Medium.ttf
Executable file
BIN
ui/public/fonts/Kanit-Medium.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Kanit-MediumItalic.ttf
Executable file
BIN
ui/public/fonts/Kanit-MediumItalic.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Kanit-Regular.ttf
Executable file
BIN
ui/public/fonts/Kanit-Regular.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Kanit-SemiBold.ttf
Executable file
BIN
ui/public/fonts/Kanit-SemiBold.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Kanit-SemiBoldItalic.ttf
Executable file
BIN
ui/public/fonts/Kanit-SemiBoldItalic.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Kanit-Thin.ttf
Executable file
BIN
ui/public/fonts/Kanit-Thin.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Kanit-ThinItalic.ttf
Executable file
BIN
ui/public/fonts/Kanit-ThinItalic.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Kodchasan-Bold.ttf
Executable file
BIN
ui/public/fonts/Kodchasan-Bold.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Kodchasan-BoldItalic.ttf
Executable file
BIN
ui/public/fonts/Kodchasan-BoldItalic.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Kodchasan-ExtraLight.ttf
Executable file
BIN
ui/public/fonts/Kodchasan-ExtraLight.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Kodchasan-ExtraLightItalic.ttf
Executable file
BIN
ui/public/fonts/Kodchasan-ExtraLightItalic.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Kodchasan-Italic.ttf
Executable file
BIN
ui/public/fonts/Kodchasan-Italic.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Kodchasan-Light.ttf
Executable file
BIN
ui/public/fonts/Kodchasan-Light.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Kodchasan-LightItalic.ttf
Executable file
BIN
ui/public/fonts/Kodchasan-LightItalic.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Kodchasan-Medium.ttf
Executable file
BIN
ui/public/fonts/Kodchasan-Medium.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Kodchasan-MediumItalic.ttf
Executable file
BIN
ui/public/fonts/Kodchasan-MediumItalic.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Kodchasan-Regular.ttf
Executable file
BIN
ui/public/fonts/Kodchasan-Regular.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Kodchasan-SemiBold.ttf
Executable file
BIN
ui/public/fonts/Kodchasan-SemiBold.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Kodchasan-SemiBoldItalic.ttf
Executable file
BIN
ui/public/fonts/Kodchasan-SemiBoldItalic.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Mali-Bold.ttf
Executable file
BIN
ui/public/fonts/Mali-Bold.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Mali-BoldItalic.ttf
Executable file
BIN
ui/public/fonts/Mali-BoldItalic.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Mali-ExtraLight.ttf
Executable file
BIN
ui/public/fonts/Mali-ExtraLight.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Mali-ExtraLightItalic.ttf
Executable file
BIN
ui/public/fonts/Mali-ExtraLightItalic.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Mali-Italic.ttf
Executable file
BIN
ui/public/fonts/Mali-Italic.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Mali-Light.ttf
Executable file
BIN
ui/public/fonts/Mali-Light.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Mali-LightItalic.ttf
Executable file
BIN
ui/public/fonts/Mali-LightItalic.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Mali-Medium.ttf
Executable file
BIN
ui/public/fonts/Mali-Medium.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Mali-MediumItalic.ttf
Executable file
BIN
ui/public/fonts/Mali-MediumItalic.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Mali-Regular.ttf
Executable file
BIN
ui/public/fonts/Mali-Regular.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Mali-SemiBold.ttf
Executable file
BIN
ui/public/fonts/Mali-SemiBold.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Mali-SemiBoldItalic.ttf
Executable file
BIN
ui/public/fonts/Mali-SemiBoldItalic.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/NotoSans-Italic-VariableFont_wdth,wght.ttf
Executable file
BIN
ui/public/fonts/NotoSans-Italic-VariableFont_wdth,wght.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/NotoSans-VariableFont_wdth,wght.ttf
Executable file
BIN
ui/public/fonts/NotoSans-VariableFont_wdth,wght.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Sarabun-Bold.ttf
Executable file
BIN
ui/public/fonts/Sarabun-Bold.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Sarabun-BoldItalic.ttf
Executable file
BIN
ui/public/fonts/Sarabun-BoldItalic.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Sarabun-ExtraBold.ttf
Executable file
BIN
ui/public/fonts/Sarabun-ExtraBold.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Sarabun-ExtraBoldItalic.ttf
Executable file
BIN
ui/public/fonts/Sarabun-ExtraBoldItalic.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Sarabun-ExtraLight.ttf
Executable file
BIN
ui/public/fonts/Sarabun-ExtraLight.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Sarabun-ExtraLightItalic.ttf
Executable file
BIN
ui/public/fonts/Sarabun-ExtraLightItalic.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Sarabun-Italic.ttf
Executable file
BIN
ui/public/fonts/Sarabun-Italic.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Sarabun-Light.ttf
Executable file
BIN
ui/public/fonts/Sarabun-Light.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Sarabun-LightItalic.ttf
Executable file
BIN
ui/public/fonts/Sarabun-LightItalic.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Sarabun-Medium.ttf
Executable file
BIN
ui/public/fonts/Sarabun-Medium.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Sarabun-MediumItalic.ttf
Executable file
BIN
ui/public/fonts/Sarabun-MediumItalic.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Sarabun-Regular.ttf
Executable file
BIN
ui/public/fonts/Sarabun-Regular.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Sarabun-SemiBold.ttf
Executable file
BIN
ui/public/fonts/Sarabun-SemiBold.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Sarabun-SemiBoldItalic.ttf
Executable file
BIN
ui/public/fonts/Sarabun-SemiBoldItalic.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Sarabun-Thin.ttf
Executable file
BIN
ui/public/fonts/Sarabun-Thin.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Sarabun-ThinItalic.ttf
Executable file
BIN
ui/public/fonts/Sarabun-ThinItalic.ttf
Executable file
Binary file not shown.
BIN
ui/public/fonts/Voces-Regular.ttf
Executable file
BIN
ui/public/fonts/Voces-Regular.ttf
Executable file
Binary file not shown.
BIN
ui/public/icon-192-maskable.png
Executable file
BIN
ui/public/icon-192-maskable.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
BIN
ui/public/icon-192.png
Executable file
BIN
ui/public/icon-192.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
BIN
ui/public/icon-512-maskable.png
Executable file
BIN
ui/public/icon-512-maskable.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
BIN
ui/public/icon-512.png
Executable file
BIN
ui/public/icon-512.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user