notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

args.rs (11316B)


      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::app::Damus;
    249 
    250     use std::path::{Path, PathBuf};
    251 
    252     fn create_tmp_dir() -> PathBuf {
    253         tempfile::TempDir::new()
    254             .expect("tmp path")
    255             .path()
    256             .to_path_buf()
    257     }
    258 
    259     fn rmrf(path: impl AsRef<Path>) {
    260         let _ = std::fs::remove_dir_all(path);
    261     }
    262 
    263     /// Ensure dbpath actually sets the dbpath correctly.
    264     #[tokio::test]
    265     async fn test_dbpath() {
    266         let datapath = create_tmp_dir();
    267         let dbpath = create_tmp_dir();
    268         let args = vec![
    269             "--datapath",
    270             &datapath.to_str().unwrap(),
    271             "--dbpath",
    272             &dbpath.to_str().unwrap(),
    273         ]
    274         .iter()
    275         .map(|s| s.to_string())
    276         .collect();
    277 
    278         let ctx = egui::Context::default();
    279         let _app = Damus::new(&ctx, &datapath, args);
    280 
    281         assert!(Path::new(&dbpath.join("data.mdb")).exists());
    282         assert!(Path::new(&dbpath.join("lock.mdb")).exists());
    283         assert!(!Path::new(&datapath.join("db")).exists());
    284 
    285         rmrf(datapath);
    286         rmrf(dbpath);
    287     }
    288 
    289     #[tokio::test]
    290     async fn test_column_args() {
    291         let tmpdir = create_tmp_dir();
    292         let npub = "npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s";
    293         let args = vec![
    294             "--no-keystore",
    295             "--pub",
    296             npub,
    297             "-c",
    298             "notifications",
    299             "-c",
    300             "contacts",
    301         ]
    302         .iter()
    303         .map(|s| s.to_string())
    304         .collect();
    305 
    306         let ctx = egui::Context::default();
    307         let app = Damus::new(&ctx, &tmpdir, args);
    308 
    309         assert_eq!(app.columns.columns().len(), 2);
    310 
    311         let tl1 = app.columns.column(0).router().top().timeline_id();
    312         let tl2 = app.columns.column(1).router().top().timeline_id();
    313 
    314         assert_eq!(tl1.is_some(), true);
    315         assert_eq!(tl2.is_some(), true);
    316 
    317         let timelines = app.columns.timelines();
    318         assert!(timelines[0].kind.is_notifications());
    319         assert!(timelines[1].kind.is_contacts());
    320 
    321         rmrf(tmpdir);
    322     }
    323 }