1use serde::{Deserialize, Serialize};
10
11use crate::files::encryption::EncryptedFileMeta;
12
13pub const ROOMS_TOPIC: &str = "huddle-rooms-v1";
14pub const ROOM_TOPIC_PREFIX: &str = "huddle-room-";
15
16pub fn room_topic(room_id: &str) -> String {
17 format!("{ROOM_TOPIC_PREFIX}{room_id}")
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct RoomAnnouncement {
24 pub room_id: String,
25 pub name: String,
26 pub encrypted: bool,
27 pub passphrase_salt: Option<Vec<u8>>,
30 pub member_count: u32,
31 pub creator_fingerprint: String,
32 pub announced_at: i64,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub enum RoomMessage {
39 MemberAnnounce {
42 sender_fingerprint: String,
43 wrapped_session_key: Option<String>,
47 #[serde(default)]
50 display_name: Option<String>,
51 },
52 SessionKeyRequest {
55 requester_fingerprint: String,
56 },
57 Encrypted {
59 sender_fingerprint: String,
60 session_id: String,
61 ciphertext_b64: String,
63 },
64 Plain {
66 sender_fingerprint: String,
67 body: String,
68 },
69 MemberLeave {
71 sender_fingerprint: String,
72 },
73 RotateRoomKey {
79 rotator_fingerprint: String,
80 new_salt: Vec<u8>,
82 },
83 Typing {
85 sender_fingerprint: String,
86 },
87 FileOffer {
92 sender_fingerprint: String,
93 file_id: String,
94 name: String,
95 size_bytes: u64,
96 mime: Option<String>,
97 chunk_count: u32,
98 encrypted_meta: Option<EncryptedFileMeta>,
99 },
100 FileChunk {
103 sender_fingerprint: String,
104 file_id: String,
105 chunk_index: u32,
106 total_chunks: u32,
107 data_b64: String,
110 },
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 #[test]
118 fn room_announcement_round_trip() {
119 let ann = RoomAnnouncement {
120 room_id: "rid".into(),
121 name: "general".into(),
122 encrypted: true,
123 passphrase_salt: Some(vec![1, 2, 3, 4]),
124 member_count: 3,
125 creator_fingerprint: "creator-fp".into(),
126 announced_at: 100,
127 };
128 let json = serde_json::to_vec(&ann).unwrap();
129 let back: RoomAnnouncement = serde_json::from_slice(&json).unwrap();
130 assert_eq!(back.name, "general");
131 assert_eq!(back.passphrase_salt, Some(vec![1, 2, 3, 4]));
132 }
133
134 #[test]
135 fn room_message_variants_round_trip() {
136 let msgs = vec![
137 RoomMessage::MemberAnnounce {
138 sender_fingerprint: "fp".into(),
139 wrapped_session_key: Some("base64data".into()),
140 display_name: Some("Daisy".into()),
141 },
142 RoomMessage::Plain {
143 sender_fingerprint: "fp".into(),
144 body: "hi".into(),
145 },
146 RoomMessage::Encrypted {
147 sender_fingerprint: "fp".into(),
148 session_id: "sid".into(),
149 ciphertext_b64: "ct".into(),
150 },
151 RoomMessage::SessionKeyRequest {
152 requester_fingerprint: "fp".into(),
153 },
154 RoomMessage::MemberLeave {
155 sender_fingerprint: "fp".into(),
156 },
157 RoomMessage::FileOffer {
158 sender_fingerprint: "fp".into(),
159 file_id: "fid".into(),
160 name: "f.bin".into(),
161 size_bytes: 1024,
162 mime: Some("application/octet-stream".into()),
163 chunk_count: 2,
164 encrypted_meta: None,
165 },
166 RoomMessage::FileChunk {
167 sender_fingerprint: "fp".into(),
168 file_id: "fid".into(),
169 chunk_index: 0,
170 total_chunks: 2,
171 data_b64: "AAA=".into(),
172 },
173 RoomMessage::RotateRoomKey {
174 rotator_fingerprint: "fp".into(),
175 new_salt: vec![1u8; 16],
176 },
177 RoomMessage::Typing {
178 sender_fingerprint: "fp".into(),
179 },
180 ];
181 for m in msgs {
182 let json = serde_json::to_vec(&m).unwrap();
183 let back: RoomMessage = serde_json::from_slice(&json).unwrap();
184 assert_eq!(format!("{m:?}"), format!("{back:?}"));
185 }
186 }
187
188 #[test]
189 fn room_topic_format() {
190 assert_eq!(room_topic("abc123"), "huddle-room-abc123");
191 }
192}