1pub mod list;
2pub mod poll;
3pub mod subscribe;
4pub mod token;
5pub mod transport;
6pub mod who;
7
8pub use list::{cmd_list, discover_daemon_rooms, discover_joined_rooms};
9pub use poll::{
10 cmd_poll, cmd_poll_multi, cmd_pull, cmd_query, cmd_watch, poll_messages, poll_messages_multi,
11 pull_messages, QueryOptions,
12};
13pub use subscribe::cmd_subscribe;
14pub use token::{cmd_join, username_from_token};
15#[allow(deprecated)]
16pub use transport::send_message;
17pub use transport::{create_room, destroy_room};
18pub use transport::{
19 ensure_daemon_running, global_join_session, join_session, join_session_target,
20 resolve_socket_target, send_message_with_token, send_message_with_token_target, SocketTarget,
21};
22pub use who::cmd_who;
23
24use room_protocol::dm_room_id;
25use transport::send_message_with_token_target as transport_send_target;
26
27pub async fn cmd_send(
38 room_id: &str,
39 token: &str,
40 to: Option<&str>,
41 content: &str,
42 socket: Option<&std::path::Path>,
43) -> anyhow::Result<()> {
44 let target = resolve_socket_target(room_id, socket);
45 let wire = match to {
46 Some(recipient) => {
47 serde_json::json!({"type": "dm", "to": recipient, "content": content}).to_string()
48 }
49 None => build_wire_payload(content),
50 };
51 let msg = transport_send_target(&target, token, &wire)
52 .await
53 .map_err(|e| {
54 if e.to_string().contains("invalid token") {
55 anyhow::anyhow!("invalid token — run: room join <username>")
56 } else {
57 e
58 }
59 })?;
60 println!("{}", serde_json::to_string(&msg)?);
61 Ok(())
62}
63
64pub async fn cmd_dm(
76 recipient: &str,
77 token: &str,
78 content: &str,
79 socket: Option<&std::path::Path>,
80) -> anyhow::Result<()> {
81 let caller = username_from_token(token)?;
83
84 let dm_id = dm_room_id(&caller, recipient).map_err(|e| anyhow::anyhow!("{e}"))?;
86
87 let wire = serde_json::json!({"type": "dm", "to": recipient, "content": content}).to_string();
89
90 let target = resolve_socket_target(&dm_id, socket);
92 let msg = transport_send_target(&target, token, &wire)
93 .await
94 .map_err(|e| {
95 if e.to_string().contains("No such file")
96 || e.to_string().contains("Connection refused")
97 {
98 anyhow::anyhow!(
99 "DM room '{dm_id}' is not running — start it or use a daemon with the room pre-created"
100 )
101 } else if e.to_string().contains("invalid token") {
102 anyhow::anyhow!(
103 "invalid token for DM room '{dm_id}' — you may need to join it first"
104 )
105 } else {
106 e
107 }
108 })?;
109 println!("{}", serde_json::to_string(&msg)?);
110 Ok(())
111}
112
113pub async fn cmd_create(
123 room_id: &str,
124 socket: Option<&std::path::Path>,
125 visibility: &str,
126 invite: &[String],
127 token: &str,
128) -> anyhow::Result<()> {
129 let daemon_socket = socket
130 .map(|p| p.to_owned())
131 .unwrap_or_else(crate::paths::room_socket_path);
132
133 if !daemon_socket.exists() {
134 anyhow::bail!(
135 "daemon socket not found at {} — is the daemon running?",
136 daemon_socket.display()
137 );
138 }
139
140 let config = serde_json::json!({
141 "visibility": visibility,
142 "invite": invite,
143 "token": token,
144 });
145
146 let result = transport::create_room(&daemon_socket, room_id, &config.to_string()).await?;
147 println!("{}", serde_json::to_string(&result)?);
148 Ok(())
149}
150
151pub async fn cmd_destroy(
161 room_id: &str,
162 socket: Option<&std::path::Path>,
163 token: &str,
164) -> anyhow::Result<()> {
165 let daemon_socket = socket
166 .map(|p| p.to_owned())
167 .unwrap_or_else(crate::paths::room_socket_path);
168
169 if !daemon_socket.exists() {
170 anyhow::bail!(
171 "daemon socket not found at {} — is the daemon running?",
172 daemon_socket.display()
173 );
174 }
175
176 let result = transport::destroy_room(&daemon_socket, room_id, token).await?;
177 println!("{}", serde_json::to_string(&result)?);
178 Ok(())
179}
180
181fn build_wire_payload(input: &str) -> String {
184 if let Some(rest) = input.strip_prefix("/dm ") {
186 let mut parts = rest.splitn(2, ' ');
187 let to = parts.next().unwrap_or("");
188 let content = parts.next().unwrap_or("");
189 return serde_json::json!({"type": "dm", "to": to, "content": content}).to_string();
190 }
191 if let Some(rest) = input.strip_prefix('/') {
193 let mut parts = rest.splitn(2, ' ');
194 let cmd = parts.next().unwrap_or("");
195 let params: Vec<&str> = parts.next().unwrap_or("").split_whitespace().collect();
196 return serde_json::json!({"type": "command", "cmd": cmd, "params": params}).to_string();
197 }
198 serde_json::json!({"type": "message", "content": input}).to_string()
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn plain_message() {
208 let wire = build_wire_payload("hello world");
209 let v: serde_json::Value = serde_json::from_str(&wire).unwrap();
210 assert_eq!(v["type"], "message");
211 assert_eq!(v["content"], "hello world");
212 }
213
214 #[test]
215 fn who_command() {
216 let wire = build_wire_payload("/who");
217 let v: serde_json::Value = serde_json::from_str(&wire).unwrap();
218 assert_eq!(v["type"], "command");
219 assert_eq!(v["cmd"], "who");
220 let params = v["params"].as_array().unwrap();
221 assert!(params.is_empty());
222 }
223
224 #[test]
225 fn command_with_params() {
226 let wire = build_wire_payload("/kick alice");
227 let v: serde_json::Value = serde_json::from_str(&wire).unwrap();
228 assert_eq!(v["type"], "command");
229 assert_eq!(v["cmd"], "kick");
230 let params: Vec<&str> = v["params"]
231 .as_array()
232 .unwrap()
233 .iter()
234 .map(|p| p.as_str().unwrap())
235 .collect();
236 assert_eq!(params, vec!["alice"]);
237 }
238
239 #[test]
240 fn command_with_multiple_params() {
241 let wire = build_wire_payload("/set_status away brb");
242 let v: serde_json::Value = serde_json::from_str(&wire).unwrap();
243 assert_eq!(v["type"], "command");
244 assert_eq!(v["cmd"], "set_status");
245 let params: Vec<&str> = v["params"]
246 .as_array()
247 .unwrap()
248 .iter()
249 .map(|p| p.as_str().unwrap())
250 .collect();
251 assert_eq!(params, vec!["away", "brb"]);
252 }
253
254 #[test]
255 fn dm_via_slash() {
256 let wire = build_wire_payload("/dm bob hey there");
257 let v: serde_json::Value = serde_json::from_str(&wire).unwrap();
258 assert_eq!(v["type"], "dm");
259 assert_eq!(v["to"], "bob");
260 assert_eq!(v["content"], "hey there");
261 }
262
263 #[test]
264 fn dm_slash_no_message() {
265 let wire = build_wire_payload("/dm bob");
266 let v: serde_json::Value = serde_json::from_str(&wire).unwrap();
267 assert_eq!(v["type"], "dm");
268 assert_eq!(v["to"], "bob");
269 assert_eq!(v["content"], "");
270 }
271
272 #[test]
273 fn slash_only() {
274 let wire = build_wire_payload("/");
275 let v: serde_json::Value = serde_json::from_str(&wire).unwrap();
276 assert_eq!(v["type"], "command");
277 assert_eq!(v["cmd"], "");
278 }
279
280 #[test]
281 fn message_starting_with_slash_like_path() {
282 let wire = build_wire_payload("/tmp/foo");
285 let v: serde_json::Value = serde_json::from_str(&wire).unwrap();
286 assert_eq!(v["type"], "command");
287 assert_eq!(v["cmd"], "tmp/foo");
288 }
289
290 #[test]
291 fn empty_string() {
292 let wire = build_wire_payload("");
293 let v: serde_json::Value = serde_json::from_str(&wire).unwrap();
294 assert_eq!(v["type"], "message");
295 assert_eq!(v["content"], "");
296 }
297}