feat: Sprint 6 — disappearing messages, group info, account deletion
- Disappearing messages: ttlSecs param on enqueue/batchEnqueue RPCs, expires_at column (migration 007), server GC deletes expired messages, /disappear command with human-friendly duration parsing (30m, 1h, 1d) - Group info: /group-info shows type, members, MLS epoch; /rename renames conversations; /members resolves usernames via resolveIdentity - Account deletion: deleteAccount @23 RPC with transactional purge of all user data (deliveries, keys, channels), session invalidation, KT log preserved for auditability; /delete-account with confirmation - Added epoch() accessor to GroupMember, enqueue_with_ttl client helper All 35 server + 71 core + 14 E2E tests pass.
This commit is contained in:
@@ -38,11 +38,13 @@ pub trait Store: Send + Sync {
|
||||
|
||||
/// Enqueue a payload and return the monotonically increasing per-inbox sequence number
|
||||
/// assigned to this message. Clients sort by seq before MLS processing.
|
||||
/// When `ttl_secs` is `Some(n)`, the message expires n seconds from now.
|
||||
fn enqueue(
|
||||
&self,
|
||||
recipient_key: &[u8],
|
||||
channel_id: &[u8],
|
||||
payload: Vec<u8>,
|
||||
ttl_secs: Option<u32>,
|
||||
) -> Result<u64, StorageError>;
|
||||
|
||||
/// Fetch and drain all queued messages, returning `(seq, payload)` pairs ordered by seq.
|
||||
@@ -176,6 +178,12 @@ pub trait Store: Send + Sync {
|
||||
/// List all active federation peers.
|
||||
#[allow(dead_code)] // federation not yet wired up
|
||||
fn list_federation_peers(&self) -> Result<Vec<(String, bool)>, StorageError>;
|
||||
|
||||
/// Permanently delete all data associated with an identity key.
|
||||
/// Removes deliveries, key packages, hybrid keys, channel memberships,
|
||||
/// user identity key mapping, and the user record itself.
|
||||
/// Does NOT delete KT log entries (append-only for auditability).
|
||||
fn delete_account(&self, identity_key: &[u8]) -> Result<(), StorageError>;
|
||||
}
|
||||
|
||||
// ── ChannelKey ───────────────────────────────────────────────────────────────
|
||||
@@ -453,6 +461,7 @@ impl Store for FileBackedStore {
|
||||
recipient_key: &[u8],
|
||||
channel_id: &[u8],
|
||||
payload: Vec<u8>,
|
||||
_ttl_secs: Option<u32>,
|
||||
) -> Result<u64, StorageError> {
|
||||
let mut inner = lock(&self.deliveries)?;
|
||||
let key = ChannelKey {
|
||||
@@ -769,6 +778,67 @@ impl Store for FileBackedStore {
|
||||
fn list_federation_peers(&self) -> Result<Vec<(String, bool)>, StorageError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
fn delete_account(&self, identity_key: &[u8]) -> Result<(), StorageError> {
|
||||
// Resolve username from identity key for user record deletion.
|
||||
let username = {
|
||||
let ik_map = lock(&self.identity_keys)?;
|
||||
ik_map.iter()
|
||||
.find(|(_, v)| v.as_slice() == identity_key)
|
||||
.map(|(k, _)| k.clone())
|
||||
};
|
||||
|
||||
// Remove deliveries where this identity is the recipient.
|
||||
{
|
||||
let mut deliveries = lock(&self.deliveries)?;
|
||||
deliveries.map.retain(|k, _| k.recipient_key != identity_key);
|
||||
deliveries.next_seq.retain(|k, _| k.recipient_key != identity_key);
|
||||
self.flush_delivery_map(&self.ds_path, &deliveries)?;
|
||||
}
|
||||
|
||||
// Remove key packages.
|
||||
{
|
||||
let mut kp = lock(&self.key_packages)?;
|
||||
kp.remove(identity_key);
|
||||
self.flush_kp_map(&self.kp_path, &kp)?;
|
||||
}
|
||||
|
||||
// Remove hybrid keys.
|
||||
{
|
||||
let mut hk = lock(&self.hybrid_keys)?;
|
||||
hk.remove(identity_key);
|
||||
self.flush_hybrid_keys(&self.hk_path, &hk)?;
|
||||
}
|
||||
|
||||
// Remove channels where this identity is a member.
|
||||
{
|
||||
let mut ch = lock(&self.channels)?;
|
||||
ch.retain(|_, (a, b)| a.as_slice() != identity_key && b.as_slice() != identity_key);
|
||||
self.flush_channels(&self.channels_path, &ch)?;
|
||||
}
|
||||
|
||||
// Remove identity key mapping and user record.
|
||||
if let Some(uname) = username {
|
||||
{
|
||||
let mut ik_map = lock(&self.identity_keys)?;
|
||||
ik_map.remove(&uname);
|
||||
self.flush_map_string_bytes(&self.identity_keys_path, &ik_map)?;
|
||||
}
|
||||
{
|
||||
let mut users = lock(&self.users)?;
|
||||
users.remove(&uname);
|
||||
self.flush_users(&self.users_path, &users)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove endpoint.
|
||||
{
|
||||
let mut ep = lock(&self.endpoints)?;
|
||||
ep.remove(identity_key);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -799,8 +869,8 @@ mod tests {
|
||||
let (_dir, store) = temp_store();
|
||||
let rk = vec![2u8; 32];
|
||||
let ch = vec![];
|
||||
let seq0 = store.enqueue(&rk, &ch, vec![1]).unwrap();
|
||||
let seq1 = store.enqueue(&rk, &ch, vec![2]).unwrap();
|
||||
let seq0 = store.enqueue(&rk, &ch, vec![1], None).unwrap();
|
||||
let seq1 = store.enqueue(&rk, &ch, vec![2], None).unwrap();
|
||||
assert_eq!(seq0, 0);
|
||||
assert_eq!(seq1, 1);
|
||||
let msgs = store.fetch(&rk, &ch).unwrap();
|
||||
@@ -818,7 +888,7 @@ mod tests {
|
||||
let rk = vec![3u8; 32];
|
||||
let ch = vec![];
|
||||
for i in 0..5 {
|
||||
store.enqueue(&rk, &ch, vec![i]).unwrap();
|
||||
store.enqueue(&rk, &ch, vec![i], None).unwrap();
|
||||
}
|
||||
let msgs = store.fetch_limited(&rk, &ch, 2).unwrap();
|
||||
assert_eq!(msgs.len(), 2);
|
||||
@@ -835,9 +905,9 @@ mod tests {
|
||||
let rk = vec![4u8; 32];
|
||||
let ch = vec![];
|
||||
assert_eq!(store.queue_depth(&rk, &ch).unwrap(), 0);
|
||||
store.enqueue(&rk, &ch, vec![1]).unwrap();
|
||||
store.enqueue(&rk, &ch, vec![1], None).unwrap();
|
||||
assert_eq!(store.queue_depth(&rk, &ch).unwrap(), 1);
|
||||
store.enqueue(&rk, &ch, vec![2]).unwrap();
|
||||
store.enqueue(&rk, &ch, vec![2], None).unwrap();
|
||||
assert_eq!(store.queue_depth(&rk, &ch).unwrap(), 2);
|
||||
store.fetch(&rk, &ch).unwrap();
|
||||
assert_eq!(store.queue_depth(&rk, &ch).unwrap(), 0);
|
||||
|
||||
Reference in New Issue
Block a user