This commit is contained in:
polwex 2024-10-22 20:27:48 +07:00
commit f1791b8699
142 changed files with 11477 additions and 0 deletions

9
.gitignore vendored Normal file
View 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

File diff suppressed because it is too large Load Diff

12
Cargo.toml Normal file
View 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
View File

@ -0,0 +1,6 @@
world tok-sortugdev-dot-os-v0{
import all;
include process-v0;
}
interface all {}

18
cookies/Cargo.toml Normal file
View 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
View 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
View 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

Binary file not shown.

44
pkg/manifest.json Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

37
ui/package.json Normal file
View 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"
}
}

View 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

Binary file not shown.

BIN
ui/public/fonts/Charm-Regular.ttf Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
ui/public/fonts/Judson-Bold.ttf Executable file

Binary file not shown.

BIN
ui/public/fonts/Judson-Italic.ttf Executable file

Binary file not shown.

Binary file not shown.

BIN
ui/public/fonts/Kanit-Black.ttf Executable file

Binary file not shown.

Binary file not shown.

BIN
ui/public/fonts/Kanit-Bold.ttf Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
ui/public/fonts/Kanit-Italic.ttf Executable file

Binary file not shown.

BIN
ui/public/fonts/Kanit-Light.ttf Executable file

Binary file not shown.

Binary file not shown.

BIN
ui/public/fonts/Kanit-Medium.ttf Executable file

Binary file not shown.

Binary file not shown.

BIN
ui/public/fonts/Kanit-Regular.ttf Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
ui/public/fonts/Kanit-Thin.ttf Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
ui/public/fonts/Mali-Bold.ttf Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
ui/public/fonts/Mali-Italic.ttf Executable file

Binary file not shown.

BIN
ui/public/fonts/Mali-Light.ttf Executable file

Binary file not shown.

Binary file not shown.

BIN
ui/public/fonts/Mali-Medium.ttf Executable file

Binary file not shown.

Binary file not shown.

BIN
ui/public/fonts/Mali-Regular.ttf Executable file

Binary file not shown.

BIN
ui/public/fonts/Mali-SemiBold.ttf Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
ui/public/fonts/Sarabun-Bold.ttf Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
ui/public/fonts/Sarabun-Light.ttf Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
ui/public/fonts/Sarabun-Thin.ttf Executable file

Binary file not shown.

Binary file not shown.

BIN
ui/public/fonts/Voces-Regular.ttf Executable file

Binary file not shown.

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

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

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