Skip to main content

diavlos_core/
control.rs

1//! Control messages: pause, mute, revoke, grant. Owner key only. Changes
2//! who may do what. Signed, instant, everyone obeys.
3
4use serde::{Deserialize, Serialize};
5
6use crate::error::{Error, Result};
7use crate::message::{Message, MessageType};
8use crate::room::Role;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(tag = "op", rename_all = "lowercase")]
12pub enum ControlOp {
13    /// Give a member a role. Can expire.
14    Grant {
15        name: String,
16        role: Role,
17        #[serde(default, skip_serializing_if = "Option::is_none")]
18        until: Option<String>,
19    },
20    /// Kill switch. Nothing moves until resume.
21    Pause,
22    Resume,
23    /// Silence one member.
24    Mute {
25        name: String,
26    },
27    Unmute {
28        name: String,
29    },
30    /// Cut one key for good. A signed blocklist entry.
31    Revoke {
32        name: String,
33    },
34    /// Legal hold. Retention stops deleting.
35    Hold {
36        on: bool,
37    },
38    /// The room was rotated: a new room key. Everyone out.
39    Rotated {
40        new_room_id: String,
41    },
42}
43
44impl ControlOp {
45    /// Read the op out of a control message.
46    pub fn from_message(msg: &Message) -> Result<ControlOp> {
47        if msg.kind != MessageType::Control {
48            return Err(Error::Invalid("not a control message".into()));
49        }
50        serde_json::from_value(msg.data.clone())
51            .map_err(|e| Error::Invalid(format!("bad control message: {e}")))
52    }
53
54    /// One line for the message text.
55    pub fn describe(&self) -> String {
56        match self {
57            ControlOp::Grant { name, role, until } => match until {
58                Some(u) => format!("{name} is now {role} until {u}"),
59                None => format!("{name} is now {role}"),
60            },
61            ControlOp::Pause => "room paused".into(),
62            ControlOp::Resume => "room resumed".into(),
63            ControlOp::Mute { name } => format!("{name} muted"),
64            ControlOp::Unmute { name } => format!("{name} unmuted"),
65            ControlOp::Revoke { name } => format!("{name} revoked"),
66            ControlOp::Hold { on } => {
67                if *on {
68                    "legal hold on".into()
69                } else {
70                    "legal hold off".into()
71                }
72            }
73            ControlOp::Rotated { new_room_id } => format!("room rotated to {new_room_id}"),
74        }
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn roundtrip() {
84        let op = ControlOp::Grant {
85            name: "bob".into(),
86            role: Role::Approver,
87            until: Some("2026-12-31T00:00:00Z".into()),
88        };
89        let v = serde_json::to_value(&op).unwrap();
90        assert_eq!(v["op"], "grant");
91        assert_eq!(v["role"], "approver");
92        let back: ControlOp = serde_json::from_value(v).unwrap();
93        assert_eq!(back, op);
94        assert_eq!(ControlOp::Pause.describe(), "room paused");
95    }
96}