use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use chrono::Utc;
use uuid::Uuid;
use crate::error::CollabError;
use crate::remote::CollabRemote;
use crate::types::{DekEnvelope, InviteRecord, InviteToken, MemberEntry};
const INVITE_TTL_HOURS: i64 = 48;
#[derive(Debug, Default)]
struct StubState {
members: HashMap<String, Vec<MemberEntry>>,
dek_inbox: HashMap<(String, String), DekEnvelope>,
share_inbox: HashMap<(String, String), String>,
invites: HashMap<String, InviteRecord>,
}
impl StubState {
fn is_member(&self, team_id: &str, pubkey: &str) -> bool {
self.members
.get(team_id)
.map(|ms| ms.iter().any(|m| m.pubkey == pubkey))
.unwrap_or(false)
}
fn assert_member(&self, team_id: &str, pubkey: &str) -> Result<(), CollabError> {
if self.is_member(team_id, pubkey) {
Ok(())
} else {
Err(CollabError::NotMember {
team_id: team_id.to_owned(),
})
}
}
}
#[derive(Debug, Clone, Default)]
pub struct StubServer {
state: Arc<Mutex<StubState>>,
}
impl StubServer {
pub fn new() -> Self {
Self::default()
}
}
impl CollabRemote for StubServer {
fn join(&self, team_id: &str, pubkey: &str) -> Result<(), CollabError> {
let mut state = self.state.lock().expect("stub state poisoned");
let members = state.members.entry(team_id.to_owned()).or_default();
if !members.iter().any(|m| m.pubkey == pubkey) {
members.push(MemberEntry {
pubkey: pubkey.to_owned(),
joined_at: Utc::now(),
});
}
Ok(())
}
fn members(&self, team_id: &str) -> Result<Vec<MemberEntry>, CollabError> {
let state = self.state.lock().expect("stub state poisoned");
Ok(state.members.get(team_id).cloned().unwrap_or_default())
}
fn deliver_dek(
&self,
team_id: &str,
recipient_pubkey: &str,
envelope: DekEnvelope,
) -> Result<(), CollabError> {
let mut state = self.state.lock().expect("stub state poisoned");
state.assert_member(team_id, recipient_pubkey)?;
state
.dek_inbox
.insert((team_id.to_owned(), recipient_pubkey.to_owned()), envelope);
Ok(())
}
fn fetch_dek(
&self,
team_id: &str,
recipient_pubkey: &str,
) -> Result<Option<DekEnvelope>, CollabError> {
let state = self.state.lock().expect("stub state poisoned");
state.assert_member(team_id, recipient_pubkey)?;
Ok(state
.dek_inbox
.get(&(team_id.to_owned(), recipient_pubkey.to_owned()))
.cloned())
}
fn create_invite(
&self,
team_id: &str,
invitee_pubkey: &str,
) -> Result<InviteToken, CollabError> {
let mut state = self.state.lock().expect("stub state poisoned");
let token_str = Uuid::new_v4().to_string();
let expires_at = Utc::now() + chrono::Duration::hours(INVITE_TTL_HOURS);
let record = InviteRecord {
token: token_str.clone(),
team_id: team_id.to_owned(),
invitee_pubkey: invitee_pubkey.to_owned(),
expires_at,
};
state.invites.insert(token_str.clone(), record);
Ok(InviteToken {
token: token_str,
team_id: team_id.to_owned(),
bound_pubkey: invitee_pubkey.to_owned(),
})
}
fn confirm_invite(
&self,
team_id: &str,
token: &InviteToken,
confirming_pubkey: &str,
) -> Result<(), CollabError> {
let mut state = self.state.lock().expect("stub state poisoned");
let record = state
.invites
.get(&token.token)
.ok_or(CollabError::InviteNotFound)?;
if record.invitee_pubkey != confirming_pubkey {
return Err(CollabError::PubkeyMismatch);
}
if Utc::now() > record.expires_at {
return Err(CollabError::InviteNotFound);
}
let confirmed_pubkey = record.invitee_pubkey.clone();
let confirmed_team = record.team_id.clone();
let token_str = token.token.clone();
if confirmed_team != team_id {
return Err(CollabError::InviteNotFound);
}
state.invites.remove(&token_str);
let members = state.members.entry(team_id.to_owned()).or_default();
if !members.iter().any(|m| m.pubkey == confirmed_pubkey) {
members.push(MemberEntry {
pubkey: confirmed_pubkey,
joined_at: Utc::now(),
});
}
Ok(())
}
fn deliver_recovery_share(
&self,
team_id: &str,
custodian_pubkey: &str,
share_ciphertext: &str,
) -> Result<(), CollabError> {
let mut state = self.state.lock().expect("stub state poisoned");
state.assert_member(team_id, custodian_pubkey)?;
state.share_inbox.insert(
(team_id.to_owned(), custodian_pubkey.to_owned()),
share_ciphertext.to_owned(),
);
Ok(())
}
fn fetch_recovery_share(
&self,
team_id: &str,
custodian_pubkey: &str,
) -> Result<Option<String>, CollabError> {
let state = self.state.lock().expect("stub state poisoned");
state.assert_member(team_id, custodian_pubkey)?;
Ok(state
.share_inbox
.get(&(team_id.to_owned(), custodian_pubkey.to_owned()))
.cloned())
}
}