made session, network and help have the new structure. + removed cli temporarily from the workspace

This commit is contained in:
Mahesh Bansod 2021-11-02 14:09:15 +05:30
parent 370cc97f61
commit 02559e2fbc
11 changed files with 528 additions and 848 deletions

183
Cargo.lock generated
View File

@ -2,26 +2,6 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "ansi_term"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
dependencies = [
"winapi",
]
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi",
]
[[package]]
name = "autocfg"
version = "1.0.1"
@ -85,35 +65,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "2.33.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
dependencies = [
"ansi_term",
"atty",
"bitflags",
"strsim",
"textwrap",
"unicode-width",
"vec_map",
]
[[package]]
name = "cli"
version = "0.1.0"
dependencies = [
"rpassword",
"serde",
"serde_json",
"shellexpand",
"socialvoid",
"socialvoid_rawclient",
"structopt",
"tokio",
]
[[package]]
name = "core-foundation"
version = "0.9.1"
@ -148,27 +99,6 @@ dependencies = [
"generic-array",
]
[[package]]
name = "dirs-next"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
dependencies = [
"cfg-if",
"dirs-sys-next",
]
[[package]]
name = "dirs-sys-next"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]]
name = "encoding_rs"
version = "0.8.28"
@ -370,15 +300,6 @@ version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
[[package]]
name = "heck"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "hermit-abi"
version = "0.1.19"
@ -771,30 +692,6 @@ version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro-hack"
version = "0.5.19"
@ -921,16 +818,6 @@ dependencies = [
"bitflags",
]
[[package]]
name = "redox_users"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
dependencies = [
"getrandom",
"redox_syscall",
]
[[package]]
name = "remove_dir_all"
version = "0.5.3"
@ -976,16 +863,6 @@ dependencies = [
"winreg",
]
[[package]]
name = "rpassword"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffc936cf8a7ea60c58f030fd36a612a48f440610214dc54bc36431f9ea0c3efb"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "rust-crypto"
version = "0.2.36"
@ -1116,15 +993,6 @@ dependencies = [
"sha2",
]
[[package]]
name = "shellexpand"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83bdb7831b2d85ddf4a7b148aa19d0587eddbe8671a436b7bd1182eaad0f2829"
dependencies = [
"dirs-next",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.0"
@ -1197,36 +1065,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "strsim"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
[[package]]
name = "structopt"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf9d950ef167e25e0bdb073cf1d68e9ad2795ac826f2f3f59647817cf23c0bfa"
dependencies = [
"clap",
"lazy_static",
"structopt-derive",
]
[[package]]
name = "structopt-derive"
version = "0.4.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "134d838a2c9943ac3125cf6df165eda53493451b719f3255b2a26b85f772d0ba"
dependencies = [
"heck",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syn"
version = "1.0.76"
@ -1252,15 +1090,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "textwrap"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
dependencies = [
"unicode-width",
]
[[package]]
name = "time"
version = "0.1.43"
@ -1403,12 +1232,6 @@ dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-segmentation"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
[[package]]
name = "unicode-width"
version = "0.1.9"
@ -1439,12 +1262,6 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "version_check"
version = "0.9.3"

View File

@ -3,6 +3,5 @@ members = [
"client",
"types",
"rawclient",
"jsonrpc2-client",
"cli"
"jsonrpc2-client"
]

View File

@ -9,27 +9,27 @@
#[tokio::main]
async fn main() {
let creds1: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string("test_creds.test").unwrap())
.expect("Couldn't read the credentials. Check the JSON format or something");
// WIP
// let creds1: serde_json::Value =
// serde_json::from_str(&std::fs::read_to_string("test_creds.test").unwrap())
// .expect("Couldn't read the credentials. Check the JSON format or something");
let mut client1 = socialvoid::new_with_defaults().await.unwrap();
client1
.authenticate_user(
creds1["username"].as_str().unwrap().to_string(),
creds1["password"].as_str().unwrap().to_string(),
None,
)
.await
.unwrap();
// let mut sv = socialvoid::new_with_defaults().await.unwrap();
// sv
// .authenticate_user(
// creds1["username"].as_str().unwrap().to_string(),
// creds1["password"].as_str().unwrap().to_string(),
// None,
// )
// .await
// .unwrap();
let handle = tokio::spawn(async move {
let post = client1.compose_post("Yayaya", vec![]).await.unwrap();
println!("Made post!");
if client1.delete_post(post.id).await.unwrap() {
println!("Deleted successfully!");
}
});
handle.await.unwrap();
// let handle = tokio::spawn(async move {
// let post = sv.compose_post("Yayaya", vec![]).await.unwrap();
// println!("Made post!");
// if sv.delete_post(post.id).await.unwrap() {
// println!("Deleted successfully!");
// }
// });
// handle.await.unwrap();
}

View File

@ -13,8 +13,8 @@ async fn main() {
serde_json::from_str(&std::fs::read_to_string("test_creds.test").unwrap())
.expect("Couldn't read the credentials. Check the JSON format or something");
let mut client = socialvoid::new_with_defaults().await.unwrap();
client
let sv = socialvoid::new_with_defaults().await.unwrap();
sv.session
.authenticate_user(
creds["username"].as_str().unwrap().to_string(),
creds["password"].as_str().unwrap().to_string(),
@ -23,10 +23,10 @@ async fn main() {
.await
.unwrap();
let peer = client.get_me().await.unwrap();
let peer = sv.network.get_me().await.unwrap();
println!("{:?}", peer);
client.logout().await.unwrap();
sv.session.logout().await.unwrap();
assert_eq!(
peer.username,

View File

@ -1,19 +1,30 @@
use serde_json::json;
use socialvoid_rawclient::Error;
use socialvoid_types::SessionIdentification;
use std::sync::Arc;
pub async fn set_profile_picture(
client: &socialvoid_rawclient::Client,
session_identification: SessionIdentification,
document_id: String,
) -> Result<bool, Error> {
client
.send_request(
"account.set_profile_picture",
json!({
"session_identification": serde_json::to_value(session_identification)?,
"document": document_id,
}),
)
.await
pub struct SVAccountMethods {
client: Arc<socialvoid_rawclient::Client>,
}
impl SVAccountMethods {
pub fn new(client: Arc<socialvoid_rawclient::Client>) -> Self {
Self { client }
}
pub async fn set_profile_picture(
&self,
session_identification: SessionIdentification,
document_id: String,
) -> Result<bool, Error> {
self.client
.send_request(
"account.set_profile_picture",
json!({
"session_identification": serde_json::to_value(session_identification)?,
"document": document_id,
}),
)
.await
}
}

View File

@ -5,11 +5,14 @@ pub mod network;
pub mod session;
pub mod timeline;
use account::SVAccountMethods;
pub use error::ClientError;
pub use error::SocialvoidError;
use help::SVHelpMethods;
use network::SVNetworkMethods;
use session::ClientInfo;
use session::RegisterRequest;
use session::SVSessionMethods;
use session::Session;
use session::SessionHolder;
use socialvoid_types::Document;
@ -17,34 +20,46 @@ use socialvoid_types::HelpDocument;
use socialvoid_types::Peer;
use socialvoid_types::Post;
use socialvoid_types::Profile;
use std::sync::Arc;
use std::sync::{Arc, Mutex};
/// A client that can be used to call methods and manage sessions for Social Void
pub struct Client {
client_info: Arc<ClientInfo>,
session_holder: Arc<Mutex<SessionHolder>>,
rpc_client: Arc<socialvoid_rawclient::Client>,
cdn_client: Arc<socialvoid_rawclient::CdnClient>,
pub help: Arc<SVHelpMethods>,
pub session: Arc<SVSessionMethods>,
pub network: Arc<SVNetworkMethods>,
pub account: Arc<SVAccountMethods>,
}
/// Create a client and establish a new session
pub async fn new_with_defaults() -> Result<Client, SocialvoidError> {
let rpc_client = Arc::new(socialvoid_rawclient::new());
let cdn_client = make_cdn_client_from(Arc::clone(&rpc_client)).await?;
let client_info = ClientInfo::generate();
let mut session = SessionHolder::new(client_info.clone());
session.create(&rpc_client).await?;
let sessions = vec![session];
let (help) = init_methods(Arc::clone(&rpc_client));
let cdn_client = Arc::new(make_cdn_client_from(Arc::clone(&rpc_client)).await?);
let client_info = Arc::new(ClientInfo::generate());
let session_holder = Arc::new(Mutex::new(SessionHolder::new(Arc::clone(&client_info))));
let (session, network, account, help) = init_methods(
Arc::clone(&rpc_client),
Arc::clone(&cdn_client),
Arc::clone(&session_holder),
);
session.create().await?;
let client = Client {
current_session: Some(0),
sessions,
client_info,
rpc_client: socialvoid_rawclient::new(), //temporary,.. TODO: remove this
// rpc_client: &rpc_client,
session_holder,
rpc_client,
cdn_client,
help,
network,
session,
account,
};
Ok(client)
}
pub fn init_methods(client: Arc<socialvoid_rawclient::Client>) -> (SVHelpMethods) {
(SVHelpMethods::new(client))
}
/// Creates the CDN client by resolving the host url from server information
async fn make_cdn_client_from(
rpc_client: Arc<socialvoid_rawclient::Client>,
@ -58,24 +73,30 @@ async fn make_cdn_client_from(
))
}
/// Create a client with user defined client info and sessions
/// Create a client with user defined client info and session
/// And CDN as gven in the server information
/// TODO: maybe verify the session and return an error if session is invalid
pub async fn new(
client_info: ClientInfo,
sessions: Vec<SessionHolder>,
session: SessionHolder,
) -> Result<Client, SocialvoidError> {
let rpc_client = Arc::new(socialvoid_rawclient::new());
let cdn_client = make_cdn_client_from(Arc::clone(&rpc_client)).await?;
let current_session = if sessions.is_empty() { None } else { Some(0) };
let (help) = init_methods(Arc::clone(&rpc_client));
let cdn_client = Arc::new(make_cdn_client_from(Arc::clone(&rpc_client)).await?);
let session_holder = Arc::new(Mutex::new(session));
let (session, network, account, help) = init_methods(
Arc::clone(&rpc_client),
Arc::clone(&cdn_client),
Arc::clone(&session_holder),
);
Ok(Client {
current_session,
sessions,
client_info,
rpc_client: socialvoid_rawclient::new(),
session,
session_holder,
client_info: Arc::new(client_info),
rpc_client,
cdn_client,
help,
network,
account,
})
}
@ -83,252 +104,58 @@ pub async fn new(
/// Note that, cdn client may not be the one taken from server information
pub fn new_empty_client() -> Client {
let rpc_client = Arc::new(socialvoid_rawclient::new());
let (help) = init_methods(Arc::clone(&rpc_client));
let client_info = Arc::new(ClientInfo::generate());
let session_holder = Arc::new(Mutex::new(SessionHolder::new(Arc::clone(&client_info))));
let (session, network, account, help) = init_methods(
Arc::clone(&rpc_client),
Arc::new(socialvoid_rawclient::CdnClient::new()),
Arc::clone(&session_holder),
);
Client {
current_session: None,
sessions: Vec::new(),
client_info: ClientInfo::generate(),
rpc_client: socialvoid_rawclient::new(),
cdn_client: socialvoid_rawclient::CdnClient::new(),
client_info,
session_holder,
rpc_client,
cdn_client: Arc::new(socialvoid_rawclient::CdnClient::new()),
help,
network,
session,
account,
}
}
/// A client that can be used to call methods and manage sessions for Social Void
pub struct Client {
pub sessions: Vec<SessionHolder>,
current_session: Option<usize>, //Index of the current session
client_info: ClientInfo,
rpc_client: socialvoid_rawclient::Client,
cdn_client: socialvoid_rawclient::CdnClient,
pub help: SVHelpMethods,
pub fn init_methods(
client: Arc<socialvoid_rawclient::Client>,
cdn_client: Arc<socialvoid_rawclient::CdnClient>,
session_holder: Arc<Mutex<SessionHolder>>,
) -> (
Arc<SVSessionMethods>,
Arc<SVNetworkMethods>,
Arc<SVAccountMethods>,
Arc<SVHelpMethods>,
) {
let session = Arc::new(SVSessionMethods::new(
Arc::clone(&client),
Arc::clone(&cdn_client),
Arc::clone(&session_holder),
));
(
Arc::clone(&session),
Arc::new(SVNetworkMethods::new(
Arc::clone(&client),
Arc::clone(&session),
)),
Arc::new(SVAccountMethods::new(Arc::clone(&client))),
Arc::new(SVHelpMethods::new(client)),
)
}
impl Client {
/// Set the CDN server URL from the ServerInfomation
pub async fn reset_cdn_url(&mut self) -> Result<(), SocialvoidError> {
self.cdn_client = make_cdn_client_from(Arc::new(socialvoid_rawclient::new())).await?; //todo: fix
self.cdn_client =
Arc::new(make_cdn_client_from(Arc::new(socialvoid_rawclient::new())).await?); //todo: maybe propagate the change??
Ok(())
}
/// Saves all your sessions to a file
pub fn save_sessions(&self, filename: &str) -> Result<(), std::io::Error> {
// let filename = "social-void-rust.sessions";
serde_json::to_writer(&std::fs::File::create(filename)?, &self.sessions)?;
Ok(())
}
/// Loads all sessions from a file and adds them to the client
pub fn load_sessions(&mut self, fpath: &str) -> Result<(), std::io::Error> {
let sessions: Vec<SessionHolder> = serde_json::from_reader(&std::fs::File::open(fpath)?)?;
if self.sessions.is_empty() && !sessions.is_empty() {
self.current_session = Some(0);
}
self.sessions.extend(sessions);
Ok(())
}
/// Get another video
/// Tries to establish another session adds it to the client if successful and returns the key of the session
pub async fn new_session(&mut self) -> Result<usize, SocialvoidError> {
let mut session = SessionHolder::new(self.client_info.clone());
session.create(&self.rpc_client).await?;
self.sessions.push(session);
Ok(self.sessions.len() - 1)
}
/// Removes the current session and returns it
pub fn delete_session(&mut self) -> Result<SessionHolder, SocialvoidError> {
if self.current_session.is_none() {
Err(SocialvoidError::Client(ClientError::NoSessionsExist))
} else {
let sesh_key = self.current_session.unwrap();
self.current_session = if sesh_key == self.sessions.len() - 1 {
if sesh_key != 0 {
Some(sesh_key - 1)
} else {
None
}
} else {
Some(sesh_key)
};
Ok(self.sessions.remove(sesh_key))
}
}
/// Set the current session to session_key if exists
pub fn set_current_session(&mut self, session_key: usize) -> Result<(), SocialvoidError> {
if self.sessions.len() > session_key {
self.current_session = Some(session_key);
Ok(())
} else {
Err(SocialvoidError::Client(
ClientError::SessionIndexOutOfBounds {
session_count: self.sessions.len(),
},
))
}
}
/// Get the current session key
pub fn get_current_session_key(&self) -> Option<usize> {
self.current_session
}
/// Gets a Session object for the current session
pub async fn get_session(&mut self) -> Result<Session, SocialvoidError> {
match self.current_session {
Some(session_key) => Ok(self.sessions[session_key].get(&self.rpc_client).await?),
None => Err(SocialvoidError::Client(ClientError::NoSessionsExist)),
}
}
/// Get terms of service
pub async fn get_terms_of_service(&self) -> Result<HelpDocument, SocialvoidError> {
Ok(self.help.get_terms_of_service().await?)
}
/// Accept terms of service for the current session
pub fn accept_tos(&mut self, tos: HelpDocument) -> Result<(), SocialvoidError> {
match self.current_session {
Some(session_key) => {
self.sessions[session_key].accept_terms_of_service(tos);
Ok(())
}
None => Err(SocialvoidError::Client(ClientError::NoSessionsExist)),
}
}
/// Register an account using the current session
pub async fn register(&mut self, req: RegisterRequest) -> Result<Peer, SocialvoidError> {
match self.current_session {
Some(session_key) => Ok(self.sessions[session_key]
.register(req, &self.rpc_client)
.await?),
None => Err(SocialvoidError::Client(ClientError::NoSessionsExist)),
}
}
/// Login to an account using the current session
pub async fn authenticate_user(
&mut self,
username: String,
password: String,
otp: Option<String>,
) -> Result<bool, SocialvoidError> {
match self.current_session {
Some(session_key) => Ok(self.sessions[session_key]
.authenticate_user(&self.rpc_client, username, password, otp)
.await?),
None => Err(SocialvoidError::Client(ClientError::NoSessionsExist)),
}
}
/// Check if current session is authenticated
pub fn is_authenticated(&self) -> Result<bool, SocialvoidError> {
match self.current_session {
Some(session_key) => Ok(self.sessions[session_key].authenticated()),
None => Err(SocialvoidError::Client(ClientError::NoSessionsExist)),
}
}
/// Log out from the current session
pub async fn logout(&mut self) -> Result<bool, SocialvoidError> {
match self.current_session {
Some(session_key) => {
let log_out_resp = self.sessions[session_key].logout(&self.rpc_client).await?;
self.delete_session()?;
Ok(log_out_resp)
}
None => Err(SocialvoidError::Client(ClientError::NoSessionsExist)),
}
}
/// Get Peer object of the authenticated user on the current session.
pub async fn get_me(&self) -> Result<Peer, SocialvoidError> {
match self.current_session {
Some(session_key) => Ok(network::get_me(
&self.rpc_client,
self.sessions[session_key].session_identification()?,
)
.await?),
None => Err(SocialvoidError::Client(ClientError::NoSessionsExist)),
}
}
/// Get the profile of the authenticated user on the current session
pub async fn get_my_profile(&self) -> Result<Profile, SocialvoidError> {
match self.current_session {
Some(session_key) => Ok(network::get_profile(
&self.rpc_client,
self.sessions[session_key].session_identification()?,
None,
)
.await?),
None => Err(SocialvoidError::Client(ClientError::NoSessionsExist)),
}
}
/// Set the profile picture of the user on current session
pub async fn set_profile_picture(&self, filepath: String) -> Result<Document, SocialvoidError> {
match self.current_session {
Some(session_key) => {
let sesh_id = self.sessions[session_key].session_identification()?;
let document = self.cdn_client.upload(sesh_id.clone(), filepath).await?;
account::set_profile_picture(&self.rpc_client, sesh_id, document.id.clone())
.await?; //TODO: use result and send client error if false
Ok(document)
}
None => Err(SocialvoidError::Client(ClientError::NoSessionsExist)),
}
}
/// Compose a new post to put on the timeline
/// text: The text contents of the post to compose
/// attachments: A vector of Document IDs to attach to the post
pub async fn compose_post(
&self,
text: &str,
attachments: Vec<String>,
) -> Result<Post, SocialvoidError> {
match self.current_session {
Some(session_key) => {
let sesh_id = self.sessions[session_key].session_identification()?;
Ok(
timeline::compose(&self.rpc_client, sesh_id, text.to_string(), attachments)
.await?,
)
}
None => Err(SocialvoidError::Client(ClientError::NoSessionsExist)),
}
}
/// Delete your post to from the timeline
/// post: ID of the post you want to delete
pub async fn delete_post(&self, post: String) -> Result<bool, SocialvoidError> {
match self.current_session {
Some(session_key) => {
let sesh_id = self.sessions[session_key].session_identification()?;
Ok(timeline::delete(&self.rpc_client, sesh_id, post).await?)
}
None => Err(SocialvoidError::Client(ClientError::NoSessionsExist)),
}
}
/// Retrieve the posts from the authenticated users timeline
/// post: ID of the post you want to delete
pub async fn retrieve_feed_max(&self) -> Result<Vec<Post>, SocialvoidError> {
match self.current_session {
Some(session_key) => {
let sesh_id = self.sessions[session_key].session_identification()?;
let page = Some(10); //TODO:GET THIS FROM server information which will be cached or smn
Ok(timeline::retrieve_feed(&self.rpc_client, sesh_id, page).await?)
}
None => Err(SocialvoidError::Client(ClientError::NoSessionsExist)),
}
}
}
#[cfg(test)]
@ -346,8 +173,8 @@ mod tests {
let creds: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(CREDS_FILE_1).unwrap())?;
let mut client = new_with_defaults().await?;
client
let mut sv = new_with_defaults().await?;
sv.session
.authenticate_user(
creds["username"].as_str().unwrap().to_string(),
creds["password"].as_str().unwrap().to_string(),
@ -355,10 +182,10 @@ mod tests {
)
.await?;
let peer = client.get_me().await?;
let peer = sv.network.get_me().await?;
println!("{:?}", peer);
client.logout().await?;
sv.session.logout().await?;
assert_eq!(
peer.username,
creds["username"].as_str().unwrap().to_string()
@ -367,28 +194,28 @@ mod tests {
Ok(())
}
#[tokio::test]
async fn it_should_create_post_and_delete_it() -> Result<(), SocialvoidError> {
let creds: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(CREDS_FILE_1).unwrap())?;
let mut client = new_with_defaults().await?;
client
.authenticate_user(
creds["username"].as_str().unwrap().to_string(),
creds["password"].as_str().unwrap().to_string(),
None,
)
.await?;
// #[tokio::test]
// async fn it_should_create_post_and_delete_it() -> Result<(), SocialvoidError> {
// let creds: serde_json::Value =
// serde_json::from_str(&std::fs::read_to_string(CREDS_FILE_1).unwrap())?;
// let mut sv = new_with_defaults().await?;
// sv.session
// .authenticate_user(
// creds["username"].as_str().unwrap().to_string(),
// creds["password"].as_str().unwrap().to_string(),
// None,
// )
// .await?;
let post_text = thread_rng()
.sample_iter(&Alphanumeric)
.take(30)
.map(char::from)
.collect::<String>();
let post = client.compose_post(&post_text, Vec::new()).await?;
if !client.delete_post(post.id).await? {
panic!("Delete post returned false unexpectedly.")
}
Ok(())
}
// let post_text = thread_rng()
// .sample_iter(&Alphanumeric)
// .take(30)
// .map(char::from)
// .collect::<String>();
// let post = client.compose_post(&post_text, Vec::new()).await?;
// if !client.delete_post(post.id).await? {
// panic!("Delete post returned false unexpectedly.")
// }
// Ok(())
// }
}

View File

@ -1,118 +1,115 @@
use crate::SVSessionMethods;
use serde_json::json;
use socialvoid_rawclient as rawclient;
use socialvoid_rawclient::Error;
use socialvoid_types::Peer;
use socialvoid_types::Profile;
use socialvoid_types::RelationshipType;
use socialvoid_types::SessionIdentification;
use std::sync::Arc;
/// GetMe
/// Returns the peer object of the authenticated peer
pub async fn get_me(
client: &rawclient::Client,
session_identification: SessionIdentification,
) -> Result<Peer, Error> {
client
.send_request(
"network.get_me",
json!({
"session_identification": serde_json::to_value(session_identification)?
}),
)
.await
pub struct SVNetworkMethods {
client: Arc<rawclient::Client>,
session: Arc<SVSessionMethods>,
}
/// GetProfile
/// `peer` can be 'None' for own profile, otherwise,
/// 'peer' can be Some(p) where p can be the id or username(with leading @) of the peer.
pub async fn get_profile(
client: &rawclient::Client,
session_identification: SessionIdentification,
peer: Option<String>,
) -> Result<Profile, Error> {
client
.send_request(
"network.get_profile",
json!({
"session_identification": serde_json::to_value(session_identification)?,
"peer": peer,
}),
)
.await
}
impl SVNetworkMethods {
pub fn new(client: Arc<rawclient::Client>, session: Arc<SVSessionMethods>) -> SVNetworkMethods {
SVNetworkMethods { client, session }
}
/// ResolvePeer
pub async fn resolve_peer(
client: &rawclient::Client,
session_identification: SessionIdentification,
peer: String,
) -> Result<Peer, Error> {
client
.send_request(
"network.resolve_peer",
json!({
"session_identification": serde_json::to_value(session_identification)?,
"peer": peer,
}),
)
.await
}
/// GetMe
/// Returns the peer object of the authenticated peer
pub async fn get_me(&self) -> Result<Peer, Error> {
let session_identification = self.session.session_identification()?;
self.client
.send_request(
"network.get_me",
json!({
"session_identification": serde_json::to_value(session_identification)?
}),
)
.await
}
/// UnfollowPeer
pub async fn unfollow_peer(
client: &rawclient::Client,
session_identification: SessionIdentification,
peer: String,
) -> Result<RelationshipType, Error> {
client
.send_request(
"network.unfollow_peer",
json!({
"session_identification": serde_json::to_value(session_identification)?,
"peer": peer,
}),
)
.await
}
/// GetProfile
/// `peer` can be 'None' for own profile, otherwise,
/// 'peer' can be Some(p) where p can be the id or username(with leading @) of the peer.
pub async fn get_profile(&self, peer: Option<String>) -> Result<Profile, Error> {
let session_identification = self.session.session_identification()?;
self.client
.send_request(
"network.get_profile",
json!({
"session_identification": serde_json::to_value(session_identification)?,
"peer": peer,
}),
)
.await
}
/// FollowPeer
pub async fn follow_peer(
client: &rawclient::Client,
session_identification: SessionIdentification,
peer: String,
) -> Result<RelationshipType, Error> {
client
.send_request(
"network.follow_peer",
json!({
"session_identification": serde_json::to_value(session_identification)?,
"peer": peer,
}),
)
.await
/// ResolvePeer
pub async fn resolve_peer(&self, peer: String) -> Result<Peer, Error> {
let session_identification = self.session.session_identification()?;
self.client
.send_request(
"network.resolve_peer",
json!({
"session_identification": serde_json::to_value(session_identification)?,
"peer": peer,
}),
)
.await
}
/// UnfollowPeer
pub async fn unfollow_peer(&self, peer: String) -> Result<RelationshipType, Error> {
let session_identification = self.session.session_identification()?;
self.client
.send_request(
"network.unfollow_peer",
json!({
"session_identification": serde_json::to_value(session_identification)?,
"peer": peer,
}),
)
.await
}
/// FollowPeer
pub async fn follow_peer(&self, peer: String) -> Result<RelationshipType, Error> {
let session_identification = self.session.session_identification()?;
self.client
.send_request(
"network.follow_peer",
json!({
"session_identification": serde_json::to_value(session_identification)?,
"peer": peer,
}),
)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::session::{ClientInfo, SessionHolder};
use crate::session::{ClientInfo, SVSessionMethods, SessionHolder};
use rawclient::{AuthenticationError, ErrorKind};
use std::sync::{Arc, Mutex};
#[tokio::test]
async fn it_should_return_a_notauthenticated_error() {
let client = rawclient::new();
let mut session = SessionHolder::new(ClientInfo::generate());
session
.create(&client)
.await
.expect("Couldn't create a session.");
match get_me(
&client,
session
.session_identification()
.expect("Couldn't get session identification object (unestablished session)"),
)
.await
{
let client = Arc::new(socialvoid_rawclient::new());
let session = Arc::new(SVSessionMethods::new(
Arc::clone(&client),
Arc::new(socialvoid_rawclient::CdnClient::new()),
Arc::new(Mutex::new(SessionHolder::new(Arc::new(
ClientInfo::generate(),
)))),
));
session.create().await.expect("Couldn't create a session.");
let network = SVNetworkMethods::new(Arc::clone(&client), Arc::clone(&session));
match network.get_me().await {
Ok(_) => panic!("Session found for some reason.?"),
Err(error) => match error.kind {
ErrorKind::Authentication(error) => match error {

View File

@ -1,8 +1,8 @@
mod client_info;
mod session;
mod session_challenge;
pub use client_info::ClientInfo;
pub use session::RegisterRequest;
pub use session::Session;
pub use session::SessionEstablished;
pub use session::SessionHolder;
pub use session::SessionRegisterInput;

View File

@ -1,14 +1,7 @@
use super::ClientInfo;
use serde::{Deserialize, Serialize};
use serde_json::json;
use socialvoid_rawclient::ClientError;
use socialvoid_rawclient::Error;
use socialvoid_types::Document;
pub use socialvoid_types::HelpDocument;
use socialvoid_types::Peer;
use socialvoid_types::SessionIdentification;
use super::session_challenge::answer_challenge;
use std::sync::Arc;
#[derive(Serialize, Deserialize, Debug)]
pub struct Session {
@ -25,16 +18,17 @@ pub struct SessionEstablished {
pub challenge: String,
}
#[derive(Serialize, Deserialize, Debug)]
//TODO: maybe do serde thing
#[derive(Debug)]
pub struct SessionHolder {
pub established: Option<SessionEstablished>,
authenticated: bool,
client_info: ClientInfo,
tos_read: Option<String>, //Holds the terms of service ID
pub authenticated: bool,
pub client_info: Arc<ClientInfo>,
pub tos_read: Option<String>, //Holds the terms of service ID
}
impl SessionHolder {
pub fn new(client_info: ClientInfo) -> SessionHolder {
pub fn new(client_info: Arc<ClientInfo>) -> SessionHolder {
SessionHolder {
established: None,
client_info,
@ -42,178 +36,6 @@ impl SessionHolder {
authenticated: false,
}
}
/// `session.create`
/// Creates a session and sets a session established object which contains a challenge.
/// A session object is not yet returned - the challenge needs to be solved and sent inside a session identification
/// object using the `get_session` method to get the Session object.
pub async fn create(&mut self, rpc_client: &socialvoid_rawclient::Client) -> Result<(), Error> {
let client_info = &self.client_info;
self.established = Some(
rpc_client
.send_request("session.create", serde_json::value::to_value(client_info)?)
.await?,
);
Ok(())
}
/// `session.get`
/// Returns a `Session`
pub async fn get(
&mut self,
rpc_client: &socialvoid_rawclient::Client,
) -> Result<Session, Error> {
let session_identification = self.session_identification()?;
let sesh: Session = rpc_client
.send_request(
"session.get",
json!({"session_identification": serde_json::value::to_value(session_identification)?}),
)
.await?;
self.authenticated = sesh.authenticated;
Ok(sesh)
}
/// `session.authenticate_user`
/// Authenticates a user via a username & password and optionally an OTP - extends session expiration time
pub async fn authenticate_user(
&mut self,
rpc_client: &socialvoid_rawclient::Client,
username: String,
password: String,
otp: Option<String>,
) -> Result<bool, Error> {
let session_identification = self.session_identification()?;
let response = rpc_client
.send_request(
"session.authenticate_user",
json!({
"session_identification": serde_json::to_value(session_identification)?,
"username": username,
"password": password,
"otp": otp
}),
)
.await?;
self.authenticated = true;
Ok(response)
}
/// `session.logout`
/// Log out without destroying the session - changes the session expiration date too
pub async fn logout(
&mut self,
rpc_client: &socialvoid_rawclient::Client,
) -> Result<bool, Error> {
let session_identification = self.session_identification()?;
let response = rpc_client
.send_request(
"session.logout",
json!({
"session_identification":serde_json::value::to_value(session_identification)?
}),
)
.await?;
self.authenticated = false;
Ok(response)
}
/// session.register
/// Registers a new user to the network
pub async fn register(
&mut self,
request: RegisterRequest,
rpc_client: &socialvoid_rawclient::Client,
) -> Result<Peer, Error> {
let session_identification = self.session_identification()?;
let request = SessionRegisterInput {
session_identification,
terms_of_service_id: self
.tos_read
.take()
.ok_or_else(|| Error::new_client_error(ClientError::TermsOfServiceNotAgreed))?,
terms_of_service_agree: true,
username: request.username,
password: request.password,
first_name: request.first_name,
last_name: request.last_name,
};
rpc_client
.send_request("session.register", serde_json::to_value(request)?)
.await
}
/// Upload a file to the CDN
pub async fn upload_file(
&self,
file: &str,
cdn_client: &socialvoid_rawclient::CdnClient,
) -> Result<Document, Error> {
let session_identification = self.session_identification()?;
cdn_client
.upload(session_identification, file.to_string())
.await
}
/// Download a file from the CDN
pub async fn download_file(
&self,
document_id: String,
cdn_client: &socialvoid_rawclient::CdnClient,
) -> Result<Vec<u8>, Error> {
let session_identification = self.session_identification()?;
cdn_client
.download(session_identification, document_id)
.await
}
/// Accepts the terms of service
/// The client must explicitly call `session.accept_terms_of_service(terms_of_service)` to
/// accept the terms of service. The HelpDocument can be acquired via `help::get_terms_of_service(socialvoid_rawclient)`
pub fn accept_terms_of_service(&mut self, tos: HelpDocument) {
self.tos_read = Some(tos.id);
}
pub fn session_identification(&self) -> Result<SessionIdentification, Error> {
if self.established.is_none() {
return Err(Error::new_client_error(ClientError::SessionNotEstablished));
}
let session_id = self
.established
.as_ref()
.map(|s| &s.id)
.unwrap()
.to_string();
let challenge = self
.established
.as_ref()
.map(|s| &s.challenge)
.unwrap()
.to_string();
let client_public_hash = self.client_info.public_hash.clone();
Ok(SessionIdentification {
session_id,
client_public_hash,
challenge_answer: answer_challenge(self.client_info.private_hash.clone(), challenge),
})
}
pub fn authenticated(&self) -> bool {
self.authenticated
}
}
#[derive(Serialize, Debug)]
struct SessionRegisterInput {
session_identification: SessionIdentification,
terms_of_service_id: String,
terms_of_service_agree: bool,
username: String,
password: String,
first_name: String,
last_name: Option<String>,
}
pub struct RegisterRequest {
@ -222,3 +44,14 @@ pub struct RegisterRequest {
pub first_name: String,
pub last_name: Option<String>,
}
#[derive(Serialize, Debug)]
pub struct SessionRegisterInput {
pub session_identification: SessionIdentification,
pub terms_of_service_id: String,
pub terms_of_service_agree: bool,
pub username: String,
pub password: String,
pub first_name: String,
pub last_name: Option<String>,
}

View File

@ -1,10 +1,193 @@
mod entities;
mod session_challenge;
pub use entities::ClientInfo;
pub use entities::RegisterRequest;
pub use entities::Session;
pub use entities::SessionEstablished;
pub use entities::SessionHolder;
use entities::SessionRegisterInput;
use session_challenge::answer_challenge;
use socialvoid_rawclient::ClientError;
use socialvoid_rawclient::Error;
use socialvoid_types::Document;
pub use socialvoid_types::HelpDocument;
use socialvoid_types::Peer;
use socialvoid_types::SessionIdentification;
use serde_json::json;
use std::sync::Arc;
use std::sync::Mutex;
pub struct SVSessionMethods {
client: Arc<socialvoid_rawclient::Client>,
cdn_client: Arc<socialvoid_rawclient::CdnClient>,
session: Arc<Mutex<SessionHolder>>,
}
impl SVSessionMethods {
pub fn new(
client: Arc<socialvoid_rawclient::Client>,
cdn_client: Arc<socialvoid_rawclient::CdnClient>,
session: Arc<Mutex<SessionHolder>>,
) -> Self {
Self {
client,
cdn_client,
session,
}
}
/// `session.create`
/// Creates a session and sets a session established object which contains a challenge.
/// A session object is not yet returned - the challenge needs to be solved and sent inside a session identification
/// object using the `get_session` method to get the Session object.
pub async fn create(&self) -> Result<(), Error> {
let mut session = self.session.lock().unwrap();
let client_info = &*session.client_info.clone();
session.established = Some(
self.client
.send_request("session.create", serde_json::value::to_value(client_info)?)
.await?,
);
Ok(())
}
/// `session.get`
/// Returns a `Session`
pub async fn get(&self) -> Result<Session, Error> {
let session_identification = self.session_identification()?;
let mut session = self.session.lock().unwrap();
let sesh: Session = self.client
.send_request(
"session.get",
json!({"session_identification": serde_json::value::to_value(session_identification)?}),
)
.await?;
session.authenticated = sesh.authenticated;
Ok(sesh)
}
/// `session.authenticate_user`
/// Authenticates a user via a username & password and optionally an OTP - extends session expiration time
pub async fn authenticate_user(
&self,
username: String,
password: String,
otp: Option<String>,
) -> Result<bool, Error> {
let session_identification = self.session_identification()?;
let response = self
.client
.send_request(
"session.authenticate_user",
json!({
"session_identification": serde_json::to_value(session_identification)?,
"username": username,
"password": password,
"otp": otp
}),
)
.await?;
self.session.lock().unwrap().authenticated = true;
Ok(response)
}
/// `session.logout`
/// Log out without destroying the session - changes the session expiration date too
pub async fn logout(&self) -> Result<bool, Error> {
let session_identification = self.session_identification()?;
let response = self
.client
.send_request(
"session.logout",
json!({
"session_identification":serde_json::value::to_value(session_identification)?
}),
)
.await?;
self.session.lock().unwrap().authenticated = false;
Ok(response)
}
/// session.register
/// Registers a new user to the network
pub async fn register(&self, request: RegisterRequest) -> Result<Peer, Error> {
let session_identification = self.session_identification()?;
let request = SessionRegisterInput {
session_identification,
terms_of_service_id: self
.session
.lock()
.unwrap()
.tos_read
.take()
.ok_or_else(|| Error::new_client_error(ClientError::TermsOfServiceNotAgreed))?,
terms_of_service_agree: true,
username: request.username,
password: request.password,
first_name: request.first_name,
last_name: request.last_name,
};
self.client
.send_request("session.register", serde_json::to_value(request)?)
.await
}
/// Upload a file to the CDN
pub async fn upload_file(&self, file: &str) -> Result<Document, Error> {
let session_identification = self.session_identification()?;
self.cdn_client
.upload(session_identification, file.to_string())
.await
}
/// Download a file from the CDN
pub async fn download_file(&self, document_id: String) -> Result<Vec<u8>, Error> {
let session_identification = self.session_identification()?;
self.cdn_client
.download(session_identification, document_id)
.await
}
/// Accepts the terms of service
/// The client must explicitly call `session.accept_terms_of_service(terms_of_service)` to
/// accept the terms of service. The HelpDocument can be acquired via `help::get_terms_of_service(socialvoid_rawclient)`
pub fn accept_terms_of_service(&self, tos: HelpDocument) {
self.session.lock().unwrap().tos_read = Some(tos.id);
}
pub fn session_identification(&self) -> Result<SessionIdentification, Error> {
let session = self.session.lock().unwrap();
if session.established.is_none() {
return Err(Error::new_client_error(ClientError::SessionNotEstablished));
}
let session_id = session
.established
.as_ref()
.map(|s| &s.id)
.unwrap()
.to_string();
let challenge = session
.established
.as_ref()
.map(|s| &s.challenge)
.unwrap()
.to_string();
let client_public_hash = session.client_info.public_hash.clone();
Ok(SessionIdentification {
session_id,
client_public_hash,
challenge_answer: answer_challenge(session.client_info.private_hash.clone(), challenge),
})
}
pub fn authenticated(&self) -> bool {
self.session.lock().unwrap().authenticated
}
}
#[cfg(test)]
mod tests {
@ -15,11 +198,16 @@ mod tests {
use socialvoid_rawclient::{ClientError, Error, ErrorKind};
#[tokio::test]
async fn it_should_establish_a_session_and_get_it() -> Result<(), Error> {
let mut session = SessionHolder::new(ClientInfo::generate());
let client = socialvoid_rawclient::new();
session.create(&client).await?;
let mut session = SVSessionMethods::new(
Arc::new(socialvoid_rawclient::new()),
Arc::new(socialvoid_rawclient::CdnClient::new()),
Arc::new(Mutex::new(SessionHolder::new(Arc::new(
ClientInfo::generate(),
)))),
);
session.create().await?;
let sesh = session.get(&client).await?;
let sesh = session.get().await?;
println!("{:?}", sesh);
// assert_eq!(established.id, sesh.id);
Ok(())
@ -28,14 +216,18 @@ mod tests {
async fn it_should_establish_a_session_and_upload_and_download_a_file() -> Result<(), Error> {
let creds: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string("../client/test_creds.test").unwrap())?;
let mut session = SessionHolder::new(ClientInfo::generate());
let client = socialvoid_rawclient::new();
session.create(&client).await?;
let mut session = SVSessionMethods::new(
Arc::new(socialvoid_rawclient::new()),
Arc::new(socialvoid_rawclient::CdnClient::new()),
Arc::new(Mutex::new(SessionHolder::new(Arc::new(
ClientInfo::generate(),
)))),
);
session.create().await?;
assert_eq!(
session
.authenticate_user(
&client,
creds["username"].as_str().unwrap().to_string(),
creds["password"].as_str().unwrap().to_string(),
None
@ -51,13 +243,13 @@ mod tests {
.map(char::from)
.collect();
std::fs::write(file_name, file_content.clone()).unwrap();
let cdn_client = socialvoid_rawclient::CdnClient::new();
let document = session.upload_file(file_name, &cdn_client).await?;
let document = session.upload_file(file_name).await?;
println!("Document: {:#?}", document);
std::fs::remove_file(file_name).unwrap();
let id = document.id;
let downloaded_file = session.download_file(id, &cdn_client).await?;
let downloaded_file = session.download_file(id).await?;
assert_eq!(String::from_utf8_lossy(&downloaded_file), file_content);
// assert_eq!(established.id, sesh.id);
Ok(())
@ -78,19 +270,22 @@ mod tests {
#[tokio::test]
async fn it_should_throw_a_terms_of_service_not_agreed_error() -> Result<(), Error> {
let mut session = SessionHolder::new(ClientInfo::generate());
let client = socialvoid_rawclient::new();
session.create(&client).await?;
let mut session = SVSessionMethods::new(
Arc::new(socialvoid_rawclient::new()),
Arc::new(socialvoid_rawclient::CdnClient::new()),
Arc::new(Mutex::new(SessionHolder::new(Arc::new(
ClientInfo::generate(),
)))),
);
session.create().await?;
let response = session
.register(
RegisterRequest {
first_name: "Light".to_string(),
last_name: None,
username: "justanotherlight".to_string(),
password: "SuperStrongPassword".to_string(),
},
&client,
)
.register(RegisterRequest {
first_name: "Light".to_string(),
last_name: None,
username: "justanotherlight".to_string(),
password: "SuperStrongPassword".to_string(),
})
.await;
match response {
Err(e) => match e.kind {
@ -104,4 +299,40 @@ mod tests {
}
Ok(())
}
#[tokio::test]
async fn it_should_calculate_the_correct_answer() {
//This test creates a session and calculates the challenge answer
// using the python script in the standard and the current implementation and
// compares both
use crate::ClientInfo;
use std::process::Command;
let client_info = ClientInfo::generate();
let private_hash = client_info.private_hash.clone();
let mut session = SVSessionMethods::new(
Arc::new(socialvoid_rawclient::new()),
Arc::new(socialvoid_rawclient::CdnClient::new()),
Arc::new(Mutex::new(SessionHolder::new(Arc::new(
ClientInfo::generate(),
)))),
);
session.create().await.expect("Couldn't create the session");
let established = session.session.lock().unwrap().established.unwrap();
let challenge_answer =
answer_challenge(private_hash.clone(), established.challenge.clone());
let output = Command::new("python3")
.arg("src/session/test-hotp.py")
.arg(&private_hash)
.arg(&established.challenge)
.output()
.expect("Couldn't run python script");
println!(
"output: {}, err: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(String::from_utf8_lossy(&output.stdout), challenge_answer);
}
}

View File

@ -69,39 +69,4 @@ mod tests {
println!("{}", otp_string);
assert_eq!(otp_string.len(), 6);
}
#[tokio::test]
async fn it_should_calculate_the_correct_answer() {
//This test creates a session and calculates the challenge answer
// using the python script in the standard and the current implementation and
// compares both
use crate::ClientInfo;
use crate::SessionHolder;
use std::process::Command;
let client_info = ClientInfo::generate();
let private_hash = client_info.private_hash.clone();
let mut session = SessionHolder::new(client_info);
let client = socialvoid_rawclient::new();
session
.create(&client)
.await
.expect("Couldn't create the session");
let established = session.established.unwrap();
let challenge_answer =
answer_challenge(private_hash.clone(), established.challenge.clone());
let output = Command::new("python3")
.arg("src/session/test-hotp.py")
.arg(&private_hash)
.arg(&established.challenge)
.output()
.expect("Couldn't run python script");
println!(
"output: {}, err: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(String::from_utf8_lossy(&output.stdout), challenge_answer);
}
}