notedeck

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

args.rs (8179B)


      1 use std::collections::BTreeSet;
      2 
      3 use crate::timeline::TimelineKind;
      4 use enostr::{Filter, Pubkey};
      5 use oot_bitset::{bitset_clear, bitset_get, bitset_set};
      6 use tracing::{debug, error, info};
      7 
      8 #[repr(u16)]
      9 pub enum ColumnsFlag {
     10     SinceOptimize,
     11     Textmode,
     12     Scramble,
     13     NoMedia,
     14     ShowNoteClientTop,
     15     ShowNoteClientBottom,
     16 }
     17 
     18 pub struct ColumnsArgs {
     19     pub columns: Vec<ArgColumn>,
     20     flags: [u16; 2],
     21 }
     22 
     23 impl ColumnsArgs {
     24     pub fn is_flag_set(&self, flag: ColumnsFlag) -> bool {
     25         bitset_get(&self.flags, flag as u16)
     26     }
     27 
     28     pub fn set_flag(&mut self, flag: ColumnsFlag) {
     29         bitset_set(&mut self.flags, flag as u16)
     30     }
     31 
     32     pub fn clear_flag(&mut self, flag: ColumnsFlag) {
     33         bitset_clear(&mut self.flags, flag as u16)
     34     }
     35 
     36     pub fn parse(args: &[String], deck_author: Option<&Pubkey>) -> (Self, BTreeSet<String>) {
     37         let mut unrecognized_args = BTreeSet::new();
     38         let mut res = Self {
     39             columns: vec![],
     40             flags: [0; 2],
     41         };
     42 
     43         // flag defaults
     44         res.set_flag(ColumnsFlag::SinceOptimize);
     45 
     46         let mut i = 0;
     47         let len = args.len();
     48         while i < len {
     49             let arg = &args[i];
     50 
     51             if arg == "--textmode" {
     52                 res.set_flag(ColumnsFlag::Textmode);
     53             } else if arg == "--no-since-optimize" {
     54                 res.clear_flag(ColumnsFlag::SinceOptimize);
     55             } else if arg == "--scramble" {
     56                 res.set_flag(ColumnsFlag::Scramble);
     57             } else if arg == "--show-note-client=top" {
     58                 res.set_flag(ColumnsFlag::ShowNoteClientTop);
     59             } else if arg == "--show-note-client=bottom" {
     60                 res.set_flag(ColumnsFlag::ShowNoteClientBottom);
     61             } else if arg == "--no-media" {
     62                 res.set_flag(ColumnsFlag::NoMedia);
     63             } else if arg == "--filter" {
     64                 i += 1;
     65                 let filter = if let Some(next_arg) = args.get(i) {
     66                     next_arg
     67                 } else {
     68                     error!("filter argument missing?");
     69                     continue;
     70                 };
     71 
     72                 if let Ok(filter) = Filter::from_json(filter) {
     73                     res.columns.push(ArgColumn::Generic(vec![filter]));
     74                 } else {
     75                     error!("failed to parse filter '{}'", filter);
     76                 }
     77             } else if arg == "--column" || arg == "-c" {
     78                 i += 1;
     79                 let column_name = if let Some(next_arg) = args.get(i) {
     80                     next_arg
     81                 } else {
     82                     error!("column argument missing");
     83                     continue;
     84                 };
     85 
     86                 if let Some(rest) = column_name.strip_prefix("contacts:") {
     87                     if let Ok(pubkey) = Pubkey::parse(rest) {
     88                         info!("contact column for user {}", pubkey.hex());
     89                         res.columns
     90                             .push(ArgColumn::Timeline(TimelineKind::contact_list(pubkey)))
     91                     } else {
     92                         error!("error parsing contacts pubkey {}", rest);
     93                         continue;
     94                     }
     95                 } else if column_name == "contacts" {
     96                     if let Some(deck_author) = deck_author {
     97                         res.columns
     98                             .push(ArgColumn::Timeline(TimelineKind::contact_list(
     99                                 deck_author.to_owned(),
    100                             )));
    101                     } else {
    102                         panic!(
    103                             "No accounts available, could not handle implicit pubkey contacts column"
    104                         );
    105                     }
    106                 } else if column_name == "search" {
    107                     i += 1;
    108                     let search = if let Some(next_arg) = args.get(i) {
    109                         next_arg
    110                     } else {
    111                         error!("search filter argument missing?");
    112                         continue;
    113                     };
    114 
    115                     res.columns.push(ArgColumn::Timeline(TimelineKind::search(
    116                         search.to_string(),
    117                     )));
    118                 } else if let Some(notif_pk_str) = column_name.strip_prefix("notifications:") {
    119                     if let Ok(pubkey) = Pubkey::parse(notif_pk_str) {
    120                         info!("got notifications column for user {}", pubkey.hex());
    121                         res.columns
    122                             .push(ArgColumn::Timeline(TimelineKind::notifications(pubkey)));
    123                     } else {
    124                         error!("error parsing notifications pubkey {}", notif_pk_str);
    125                         continue;
    126                     }
    127                 } else if column_name == "notifications" {
    128                     debug!("got notification column for default user");
    129                     if let Some(deck_author) = deck_author {
    130                         res.columns
    131                             .push(ArgColumn::Timeline(TimelineKind::notifications(
    132                                 deck_author.to_owned(),
    133                             )));
    134                     } else {
    135                         panic!("Tried to push notifications timeline with no available users");
    136                     }
    137                 } else if column_name == "profile" {
    138                     debug!("got profile column for default user");
    139                     if let Some(deck_author) = deck_author {
    140                         res.columns.push(ArgColumn::Timeline(TimelineKind::profile(
    141                             deck_author.to_owned(),
    142                         )));
    143                     } else {
    144                         panic!("Tried to push profile timeline with no available users");
    145                     }
    146                 } else if column_name == "universe" {
    147                     debug!("got universe column");
    148                     res.columns
    149                         .push(ArgColumn::Timeline(TimelineKind::Universe))
    150                 } else if let Some(profile_pk_str) = column_name.strip_prefix("profile:") {
    151                     if let Ok(pubkey) = Pubkey::parse(profile_pk_str) {
    152                         info!("got profile column for user {}", pubkey.hex());
    153                         res.columns
    154                             .push(ArgColumn::Timeline(TimelineKind::profile(pubkey)))
    155                     } else {
    156                         error!("error parsing profile pubkey {}", profile_pk_str);
    157                         continue;
    158                     }
    159                 }
    160             } else if arg == "--filter-file" || arg == "-f" {
    161                 i += 1;
    162                 let filter_file = if let Some(next_arg) = args.get(i) {
    163                     next_arg
    164                 } else {
    165                     error!("filter file argument missing?");
    166                     continue;
    167                 };
    168 
    169                 let data = if let Ok(data) = std::fs::read(filter_file) {
    170                     data
    171                 } else {
    172                     error!("failed to read filter file '{}'", filter_file);
    173                     continue;
    174                 };
    175 
    176                 if let Some(filter) = std::str::from_utf8(&data)
    177                     .ok()
    178                     .and_then(|s| Filter::from_json(s).ok())
    179                 {
    180                     res.columns.push(ArgColumn::Generic(vec![filter]));
    181                 } else {
    182                     error!("failed to parse filter in '{}'", filter_file);
    183                 }
    184             } else {
    185                 unrecognized_args.insert(arg.clone());
    186             }
    187 
    188             i += 1;
    189         }
    190 
    191         (res, unrecognized_args)
    192     }
    193 }
    194 
    195 /// A way to define columns from the commandline. Can be column kinds or
    196 /// generic queries
    197 #[derive(Debug, Clone)]
    198 pub enum ArgColumn {
    199     Timeline(TimelineKind),
    200     Generic(Vec<Filter>),
    201 }
    202 
    203 impl ArgColumn {
    204     pub fn into_timeline_kind(self) -> TimelineKind {
    205         match self {
    206             ArgColumn::Generic(_filters) => {
    207                 // TODO: fix generic filters by referencing some filter map
    208                 TimelineKind::Generic(0)
    209             }
    210             ArgColumn::Timeline(tk) => tk,
    211         }
    212     }
    213 }