WIP: Change go-sbotcli-rs to parse json responses instead of using regex matches #8
|
@ -11,3 +11,7 @@ license = "LGPL-3.0-only"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
dirs = "4.0.0"
|
dirs = "4.0.0"
|
||||||
regex = "1.5.4"
|
regex = "1.5.4"
|
||||||
|
serde_json = "1.0"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
ssb-legacy-msg-data = "0.1.4"
|
||||||
|
ssb-multiformats = "0.4.2"
|
||||||
|
|
11
src/error.rs
11
src/error.rs
|
@ -13,6 +13,7 @@ pub enum SbotCliError {
|
||||||
Blob(String),
|
Blob(String),
|
||||||
Contact(String),
|
Contact(String),
|
||||||
GetAboutMsgs(String),
|
GetAboutMsgs(String),
|
||||||
|
GetRelationship(String),
|
||||||
Invite(String),
|
Invite(String),
|
||||||
Publish(String),
|
Publish(String),
|
||||||
WhoAmI(String),
|
WhoAmI(String),
|
||||||
|
@ -22,6 +23,7 @@ pub enum SbotCliError {
|
||||||
InvalidUtf8(Utf8Error),
|
InvalidUtf8(Utf8Error),
|
||||||
// external errors
|
// external errors
|
||||||
InvalidRegex(RegexError),
|
InvalidRegex(RegexError),
|
||||||
|
ParseJson(serde_json::Error),
|
||||||
NoHomeDir,
|
NoHomeDir,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,6 +40,9 @@ impl fmt::Display for SbotCliError {
|
||||||
}
|
}
|
||||||
SbotCliError::GetAboutMsgs(ref err) => {
|
SbotCliError::GetAboutMsgs(ref err) => {
|
||||||
write!(f, "{}", err)
|
write!(f, "{}", err)
|
||||||
|
},
|
||||||
|
SbotCliError::GetRelationship(ref err) => {
|
||||||
|
write!(f, "{}", err)
|
||||||
}
|
}
|
||||||
SbotCliError::Invite(ref err) => {
|
SbotCliError::Invite(ref err) => {
|
||||||
write!(f, "{}", err)
|
write!(f, "{}", err)
|
||||||
|
@ -60,6 +65,9 @@ impl fmt::Display for SbotCliError {
|
||||||
SbotCliError::InvalidRegex(ref err) => {
|
SbotCliError::InvalidRegex(ref err) => {
|
||||||
write!(f, "{}", err)
|
write!(f, "{}", err)
|
||||||
}
|
}
|
||||||
|
SbotCliError::ParseJson(ref err) => {
|
||||||
|
write!(f, "{}", err)
|
||||||
|
}
|
||||||
SbotCliError::NoHomeDir => {
|
SbotCliError::NoHomeDir => {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
|
@ -93,3 +101,6 @@ impl From<RegexError> for SbotCliError {
|
||||||
SbotCliError::InvalidRegex(err)
|
SbotCliError::InvalidRegex(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
290
src/lib.rs
290
src/lib.rs
|
@ -35,6 +35,10 @@
|
||||||
//! ## License
|
//! ## License
|
||||||
//!
|
//!
|
||||||
//! LGPL-3.0.
|
//! LGPL-3.0.
|
||||||
|
use serde_json::Value;
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use ssb_multiformats::multihash::Multihash;
|
||||||
|
use ssb_legacy_msg_data::LegacyF64;
|
||||||
|
|
||||||
pub mod error;
|
pub mod error;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
@ -42,6 +46,42 @@ mod utils;
|
||||||
pub use crate::error::SbotCliError;
|
pub use crate::error::SbotCliError;
|
||||||
use std::{ffi::OsString, path::PathBuf, process::Command, result::Result};
|
use std::{ffi::OsString, path::PathBuf, process::Command, result::Result};
|
||||||
|
|
||||||
|
|
||||||
|
/// HELPER STRUCTS FOR GETTING AND SETTING VALUES FROM SBOT
|
||||||
|
|
||||||
|
/// A struct which represents any ssb message in the log.
|
||||||
|
///
|
||||||
|
/// Modified from https://github.com/sunrise-choir/ssb-validate.
|
||||||
|
///
|
||||||
|
/// Every ssb message in the log has a content field, as a first-level key.
|
||||||
|
/// This content field is further parsed based on the specific type of message.
|
||||||
|
///
|
||||||
|
/// Data type representing the `value` of a message object (`KVT`). More information concerning the
|
||||||
|
/// data model can be found
|
||||||
|
/// in the [`Metadata` documentation](https://spec.scuttlebutt.nz/feed/messages.html#metadata).
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct SsbMessageValue {
|
||||||
|
pub previous: Option<Multihash>,
|
||||||
|
pub author: String,
|
||||||
|
pub sequence: u64,
|
||||||
|
pub timestamp: LegacyF64,
|
||||||
|
pub hash: String,
|
||||||
|
pub content: Value,
|
||||||
|
pub signature: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A struct which represents a relationship with another user (peer).
|
||||||
|
/// Any combination of following and blocking is possible except
|
||||||
|
/// for following=true AND blocking=true.
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct PeerRelationship {
|
||||||
|
following: bool,
|
||||||
|
blocking: bool
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SBOT METHODS
|
||||||
|
|
||||||
/// An `sbotcli` instance with associated methods for querying a Go sbot server.
|
/// An `sbotcli` instance with associated methods for querying a Go sbot server.
|
||||||
pub struct Sbot {
|
pub struct Sbot {
|
||||||
/// The path to the `sbotcli` binary.
|
/// The path to the `sbotcli` binary.
|
||||||
|
@ -57,8 +97,8 @@ impl Sbot {
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// * `sbotcli_path` - an optional string slice representing a file path
|
/// * `sbotcli_path` - an optional string slice representing the file path to the sbotcli binary
|
||||||
/// * `sbot_working_dir` - an optional string slice representing a directory path
|
/// * `sbot_working_dir` - an optional string slice representing a directory path where sbotcli stores its data
|
||||||
///
|
///
|
||||||
pub fn init(
|
pub fn init(
|
||||||
sbotcli_path: Option<&str>,
|
sbotcli_path: Option<&str>,
|
||||||
|
@ -110,7 +150,7 @@ impl Sbot {
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// * `file_path` - A string slice representing a file path
|
/// * `file_path` - A string slice representing a file path to the blob to be added
|
||||||
///
|
///
|
||||||
pub fn add_blob(&self, file_path: &str) -> Result<String, SbotCliError> {
|
pub fn add_blob(&self, file_path: &str) -> Result<String, SbotCliError> {
|
||||||
let output = Command::new(&self.sbotcli_path)
|
let output = Command::new(&self.sbotcli_path)
|
||||||
|
@ -131,122 +171,235 @@ impl Sbot {
|
||||||
|
|
||||||
/* CONTACTS */
|
/* CONTACTS */
|
||||||
|
|
||||||
/// Follow a peer.
|
/// Set relationship with a peer.
|
||||||
///
|
///
|
||||||
/// Calls `sbotcli publish contact --following [id]`. On success: trims the trailing whitespace from `stdout`
|
/// A relationship has two properties:
|
||||||
|
/// - following (either true or false)
|
||||||
|
/// - blocking (either true or false)
|
||||||
|
///
|
||||||
|
/// This is an idempotent function which sets the relationship with a peer for following and blocking,
|
||||||
|
/// based on the arguments that are passed in.
|
||||||
|
///
|
||||||
|
/// It uses `sbotcli publish contact [id]`. Passing the --following and/or --blocking flags,
|
||||||
|
/// if following or blocking is set to true, respectively.
|
||||||
|
///
|
||||||
|
/// On success: trims the trailing whitespace from `stdout`
|
||||||
/// and returns the message reference. On error: returns the `stderr` output with a description.
|
/// and returns the message reference. On error: returns the `stderr` output with a description.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// * `id` - A string slice representing an id (profile reference / public key)
|
/// * `id` - A string slice representing an id (profile reference / public key) of the account to follow
|
||||||
|
/// * `following` - a boolean representing whether to follow this account or not
|
||||||
|
/// * `blocking` - a boolean representing whether to block this account or not
|
||||||
///
|
///
|
||||||
pub fn follow(&self, id: &str) -> Result<String, SbotCliError> {
|
pub fn set_relationship(&self, id: &str, following: bool, blocking: bool) -> Result<String, SbotCliError> {
|
||||||
let output = Command::new(&self.sbotcli_path)
|
let mut command = Command::new(&self.sbotcli_path);
|
||||||
|
command
|
||||||
.arg("publish")
|
.arg("publish")
|
||||||
.arg("contact")
|
.arg("contact");
|
||||||
.arg("--following")
|
if following {
|
||||||
|
command.arg("--following");
|
||||||
|
}
|
||||||
|
if blocking {
|
||||||
|
command.arg("--blocking");
|
||||||
|
}
|
||||||
|
let output = command
|
||||||
.arg(id)
|
.arg(id)
|
||||||
.output()?;
|
.output()?;
|
||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
let stdout = String::from_utf8(output.stdout)?;
|
let stdout = String::from_utf8(output.stdout)?;
|
||||||
let msg_ref = stdout.trim_end().to_string();
|
let msg_ref = stdout.trim_end().to_string();
|
||||||
|
|
||||||
Ok(msg_ref)
|
Ok(msg_ref)
|
||||||
} else {
|
} else {
|
||||||
let stderr = std::str::from_utf8(&output.stderr)?;
|
let stderr = std::str::from_utf8(&output.stderr)?;
|
||||||
Err(SbotCliError::Contact(format!(
|
Err(SbotCliError::Contact(format!(
|
||||||
"Error following peer: {}",
|
"Error setting relationship with peer: {}",
|
||||||
stderr
|
stderr
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Block a peer.
|
/// Get the latest value of a current relationship with a peer.
|
||||||
|
///
|
||||||
|
/// We determine the value of the relationship by looking up the most recent contact
|
||||||
|
/// message made about that peer.
|
||||||
|
///
|
||||||
|
/// If no contact message is found, then a relationship of following=false blocking=false
|
||||||
|
/// is returned.
|
||||||
|
///
|
||||||
|
/// Calls `sbotcli bytype --limit 1 --reverse contact`.
|
||||||
|
///
|
||||||
|
/// On success: parses a PeerRelationship from the ssb_message or returns one with default values.
|
||||||
|
/// On error: returns the `stderr` output with a description.
|
||||||
|
///
|
||||||
|
/// # arguments
|
||||||
|
///
|
||||||
|
/// * `id` - a string slice representing the id (profile reference / public key)
|
||||||
|
/// of the peer to look up the relationship with.
|
||||||
|
///
|
||||||
|
pub fn get_relationship(&self, _id: &str) -> Result<PeerRelationship, SbotCliError> {
|
||||||
|
// TODO: need to filter this query down to the actual user it should be for
|
||||||
|
// right now this is not a real query
|
||||||
|
let output = Command::new(&self.sbotcli_path)
|
||||||
|
.arg("bytype")
|
||||||
|
.arg("--limit")
|
||||||
|
.arg("1")
|
||||||
|
.arg("--reverse")
|
||||||
|
.arg("contact")
|
||||||
|
.output()?;
|
||||||
|
if output.status.success() {
|
||||||
|
let stdout = String::from_utf8(output.stdout)?;
|
||||||
|
println!("stdout: {:?}", stdout);
|
||||||
|
let ssb_message : SsbMessageValue = serde_json::from_str(&stdout).map_err(|err| {
|
||||||
|
SbotCliError::GetRelationship(format!("error deserializing ssb message while getting relationship to peer: {}", err))
|
||||||
|
})?;
|
||||||
|
let relationship : PeerRelationship = serde_json::from_value(ssb_message.content).map_err(|err| {
|
||||||
|
SbotCliError::GetRelationship(format!("error deserializing ssb message content while getting relationship to peer: {}", err))
|
||||||
|
})?;
|
||||||
|
Ok(relationship)
|
||||||
|
} else {
|
||||||
|
let stderr = std::str::from_utf8(&output.stderr)?;
|
||||||
|
Err(SbotCliError::GetRelationship(format!(
|
||||||
|
"error getting relationship to peer: {}",
|
||||||
|
stderr
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Follow a peer. Does not change whatever blocking relationship user already has with this peer.
|
||||||
|
///
|
||||||
|
/// On success: trims the trailing whitespace from `stdout`
|
||||||
|
/// and returns the message reference. On error: returns the `stderr` output with a description.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `id` - A string slice representing an id (profile reference / public key) of the account to follow
|
||||||
|
///
|
||||||
|
pub fn follow(&self, id: &str) -> Result<String, SbotCliError> {
|
||||||
|
let current_relationship = self.get_relationship(id)?;
|
||||||
|
self.set_relationship(id, true, current_relationship.blocking)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Block a peer. Does not change whatever following relationship user already has with this peer.
|
||||||
///
|
///
|
||||||
/// Calls `sbotcli publish contact --blocking [id]`. On success: trims the trailing whitespace from `stdout` and returns the message reference. On error: returns the `stderr` output with a description.
|
/// Calls `sbotcli publish contact --blocking [id]`. On success: trims the trailing whitespace from `stdout` and returns the message reference. On error: returns the `stderr` output with a description.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// * `id` - A string slice representing an id (profile reference / public key)
|
/// * `id` - A string slice representing an id (profile reference / public key) of the account to block
|
||||||
///
|
///
|
||||||
pub fn block(&self, id: &str) -> Result<String, SbotCliError> {
|
pub fn block(&self, id: &str) -> Result<String, SbotCliError> {
|
||||||
|
let current_relationship = self.get_relationship(id)?;
|
||||||
|
self.set_relationship(id, current_relationship.following, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unfollow a peer. Does not change whatever blocking relationship user already has with this peer.
|
||||||
|
///
|
||||||
|
/// On success: trims the trailing whitespace from `stdout`
|
||||||
|
/// and returns the message reference. On error: returns the `stderr` output with a description.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `id` - A string slice representing an id (profile reference / public key) of the account to follow
|
||||||
|
///
|
||||||
|
pub fn unfollow(&self, id: &str) -> Result<String, SbotCliError> {
|
||||||
|
let current_relationship = self.get_relationship(id)?;
|
||||||
|
self.set_relationship(id, false, current_relationship.blocking)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unblock a peer. Does not change whatever following relationship user already has with this peer.
|
||||||
|
///
|
||||||
|
/// Calls `sbotcli publish contact --blocking [id]`. On success: trims the trailing whitespace from `stdout` and returns the message reference. On error: returns the `stderr` output with a description.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `id` - A string slice representing an id (profile reference / public key) of the account to block
|
||||||
|
///
|
||||||
|
pub fn unblock(&self, id: &str) -> Result<String, SbotCliError> {
|
||||||
|
let current_relationship = self.get_relationship(id)?;
|
||||||
|
self.set_relationship(id, current_relationship.following, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* GET ABOUT MESSAGES */
|
||||||
|
|
||||||
|
/// Get the value of an about message with the given key.
|
||||||
|
///
|
||||||
|
/// Calls `sbotcli bytype --limit 10 --reverse about`. On success: parses the `stdout` to extract the
|
||||||
|
/// `key` and returns it. On error: returns the `stderr` output with a description.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `key` - A string slice representing the name of they that is being retrieved
|
||||||
|
/// * `id` - A string slice representing the id (profile reference / public key)
|
||||||
|
/// of the user to get the about message from
|
||||||
|
///
|
||||||
|
pub fn get_about_message(&self, key: &str, _id: &str) -> Result<Option<String>, SbotCliError> {
|
||||||
let output = Command::new(&self.sbotcli_path)
|
let output = Command::new(&self.sbotcli_path)
|
||||||
.arg("publish")
|
.arg("bytype")
|
||||||
.arg("contact")
|
.arg("--limit")
|
||||||
.arg("--blocking")
|
.arg("1")
|
||||||
.arg(id)
|
.arg("--reverse")
|
||||||
|
.arg("about")
|
||||||
.output()?;
|
.output()?;
|
||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
|
// TODO: figure out how to take a stream of messages, and filter down to just ones with the value we are looking for
|
||||||
|
// this code is not real dont look at it
|
||||||
let stdout = String::from_utf8(output.stdout)?;
|
let stdout = String::from_utf8(output.stdout)?;
|
||||||
let msg_ref = stdout.trim_end().to_string();
|
let ssb_message : SsbMessageValue = serde_json::from_str(&stdout).map_err(|err| {
|
||||||
|
SbotCliError::GetAboutMsgs(format!("error deserializing ssb message while getting about message: {}", err))
|
||||||
Ok(msg_ref)
|
})?;
|
||||||
|
let value = ssb_message.content.get(key);
|
||||||
|
match value {
|
||||||
|
Some(val) => {
|
||||||
|
let value_as_str = val.as_str();
|
||||||
|
match value_as_str {
|
||||||
|
Some(v) => Ok(Some(v.to_string())),
|
||||||
|
None => Err(SbotCliError::GetAboutMsgs(format!("error parsing {} from about message", key)))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => Err(SbotCliError::GetAboutMsgs(format!("error parsing {} from about message", key)))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let stderr = std::str::from_utf8(&output.stderr)?;
|
let stderr = std::str::from_utf8(&output.stderr)?;
|
||||||
Err(SbotCliError::Contact(format!(
|
// TODO: create a more generic error variant
|
||||||
"Error blocking peer: {}",
|
Err(SbotCliError::GetAboutMsgs(format!(
|
||||||
|
"Error fetching about messages: {}",
|
||||||
stderr
|
stderr
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* GET ABOUT MESSAGES */
|
/// Return latest name assignment from `about` msgs, for the public key
|
||||||
|
/// associated with the local sbot instance.
|
||||||
/// Return latest name assignment from `about` msgs (the name in this case is for the public key
|
|
||||||
/// associated with the local sbot instance).
|
|
||||||
///
|
///
|
||||||
/// Calls `sbotcli bytype --limit 10 --reverse about`. On success: parses the `stdout` to extract the
|
/// Calls `sbotcli bytype --limit 10 --reverse about`. On success: parses the `stdout` to extract the
|
||||||
/// `name` and returns it. On error: returns the `stderr` output with a description.
|
/// `name` and returns it. On error: returns the `stderr` output with a description.
|
||||||
///
|
///
|
||||||
pub fn get_name(&self) -> Result<Option<String>, SbotCliError> {
|
pub fn get_name(&self) -> Result<Option<String>, SbotCliError> {
|
||||||
let output = Command::new(&self.sbotcli_path)
|
let self_id = self.whoami()?;
|
||||||
.arg("bytype")
|
self.get_about_message("name", &self_id)
|
||||||
.arg("--limit")
|
|
||||||
.arg("10")
|
|
||||||
.arg("--reverse")
|
|
||||||
.arg("about")
|
|
||||||
.output()?;
|
|
||||||
if output.status.success() {
|
|
||||||
let stdout = String::from_utf8(output.stdout)?;
|
|
||||||
let name = utils::regex_finder(r#""name": "(.*)""#, &stdout)?;
|
|
||||||
|
|
||||||
Ok(name)
|
|
||||||
} else {
|
|
||||||
let stderr = std::str::from_utf8(&output.stderr)?;
|
|
||||||
// TODO: create a more generic error variant
|
|
||||||
Err(SbotCliError::GetAboutMsgs(format!(
|
|
||||||
"Error fetching about messages: {}",
|
|
||||||
stderr
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return latest description assignment from `about` msgs
|
/// Return latest description assignment from `about` msgs, for the public key
|
||||||
/// (the description associated with the public key of the local sbot instance).
|
/// associated with the local sbot instance.
|
||||||
///
|
///
|
||||||
/// Calls `sbotcli bytype --limit 10 --reverse about`. On success: parses the `stdout` to extract the
|
/// Calls `sbotcli bytype --limit 10 --reverse about`. On success: parses the `stdout` to extract the
|
||||||
/// `description` and returns it. On error: returns the `stderr` output with a description.
|
/// `description` and returns it. On error: returns the `stderr` output with a description.
|
||||||
///
|
///
|
||||||
pub fn get_description(&self) -> Result<Option<String>, SbotCliError> {
|
pub fn get_description(&self) -> Result<Option<String>, SbotCliError> {
|
||||||
let output = Command::new(&self.sbotcli_path)
|
let self_id = self.whoami()?;
|
||||||
.arg("bytype")
|
self.get_about_message("description", &self_id)
|
||||||
.arg("--limit")
|
}
|
||||||
.arg("10")
|
|
||||||
.arg("--reverse")
|
/// Return latest profile image assignment from `about` msgs, for the public key
|
||||||
.arg("about")
|
/// associated with the local sbot instance.
|
||||||
.output()?;
|
///
|
||||||
if output.status.success() {
|
/// Calls `sbotcli bytype --limit 10 --reverse about`. On success: parses the `stdout` to extract the
|
||||||
let stdout = String::from_utf8(output.stdout)?;
|
/// `image` and returns it. On error: returns the `stderr` output with a description.
|
||||||
let description = utils::regex_finder(r#""description": "(.*)""#, &stdout)?;
|
///
|
||||||
Ok(description)
|
pub fn get_profile_image(&self) -> Result<Option<String>, SbotCliError> {
|
||||||
} else {
|
let self_id = self.whoami()?;
|
||||||
let stderr = std::str::from_utf8(&output.stderr)?;
|
self.get_about_message("image", &self_id)
|
||||||
// TODO: create a more generic error variant
|
|
||||||
Err(SbotCliError::GetAboutMsgs(format!(
|
|
||||||
"Error fetching about messages: {}",
|
|
||||||
stderr
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -402,6 +555,7 @@ impl Sbot {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// Publish an about message with a name for one's own profile,
|
/// Publish an about message with a name for one's own profile,
|
||||||
/// using whoami to find your own id.
|
/// using whoami to find your own id.
|
||||||
///
|
///
|
||||||
|
|
Loading…
Reference in New Issue