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 }