notedeck

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

args.rs (8358B)


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