notedeck

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

filter.rs (5581B)


      1 use crate::error::{Error, FilterError};
      2 use crate::note::NoteRef;
      3 use crate::Result;
      4 use nostrdb::{Filter, FilterBuilder, Note, Subscription};
      5 use tracing::{debug, warn};
      6 
      7 /// A unified subscription has a local and remote component. The remote subid
      8 /// tracks data received remotely, and local
      9 #[derive(Debug, Clone)]
     10 pub struct UnifiedSubscription {
     11     pub local: Subscription,
     12     pub remote: String,
     13 }
     14 
     15 /// We may need to fetch some data from relays before our filter is ready.
     16 /// [`FilterState`] tracks this.
     17 #[derive(Debug, Clone)]
     18 pub enum FilterState {
     19     NeedsRemote(Vec<Filter>),
     20     FetchingRemote(UnifiedSubscription),
     21     GotRemote(Subscription),
     22     Ready(Vec<Filter>),
     23     Broken(FilterError),
     24 }
     25 
     26 impl FilterState {
     27     /// We tried to fetch a filter but we wither got no data or the data
     28     /// was corrupted, preventing us from getting to the Ready state.
     29     /// Just mark the timeline as broken so that we can signal to the
     30     /// user that something went wrong
     31     pub fn broken(reason: FilterError) -> Self {
     32         Self::Broken(reason)
     33     }
     34 
     35     /// The filter is ready
     36     pub fn ready(filter: Vec<Filter>) -> Self {
     37         Self::Ready(filter)
     38     }
     39 
     40     /// We need some data from relays before we can continue. Example:
     41     /// for home timelines where we don't have a contact list yet. We
     42     /// need to fetch the contact list before we have the right timeline
     43     /// filter.
     44     pub fn needs_remote(filter: Vec<Filter>) -> Self {
     45         Self::NeedsRemote(filter)
     46     }
     47 
     48     /// We got the remote data. Local data should be available to build
     49     /// the filter for the [`FilterState::Ready`] state
     50     pub fn got_remote(local_sub: Subscription) -> Self {
     51         Self::GotRemote(local_sub)
     52     }
     53 
     54     /// We have sent off a remote subscription to get data needed for the
     55     /// filter. The string is the subscription id
     56     pub fn fetching_remote(sub_id: String, local_sub: Subscription) -> Self {
     57         let unified_sub = UnifiedSubscription {
     58             local: local_sub,
     59             remote: sub_id,
     60         };
     61         Self::FetchingRemote(unified_sub)
     62     }
     63 }
     64 
     65 pub fn should_since_optimize(limit: u64, num_notes: usize) -> bool {
     66     // rough heuristic for bailing since optimization if we don't have enough notes
     67     limit as usize <= num_notes
     68 }
     69 
     70 pub fn since_optimize_filter_with(filter: Filter, notes: &[NoteRef], since_gap: u64) -> Filter {
     71     // Get the latest entry in the events
     72     if notes.is_empty() {
     73         return filter;
     74     }
     75 
     76     // get the latest note
     77     let latest = notes[0];
     78     let since = latest.created_at - since_gap;
     79 
     80     filter.since_mut(since)
     81 }
     82 
     83 pub fn since_optimize_filter(filter: Filter, notes: &[NoteRef]) -> Filter {
     84     since_optimize_filter_with(filter, notes, 60)
     85 }
     86 
     87 pub fn default_limit() -> u64 {
     88     500
     89 }
     90 
     91 pub fn default_remote_limit() -> u64 {
     92     250
     93 }
     94 
     95 pub struct FilteredTags {
     96     pub authors: Option<FilterBuilder>,
     97     pub hashtags: Option<FilterBuilder>,
     98 }
     99 
    100 impl FilteredTags {
    101     pub fn into_follow_filter(self) -> Vec<Filter> {
    102         self.into_filter([1], default_limit())
    103     }
    104 
    105     // TODO: make this more general
    106     pub fn into_filter<I>(self, kinds: I, limit: u64) -> Vec<Filter>
    107     where
    108         I: IntoIterator<Item = u64> + Copy,
    109     {
    110         let mut filters: Vec<Filter> = Vec::with_capacity(2);
    111 
    112         if let Some(authors) = self.authors {
    113             filters.push(authors.kinds(kinds).limit(limit).build())
    114         }
    115 
    116         if let Some(hashtags) = self.hashtags {
    117             filters.push(hashtags.kinds(kinds).limit(limit).build())
    118         }
    119 
    120         filters
    121     }
    122 }
    123 
    124 /// Create a filter from tags. This can be used to create a filter
    125 /// from a contact list
    126 pub fn filter_from_tags(note: &Note) -> Result<FilteredTags> {
    127     let mut author_filter = Filter::new();
    128     let mut hashtag_filter = Filter::new();
    129     let mut author_res: Option<FilterBuilder> = None;
    130     let mut hashtag_res: Option<FilterBuilder> = None;
    131     let mut author_count = 0i32;
    132     let mut hashtag_count = 0i32;
    133 
    134     let tags = note.tags();
    135 
    136     author_filter.start_authors_field()?;
    137     hashtag_filter.start_tags_field('t')?;
    138 
    139     for tag in tags {
    140         if tag.count() < 2 {
    141             continue;
    142         }
    143 
    144         let t = if let Some(t) = tag.get_unchecked(0).variant().str() {
    145             t
    146         } else {
    147             continue;
    148         };
    149 
    150         if t == "p" {
    151             let author = if let Some(author) = tag.get_unchecked(1).variant().id() {
    152                 author
    153             } else {
    154                 continue;
    155             };
    156 
    157             author_filter.add_id_element(author)?;
    158             author_count += 1;
    159         } else if t == "t" {
    160             let hashtag = if let Some(hashtag) = tag.get_unchecked(1).variant().str() {
    161                 hashtag
    162             } else {
    163                 continue;
    164             };
    165 
    166             hashtag_filter.add_str_element(hashtag)?;
    167             hashtag_count += 1;
    168         }
    169     }
    170 
    171     author_filter.end_field();
    172     hashtag_filter.end_field();
    173 
    174     if author_count == 0 && hashtag_count == 0 {
    175         warn!("no authors or hashtags found in contact list");
    176         return Err(Error::empty_contact_list());
    177     }
    178 
    179     debug!(
    180         "adding {} authors and {} hashtags to contact filter",
    181         author_count, hashtag_count
    182     );
    183 
    184     // if we hit these ooms, we need to expand filter buffer size
    185     if author_count > 0 {
    186         author_res = Some(author_filter)
    187     }
    188 
    189     if hashtag_count > 0 {
    190         hashtag_res = Some(hashtag_filter)
    191     }
    192 
    193     Ok(FilteredTags {
    194         authors: author_res,
    195         hashtags: hashtag_res,
    196     })
    197 }