use crate::{Error, Result, Fingerprint};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ChannelType {
Direct,
Group,
Forum,
Agent,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChannelMember {
pub fingerprint: String,
pub name: Option<String>,
pub is_admin: bool,
pub can_post: bool,
pub joined_at: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Channel {
pub id: String,
pub name: String,
pub channel_type: ChannelType,
pub members: HashMap<String, ChannelMember>,
pub created_at: u64,
pub last_activity: u64,
pub unread_count: u32,
pub encrypted: bool,
pub pqc_enabled: bool,
pub on_chain: bool,
}
impl Channel {
pub fn direct(our_fingerprint: &str, their_fingerprint: &str) -> Self {
let now = Self::now();
let mut members = HashMap::new();
members.insert(
our_fingerprint.to_string(),
ChannelMember {
fingerprint: our_fingerprint.to_string(),
name: None,
is_admin: true,
can_post: true,
joined_at: now,
},
);
members.insert(
their_fingerprint.to_string(),
ChannelMember {
fingerprint: their_fingerprint.to_string(),
name: None,
is_admin: true,
can_post: true,
joined_at: now,
},
);
Self {
id: Self::dm_id(our_fingerprint, their_fingerprint),
name: their_fingerprint.to_string(),
channel_type: ChannelType::Direct,
members,
created_at: now,
last_activity: now,
unread_count: 0,
encrypted: true,
pqc_enabled: true,
on_chain: false,
}
}
pub fn group(name: impl Into<String>, creator_fingerprint: &str) -> Self {
let now = Self::now();
let mut members = HashMap::new();
members.insert(
creator_fingerprint.to_string(),
ChannelMember {
fingerprint: creator_fingerprint.to_string(),
name: None,
is_admin: true,
can_post: true,
joined_at: now,
},
);
Self {
id: Self::generate_id(),
name: name.into(),
channel_type: ChannelType::Group,
members,
created_at: now,
last_activity: now,
unread_count: 0,
encrypted: true,
pqc_enabled: true,
on_chain: false,
}
}
pub fn forum(name: impl Into<String>) -> Self {
let name = name.into();
let now = Self::now();
Self {
id: name.clone(),
name,
channel_type: ChannelType::Forum,
members: HashMap::new(), created_at: now,
last_activity: now,
unread_count: 0,
encrypted: false, pqc_enabled: false,
on_chain: true,
}
}
pub fn add_member(&mut self, fingerprint: &str, is_admin: bool) -> Result<()> {
if self.channel_type == ChannelType::Direct {
return Err(Error::Config("Cannot add members to DM".into()));
}
if self.members.contains_key(fingerprint) {
return Err(Error::Config("Member already exists".into()));
}
self.members.insert(
fingerprint.to_string(),
ChannelMember {
fingerprint: fingerprint.to_string(),
name: None,
is_admin,
can_post: true,
joined_at: Self::now(),
},
);
Ok(())
}
pub fn remove_member(&mut self, fingerprint: &str) -> Result<()> {
if self.channel_type == ChannelType::Direct {
return Err(Error::Config("Cannot remove members from DM".into()));
}
if self.members.remove(fingerprint).is_none() {
return Err(Error::Config("Member not found".into()));
}
Ok(())
}
pub fn is_member(&self, fingerprint: &str) -> bool {
self.channel_type == ChannelType::Forum || self.members.contains_key(fingerprint)
}
pub fn is_admin(&self, fingerprint: &str) -> bool {
self.members
.get(fingerprint)
.map(|m| m.is_admin)
.unwrap_or(false)
}
pub fn touch(&mut self) {
self.last_activity = Self::now();
}
pub fn increment_unread(&mut self) {
self.unread_count += 1;
}
pub fn mark_read(&mut self) {
self.unread_count = 0;
}
fn dm_id(a: &str, b: &str) -> String {
let (first, second) = if a < b { (a, b) } else { (b, a) };
use sha2::{Sha256, Digest};
let hash = Sha256::digest(format!("dm:{}:{}", first, second));
hex::encode(&hash[..16])
}
fn generate_id() -> String {
use rand::Rng;
let mut rng = rand::thread_rng();
(0..16)
.map(|_| format!("{:02x}", rng.gen::<u8>()))
.collect()
}
fn now() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64
}
}
pub struct ChannelManager {
channels: HashMap<String, Channel>,
}
impl ChannelManager {
pub fn new() -> Self {
Self {
channels: HashMap::new(),
}
}
pub fn get_or_create_dm(&mut self, our_fp: &str, their_fp: &str) -> &mut Channel {
let id = Channel::dm_id(our_fp, their_fp);
self.channels
.entry(id.clone())
.or_insert_with(|| Channel::direct(our_fp, their_fp))
}
pub fn create_group(&mut self, name: impl Into<String>, creator: &str) -> String {
let channel = Channel::group(name, creator);
let id = channel.id.clone();
self.channels.insert(id.clone(), channel);
id
}
pub fn get(&self, id: &str) -> Option<&Channel> {
self.channels.get(id)
}
pub fn get_mut(&mut self, id: &str) -> Option<&mut Channel> {
self.channels.get_mut(id)
}
pub fn list(&self) -> Vec<&Channel> {
self.channels.values().collect()
}
pub fn list_by_activity(&self) -> Vec<&Channel> {
let mut channels: Vec<_> = self.channels.values().collect();
channels.sort_by(|a, b| b.last_activity.cmp(&a.last_activity));
channels
}
}
impl Default for ChannelManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dm_channel() {
let ch = Channel::direct("alice", "bob");
assert_eq!(ch.channel_type, ChannelType::Direct);
assert!(ch.is_member("alice"));
assert!(ch.is_member("bob"));
assert!(!ch.is_member("eve"));
}
#[test]
fn test_dm_id_deterministic() {
let id1 = Channel::dm_id("alice", "bob");
let id2 = Channel::dm_id("bob", "alice");
assert_eq!(id1, id2);
}
#[test]
fn test_group_channel() {
let mut ch = Channel::group("Team Chat", "alice");
assert_eq!(ch.channel_type, ChannelType::Group);
assert!(ch.is_admin("alice"));
ch.add_member("bob", false).unwrap();
assert!(ch.is_member("bob"));
assert!(!ch.is_admin("bob"));
}
#[test]
fn test_forum_channel() {
let ch = Channel::forum("#quantum");
assert_eq!(ch.channel_type, ChannelType::Forum);
assert!(ch.on_chain);
assert!(!ch.encrypted);
}
}