//! Sbot type and connection-related methods. use async_std::net::TcpStream; use kuska_handshake::async_std::BoxStream; use kuska_sodiumoxide::crypto::{auth, sign::ed25519}; use kuska_ssb::{ api::ApiCaller, discovery, keystore, keystore::OwnedIdentity, rpc::{RpcReader, RpcWriter}, }; use crate::error::GolgiError; /// Keystore selector to specify the location of the secret file. /// /// This enum is used when initiating a connection with an sbot instance. pub enum Keystore { /// Patchwork default keystore path: `.ssb/secret` in the user's home directory. Patchwork, /// GoSbot default keystore path: `.ssb-go/secret` in the user's home directory. GoSbot, /// GoSbot keystore in a custom location CustomGoSbot(String), /// Patchwork keystore in a custom location CustomPatchwork(String), } /// A struct representing a connection with a running sbot. /// A client and an rpc_reader can together be used to make requests to the sbot /// and read the responses. /// Note there can be multiple SbotConnection at the same time. pub struct SbotConnection { /// Client for writing requests to go-bot pub client: ApiCaller, /// RpcReader object for reading responses from go-sbot pub rpc_reader: RpcReader, } /// Holds the Scuttlebutt identity, keys and configuration parameters for /// connecting to a local sbot and implements all Golgi API methods. pub struct Sbot { /// The ID (public key value) of the account associated with the local sbot instance. pub id: String, public_key: ed25519::PublicKey, private_key: ed25519::SecretKey, address: String, // aka caps key (scuttleverse identifier) network_id: auth::Key, } impl Sbot { /// Initiate a connection with an sbot instance. Define the IP address, /// port and network key for the sbot, then retrieve the public key, /// private key (secret) and identity from the `.ssb-go/secret` file. pub async fn init( keystore: Keystore, ip_port: Option, net_id: Option, ) -> Result { let mut address = if ip_port.is_none() { "127.0.0.1:8008".to_string() } else { ip_port.unwrap() }; if address.starts_with(':') { address = format!("127.0.0.1{}", address); } let network_id = if net_id.is_none() { discovery::ssb_net_id() } else { auth::Key::from_slice(&hex::decode(net_id.unwrap()).unwrap()).unwrap() }; let OwnedIdentity { pk, sk, id } = match keystore { Keystore::Patchwork => keystore::from_patchwork_local().await.map_err(|_err| { GolgiError::Sbot( "sbot initialization error: couldn't read local patchwork secret from default location".to_string(), ) })?, Keystore::GoSbot => keystore::from_gosbot_local().await.map_err(|_err| { GolgiError::Sbot( "sbot initialization error: couldn't read local go-sbot secret from default location".to_string(), ) })?, Keystore::CustomGoSbot(key_path) => { keystore::from_custom_gosbot_keypath(key_path.to_string()) .await .map_err(|_err| { GolgiError::Sbot(format!( "sbot initialization error: couldn't read local go-sbot secret from: {}", key_path )) })? } Keystore::CustomPatchwork(key_path) => { keystore::from_custom_patchwork_keypath(key_path.to_string()) .await .map_err(|_err| { GolgiError::Sbot(format!( "sbot initialization error: couldn't read local patchwork secret from: {}", key_path )) })? } }; Ok(Self { id, public_key: pk, private_key: sk, address, network_id, }) } /// Creates a new connection with the sbot, using the address, network_id, /// public_key and private_key supplied when Sbot was initialized. /// /// Note that a single Sbot can have multiple SbotConnection at the same time. pub async fn get_sbot_connection(&self) -> Result { let address = self.address.clone(); let network_id = self.network_id.clone(); let public_key = self.public_key; let private_key = self.private_key.clone(); Sbot::_get_sbot_connection_helper(address, network_id, public_key, private_key).await } /// Private helper function which creates a new connection with sbot, /// but with all variables passed as arguments. /// /// Open a TCP stream to the sbot and perform the secret handshake. If /// successful, create a box stream and split it into a writer and reader. /// Return RPC handles to the sbot as part of the `struct` output. async fn _get_sbot_connection_helper( address: String, network_id: auth::Key, public_key: ed25519::PublicKey, private_key: ed25519::SecretKey, ) -> Result { let socket = TcpStream::connect(&address) .await .map_err(|source| GolgiError::Io { source, context: "failed to initiate tcp stream connection".to_string(), })?; let handshake = kuska_handshake::async_std::handshake_client( &mut &socket, network_id.clone(), public_key, private_key.clone(), public_key, ) .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)); let sbot_connection = SbotConnection { rpc_reader, client }; Ok(sbot_connection) } }