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