use anyhow::{Context, Result};
use std::cmp::Ordering;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConversationType {
Single = 1,
Group = 2,
Ai = 3,
System = 4,
Customer = 5,
Temp = 6,
Channel = 7,
Broadcast = 8,
}
impl FromStr for ConversationType {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
match s {
"1" | "single" => Ok(ConversationType::Single),
"2" | "group" => Ok(ConversationType::Group),
"3" | "ai" => Ok(ConversationType::Ai),
"4" | "system" => Ok(ConversationType::System),
"5" | "customer" => Ok(ConversationType::Customer),
"6" | "temp" => Ok(ConversationType::Temp),
"7" | "channel" => Ok(ConversationType::Channel),
"8" | "broadcast" => Ok(ConversationType::Broadcast),
_ => Err(anyhow::anyhow!("Unknown session type: {}", s)),
}
}
}
impl ConversationType {
pub fn prefix(&self) -> &'static str {
match self {
ConversationType::Single => "1",
ConversationType::Group => "2",
ConversationType::Ai => "3",
ConversationType::System => "4",
ConversationType::Customer => "5",
ConversationType::Temp => "6",
ConversationType::Channel => "7",
ConversationType::Broadcast => "8",
}
}
pub fn from_prefix(prefix: &str) -> Result<Self> {
match prefix {
"1" => Ok(ConversationType::Single),
"2" => Ok(ConversationType::Group),
"3" => Ok(ConversationType::Ai),
"4" => Ok(ConversationType::System),
"5" => Ok(ConversationType::Customer),
"6" => Ok(ConversationType::Temp),
"7" => Ok(ConversationType::Channel),
"8" => Ok(ConversationType::Broadcast),
_ => Err(anyhow::anyhow!("Unknown session type prefix: {}", prefix)),
}
}
}
pub fn generate_single_chat_conversation_id(user1: &str, user2: &str) -> String {
use sha2::{Digest, Sha256};
let (min_id, max_id) = match user1.cmp(user2) {
Ordering::Less | Ordering::Equal => (user1, user2),
Ordering::Greater => (user2, user1),
};
let input = format!("DM:v1:{}:{}", min_id, max_id);
let mut hasher = Sha256::new();
hasher.update(input.as_bytes());
let hash = hasher.finalize();
let truncated = &hash[..10];
let opaque_id = base32::encode(base32::Alphabet::Crockford, truncated);
format!("1A{}", opaque_id)
}
pub fn generate_group_conversation_id(group_id: &str) -> String {
use sha2::{Digest, Sha256};
let input = format!("GROUP:v1:{}", group_id);
let mut hasher = Sha256::new();
hasher.update(input.as_bytes());
let hash = hasher.finalize();
let truncated = &hash[..10];
let opaque_id = base32::encode(base32::Alphabet::Crockford, truncated);
format!("2A{}", opaque_id)
}
pub fn generate_channel_conversation_id(channel_id: &str) -> String {
use sha2::{Digest, Sha256};
let input = format!("CHANNEL:v1:{}", channel_id);
let mut hasher = Sha256::new();
hasher.update(input.as_bytes());
let hash = hasher.finalize();
let opaque_id = base32::encode(base32::Alphabet::Crockford, &hash[..10]);
format!("7A{}", opaque_id)
}
pub fn generate_broadcast_conversation_id(broadcast_id: &str) -> String {
use sha2::{Digest, Sha256};
let input = format!("BROADCAST:v1:{}", broadcast_id);
let mut hasher = Sha256::new();
hasher.update(input.as_bytes());
let hash = hasher.finalize();
let opaque_id = base32::encode(base32::Alphabet::Crockford, &hash[..10]);
format!("8A{}", opaque_id)
}
pub fn generate_ai_conversation_id(user_id: &str, ai_scope: &str) -> String {
use sha2::{Digest, Sha256};
let input = format!("AI:v1:{}:{}", user_id, ai_scope);
let mut hasher = Sha256::new();
hasher.update(input.as_bytes());
let hash = hasher.finalize();
let truncated = &hash[..10];
let opaque_id = base32::encode(base32::Alphabet::Crockford, truncated);
format!("3A{}", opaque_id)
}
pub fn generate_customer_conversation_id(customer_id: &str, channel: &str) -> String {
use sha2::{Digest, Sha256};
let input = format!("CS:v1:{}:{}", customer_id, channel);
let mut hasher = Sha256::new();
hasher.update(input.as_bytes());
let hash = hasher.finalize();
let truncated = &hash[..10];
let opaque_id = base32::encode(base32::Alphabet::Crockford, truncated);
format!("5A{}", opaque_id)
}
pub fn generate_system_conversation_id(system_id: &str, scope: Option<String>) -> String {
use sha2::{Digest, Sha256};
let scope_str = scope.unwrap_or_default();
let input = format!("SYS:v1:{}:{}", system_id, scope_str);
let mut hasher = Sha256::new();
hasher.update(input.as_bytes());
let hash = hasher.finalize();
let truncated = &hash[..10];
let opaque_id = base32::encode(base32::Alphabet::Crockford, truncated);
format!("4A{}", opaque_id)
}
pub fn generate_temp_conversation_id() -> String {
#[cfg(not(target_arch = "wasm32"))]
{
use ulid::Ulid;
format!("6A{}", Ulid::new().to_string())
}
#[cfg(target_arch = "wasm32")]
{
let opaque: u128 = rand::random();
format!("6A{:032x}", opaque)
}
}
#[deprecated(note = "Use specific generation functions like generate_temp_conversation_id()")]
pub fn generate_server_conversation_id(conversation_type: ConversationType) -> String {
#[cfg(not(target_arch = "wasm32"))]
{
use ulid::Ulid;
format!("{}-{}", conversation_type.prefix(), Ulid::new().to_string())
}
#[cfg(target_arch = "wasm32")]
{
let opaque: u128 = rand::random();
format!("{}-{:032x}", conversation_type.prefix(), opaque)
}
}
pub fn validate_conversation_id(conversation_id: &str) -> Result<ConversationType> {
if conversation_id.is_empty() {
return Err(anyhow::anyhow!("Session ID cannot be empty"));
}
if conversation_id.len() < 3 {
return Err(anyhow::anyhow!("Session ID too short, expected CID format"));
}
let prefix = &conversation_id[..1];
let version = &conversation_id[1..2];
if version != "A" {
return Err(anyhow::anyhow!(
"Unsupported CID version: {}, expected 'A'",
version
));
}
let conversation_type = ConversationType::from_prefix(prefix)
.with_context(|| format!("Invalid CID type prefix: {}", prefix))?;
let opaque_id = &conversation_id[2..];
match conversation_type {
ConversationType::Temp => {
if opaque_id.len() != 26 {
return Err(anyhow::anyhow!(
"Invalid temp session CID: expected 26 characters ULID, got {}",
opaque_id.len()
));
}
}
_ => {
if opaque_id.len() != 16 {
return Err(anyhow::anyhow!(
"Invalid CID opaque ID length: expected 16 characters, got {}",
opaque_id.len()
));
}
if !opaque_id.chars().all(|c| {
matches!(c, '0'..='9' | 'A'..='H' | 'J'..='K' | 'M'..='N' | 'P'..='T' | 'V'..='Z')
}) {
return Err(anyhow::anyhow!(
"Invalid CID opaque ID: contains invalid Base32 characters"
));
}
}
}
Ok(conversation_type)
}
pub fn extract_conversation_type(conversation_id: &str) -> Option<ConversationType> {
validate_conversation_id(conversation_id).ok()
}
pub fn is_single_chat_conversation(conversation_id: &str) -> bool {
matches!(
extract_conversation_type(conversation_id),
Some(ConversationType::Single)
)
}
pub fn is_group_chat_conversation(conversation_id: &str) -> bool {
matches!(
extract_conversation_type(conversation_id),
Some(ConversationType::Group)
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_single_chat_conversation_id_consistency() {
let id1 = generate_single_chat_conversation_id("user1", "user2");
let id2 = generate_single_chat_conversation_id("user2", "user1");
assert_eq!(id1, id2);
assert!(id1.starts_with("1A"));
assert_eq!(id1.len(), 18);
let id3 = generate_single_chat_conversation_id("user1", "user3");
assert_ne!(id1, id3);
}
#[test]
fn test_group_conversation_id_generation() {
let id1 = generate_group_conversation_id("group_12345");
let id2 = generate_group_conversation_id("group_12345");
assert!(id1.starts_with("2A"));
assert_eq!(id1.len(), 18); assert_eq!(id1, id2);
let id3 = generate_group_conversation_id("group_67890");
assert_ne!(id1, id3);
}
#[test]
fn test_ai_conversation_id_generation() {
let id1 = generate_ai_conversation_id("user_001", "openai:gpt-4");
let id2 = generate_ai_conversation_id("user_001", "openai:gpt-4");
assert!(id1.starts_with("3A"));
assert_eq!(id1.len(), 18); assert_eq!(id1, id2);
let id3 = generate_ai_conversation_id("user_001", "claude:sonnet");
assert_ne!(id1, id3);
}
#[test]
fn test_customer_conversation_id_generation() {
let id1 = generate_customer_conversation_id("customer_001", "channel_001");
assert!(id1.starts_with("5A"));
assert_eq!(id1.len(), 18); }
#[test]
fn test_system_conversation_id_generation() {
let id1 = generate_system_conversation_id("system_notification", None);
assert!(id1.starts_with("4A"));
assert_eq!(id1.len(), 18);
let id2 =
generate_system_conversation_id("system_announcement", Some("scope1".to_string()));
assert!(id2.starts_with("4A"));
assert_eq!(id2.len(), 18);
assert_ne!(id1, id2);
}
#[test]
fn test_temp_conversation_id_generation() {
let id1 = generate_temp_conversation_id();
let id2 = generate_temp_conversation_id();
assert!(id1.starts_with("6A"));
assert!(id2.starts_with("6A"));
assert_ne!(id1, id2);
}
#[test]
fn test_validate_conversation_id() {
let single_id = generate_single_chat_conversation_id("user1", "user2");
assert!(validate_conversation_id(&single_id).is_ok());
assert_eq!(
validate_conversation_id(&single_id).unwrap(),
ConversationType::Single
);
let group_id = generate_group_conversation_id("group_12345");
assert!(validate_conversation_id(&group_id).is_ok());
assert_eq!(
validate_conversation_id(&group_id).unwrap(),
ConversationType::Group
);
let ai_id = generate_ai_conversation_id("user_001", "gpt-4");
assert!(validate_conversation_id(&ai_id).is_ok());
assert_eq!(
validate_conversation_id(&ai_id).unwrap(),
ConversationType::Ai
);
let system_id =
generate_system_conversation_id("system_notification", Some("1734567890".to_string()));
assert!(validate_conversation_id(&system_id).is_ok());
assert_eq!(
validate_conversation_id(&system_id).unwrap(),
ConversationType::System
);
let temp_id = generate_temp_conversation_id();
assert!(validate_conversation_id(&temp_id).is_ok());
assert_eq!(
validate_conversation_id(&temp_id).unwrap(),
ConversationType::Temp
);
assert!(validate_conversation_id("").is_err());
assert!(validate_conversation_id("1A").is_err()); assert!(validate_conversation_id("1B1234567890123456").is_err()); assert!(validate_conversation_id("0A1234567890123456").is_err()); assert!(validate_conversation_id("1A123456789012345").is_err()); }
#[test]
fn test_extract_conversation_type() {
let single_id = generate_single_chat_conversation_id("user1", "user2");
assert_eq!(
extract_conversation_type(&single_id),
Some(ConversationType::Single)
);
let group_id = generate_group_conversation_id("group_12345");
assert_eq!(
extract_conversation_type(&group_id),
Some(ConversationType::Group)
);
let ai_id = generate_ai_conversation_id("user_001", "ai_001");
assert_eq!(
extract_conversation_type(&ai_id),
Some(ConversationType::Ai)
);
assert_eq!(extract_conversation_type("invalid"), None);
assert_eq!(extract_conversation_type("1B1234567890123456"), None); }
#[test]
fn test_is_single_chat_conversation() {
let single_id = generate_single_chat_conversation_id("user1", "user2");
assert!(is_single_chat_conversation(&single_id));
let group_id = generate_group_conversation_id("group_12345");
assert!(!is_single_chat_conversation(&group_id));
}
#[test]
fn test_is_group_chat_conversation() {
let single_id = generate_single_chat_conversation_id("user1", "user2");
assert!(!is_group_chat_conversation(&single_id));
let group_id = generate_group_conversation_id("group_12345");
assert!(is_group_chat_conversation(&group_id));
}
}