use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use crate::error::{Error, Result};
use crate::keys::{Kind, PublicKey};
use crate::message::{DataClass, MessageType};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Role {
Observer,
Chat,
TaskGiver,
Approver,
}
impl Role {
pub fn as_str(&self) -> &'static str {
match self {
Role::Observer => "observer",
Role::Chat => "chat",
Role::TaskGiver => "task-giver",
Role::Approver => "approver",
}
}
pub fn may_send(&self, kind: MessageType) -> bool {
use MessageType::*;
match self {
Role::Observer => false,
Role::Chat => matches!(kind, Chat | Question | Reply | Done | Claim | Release),
Role::TaskGiver => matches!(
kind,
Chat | Question | Reply | Done | Claim | Release | Task
),
Role::Approver => matches!(
kind,
Chat | Question | Reply | Done | Claim | Release | Task | Approve | Deny
),
}
}
}
impl fmt::Display for Role {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for Role {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
match s {
"observer" => Ok(Role::Observer),
"chat" => Ok(Role::Chat),
"task-giver" | "task_giver" | "taskgiver" => Ok(Role::TaskGiver),
"approver" => Ok(Role::Approver),
other => Err(Error::Invalid(format!("unknown role: {other}"))),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Room {
pub id: String,
pub name: String,
#[serde(default)]
pub about: String,
pub owner: PublicKey,
pub created: String,
#[serde(default)]
pub retention_days: Option<u32>,
#[serde(default)]
pub class: DataClass,
#[serde(default)]
pub paused: bool,
#[serde(default)]
pub hold: bool,
#[serde(default)]
pub closed: bool,
pub home_node: String,
#[serde(default)]
pub home_hints: serde_json::Value,
}
pub fn new_room_id() -> String {
let bytes: [u8; 16] = rand::random();
format!(
"r_{}",
data_encoding::BASE32_NOPAD.encode(&bytes).to_lowercase()
)
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Member {
pub room_id: String,
pub name: String,
pub key: PublicKey,
pub kind: Kind,
pub role: Role,
#[serde(default)]
pub node: Option<String>,
pub granted_by: PublicKey,
#[serde(default)]
pub expires_at: Option<String>,
pub joined_at: String,
#[serde(default)]
pub last_seen: Option<String>,
#[serde(default)]
pub muted: bool,
#[serde(default)]
pub revoked: bool,
#[serde(default)]
pub profile: serde_json::Value,
}
impl Member {
pub fn is_expired(&self, now: &str) -> bool {
match &self.expires_at {
Some(exp) => exp.as_str() <= now,
None => false,
}
}
pub fn check_may_send(&self, kind: MessageType, is_owner: bool, now: &str) -> Result<()> {
if self.revoked {
return Err(Error::Denied(format!("{} was revoked", self.name)));
}
if self.muted {
return Err(Error::Denied(format!("{} is muted", self.name)));
}
if self.is_expired(now) {
return Err(Error::Denied(format!("{}'s role has expired", self.name)));
}
if matches!(kind, MessageType::Approve | MessageType::Deny) && self.kind != Kind::Human {
return Err(Error::Denied(format!("only a human key may send {kind}")));
}
if is_owner {
return Ok(());
}
if matches!(kind, MessageType::Control | MessageType::System) {
return Err(Error::Denied(format!("only the owner may send {kind}")));
}
if !self.role.may_send(kind) {
return Err(Error::Denied(format!(
"{} has role {} and may not send {kind}",
self.name, self.role
)));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::keys::Identity;
fn member(kind: Kind, role: Role) -> Member {
let id = Identity::generate("x", kind);
Member {
room_id: "r".into(),
name: "x".into(),
key: id.public(),
kind,
role,
node: None,
granted_by: id.public(),
expires_at: None,
joined_at: "2026-01-01T00:00:00Z".into(),
last_seen: None,
muted: false,
revoked: false,
profile: serde_json::Value::Null,
}
}
#[test]
fn chat_cannot_give_orders() {
let m = member(Kind::Agent, Role::Chat);
assert!(m
.check_may_send(MessageType::Chat, false, "2026-01-02T00:00:00Z")
.is_ok());
assert!(m
.check_may_send(MessageType::Task, false, "2026-01-02T00:00:00Z")
.is_err());
}
#[test]
fn approve_needs_human_key() {
let agent = member(Kind::Agent, Role::Approver);
assert!(agent
.check_may_send(MessageType::Approve, false, "2026-01-02T00:00:00Z")
.is_err());
let human = member(Kind::Human, Role::Approver);
assert!(human
.check_may_send(MessageType::Approve, false, "2026-01-02T00:00:00Z")
.is_ok());
let owner = member(Kind::Agent, Role::Approver);
assert!(owner
.check_may_send(MessageType::Approve, true, "2026-01-02T00:00:00Z")
.is_err());
assert!(owner
.check_may_send(MessageType::Control, true, "2026-01-02T00:00:00Z")
.is_ok());
}
#[test]
fn expiry_muting_and_revocation() {
let mut m = member(Kind::Agent, Role::TaskGiver);
m.expires_at = Some("2026-01-01T12:00:00Z".into());
assert!(m
.check_may_send(MessageType::Task, false, "2026-01-01T11:00:00Z")
.is_ok());
assert!(m
.check_may_send(MessageType::Task, false, "2026-01-01T13:00:00Z")
.is_err());
m.expires_at = None;
m.muted = true;
assert!(m
.check_may_send(MessageType::Chat, false, "2026-01-01T13:00:00Z")
.is_err());
m.muted = false;
m.revoked = true;
assert!(m
.check_may_send(MessageType::Chat, true, "2026-01-01T13:00:00Z")
.is_err());
}
#[test]
fn role_strings() {
assert_eq!("task-giver".parse::<Role>().unwrap(), Role::TaskGiver);
assert_eq!(
serde_json::to_string(&Role::TaskGiver).unwrap(),
"\"task-giver\""
);
}
}