room_cli/oneshot/
token.rs1use std::path::{Path, PathBuf};
2
3use super::transport::global_join_session;
4use crate::paths;
5
6pub async fn cmd_join(username: &str, socket: Option<&std::path::Path>) -> anyhow::Result<()> {
15 paths::ensure_room_dirs().map_err(|e| anyhow::anyhow!("cannot create ~/.room: {e}"))?;
16 let socket_path = paths::effective_socket_path(socket);
17 let (returned_user, token) = global_join_session(&socket_path, username).await?;
18 let token_data = serde_json::json!({"username": returned_user, "token": token});
19 let token_path = paths::global_token_path(&returned_user);
20 std::fs::write(&token_path, format!("{token_data}\n"))?;
21 println!("{token_data}");
22 Ok(())
23}
24
25pub fn username_from_token(token: &str) -> anyhow::Result<String> {
31 let state_dir = paths::room_state_dir();
32 let prefix = "room-";
33 let suffix = ".token";
34 let files: Vec<PathBuf> = std::fs::read_dir(&state_dir)
35 .map_err(|e| anyhow::anyhow!("cannot read {}: {e}", state_dir.display()))?
36 .filter_map(|e| e.ok())
37 .map(|e| e.path())
38 .filter(|p| {
39 p.file_name()
40 .and_then(|n| n.to_str())
41 .map(|n| n.starts_with(prefix) && n.ends_with(suffix))
42 .unwrap_or(false)
43 })
44 .collect();
45
46 for path in files {
47 if let Ok(data) = std::fs::read_to_string(&path) {
48 if let Ok(v) = serde_json::from_str::<serde_json::Value>(data.trim()) {
49 if v["token"].as_str() == Some(token) {
50 if let Some(u) = v["username"].as_str() {
51 return Ok(u.to_owned());
52 }
53 }
54 }
55 }
56 }
57
58 anyhow::bail!("token not recognised — run: room join <username> to get a fresh token")
59}
60
61pub fn read_cursor(cursor_path: &Path) -> Option<String> {
63 std::fs::read_to_string(cursor_path)
64 .ok()
65 .map(|s| s.trim().to_owned())
66 .filter(|s| !s.is_empty())
67}
68
69pub fn write_cursor(cursor_path: &Path, id: &str) -> anyhow::Result<()> {
73 if let Some(parent) = cursor_path.parent() {
74 std::fs::create_dir_all(parent)?;
75 }
76 std::fs::write(cursor_path, id)?;
77 Ok(())
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83 use std::fs;
84 use tempfile::TempDir;
85
86 fn write_token_file(dir: &std::path::Path, username: &str, token: &str) {
88 let name = format!("room-{username}.token");
89 let data = serde_json::json!({"username": username, "token": token});
90 fs::write(dir.join(name), format!("{data}\n")).unwrap();
91 }
92
93 fn username_from_token_in(dir: &std::path::Path, token: &str) -> anyhow::Result<String> {
95 let prefix = "room-";
96 let suffix = ".token";
97 let files: Vec<PathBuf> = fs::read_dir(dir)
98 .unwrap()
99 .filter_map(|e| e.ok())
100 .map(|e| e.path())
101 .filter(|p| {
102 p.file_name()
103 .and_then(|n| n.to_str())
104 .map(|n| n.starts_with(prefix) && n.ends_with(suffix))
105 .unwrap_or(false)
106 })
107 .collect();
108
109 for path in files {
110 if let Ok(data) = fs::read_to_string(&path) {
111 if let Ok(v) = serde_json::from_str::<serde_json::Value>(data.trim()) {
112 if v["token"].as_str() == Some(token) {
113 if let Some(u) = v["username"].as_str() {
114 return Ok(u.to_owned());
115 }
116 }
117 }
118 }
119 }
120 anyhow::bail!("token not recognised — run: room join <username>")
121 }
122
123 #[test]
124 fn finds_correct_user() {
125 let dir = TempDir::new().unwrap();
126 write_token_file(dir.path(), "alice", "tok-alice");
127 let user = username_from_token_in(dir.path(), "tok-alice").unwrap();
128 assert_eq!(user, "alice");
129 }
130
131 #[test]
132 fn disambiguates_multiple_users() {
133 let dir = TempDir::new().unwrap();
134 write_token_file(dir.path(), "alice", "tok-alice");
135 write_token_file(dir.path(), "bob", "tok-bob");
136
137 assert_eq!(
138 username_from_token_in(dir.path(), "tok-alice").unwrap(),
139 "alice"
140 );
141 assert_eq!(
142 username_from_token_in(dir.path(), "tok-bob").unwrap(),
143 "bob"
144 );
145 }
146
147 #[test]
148 fn unknown_token_errors_with_join_hint() {
149 let dir = TempDir::new().unwrap();
150 let err = username_from_token_in(dir.path(), "not-a-real-token").unwrap_err();
151 assert!(
152 err.to_string().contains("room join"),
153 "expected 'room join' hint in: {err}"
154 );
155 }
156
157 #[test]
158 fn two_agents_tokens_do_not_collide() {
159 let dir = TempDir::new().unwrap();
160 write_token_file(dir.path(), "alice", "tok-alice");
161 write_token_file(dir.path(), "bob", "tok-bob");
162
163 assert_eq!(
164 username_from_token_in(dir.path(), "tok-alice").unwrap(),
165 "alice"
166 );
167 assert_eq!(
168 username_from_token_in(dir.path(), "tok-bob").unwrap(),
169 "bob"
170 );
171 }
172
173 #[test]
174 fn ignores_non_token_files() {
175 let dir = TempDir::new().unwrap();
176 fs::write(dir.path().join("room-lobby.sock"), "not a token").unwrap();
178 write_token_file(dir.path(), "bob", "tok-bob");
179
180 assert_eq!(
181 username_from_token_in(dir.path(), "tok-bob").unwrap(),
182 "bob"
183 );
184 }
185
186 #[test]
187 fn read_cursor_returns_none_when_file_absent() {
188 let dir = TempDir::new().unwrap();
189 let path = dir.path().join("cursor");
190 assert!(read_cursor(&path).is_none());
191 }
192
193 #[test]
194 fn write_then_read_cursor_round_trips() {
195 let dir = TempDir::new().unwrap();
196 let path = dir.path().join("cursor");
197 write_cursor(&path, "abc-123").unwrap();
198 assert_eq!(read_cursor(&path).unwrap(), "abc-123");
199 }
200}