diff --git a/Cargo.lock b/Cargo.lock index c88041476c..0a6ae61e3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3073,10 +3073,12 @@ dependencies = [ "log4rs", "minotari_app_utilities", "openssl", + "rand", "tari_chat_client", "tari_common", "tari_common_types", "tari_contacts", + "tari_crypto", "tari_p2p", "tari_shutdown", "tari_utilities", diff --git a/base_layer/chat_ffi/Cargo.toml b/base_layer/chat_ffi/Cargo.toml index e00273b9de..2229e68147 100644 --- a/base_layer/chat_ffi/Cargo.toml +++ b/base_layer/chat_ffi/Cargo.toml @@ -31,6 +31,8 @@ crate-type = ["staticlib","cdylib"] [dev-dependencies] chrono = { version = "0.4.19", default-features = false } +rand = "0.8" +tari_crypto = "0.18.0" [build-dependencies] cbindgen = "0.24.3" diff --git a/base_layer/chat_ffi/chat.h b/base_layer/chat_ffi/chat.h index 00701fccc8..60a14a9dfc 100644 --- a/base_layer/chat_ffi/chat.h +++ b/base_layer/chat_ffi/chat.h @@ -18,6 +18,8 @@ struct Confirmation; struct ContactsLivenessData; +struct ConversationalistsVector; + struct Message; struct MessageMetadata; @@ -373,6 +375,70 @@ long long read_liveness_data_last_seen(struct ContactsLivenessData *liveness, */ void destroy_contacts_liveness_data(struct ContactsLivenessData *ptr); +/** + * Return a ptr to a ConversationalistsVector + * + * ## Arguments + * `client` - The ChatClient + * `error_out` - Pointer to an int which will be modified + * + * ## Returns + * `*mut ptr ConversationalistsVector` - a pointer to a ConversationalistsVector + * + * ## Safety + * The `ConversationalistsVector` should be destroyed after use + */ +struct ConversationalistsVector *get_conversationalists(struct ChatClient *client, int *error_out); + +/** + * Returns the length of the ConversationalistsVector + * + * ## Arguments + * `conversationalists` - A pointer to a ConversationalistsVector + * `error_out` - Pointer to an int which will be modified + * + * ## Returns + * `c_int` - The length of the vector. May return -1 if something goes wrong + * + * ## Safety + * `conversationalists` should be destroyed eventually + */ +int conversationalists_vector_len(struct ConversationalistsVector *conversationalists, + int *error_out); + +/** + * Reads the ConversationalistsVector and returns a pointer to a TariAddress at a given position + * + * ## Arguments + * `conversationalists` - A pointer to a ConversationalistsVector + * `position` - The index of the vector for a TariAddress + * `error_out` - Pointer to an int which will be modified + * + * ## Returns + * `*mut ptr TariAddress` - A pointer to a TariAddress + * + * ## Safety + * `conversationalists` should be destroyed eventually + * the returned `TariAddress` should be destroyed eventually + */ +struct TariAddress *conversationalists_vector_get_at(struct ConversationalistsVector *conversationalists, + unsigned int position, + int *error_out); + +/** + * Frees memory for ConversationalistsVector + * + * ## Arguments + * `ptr` - The pointer of a ConversationalistsVector + * + * ## Returns + * `()` - Does not return a value, equivalent to void in C + * + * # Safety + * None + */ +void destroy_conversationalists_vector(struct ConversationalistsVector *ptr); + /** * Creates a message and returns a pointer to it * diff --git a/base_layer/chat_ffi/src/conversationalists.rs b/base_layer/chat_ffi/src/conversationalists.rs new file mode 100644 index 0000000000..ec096fc498 --- /dev/null +++ b/base_layer/chat_ffi/src/conversationalists.rs @@ -0,0 +1,186 @@ +// Copyright 2023, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::{convert::TryFrom, ptr}; + +use libc::{c_int, c_uint}; +use tari_chat_client::ChatClient as ChatClientTrait; +use tari_common_types::tari_address::TariAddress; + +use crate::{ + error::{InterfaceError, LibChatError}, + ChatClient, +}; + +#[derive(Debug, PartialEq, Clone)] +pub struct ConversationalistsVector(pub Vec); + +/// Return a ptr to a ConversationalistsVector +/// +/// ## Arguments +/// `client` - The ChatClient +/// `error_out` - Pointer to an int which will be modified +/// +/// ## Returns +/// `*mut ptr ConversationalistsVector` - a pointer to a ConversationalistsVector +/// +/// ## Safety +/// The `ConversationalistsVector` should be destroyed after use +#[no_mangle] +pub unsafe extern "C" fn get_conversationalists( + client: *mut ChatClient, + error_out: *mut c_int, +) -> *mut ConversationalistsVector { + let mut error = 0; + ptr::swap(error_out, &mut error as *mut c_int); + + if client.is_null() { + error = LibChatError::from(InterfaceError::NullError("client".to_string())).code; + ptr::swap(error_out, &mut error as *mut c_int); + } + + let conversationalists = (*client).runtime.block_on((*client).client.get_conversationalists()); + + Box::into_raw(Box::new(ConversationalistsVector(conversationalists))) +} + +/// Returns the length of the ConversationalistsVector +/// +/// ## Arguments +/// `conversationalists` - A pointer to a ConversationalistsVector +/// `error_out` - Pointer to an int which will be modified +/// +/// ## Returns +/// `c_int` - The length of the vector. May return -1 if something goes wrong +/// +/// ## Safety +/// `conversationalists` should be destroyed eventually +#[no_mangle] +pub unsafe extern "C" fn conversationalists_vector_len( + conversationalists: *mut ConversationalistsVector, + error_out: *mut c_int, +) -> c_int { + let mut error = 0; + ptr::swap(error_out, &mut error as *mut c_int); + + if conversationalists.is_null() { + error = LibChatError::from(InterfaceError::NullError("conversationalists".to_string())).code; + ptr::swap(error_out, &mut error as *mut c_int); + return -1; + } + + let conversationalists = &(*conversationalists); + c_int::try_from(conversationalists.0.len()).unwrap_or(-1) +} + +/// Reads the ConversationalistsVector and returns a pointer to a TariAddress at a given position +/// +/// ## Arguments +/// `conversationalists` - A pointer to a ConversationalistsVector +/// `position` - The index of the vector for a TariAddress +/// `error_out` - Pointer to an int which will be modified +/// +/// ## Returns +/// `*mut ptr TariAddress` - A pointer to a TariAddress +/// +/// ## Safety +/// `conversationalists` should be destroyed eventually +/// the returned `TariAddress` should be destroyed eventually +#[no_mangle] +pub unsafe extern "C" fn conversationalists_vector_get_at( + conversationalists: *mut ConversationalistsVector, + position: c_uint, + error_out: *mut c_int, +) -> *mut TariAddress { + let mut error = 0; + ptr::swap(error_out, &mut error as *mut c_int); + + if conversationalists.is_null() { + error = LibChatError::from(InterfaceError::NullError("conversationalists".to_string())).code; + ptr::swap(error_out, &mut error as *mut c_int); + return ptr::null_mut(); + } + + let conversationalists = &(*conversationalists); + + let len = conversationalists.0.len() - 1; + if position as usize > len { + error = LibChatError::from(InterfaceError::PositionInvalidError).code; + ptr::swap(error_out, &mut error as *mut c_int); + return ptr::null_mut(); + } + + Box::into_raw(Box::new(conversationalists.0[position as usize].clone())) +} + +/// Frees memory for ConversationalistsVector +/// +/// ## Arguments +/// `ptr` - The pointer of a ConversationalistsVector +/// +/// ## Returns +/// `()` - Does not return a value, equivalent to void in C +/// +/// # Safety +/// None +#[no_mangle] +pub unsafe extern "C" fn destroy_conversationalists_vector(ptr: *mut ConversationalistsVector) { + if !ptr.is_null() { + drop(Box::from_raw(ptr)) + } +} + +#[cfg(test)] +mod test { + use rand::rngs::OsRng; + use tari_common::configuration::Network; + use tari_common_types::types::PublicKey; + use tari_crypto::keys::PublicKey as PubKeyTrait; + + use super::*; + use crate::tari_address::destroy_tari_address; + + #[test] + fn test_retrieving_conversationalists_from_vector() { + let (_, pk) = PublicKey::random_keypair(&mut OsRng); + let a = TariAddress::from_public_key(&pk, Network::LocalNet); + let conversationalists = + ConversationalistsVector(vec![TariAddress::default(), TariAddress::default(), a.clone()]); + + let conversationalists_len = conversationalists.0.len(); + let conversationalist_vector_ptr = Box::into_raw(Box::new(conversationalists)); + let error_out = Box::into_raw(Box::new(0)); + + unsafe { + let conversationalist_vector_len = conversationalists_vector_len(conversationalist_vector_ptr, error_out); + assert_eq!(conversationalist_vector_len as usize, conversationalists_len); + + let address_ptr = conversationalists_vector_get_at(conversationalist_vector_ptr, 2, error_out); + let address = (*address_ptr).clone(); + assert_eq!(a, address); + + destroy_conversationalists_vector(conversationalist_vector_ptr); + destroy_tari_address(address_ptr); + drop(Box::from_raw(error_out)); + } + } +} diff --git a/base_layer/chat_ffi/src/lib.rs b/base_layer/chat_ffi/src/lib.rs index 3c12627ce1..0d1bdc7996 100644 --- a/base_layer/chat_ffi/src/lib.rs +++ b/base_layer/chat_ffi/src/lib.rs @@ -48,6 +48,7 @@ mod callback_handler; mod confirmation; mod contacts; mod contacts_liveness_data; +mod conversationalists; mod error; mod logging; mod message; diff --git a/base_layer/contacts/src/chat_client/src/client.rs b/base_layer/contacts/src/chat_client/src/client.rs index 9f46f4d57d..721f6af597 100644 --- a/base_layer/contacts/src/chat_client/src/client.rs +++ b/base_layer/contacts/src/chat_client/src/client.rs @@ -50,6 +50,7 @@ pub trait ChatClient { async fn get_messages(&self, sender: &TariAddress, limit: u64, page: u64) -> Vec; async fn send_message(&self, message: Message); async fn send_read_receipt(&self, message: Message); + async fn get_conversationalists(&self) -> Vec; fn identity(&self) -> &NodeIdentity; fn shutdown(&mut self); } @@ -194,6 +195,18 @@ impl ChatClient for Client { message.push(metadata); message } + + async fn get_conversationalists(&self) -> Vec { + let mut addresses = vec![]; + if let Some(mut contacts_service) = self.contacts.clone() { + addresses = contacts_service + .get_conversationalists() + .await + .expect("Addresses from conversations not fetched"); + } + + addresses + } } pub async fn wait_for_connectivity(comms: CommsNode) -> anyhow::Result<()> { diff --git a/base_layer/contacts/src/contacts_service/handle.rs b/base_layer/contacts/src/contacts_service/handle.rs index d69c74d7eb..67c7daa80e 100644 --- a/base_layer/contacts/src/contacts_service/handle.rs +++ b/base_layer/contacts/src/contacts_service/handle.rs @@ -139,6 +139,7 @@ pub enum ContactsServiceRequest { SendMessage(TariAddress, Message), GetMessages(TariAddress, i64, i64), SendReadConfirmation(TariAddress, Confirmation), + GetConversationalists, } #[derive(Debug)] @@ -151,6 +152,7 @@ pub enum ContactsServiceResponse { Messages(Vec), MessageSent, ReadConfirmationSent, + Conversationalists(Vec), } #[derive(Clone)] @@ -306,4 +308,15 @@ impl ContactsServiceHandle { _ => Err(ContactsServiceError::UnexpectedApiResponse), } } + + pub async fn get_conversationalists(&mut self) -> Result, ContactsServiceError> { + match self + .request_response_service + .call(ContactsServiceRequest::GetConversationalists) + .await?? + { + ContactsServiceResponse::Conversationalists(addresses) => Ok(addresses), + _ => Err(ContactsServiceError::UnexpectedApiResponse), + } + } } diff --git a/base_layer/contacts/src/contacts_service/service.rs b/base_layer/contacts/src/contacts_service/service.rs index 424276d9f2..eed8d93cfe 100644 --- a/base_layer/contacts/src/contacts_service/service.rs +++ b/base_layer/contacts/src/contacts_service/service.rs @@ -318,6 +318,10 @@ where T: ContactsBackend + 'static Ok(ContactsServiceResponse::ReadConfirmationSent) }, + ContactsServiceRequest::GetConversationalists => { + let result = self.db.get_conversationlists(); + Ok(result.map(ContactsServiceResponse::Conversationalists)?) + }, } } diff --git a/base_layer/contacts/src/contacts_service/storage/database.rs b/base_layer/contacts/src/contacts_service/storage/database.rs index e7c8ae3caf..e7dbc80180 100644 --- a/base_layer/contacts/src/contacts_service/storage/database.rs +++ b/base_layer/contacts/src/contacts_service/storage/database.rs @@ -53,6 +53,7 @@ pub enum DbKey { Contacts, Message(Vec), Messages(TariAddress, i64, i64), + Conversationalists, } pub enum DbValue { @@ -61,6 +62,7 @@ pub enum DbValue { TariAddress(Box), Message(Box), Messages(Vec), + Conversationalists(Vec), } #[allow(clippy::large_enum_variant)] @@ -223,6 +225,21 @@ where T: ContactsBackend + 'static Ok(()) } + + pub fn get_conversationlists(&mut self) -> Result, ContactsServiceStorageError> { + let db_clone = self.db.clone(); + match db_clone.fetch(&DbKey::Conversationalists) { + Ok(None) => log_error( + DbKey::Conversationalists, + ContactsServiceStorageError::UnexpectedResult( + "Could not retrieve conversation partner addresses".to_string(), + ), + ), + Ok(Some(DbValue::Conversationalists(c))) => Ok(c), + Ok(Some(other)) => unexpected_result(DbKey::Conversationalists, other), + Err(e) => log_error(DbKey::Conversationalists, e), + } + } } fn unexpected_result(req: DbKey, res: DbValue) -> Result { @@ -239,6 +256,7 @@ impl Display for DbKey { DbKey::Contacts => f.write_str("Contacts"), DbKey::Messages(c, _l, _p) => f.write_str(&format!("Messages for id: {:?}", c)), DbKey::Message(m) => f.write_str(&format!("Message for id: {:?}", m)), + DbKey::Conversationalists => f.write_str("Conversationalists"), } } } @@ -251,6 +269,7 @@ impl Display for DbValue { DbValue::TariAddress(_) => f.write_str("Address"), DbValue::Messages(_) => f.write_str("Messages"), DbValue::Message(_) => f.write_str("Message"), + DbValue::Conversationalists(_) => f.write_str("Conversationalists"), } } } diff --git a/base_layer/contacts/src/contacts_service/storage/sqlite_db.rs b/base_layer/contacts/src/contacts_service/storage/sqlite_db.rs index 4de0cdf77b..ff9541d578 100644 --- a/base_layer/contacts/src/contacts_service/storage/sqlite_db.rs +++ b/base_layer/contacts/src/contacts_service/storage/sqlite_db.rs @@ -121,6 +121,12 @@ where TContactServiceDbConnection: PooledDbConnection None, Err(e) => return Err(e), }, + DbKey::Conversationalists => Some(DbValue::Conversationalists( + MessagesSql::find_all_conversationlists(&mut conn)? + .iter() + .map(|c| TariAddress::from_bytes(c).map_err(|_e| ContactsServiceStorageError::UnknownError)) + .collect::, _>>()?, + )), }; Ok(result) @@ -192,6 +198,7 @@ where TContactServiceDbConnection: PooledDbConnection return Err(ContactsServiceStorageError::OperationNotSupported), DbKey::Messages(_pk, _l, _p) => return Err(ContactsServiceStorageError::OperationNotSupported), DbKey::Message(_id) => return Err(ContactsServiceStorageError::OperationNotSupported), + DbKey::Conversationalists => return Err(ContactsServiceStorageError::OperationNotSupported), }, WriteOperation::Insert(i) => { if let DbValue::Message(m) = *i { diff --git a/base_layer/contacts/src/contacts_service/storage/types/messages.rs b/base_layer/contacts/src/contacts_service/storage/types/messages.rs index 091bc23a18..b124db7cc4 100644 --- a/base_layer/contacts/src/contacts_service/storage/types/messages.rs +++ b/base_layer/contacts/src/contacts_service/storage/types/messages.rs @@ -119,6 +119,12 @@ impl MessagesSql { .num_rows_affected_or_not_found(1)?; MessagesSql::find_by_message_id(message_id, conn) } + + pub fn find_all_conversationlists( + conn: &mut SqliteConnection, + ) -> Result>, ContactsServiceStorageError> { + Ok(messages::table.select(messages::address).distinct().load(conn)?) + } } /// Conversion from an Message to the Sql datatype form diff --git a/integration_tests/src/chat_ffi.rs b/integration_tests/src/chat_ffi.rs index bc17de7bb2..77685f30c0 100644 --- a/integration_tests/src/chat_ffi.rs +++ b/integration_tests/src/chat_ffi.rs @@ -103,6 +103,7 @@ extern "C" { error_our: *const c_int, ) -> *mut c_void; pub fn send_read_confirmation_for_message(client: *mut ClientFFI, message: *mut c_void, error_out: *const c_int); + pub fn get_conversationalists(client: *mut ClientFFI, error_out: *const c_int) -> *mut c_void; } #[derive(Debug)] @@ -115,6 +116,9 @@ pub struct ChatFFI { pub identity: Arc, } +struct Conversationalists(Vec); +struct MessagesVector(Vec); + #[async_trait] impl ChatClient for ChatFFI { async fn add_contact(&self, address: &TariAddress) { @@ -159,8 +163,8 @@ impl ChatClient for ChatFFI { let error_out = Box::into_raw(Box::new(0)); let limit = i32::try_from(limit).expect("Truncation occurred") as c_int; let page = i32::try_from(page).expect("Truncation occurred") as c_int; - let all_messages = get_chat_messages(client.0, address_ptr, limit, page, error_out) as *mut Vec; - messages = (*all_messages).clone(); + let all_messages = get_chat_messages(client.0, address_ptr, limit, page, error_out) as *mut MessagesVector; + messages = (*all_messages).0.clone(); } messages @@ -211,6 +215,19 @@ impl ChatClient for ChatFFI { } } + async fn get_conversationalists(&self) -> Vec { + let client = self.ptr.lock().unwrap(); + + let addresses; + unsafe { + let error_out = Box::into_raw(Box::new(0)); + let vector = get_conversationalists(client.0, error_out) as *mut Conversationalists; + addresses = (*vector).0.clone(); + } + + addresses + } + fn identity(&self) -> &NodeIdentity { &self.identity } diff --git a/integration_tests/tests/features/Chat.feature b/integration_tests/tests/features/Chat.feature index 35323135d1..3f9bc2fc8f 100644 --- a/integration_tests/tests/features/Chat.feature +++ b/integration_tests/tests/features/Chat.feature @@ -64,3 +64,17 @@ Feature: Chat messaging When CHAT_B will have 1 message with CHAT_A When CHAT_B sends a read receipt to CHAT_A for message 'Hey there' Then CHAT_A and CHAT_B will have a message 'Hey there' with matching read timestamps + + Scenario: Fetches all addresses from conversations + Given I have a seed node SEED_A + When I have a chat client CHAT_A connected to seed node SEED_A + When I have a chat client CHAT_B connected to seed node SEED_A + When I have a chat client CHAT_C connected to seed node SEED_A + When I have a chat client CHAT_D connected to seed node SEED_A + When I use CHAT_A to send a message 'Hey there' to CHAT_B + When I use CHAT_C to send a message 'Hey there' to CHAT_A + When I use CHAT_A to send a message 'Hey there' to CHAT_D + When CHAT_A will have 1 message with CHAT_B + When CHAT_A will have 1 message with CHAT_C + When CHAT_A will have 1 message with CHAT_D + Then CHAT_A will have 3 conversationalists diff --git a/integration_tests/tests/features/ChatFFI.feature b/integration_tests/tests/features/ChatFFI.feature index 34cc37d58d..304ed2e05e 100644 --- a/integration_tests/tests/features/ChatFFI.feature +++ b/integration_tests/tests/features/ChatFFI.feature @@ -90,4 +90,18 @@ Feature: Chat FFI messaging When I use CHAT_A to send a message 'Hey there' to CHAT_B When CHAT_B will have 1 message with CHAT_A When CHAT_B sends a read receipt to CHAT_A for message 'Hey there' - Then CHAT_A and CHAT_B will have a message 'Hey there' with matching read timestamps \ No newline at end of file + Then CHAT_A and CHAT_B will have a message 'Hey there' with matching read timestamps + + Scenario: Fetches all addresses from FFI conversations + Given I have a seed node SEED_A + When I have a chat FFI client CHAT_A connected to seed node SEED_A + When I have a chat FFI client CHAT_B connected to seed node SEED_A + When I have a chat FFI client CHAT_C connected to seed node SEED_A + When I have a chat FFI client CHAT_D connected to seed node SEED_A + When I use CHAT_A to send a message 'Hey there' to CHAT_B + When I use CHAT_C to send a message 'Hey there' to CHAT_A + When I use CHAT_A to send a message 'Hey there' to CHAT_D + When CHAT_A will have 1 message with CHAT_B + When CHAT_A will have 1 message with CHAT_C + When CHAT_A will have 1 message with CHAT_D + Then CHAT_A will have 3 conversationalists diff --git a/integration_tests/tests/steps/chat_steps.rs b/integration_tests/tests/steps/chat_steps.rs index 9ae9ec2146..d01c19ddea 100644 --- a/integration_tests/tests/steps/chat_steps.rs +++ b/integration_tests/tests/steps/chat_steps.rs @@ -20,7 +20,7 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use std::time::Duration; +use std::{cmp::Ordering, convert::TryFrom, time::Duration}; use cucumber::{then, when}; use tari_common::configuration::Network; @@ -45,6 +45,8 @@ async fn chat_client_connected_to_base_node(world: &mut TariWorld, name: String, ) .await; + tokio::time::sleep(Duration::from_millis(5000)).await; + world.chat_clients.insert(name, Box::new(client)); } @@ -353,3 +355,27 @@ async fn send_read_receipt(world: &mut TariWorld, sender: String, receiver: Stri client_1.send_read_receipt(message).await; } } + +#[then(expr = "{word} will have {int} conversationalists")] +async fn count_conversationalists(world: &mut TariWorld, user: String, num: u64) { + let client = world.chat_clients.get(&user).unwrap(); + let mut addresses = 0; + + for _a in 0..(TWO_MINUTES_WITH_HALF_SECOND_SLEEP) { + let conversationalists = (*client).get_conversationalists().await; + + match conversationalists + .len() + .cmp(&(usize::try_from(num).expect("u64 to cast to usize"))) + { + Ordering::Less => { + tokio::time::sleep(Duration::from_millis(HALF_SECOND)).await; + addresses = conversationalists.len(); + continue; + }, + Ordering::Equal => return, + _ => addresses = conversationalists.len(), + } + } + panic!("Only found conversations with {}/{} addresses", addresses, num) +}