use std::collections::HashMap;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use bingle_core::api::bingle_api::{BingleError, StartOptions};
use bingle_core::blockchain::algo_bingle::AlgoBingle;
use bingle_local::api::bingle_local_api::{BingleLocalApi, ContactSource, Message, REQUIRED_ALGO};
use bingle_local::api::bingle_local_api_impl::{BingleApiLocalImpl, LocalApiConfig};
use crate::chat::ChatArgs;
use crate::chat_register::AccountStatus;
#[derive(Debug)]
pub enum RegisterError {
HandleTaken(String),
Other(String),
}
pub struct ChatState {
local: BingleApiLocalImpl,
state_file: Option<String>,
pub opts: StartOptions,
pub contacts: HashMap<String, String>,
}
impl ChatState {
pub fn from_chat_args(args: &ChatArgs) -> Result<ChatState, String> {
let mut opts = args.opts.clone();
let algo_config = opts.algo_provider_config.clone().unwrap_or_default();
let cfg = LocalApiConfig::with_notify(
algo_config,
opts.app_id.unwrap_or(0),
opts.asset_id.unwrap_or(0),
None,
None,
);
let mut local = BingleApiLocalImpl::new(cfg);
let state_file = args.state_file.clone();
let mut contacts: HashMap<String, String> = HashMap::new();
if let Some(path) = state_file.as_deref() {
if Path::new(path).exists() {
load_state(&mut local, path)?;
match local.get_keypair().map_err(|e| e.to_string())? {
Some(keypair) => {
if opts.algo_passphrase.is_none() {
opts.algo_passphrase = Some(keypair.passphrase);
}
if opts.handle.is_empty()
&& let Some(handle) = local.own_handle()
{
opts.handle = handle;
}
}
None => {
tracing::info!(
"chat: state file {} has no keypair yet; account setup happens on first run",
path
);
}
}
for contact in local.get_contacts().map_err(|e| e.to_string())? {
contacts.insert(contact.handle, contact.id);
}
} else {
tracing::info!(
"chat: state file {} not found; starting with empty local state",
path
);
}
}
Ok(ChatState {
local,
state_file,
opts,
contacts,
})
}
pub fn save_state(&self) -> Result<(), String> {
match self.state_file.as_deref() {
Some(path) => self
.local
.save(path)
.map_err(|e| format!("failed to save chat state to {}: {}", path, e)),
None => Ok(()),
}
}
pub fn record_message(
&mut self,
sender_handle: &str,
recipient_handles: Vec<String>,
timestamp: i64,
text: &str,
cipher_suite: Option<String>,
) -> Result<Message, String> {
self.local
.add_message(
sender_handle.to_string(),
recipient_handles,
timestamp,
text.to_string(),
cipher_suite,
)
.map_err(|e| e.to_string())?;
self.local
.get_messages()
.map_err(|e| e.to_string())?
.pop()
.ok_or_else(|| "message missing after add_message".to_string())
}
pub fn resolve_recipient(&self, handle: &str) -> Option<&str> {
self.contacts.get(handle).map(String::as_str)
}
pub fn knows_id(&self, id: &str) -> bool {
self.contacts.values().any(|known| known == id)
}
pub fn add_received_contact(&mut self, handle: &str, id: &str) -> Result<(), String> {
self.local
.add_contact(handle.to_string(), id.to_string(), ContactSource::Received)
.map_err(|e| e.to_string())?;
self.contacts.insert(handle.to_string(), id.to_string());
Ok(())
}
pub fn messages(&self) -> Result<Vec<Message>, String> {
self.local.get_messages().map_err(|e| e.to_string())
}
pub fn queue_outbound(&mut self, recipient_handle: &str, text: &str) -> Result<i64, String> {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.map_err(|e| e.to_string())?;
let sender = self.opts.handle.clone();
self.local
.add_message(
sender,
vec![recipient_handle.to_string()],
timestamp,
text.to_string(),
None,
)
.map_err(|e| e.to_string())?;
self.local
.update_message_status(timestamp, 0.0, None)
.map_err(|e| e.to_string())?;
self.save_state()?;
Ok(timestamp)
}
pub fn mark_delivered(&mut self, timestamp: i64) -> Result<(), String> {
self.local
.update_message_status(timestamp, 1.0, None)
.map_err(|e| e.to_string())?;
self.save_state()
}
pub fn mark_send_failed(
&mut self,
timestamp: i64,
reason: &str,
permanent: bool,
) -> Result<(), String> {
let progress = if permanent { 1.0 } else { 0.0 };
self.local
.update_message_status(timestamp, progress, Some(reason.to_string()))
.map_err(|e| e.to_string())?;
self.save_state()
}
pub fn pending_outbound(&self) -> Result<Vec<Message>, String> {
self.local.get_pending_messages().map_err(|e| e.to_string())
}
pub fn has_keypair(&self) -> bool {
matches!(self.local.get_keypair(), Ok(Some(_)))
}
pub fn import_keypair(&mut self, passphrase: &str) -> Result<(), String> {
self.local
.import_keypair(passphrase.to_string())
.map(|_keypair| ())
.map_err(|e| e.to_string())
}
pub fn resolve_account_status(&self) -> Result<AccountStatus, String> {
let status = self.local.keypair_status().map_err(|e| e.to_string())?;
match status.status.as_str() {
"None" => Ok(AccountStatus::NoKeypair),
"UNFUNDED" => Ok(AccountStatus::Unfunded {
id: status.id.unwrap_or_default(),
shortfall_algos: status.required_algo.unwrap_or(REQUIRED_ALGO),
}),
"FUNDED" => Ok(AccountStatus::Funded {
id: status.id.unwrap_or_default(),
}),
"ACTIVE" => {
let handle = status
.handle
.or_else(|| self.local.own_handle())
.unwrap_or_default();
let (balance_algos, operating_min_algos) = self.operating_funding()?;
Ok(AccountStatus::Active {
id: status.id.unwrap_or_default(),
handle,
balance_algos,
operating_min_algos,
})
}
"UPGRADE_REQUIRED" => Err(
"this client is out of date for the configured app; please upgrade to continue"
.to_string(),
),
other => Err(format!(
"cannot determine account status ('{other}'); is the Algorand node reachable?"
)),
}
}
fn operating_funding(&self) -> Result<(f64, f64), String> {
let ops = self.local.get_algo_ops().map_err(|e| e.to_string())?;
let balance_algos = ops
.account_balance()
.map_err(|e| e.to_string())?
.unwrap_or(0.0);
let app_id = self.opts.app_id.unwrap_or(0);
let asset_id = self.opts.asset_id.unwrap_or(0);
let operating_min_algos = if app_id != 0 {
let bingle = AlgoBingle::new(ops, app_id, asset_id);
bingle.post_registration_mbr().unwrap_or_else(|e| {
tracing::warn!(
"chat: could not read account minimum balance ({e}); not blocking a registered account"
);
0.0
})
} else {
0.0
};
Ok((balance_algos, operating_min_algos))
}
pub fn register(&mut self, handle: &str) -> Result<(), RegisterError> {
self.local
.register_keypair(handle.to_string())
.map(|_ok| ())
.map_err(|e| match e {
BingleError::HandleTaken(owner) => RegisterError::HandleTaken(owner),
other => RegisterError::Other(other.to_string()),
})?;
self.save_state().map_err(RegisterError::Other)
}
}
fn load_state(local: &mut BingleApiLocalImpl, path: &str) -> Result<(), String> {
local
.load(path)
.map_err(|e| format!("failed to load chat state from {}: {}", path, e))
}