initial commit
This commit is contained in:
commit
143f70b921
|
@ -0,0 +1 @@
|
|||
/target
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,18 @@
|
|||
[package]
|
||||
name = "golgi"
|
||||
version = "0.1.0"
|
||||
authors = ["glyph <glyph@mycelial.technology>"]
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
async-std = "1.10.0"
|
||||
base64 = "0.13.0"
|
||||
futures = "0.3.18"
|
||||
hex = "0.4.3"
|
||||
kuska-handshake = { version = "0.2.0", features = ["async_std"] }
|
||||
kuska-sodiumoxide = "0.2.5-0"
|
||||
# waiting for a pr merge upstream
|
||||
kuska-ssb = { path = "../ssb" }
|
||||
# try to replace with miniserde
|
||||
serde = "1"
|
||||
serde_json = "1"
|
|
@ -0,0 +1,16 @@
|
|||
# golgi
|
||||
|
||||
_A Scuttlebutt client written in Rust_
|
||||
|
||||
An experimental client which uses the [kuska-ssb](https://github.com/Kuska-ssb) libraries and aims to provide a high-level API for interacting with an sbot instance. Development efforts are currently oriented towards [go-sbot](https://github.com/cryptoscope/ssb) interoperability.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```rust
|
||||
pub async fn run() -> Result<(), GolgiError> {
|
||||
let mut sbot_client = Sbot::init(None, None).await?;
|
||||
|
||||
let id = sbot_client.whoami().await?;
|
||||
println!("{}", id);
|
||||
}
|
||||
```
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
use async_std::{io::Read, net::TcpStream};
|
||||
use std::fmt::Debug;
|
||||
|
||||
use kuska_handshake::async_std::BoxStream;
|
||||
use kuska_sodiumoxide::crypto::sign::ed25519;
|
||||
use kuska_ssb::api::{dto::WhoAmIOut, ApiCaller};
|
||||
use kuska_ssb::discovery;
|
||||
use kuska_ssb::keystore;
|
||||
use kuska_ssb::keystore::OwnedIdentity;
|
||||
use kuska_ssb::rpc::{RecvMsg, RequestNo, RpcReader, RpcWriter};
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
pub fn whoami_res_parse(body: &[u8]) -> Result<WhoAmIOut> {
|
||||
Ok(serde_json::from_slice(body)?)
|
||||
}
|
||||
*/
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum GolgiError {
|
||||
DecodeBase64(base64::DecodeError),
|
||||
Io {
|
||||
source: std::io::Error,
|
||||
context: String,
|
||||
},
|
||||
Handshake(kuska_handshake::async_std::Error),
|
||||
KuskaApi(kuska_ssb::api::Error),
|
||||
KuskaFeed(kuska_ssb::feed::Error),
|
||||
KuskaRpc(kuska_ssb::rpc::Error),
|
||||
// error message returned from the go-sbot
|
||||
Sbot(String),
|
||||
SerdeJson(serde_json::Error),
|
||||
WhoAmI(String),
|
||||
}
|
||||
|
||||
impl std::error::Error for GolgiError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match *self {
|
||||
GolgiError::DecodeBase64(ref err) => Some(err),
|
||||
GolgiError::Io { ref source, .. } => Some(source),
|
||||
GolgiError::Handshake(_) => None,
|
||||
GolgiError::KuskaApi(ref err) => Some(err),
|
||||
GolgiError::KuskaFeed(ref err) => Some(err),
|
||||
GolgiError::KuskaRpc(ref err) => Some(err),
|
||||
GolgiError::Sbot(_) => None,
|
||||
GolgiError::SerdeJson(ref err) => Some(err),
|
||||
GolgiError::WhoAmI(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for GolgiError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match *self {
|
||||
// TODO: add context (what were we trying to decode?)
|
||||
GolgiError::DecodeBase64(_) => write!(f, "Failed to decode base64"),
|
||||
GolgiError::Io { ref context, .. } => write!(f, "IO error: {}", context),
|
||||
GolgiError::Handshake(ref err) => write!(f, "{}", err),
|
||||
GolgiError::KuskaApi(_) => write!(f, "SSB API failure"),
|
||||
GolgiError::KuskaFeed(_) => write!(f, "SSB feed error"),
|
||||
// TODO: improve this variant with a context message
|
||||
// then have the core display msg be: "SSB RPC error: {}", context
|
||||
GolgiError::KuskaRpc(_) => write!(f, "SSB RPC failure"),
|
||||
GolgiError::Sbot(ref err) => write!(f, "Sbot returned an error response: {}", err),
|
||||
GolgiError::SerdeJson(_) => write!(f, "Failed to serialize JSON slice"),
|
||||
GolgiError::WhoAmI(ref err) => write!(f, "{}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<base64::DecodeError> for GolgiError {
|
||||
fn from(err: base64::DecodeError) -> Self {
|
||||
GolgiError::DecodeBase64(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<kuska_handshake::async_std::Error> for GolgiError {
|
||||
fn from(err: kuska_handshake::async_std::Error) -> Self {
|
||||
GolgiError::Handshake(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<kuska_ssb::api::Error> for GolgiError {
|
||||
fn from(err: kuska_ssb::api::Error) -> Self {
|
||||
GolgiError::KuskaApi(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<kuska_ssb::feed::Error> for GolgiError {
|
||||
fn from(err: kuska_ssb::feed::Error) -> Self {
|
||||
GolgiError::KuskaFeed(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<kuska_ssb::rpc::Error> for GolgiError {
|
||||
fn from(err: kuska_ssb::rpc::Error) -> Self {
|
||||
GolgiError::KuskaRpc(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for GolgiError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
GolgiError::SerdeJson(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
mod error;
|
||||
mod utils;
|
||||
|
||||
use async_std::net::TcpStream;
|
||||
//use futures::io::{self, AsyncRead as Read, AsyncWrite as Write};
|
||||
use kuska_handshake::async_std::{BoxStream, BoxStreamRead, BoxStreamWrite};
|
||||
use kuska_handshake::HandshakeComplete;
|
||||
use kuska_sodiumoxide::crypto::{auth, sign::ed25519};
|
||||
use kuska_ssb::api::{dto::CreateHistoryStreamIn, ApiCaller};
|
||||
use kuska_ssb::discovery;
|
||||
use kuska_ssb::keystore;
|
||||
use kuska_ssb::keystore::OwnedIdentity;
|
||||
use kuska_ssb::rpc::{RpcReader, RpcWriter};
|
||||
|
||||
use crate::error::GolgiError;
|
||||
|
||||
struct Sbot {
|
||||
id: String,
|
||||
public_key: ed25519::PublicKey,
|
||||
private_key: ed25519::SecretKey,
|
||||
address: String,
|
||||
// aka caps key (scuttleverse identifier)
|
||||
network_id: auth::Key,
|
||||
client: ApiCaller<TcpStream>,
|
||||
rpc_reader: RpcReader<TcpStream>,
|
||||
}
|
||||
|
||||
impl Sbot {
|
||||
async fn init(ip_port: Option<String>, net_id: Option<String>) -> Result<Sbot, GolgiError> {
|
||||
let address;
|
||||
if ip_port.is_none() {
|
||||
address = "127.0.0.1:8008".to_string();
|
||||
} else {
|
||||
address = ip_port.unwrap();
|
||||
}
|
||||
|
||||
let network_id;
|
||||
if net_id.is_none() {
|
||||
network_id = discovery::ssb_net_id();
|
||||
} else {
|
||||
network_id = auth::Key::from_slice(&hex::decode(net_id.unwrap()).unwrap()).unwrap();
|
||||
}
|
||||
|
||||
let OwnedIdentity { pk, sk, id } = keystore::from_gosbot_local()
|
||||
.await
|
||||
.expect("couldn't read local secret");
|
||||
|
||||
let socket = TcpStream::connect(&address)
|
||||
.await
|
||||
.map_err(|source| GolgiError::Io {
|
||||
source,
|
||||
context: "socket error; failed to initiate tcp stream connection".to_string(),
|
||||
})?;
|
||||
|
||||
let handshake = kuska_handshake::async_std::handshake_client(
|
||||
&mut &socket,
|
||||
network_id.clone(),
|
||||
pk,
|
||||
sk.clone(),
|
||||
pk,
|
||||
)
|
||||
.await
|
||||
.map_err(GolgiError::Handshake)?;
|
||||
|
||||
let (box_stream_read, box_stream_write) =
|
||||
BoxStream::from_handshake(socket.clone(), socket, handshake, 0x8000).split_read_write();
|
||||
|
||||
let rpc_reader = RpcReader::new(box_stream_read);
|
||||
let client = ApiCaller::new(RpcWriter::new(box_stream_write));
|
||||
|
||||
Ok(Self {
|
||||
id,
|
||||
public_key: pk,
|
||||
private_key: sk,
|
||||
address,
|
||||
network_id,
|
||||
client: client,
|
||||
rpc_reader: rpc_reader,
|
||||
})
|
||||
}
|
||||
|
||||
async fn whoami(&mut self) -> Result<String, GolgiError> {
|
||||
let req_id = self.client.whoami_req_send().await?;
|
||||
|
||||
utils::get_async(&mut self.rpc_reader, req_id, utils::whoami_res_parse)
|
||||
.await
|
||||
.map(|whoami| whoami.id)
|
||||
}
|
||||
|
||||
async fn create_history_stream(&mut self, id: String) -> Result<(), GolgiError> {
|
||||
let args = CreateHistoryStreamIn::new(id);
|
||||
let req_id = self.client.create_history_stream_req_send(&args).await?;
|
||||
utils::print_source_until_eof(&mut self.rpc_reader, req_id, utils::feed_res_parse).await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run() -> Result<(), GolgiError> {
|
||||
let mut sbot_client = Sbot::init(None, None).await?;
|
||||
|
||||
let id = sbot_client.whoami().await?;
|
||||
println!("{}", id);
|
||||
|
||||
sbot_client.create_history_stream(id).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
mod error;
|
||||
mod utils;
|
||||
|
||||
use async_std::net::TcpStream;
|
||||
//use futures::io::{self, AsyncRead as Read, AsyncWrite as Write};
|
||||
use kuska_handshake::async_std::{BoxStream, BoxStreamRead, BoxStreamWrite};
|
||||
use kuska_handshake::HandshakeComplete;
|
||||
use kuska_sodiumoxide::crypto::{auth, sign::ed25519};
|
||||
use kuska_ssb::api::{dto::CreateHistoryStreamIn, ApiCaller};
|
||||
use kuska_ssb::discovery;
|
||||
use kuska_ssb::keystore;
|
||||
use kuska_ssb::keystore::OwnedIdentity;
|
||||
use kuska_ssb::rpc::{RpcReader, RpcWriter};
|
||||
|
||||
use crate::error::GolgiError;
|
||||
|
||||
//struct Sbot<R: Read + std::marker::Unpin, W: Write + std::marker::Unpin> {
|
||||
struct Sbot<'a> {
|
||||
id: String,
|
||||
public_key: ed25519::PublicKey,
|
||||
private_key: ed25519::SecretKey,
|
||||
address: String,
|
||||
// aka caps key (scuttleverse identifier)
|
||||
network_id: auth::Key,
|
||||
//socket: TcpStream,
|
||||
//client: ApiCaller<W>,
|
||||
client: ApiCaller<TcpStream>,
|
||||
//rpc_reader: RpcReader<R>,
|
||||
rpc_reader: RpcReader<&'a TcpStream>,
|
||||
}
|
||||
|
||||
//impl<R: Read + std::marker::Unpin, W: Write + std::marker::Unpin> Sbot<R, W> {
|
||||
//impl Sbot<'static> {
|
||||
impl Sbot<'_> {
|
||||
async fn init(
|
||||
ip_port: Option<String>,
|
||||
net_id: Option<String>,
|
||||
) -> Result<Sbot<'static>, GolgiError> {
|
||||
let address;
|
||||
if ip_port.is_none() {
|
||||
// set default
|
||||
address = "127.0.0.1:8008".to_string();
|
||||
} else {
|
||||
address = ip_port.unwrap().to_string();
|
||||
}
|
||||
|
||||
let network_id;
|
||||
if net_id.is_none() {
|
||||
network_id = discovery::ssb_net_id();
|
||||
} else {
|
||||
network_id = auth::Key::from_slice(&hex::decode(net_id.unwrap()).unwrap()).unwrap();
|
||||
}
|
||||
|
||||
let OwnedIdentity { pk, sk, id } = keystore::from_gosbot_local()
|
||||
.await
|
||||
.expect("couldn't read local secret");
|
||||
|
||||
let socket = TcpStream::connect(&address)
|
||||
.await
|
||||
.map_err(|source| GolgiError::Io {
|
||||
source,
|
||||
context: "socket error; failed to initiate tcp stream connection".to_string(),
|
||||
})?;
|
||||
|
||||
let handshake = kuska_handshake::async_std::handshake_client(
|
||||
&mut &socket,
|
||||
network_id.clone(),
|
||||
pk,
|
||||
sk.clone(),
|
||||
pk,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| GolgiError::Handshake(err))?;
|
||||
|
||||
let (box_stream_read, box_stream_write) =
|
||||
BoxStream::from_handshake(socket, socket, handshake, 0x8000).split_read_write();
|
||||
|
||||
let rpc_reader = RpcReader::new(box_stream_read);
|
||||
let client = ApiCaller::new(RpcWriter::new(box_stream_write));
|
||||
|
||||
Ok(Self {
|
||||
id,
|
||||
public_key: pk,
|
||||
private_key: sk,
|
||||
address,
|
||||
network_id,
|
||||
client: client,
|
||||
rpc_reader: rpc_reader,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run() -> Result<(), GolgiError> {
|
||||
let sbot_client = Sbot::init(None, None);
|
||||
/*
|
||||
// read go-sbot id details from `/.ssb-go/secret
|
||||
let OwnedIdentity { pk, sk, id } = keystore::from_gosbot_local()
|
||||
.await
|
||||
.expect("read local secret");
|
||||
println!("connecting with identity {}", id);
|
||||
|
||||
// set the public key, ip and port for the sbot instance
|
||||
let sbot_public_key = "a0SsCiZkBu6qaQ6tWVvzQPDSzvO0JqMAqPXt0LBIl30=".to_string();
|
||||
let server_pk =
|
||||
ed25519::PublicKey::from_slice(&base64::decode(&sbot_public_key)?).expect("bad public key");
|
||||
let server_ipport = "127.0.0.1:8008".to_string();
|
||||
|
||||
// connect to the local go-sbot instance
|
||||
let mut socket = TcpStream::connect(server_ipport)
|
||||
.await
|
||||
.map_err(|source| GolgiError::Io {
|
||||
source,
|
||||
context: "socket error; failed to initiate tcp stream connection".to_string(),
|
||||
})?;
|
||||
|
||||
// initiate secret handshake
|
||||
let handshake = kuska_handshake::async_std::handshake_client(
|
||||
&mut socket,
|
||||
discovery::ssb_net_id(),
|
||||
pk,
|
||||
sk.clone(),
|
||||
server_pk,
|
||||
)
|
||||
.await
|
||||
.expect("handshake error");
|
||||
|
||||
println!("💃 handshake complete");
|
||||
|
||||
// call `whoami`
|
||||
let (box_stream_read, box_stream_write) =
|
||||
BoxStream::from_handshake(&socket, &socket, handshake, 0x8000).split_read_write();
|
||||
|
||||
let mut rpc_reader = RpcReader::new(box_stream_read);
|
||||
let mut client = ApiCaller::new(RpcWriter::new(box_stream_write));
|
||||
|
||||
let req_id = client.whoami_req_send().await?;
|
||||
let whoami = match utils::get_async(&mut rpc_reader, req_id, utils::whoami_res_parse).await {
|
||||
Ok(res) => {
|
||||
println!("😊 server says hello to {}", res.id);
|
||||
id
|
||||
}
|
||||
Err(err) => {
|
||||
if !err
|
||||
.to_string()
|
||||
.contains("method:whoami is not in list of allowed methods")
|
||||
{
|
||||
println!("Cannot ask for whoami {}", err);
|
||||
}
|
||||
id
|
||||
}
|
||||
};
|
||||
|
||||
// call `createhistorystream`
|
||||
let args = CreateHistoryStreamIn::new(whoami.clone());
|
||||
// TODO: this should return an error if the args are wrong but it doesn't?
|
||||
let req_id = client.create_history_stream_req_send(&args).await?;
|
||||
utils::print_source_until_eof(&mut rpc_reader, req_id, utils::feed_res_parse).await?;
|
||||
*/
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
use std::process;
|
||||
|
||||
#[async_std::main]
|
||||
async fn main() {
|
||||
if let Err(e) = golgi::run().await {
|
||||
eprintln!("Application error: {}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
|
||||
use kuska_handshake::async_std::BoxStream;
|
||||
use kuska_sodiumoxide::crypto::sign::ed25519;
|
||||
use kuska_ssb::discovery;
|
||||
use kuska_ssb::keystore;
|
||||
use kuska_ssb::keystore::OwnedIdentity;
|
||||
*/
|
||||
use std::fmt::Debug;
|
||||
|
||||
use async_std::io::Read;
|
||||
|
||||
use kuska_ssb::api::dto::WhoAmIOut;
|
||||
use kuska_ssb::feed::Feed;
|
||||
use kuska_ssb::rpc::{RecvMsg, RequestNo, RpcReader};
|
||||
|
||||
use crate::error::GolgiError;
|
||||
|
||||
pub fn feed_res_parse(body: &[u8]) -> Result<Feed, GolgiError> {
|
||||
Ok(Feed::from_slice(body)?)
|
||||
}
|
||||
|
||||
pub fn whoami_res_parse(body: &[u8]) -> Result<WhoAmIOut, GolgiError> {
|
||||
Ok(serde_json::from_slice(body)?)
|
||||
}
|
||||
|
||||
pub async fn get_async<'a, R, T, F>(
|
||||
rpc_reader: &mut RpcReader<R>,
|
||||
req_no: RequestNo,
|
||||
f: F,
|
||||
) -> Result<T, GolgiError>
|
||||
where
|
||||
R: Read + Unpin,
|
||||
F: Fn(&[u8]) -> Result<T, GolgiError>,
|
||||
T: Debug,
|
||||
{
|
||||
loop {
|
||||
let (id, msg) = rpc_reader.recv().await?;
|
||||
if id == req_no {
|
||||
match msg {
|
||||
RecvMsg::RpcResponse(_type, body) => {
|
||||
return f(&body).map_err(|err| err);
|
||||
}
|
||||
RecvMsg::ErrorResponse(message) => {
|
||||
return Err(GolgiError::Sbot(message));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn print_source_until_eof<'a, R, T, F>(
|
||||
rpc_reader: &mut RpcReader<R>,
|
||||
req_no: RequestNo,
|
||||
f: F,
|
||||
) -> Result<(), GolgiError>
|
||||
where
|
||||
R: Read + Unpin,
|
||||
F: Fn(&[u8]) -> Result<T, GolgiError>,
|
||||
T: Debug + serde::Deserialize<'a>,
|
||||
{
|
||||
loop {
|
||||
let (id, msg) = rpc_reader.recv().await?;
|
||||
if id == req_no {
|
||||
match msg {
|
||||
RecvMsg::RpcResponse(_type, body) => {
|
||||
let display = f(&body)?;
|
||||
println!("{:?}", display);
|
||||
}
|
||||
RecvMsg::ErrorResponse(message) => {
|
||||
return Err(GolgiError::Sbot(message));
|
||||
}
|
||||
RecvMsg::CancelStreamRespose() => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
Loading…
Reference in New Issue