args.rs (11418B)
1 use crate::filter::FilterState; 2 use crate::timeline::{PubkeySource, Timeline, TimelineKind}; 3 use enostr::{Filter, Keypair, Pubkey, SecretKey}; 4 use nostrdb::Ndb; 5 use tracing::{debug, error, info}; 6 7 pub struct Args { 8 pub columns: Vec<ArgColumn>, 9 pub relays: Vec<String>, 10 pub is_mobile: Option<bool>, 11 pub keys: Vec<Keypair>, 12 pub since_optimize: bool, 13 pub light: bool, 14 pub debug: bool, 15 pub textmode: bool, 16 pub use_keystore: bool, 17 pub dbpath: Option<String>, 18 pub datapath: Option<String>, 19 } 20 21 impl Args { 22 pub fn parse(args: &[String]) -> Self { 23 let mut res = Args { 24 columns: vec![], 25 relays: vec![], 26 is_mobile: None, 27 keys: vec![], 28 light: false, 29 since_optimize: true, 30 debug: false, 31 textmode: false, 32 use_keystore: true, 33 dbpath: None, 34 datapath: None, 35 }; 36 37 let mut i = 0; 38 let len = args.len(); 39 while i < len { 40 let arg = &args[i]; 41 42 if arg == "--mobile" { 43 res.is_mobile = Some(true); 44 } else if arg == "--light" { 45 res.light = true; 46 } else if arg == "--dark" { 47 res.light = false; 48 } else if arg == "--debug" { 49 res.debug = true; 50 } else if arg == "--textmode" { 51 res.textmode = true; 52 } else if arg == "--pub" || arg == "--npub" { 53 i += 1; 54 let pubstr = if let Some(next_arg) = args.get(i) { 55 next_arg 56 } else { 57 error!("sec argument missing?"); 58 continue; 59 }; 60 61 if let Ok(pk) = Pubkey::parse(pubstr) { 62 res.keys.push(Keypair::only_pubkey(pk)); 63 } else { 64 error!( 65 "failed to parse {} argument. Make sure to use hex or npub.", 66 arg 67 ); 68 } 69 } else if arg == "--sec" || arg == "--nsec" { 70 i += 1; 71 let secstr = if let Some(next_arg) = args.get(i) { 72 next_arg 73 } else { 74 error!("sec argument missing?"); 75 continue; 76 }; 77 78 if let Ok(sec) = SecretKey::parse(secstr) { 79 res.keys.push(Keypair::from_secret(sec)); 80 } else { 81 error!( 82 "failed to parse {} argument. Make sure to use hex or nsec.", 83 arg 84 ); 85 } 86 } else if arg == "--no-since-optimize" { 87 res.since_optimize = false; 88 } else if arg == "--filter" { 89 i += 1; 90 let filter = if let Some(next_arg) = args.get(i) { 91 next_arg 92 } else { 93 error!("filter argument missing?"); 94 continue; 95 }; 96 97 if let Ok(filter) = Filter::from_json(filter) { 98 res.columns.push(ArgColumn::Generic(vec![filter])); 99 } else { 100 error!("failed to parse filter '{}'", filter); 101 } 102 } else if arg == "--dbpath" { 103 i += 1; 104 let path = if let Some(next_arg) = args.get(i) { 105 next_arg 106 } else { 107 error!("dbpath argument missing?"); 108 continue; 109 }; 110 res.dbpath = Some(path.clone()); 111 } else if arg == "--datapath" { 112 i += 1; 113 let path = if let Some(next_arg) = args.get(i) { 114 next_arg 115 } else { 116 error!("datapath argument missing?"); 117 continue; 118 }; 119 res.datapath = Some(path.clone()); 120 } else if arg == "-r" || arg == "--relay" { 121 i += 1; 122 let relay = if let Some(next_arg) = args.get(i) { 123 next_arg 124 } else { 125 error!("relay argument missing?"); 126 continue; 127 }; 128 res.relays.push(relay.clone()); 129 } else if arg == "--column" || arg == "-c" { 130 i += 1; 131 let column_name = if let Some(next_arg) = args.get(i) { 132 next_arg 133 } else { 134 error!("column argument missing"); 135 continue; 136 }; 137 138 if let Some(rest) = column_name.strip_prefix("contacts:") { 139 if let Ok(pubkey) = Pubkey::parse(rest) { 140 info!("contact column for user {}", pubkey.hex()); 141 res.columns 142 .push(ArgColumn::Timeline(TimelineKind::contact_list( 143 PubkeySource::Explicit(pubkey), 144 ))) 145 } else { 146 error!("error parsing contacts pubkey {}", rest); 147 continue; 148 } 149 } else if column_name == "contacts" { 150 res.columns 151 .push(ArgColumn::Timeline(TimelineKind::contact_list( 152 PubkeySource::DeckAuthor, 153 ))) 154 } else if let Some(notif_pk_str) = column_name.strip_prefix("notifications:") { 155 if let Ok(pubkey) = Pubkey::parse(notif_pk_str) { 156 info!("got notifications column for user {}", pubkey.hex()); 157 res.columns 158 .push(ArgColumn::Timeline(TimelineKind::notifications( 159 PubkeySource::Explicit(pubkey), 160 ))) 161 } else { 162 error!("error parsing notifications pubkey {}", notif_pk_str); 163 continue; 164 } 165 } else if column_name == "notifications" { 166 debug!("got notification column for default user"); 167 res.columns 168 .push(ArgColumn::Timeline(TimelineKind::notifications( 169 PubkeySource::DeckAuthor, 170 ))) 171 } else if column_name == "profile" { 172 debug!("got profile column for default user"); 173 res.columns.push(ArgColumn::Timeline(TimelineKind::profile( 174 PubkeySource::DeckAuthor, 175 ))) 176 } else if column_name == "universe" { 177 debug!("got universe column"); 178 res.columns 179 .push(ArgColumn::Timeline(TimelineKind::Universe)) 180 } else if let Some(profile_pk_str) = column_name.strip_prefix("profile:") { 181 if let Ok(pubkey) = Pubkey::parse(profile_pk_str) { 182 info!("got profile column for user {}", pubkey.hex()); 183 res.columns.push(ArgColumn::Timeline(TimelineKind::profile( 184 PubkeySource::Explicit(pubkey), 185 ))) 186 } else { 187 error!("error parsing profile pubkey {}", profile_pk_str); 188 continue; 189 } 190 } 191 } else if arg == "--filter-file" || arg == "-f" { 192 i += 1; 193 let filter_file = if let Some(next_arg) = args.get(i) { 194 next_arg 195 } else { 196 error!("filter file argument missing?"); 197 continue; 198 }; 199 200 let data = if let Ok(data) = std::fs::read(filter_file) { 201 data 202 } else { 203 error!("failed to read filter file '{}'", filter_file); 204 continue; 205 }; 206 207 if let Some(filter) = std::str::from_utf8(&data) 208 .ok() 209 .and_then(|s| Filter::from_json(s).ok()) 210 { 211 res.columns.push(ArgColumn::Generic(vec![filter])); 212 } else { 213 error!("failed to parse filter in '{}'", filter_file); 214 } 215 } else if arg == "--no-keystore" { 216 res.use_keystore = false; 217 } 218 219 i += 1; 220 } 221 222 res 223 } 224 } 225 226 /// A way to define columns from the commandline. Can be column kinds or 227 /// generic queries 228 #[derive(Debug)] 229 pub enum ArgColumn { 230 Timeline(TimelineKind), 231 Generic(Vec<Filter>), 232 } 233 234 impl ArgColumn { 235 pub fn into_timeline(self, ndb: &Ndb, user: Option<&[u8; 32]>) -> Option<Timeline> { 236 match self { 237 ArgColumn::Generic(filters) => Some(Timeline::new( 238 TimelineKind::Generic, 239 FilterState::ready(filters), 240 )), 241 ArgColumn::Timeline(tk) => tk.into_timeline(ndb, user), 242 } 243 } 244 } 245 246 #[cfg(test)] 247 mod tests { 248 use crate::{ 249 app::Damus, 250 result::Result, 251 storage::{delete_file, write_file, Directory}, 252 Error, 253 }; 254 255 use std::path::{Path, PathBuf}; 256 257 fn create_tmp_dir() -> PathBuf { 258 tempfile::TempDir::new() 259 .expect("tmp path") 260 .path() 261 .to_path_buf() 262 } 263 264 fn rmrf(path: impl AsRef<Path>) { 265 std::fs::remove_dir_all(path); 266 } 267 268 /// Ensure dbpath actually sets the dbpath correctly. 269 #[tokio::test] 270 async fn test_dbpath() { 271 let datapath = create_tmp_dir(); 272 let dbpath = create_tmp_dir(); 273 let args = vec![ 274 "--datapath", 275 &datapath.to_str().unwrap(), 276 "--dbpath", 277 &dbpath.to_str().unwrap(), 278 ] 279 .iter() 280 .map(|s| s.to_string()) 281 .collect(); 282 283 let ctx = egui::Context::default(); 284 let app = Damus::new(&ctx, &datapath, args); 285 286 assert!(Path::new(&dbpath.join("data.mdb")).exists()); 287 assert!(Path::new(&dbpath.join("lock.mdb")).exists()); 288 assert!(!Path::new(&datapath.join("db")).exists()); 289 290 rmrf(datapath); 291 rmrf(dbpath); 292 } 293 294 #[tokio::test] 295 async fn test_column_args() { 296 let tmpdir = create_tmp_dir(); 297 let npub = "npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s"; 298 let args = vec![ 299 "--no-keystore", 300 "--pub", 301 npub, 302 "-c", 303 "notifications", 304 "-c", 305 "contacts", 306 ] 307 .iter() 308 .map(|s| s.to_string()) 309 .collect(); 310 311 let ctx = egui::Context::default(); 312 let app = Damus::new(&ctx, &tmpdir, args); 313 314 assert_eq!(app.columns.columns().len(), 2); 315 316 let tl1 = app.columns.column(0).router().top().timeline_id(); 317 let tl2 = app.columns.column(1).router().top().timeline_id(); 318 319 assert_eq!(tl1.is_some(), true); 320 assert_eq!(tl2.is_some(), true); 321 322 let timelines = app.columns.timelines(); 323 assert!(timelines[0].kind.is_notifications()); 324 assert!(timelines[1].kind.is_contacts()); 325 326 rmrf(tmpdir); 327 } 328 }