kind.rs (31388B)
1 use crate::error::Error; 2 use crate::search::SearchQuery; 3 use crate::timeline::{Timeline, TimelineTab}; 4 use enostr::{Filter, NoteId, Pubkey}; 5 use nostrdb::{Ndb, Transaction}; 6 use notedeck::filter::{NdbQueryPackage, ValidKind}; 7 use notedeck::{ 8 contacts::{contacts_filter, hybrid_contacts_filter}, 9 filter::{self, default_limit, default_remote_limit, HybridFilter}, 10 tr, FilterError, FilterState, Localization, NoteCache, RootIdError, RootNoteIdBuf, 11 }; 12 use serde::{Deserialize, Serialize}; 13 use std::borrow::Cow; 14 use std::hash::{Hash, Hasher}; 15 use tokenator::{ParseError, TokenParser, TokenSerializable, TokenWriter}; 16 use tracing::{debug, error, warn}; 17 18 #[derive(Clone, Hash, Copy, Default, Debug, PartialEq, Eq, Serialize, Deserialize)] 19 pub enum PubkeySource { 20 Explicit(Pubkey), 21 #[default] 22 DeckAuthor, 23 } 24 25 /// Reference to a NIP-51 people list (kind 30000), identified by author + "d" tag 26 #[derive(Debug, Clone, PartialEq, Hash, Eq)] 27 pub struct PeopleListRef { 28 pub author: Pubkey, 29 pub identifier: String, 30 } 31 32 #[derive(Debug, Clone, PartialEq, Hash, Eq)] 33 pub enum ListKind { 34 Contact(Pubkey), 35 /// A NIP-51 people list (kind 30000) 36 PeopleList(PeopleListRef), 37 } 38 39 impl ListKind { 40 pub fn pubkey(&self) -> Option<&Pubkey> { 41 match self { 42 Self::Contact(pk) => Some(pk), 43 Self::PeopleList(plr) => Some(&plr.author), 44 } 45 } 46 } 47 48 impl PubkeySource { 49 pub fn pubkey(pubkey: Pubkey) -> Self { 50 PubkeySource::Explicit(pubkey) 51 } 52 53 pub fn as_pubkey<'a>(&'a self, deck_author: &'a Pubkey) -> &'a Pubkey { 54 match self { 55 PubkeySource::Explicit(pk) => pk, 56 PubkeySource::DeckAuthor => deck_author, 57 } 58 } 59 } 60 61 impl TokenSerializable for PubkeySource { 62 fn serialize_tokens(&self, writer: &mut TokenWriter) { 63 match self { 64 PubkeySource::DeckAuthor => { 65 writer.write_token("deck_author"); 66 } 67 PubkeySource::Explicit(pk) => { 68 writer.write_token(&hex::encode(pk.bytes())); 69 } 70 } 71 } 72 73 fn parse_from_tokens<'a>(parser: &mut TokenParser<'a>) -> Result<Self, ParseError<'a>> { 74 parser.try_parse(|p| { 75 match p.pull_token() { 76 // we handle bare payloads and assume they are explicit pubkey sources 77 Ok("explicit") => { 78 if let Ok(hex) = p.pull_token() { 79 let pk = Pubkey::from_hex(hex).map_err(|_| ParseError::HexDecodeFailed)?; 80 Ok(PubkeySource::Explicit(pk)) 81 } else { 82 Err(ParseError::HexDecodeFailed) 83 } 84 } 85 86 Err(_) | Ok("deck_author") => Ok(PubkeySource::DeckAuthor), 87 88 Ok(hex) => { 89 let pk = Pubkey::from_hex(hex).map_err(|_| ParseError::HexDecodeFailed)?; 90 Ok(PubkeySource::Explicit(pk)) 91 } 92 } 93 }) 94 } 95 } 96 97 impl ListKind { 98 pub fn contact_list(pk: Pubkey) -> Self { 99 ListKind::Contact(pk) 100 } 101 102 pub fn people_list(author: Pubkey, identifier: String) -> Self { 103 ListKind::PeopleList(PeopleListRef { author, identifier }) 104 } 105 106 pub fn parse<'a>( 107 parser: &mut TokenParser<'a>, 108 deck_author: &Pubkey, 109 ) -> Result<Self, ParseError<'a>> { 110 let contact = parser.try_parse(|p| { 111 p.parse_all(|p| { 112 p.parse_token("contact")?; 113 let pk_src = PubkeySource::parse_from_tokens(p)?; 114 Ok(ListKind::Contact(*pk_src.as_pubkey(deck_author))) 115 }) 116 }); 117 if contact.is_ok() { 118 return contact; 119 } 120 121 parser.parse_all(|p| { 122 p.parse_token("people_list")?; 123 let pk_src = PubkeySource::parse_from_tokens(p)?; 124 let identifier = p.pull_token()?.to_string(); 125 Ok(ListKind::PeopleList(PeopleListRef { 126 author: *pk_src.as_pubkey(deck_author), 127 identifier, 128 })) 129 }) 130 } 131 132 pub fn serialize_tokens(&self, writer: &mut TokenWriter) { 133 match self { 134 ListKind::Contact(pk) => { 135 writer.write_token("contact"); 136 PubkeySource::pubkey(*pk).serialize_tokens(writer); 137 } 138 ListKind::PeopleList(plr) => { 139 writer.write_token("people_list"); 140 PubkeySource::pubkey(plr.author).serialize_tokens(writer); 141 writer.write_token(&plr.identifier); 142 } 143 } 144 } 145 } 146 147 #[derive(Debug, Clone)] 148 pub struct ThreadSelection { 149 pub root_id: RootNoteIdBuf, 150 151 /// The selected note, if different than the root_id. None here 152 /// means the root is selected 153 pub selected_note: Option<NoteId>, 154 } 155 156 impl ThreadSelection { 157 pub fn selected_or_root(&self) -> &[u8; 32] { 158 self.selected_note 159 .as_ref() 160 .map(|sn| sn.bytes()) 161 .unwrap_or(self.root_id.bytes()) 162 } 163 164 pub fn from_root_id(root_id: RootNoteIdBuf) -> Self { 165 Self { 166 root_id, 167 selected_note: None, 168 } 169 } 170 171 pub fn from_note_id( 172 ndb: &Ndb, 173 note_cache: &mut NoteCache, 174 txn: &Transaction, 175 note_id: NoteId, 176 ) -> Result<Self, RootIdError> { 177 let root_id = RootNoteIdBuf::new(ndb, note_cache, txn, note_id.bytes())?; 178 Ok(if root_id.bytes() == note_id.bytes() { 179 Self::from_root_id(root_id) 180 } else { 181 Self { 182 root_id, 183 selected_note: Some(note_id), 184 } 185 }) 186 } 187 } 188 189 /// Thread selection hashing is done in a specific way. For TimelineCache 190 /// lookups, we want to only let the root_id influence thread selection. 191 /// This way Thread TimelineKinds always map to the same cached timeline 192 /// for now (we will likely have to rework this since threads aren't 193 /// *really* timelines) 194 impl Hash for ThreadSelection { 195 fn hash<H: Hasher>(&self, state: &mut H) { 196 // only hash the root id for thread selection 197 self.root_id.hash(state) 198 } 199 } 200 201 // need this to only match root_id or else hash lookups will fail 202 impl PartialEq for ThreadSelection { 203 fn eq(&self, other: &Self) -> bool { 204 self.root_id == other.root_id 205 } 206 } 207 208 impl Eq for ThreadSelection {} 209 210 /// 211 /// What kind of timeline is it? 212 /// - Follow List 213 /// - Notifications 214 /// - DM 215 /// - filter 216 /// - ... etc 217 /// 218 #[derive(Debug, Clone, PartialEq, Eq, Hash)] 219 pub enum TimelineKind { 220 List(ListKind), 221 222 Search(SearchQuery), 223 224 /// The last not per pubkey 225 Algo(AlgoTimeline), 226 227 Notifications(Pubkey), 228 229 Profile(Pubkey), 230 231 Universe, 232 233 /// Generic filter, references a hash of a filter 234 Generic(u64), 235 236 Hashtag(Vec<String>), 237 } 238 239 const NOTIFS_TOKEN_DEPRECATED: &str = "notifs"; 240 const NOTIFS_TOKEN: &str = "notifications"; 241 242 /// Hardcoded algo timelines 243 #[derive(Debug, Hash, Clone, PartialEq, Eq)] 244 pub enum AlgoTimeline { 245 /// LastPerPubkey: a special nostr query that fetches the last N 246 /// notes for each pubkey on the list 247 LastPerPubkey(ListKind), 248 } 249 250 /// The identifier for our last per pubkey algo 251 const LAST_PER_PUBKEY_TOKEN: &str = "last_per_pubkey"; 252 253 impl AlgoTimeline { 254 pub fn serialize_tokens(&self, writer: &mut TokenWriter) { 255 match self { 256 AlgoTimeline::LastPerPubkey(list_kind) => { 257 writer.write_token(LAST_PER_PUBKEY_TOKEN); 258 list_kind.serialize_tokens(writer); 259 } 260 } 261 } 262 263 pub fn parse<'a>( 264 parser: &mut TokenParser<'a>, 265 deck_author: &Pubkey, 266 ) -> Result<Self, ParseError<'a>> { 267 parser.parse_all(|p| { 268 p.parse_token(LAST_PER_PUBKEY_TOKEN)?; 269 Ok(AlgoTimeline::LastPerPubkey(ListKind::parse( 270 p, 271 deck_author, 272 )?)) 273 }) 274 } 275 } 276 277 /* 278 impl Display for TimelineKind { 279 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 280 match self { 281 TimelineKind::List(ListKind::Contact(_src)) => write!( 282 f, 283 "{}", 284 tr!("Home", "Timeline kind label for contact lists") 285 ), 286 TimelineKind::Algo(AlgoTimeline::LastPerPubkey(_lk)) => write!( 287 f, 288 "{}", 289 tr!( 290 "Last Notes", 291 "Timeline kind label for last notes per pubkey" 292 ) 293 ), 294 TimelineKind::Generic(_) => { 295 write!(f, "{}", tr!("Timeline", "Generic timeline kind label")) 296 } 297 TimelineKind::Notifications(_) => write!( 298 f, 299 "{}", 300 tr!("Notifications", "Timeline kind label for notifications") 301 ), 302 TimelineKind::Profile(_) => write!( 303 f, 304 "{}", 305 tr!("Profile", "Timeline kind label for user profiles") 306 ), 307 TimelineKind::Universe => write!( 308 f, 309 "{}", 310 tr!("Universe", "Timeline kind label for universe feed") 311 ), 312 TimelineKind::Hashtag(_) => write!( 313 f, 314 "{}", 315 tr!("Hashtag", "Timeline kind label for hashtag feeds") 316 ), 317 TimelineKind::Search(_) => write!( 318 f, 319 "{}", 320 tr!("Search", "Timeline kind label for search results") 321 ), 322 } 323 } 324 } 325 */ 326 327 impl TimelineKind { 328 pub fn pubkey(&self) -> Option<&Pubkey> { 329 match self { 330 TimelineKind::List(list_kind) => list_kind.pubkey(), 331 TimelineKind::Algo(AlgoTimeline::LastPerPubkey(list_kind)) => list_kind.pubkey(), 332 TimelineKind::Notifications(pk) => Some(pk), 333 TimelineKind::Profile(pk) => Some(pk), 334 TimelineKind::Universe => None, 335 TimelineKind::Generic(_) => None, 336 TimelineKind::Hashtag(_ht) => None, 337 TimelineKind::Search(query) => query.author(), 338 } 339 } 340 341 /// Some feeds are not realtime, like certain algo feeds 342 pub fn should_subscribe_locally(&self) -> bool { 343 match self { 344 TimelineKind::Algo(AlgoTimeline::LastPerPubkey(_list_kind)) => false, 345 346 TimelineKind::List(_list_kind) => true, 347 TimelineKind::Notifications(_pk_src) => true, 348 TimelineKind::Profile(_pk_src) => true, 349 TimelineKind::Universe => true, 350 TimelineKind::Generic(_) => true, 351 TimelineKind::Hashtag(_ht) => true, 352 TimelineKind::Search(_q) => true, 353 } 354 } 355 356 // NOTE!!: if you just added a TimelineKind enum, make sure to update 357 // the parser below as well 358 pub fn serialize_tokens(&self, writer: &mut TokenWriter) { 359 match self { 360 TimelineKind::Search(query) => { 361 writer.write_token("search"); 362 query.serialize_tokens(writer) 363 } 364 TimelineKind::List(list_kind) => list_kind.serialize_tokens(writer), 365 TimelineKind::Algo(algo_timeline) => algo_timeline.serialize_tokens(writer), 366 TimelineKind::Notifications(pk) => { 367 writer.write_token(NOTIFS_TOKEN); 368 PubkeySource::pubkey(*pk).serialize_tokens(writer); 369 } 370 TimelineKind::Profile(pk) => { 371 writer.write_token("profile"); 372 PubkeySource::pubkey(*pk).serialize_tokens(writer); 373 } 374 TimelineKind::Universe => { 375 writer.write_token("universe"); 376 } 377 TimelineKind::Generic(_usize) => { 378 // TODO: lookup filter and then serialize 379 writer.write_token("generic"); 380 } 381 TimelineKind::Hashtag(ht) => { 382 writer.write_token("hashtag"); 383 writer.write_token(&ht.join(" ")); 384 } 385 } 386 } 387 388 pub fn parse<'a>( 389 parser: &mut TokenParser<'a>, 390 deck_author: &Pubkey, 391 ) -> Result<Self, ParseError<'a>> { 392 let profile = parser.try_parse(|p| { 393 p.parse_token("profile")?; 394 let pk_src = PubkeySource::parse_from_tokens(p)?; 395 Ok(TimelineKind::Profile(*pk_src.as_pubkey(deck_author))) 396 }); 397 if profile.is_ok() { 398 return profile; 399 } 400 401 let notifications = parser.try_parse(|p| { 402 // still handle deprecated form (notifs) 403 p.parse_any_token(&[NOTIFS_TOKEN, NOTIFS_TOKEN_DEPRECATED])?; 404 let pk_src = PubkeySource::parse_from_tokens(p)?; 405 Ok(TimelineKind::Notifications(*pk_src.as_pubkey(deck_author))) 406 }); 407 if notifications.is_ok() { 408 return notifications; 409 } 410 411 let list_tl = 412 parser.try_parse(|p| Ok(TimelineKind::List(ListKind::parse(p, deck_author)?))); 413 if list_tl.is_ok() { 414 return list_tl; 415 } 416 417 let algo_tl = 418 parser.try_parse(|p| Ok(TimelineKind::Algo(AlgoTimeline::parse(p, deck_author)?))); 419 if algo_tl.is_ok() { 420 return algo_tl; 421 } 422 423 TokenParser::alt( 424 parser, 425 &[ 426 |p| { 427 p.parse_token("universe")?; 428 Ok(TimelineKind::Universe) 429 }, 430 |p| { 431 p.parse_token("generic")?; 432 // TODO: generic filter serialization 433 Ok(TimelineKind::Generic(0)) 434 }, 435 |p| { 436 p.parse_token("hashtag")?; 437 Ok(TimelineKind::Hashtag( 438 p.pull_token()? 439 .split_whitespace() 440 .filter(|s| !s.is_empty()) 441 .map(|s| s.to_lowercase().to_string()) 442 .collect(), 443 )) 444 }, 445 |p| { 446 p.parse_token("search")?; 447 let search_query = SearchQuery::parse_from_tokens(p)?; 448 Ok(TimelineKind::Search(search_query)) 449 }, 450 ], 451 ) 452 } 453 454 pub fn last_per_pubkey(list_kind: ListKind) -> Self { 455 TimelineKind::Algo(AlgoTimeline::LastPerPubkey(list_kind)) 456 } 457 458 pub fn contact_list(pk: Pubkey) -> Self { 459 TimelineKind::List(ListKind::contact_list(pk)) 460 } 461 462 pub fn people_list(author: Pubkey, identifier: String) -> Self { 463 TimelineKind::List(ListKind::people_list(author, identifier)) 464 } 465 466 pub fn search(s: String) -> Self { 467 TimelineKind::Search(SearchQuery::new(s)) 468 } 469 470 pub fn is_contacts(&self) -> bool { 471 matches!(self, TimelineKind::List(ListKind::Contact(_))) 472 } 473 474 pub fn profile(pk: Pubkey) -> Self { 475 TimelineKind::Profile(pk) 476 } 477 478 pub fn is_notifications(&self) -> bool { 479 matches!(self, TimelineKind::Notifications(_)) 480 } 481 482 pub fn notifications(pk: Pubkey) -> Self { 483 TimelineKind::Notifications(pk) 484 } 485 486 // TODO: probably should set default limit here 487 /// Build the filter state for this timeline kind. 488 pub fn filters(&self, txn: &Transaction, ndb: &Ndb) -> FilterState { 489 match self { 490 TimelineKind::Search(s) => FilterState::ready(search_filter(s)), 491 492 TimelineKind::Universe => FilterState::ready(universe_filter()), 493 494 TimelineKind::List(list_k) => match list_k { 495 ListKind::Contact(pubkey) => contact_filter_state(txn, ndb, pubkey), 496 ListKind::PeopleList(plr) => people_list_filter_state(txn, ndb, plr), 497 }, 498 499 // TODO: still need to update this to fetch likes, zaps, etc 500 TimelineKind::Notifications(pubkey) => { 501 FilterState::ready(vec![notifications_filter(pubkey)]) 502 } 503 504 TimelineKind::Hashtag(hashtag) => { 505 let mut filters = Vec::new(); 506 for tag in hashtag.iter().filter(|tag| !tag.is_empty()) { 507 let tag_lower = tag.to_lowercase(); 508 filters.push( 509 Filter::new() 510 .kinds([1]) 511 .limit(filter::default_limit()) 512 .tags([tag_lower.as_str()], 't') 513 .build(), 514 ); 515 } 516 517 if filters.is_empty() { 518 warn!(?hashtag, "hashtag timeline has no usable tags"); 519 } else if filters.len() != hashtag.len() { 520 debug!( 521 ?hashtag, 522 usable_tags = filters.len(), 523 "hashtag timeline dropped empty tags" 524 ); 525 } 526 527 FilterState::ready(filters) 528 } 529 530 TimelineKind::Algo(algo_timeline) => match algo_timeline { 531 AlgoTimeline::LastPerPubkey(list_k) => match list_k { 532 ListKind::Contact(pubkey) => last_per_pubkey_filter_state(txn, ndb, pubkey), 533 ListKind::PeopleList(plr) => { 534 people_list_last_per_pubkey_filter_state(txn, ndb, plr) 535 } 536 }, 537 }, 538 539 TimelineKind::Generic(_) => { 540 todo!("implement generic filter lookups") 541 } 542 543 TimelineKind::Profile(pk) => FilterState::ready_hybrid(profile_filter(pk.bytes())), 544 } 545 } 546 547 pub fn into_timeline(self, txn: &Transaction, ndb: &Ndb) -> Option<Timeline> { 548 match self { 549 TimelineKind::Search(s) => { 550 let filter = FilterState::ready(search_filter(&s)); 551 Some(Timeline::new( 552 TimelineKind::Search(s), 553 filter, 554 TimelineTab::full_tabs(), 555 )) 556 } 557 558 TimelineKind::Universe => Some(Timeline::new( 559 TimelineKind::Universe, 560 FilterState::ready(universe_filter()), 561 TimelineTab::full_tabs(), 562 )), 563 564 TimelineKind::Generic(_filter_id) => { 565 warn!("you can't convert a TimelineKind::Generic to a Timeline"); 566 // TODO: you actually can! just need to look up the filter id 567 None 568 } 569 570 TimelineKind::Algo(AlgoTimeline::LastPerPubkey(ListKind::Contact(pk))) => { 571 let contact_filter = contacts_filter(pk.bytes()); 572 573 let results = ndb 574 .query(txn, std::slice::from_ref(&contact_filter), 1) 575 .expect("contact query failed?"); 576 577 let kind_fn = TimelineKind::last_per_pubkey; 578 let tabs = TimelineTab::only_notes_and_replies(); 579 580 if results.is_empty() { 581 return Some(Timeline::new( 582 kind_fn(ListKind::contact_list(pk)), 583 FilterState::needs_remote(), 584 tabs, 585 )); 586 } 587 588 let list_kind = ListKind::contact_list(pk); 589 590 match Timeline::last_per_pubkey(&results[0].note, &list_kind) { 591 Err(Error::App(notedeck::Error::Filter(FilterError::EmptyContactList))) => { 592 Some(Timeline::new( 593 kind_fn(list_kind), 594 FilterState::needs_remote(), 595 tabs, 596 )) 597 } 598 Err(e) => { 599 error!("Unexpected error: {e}"); 600 None 601 } 602 Ok(tl) => Some(tl), 603 } 604 } 605 606 TimelineKind::Profile(pk) => { 607 let filter = profile_filter(pk.bytes()); 608 Some(Timeline::new( 609 TimelineKind::profile(pk), 610 FilterState::ready_hybrid(filter), 611 TimelineTab::full_tabs(), 612 )) 613 } 614 615 TimelineKind::Notifications(pk) => { 616 let notifications_filter = notifications_filter(&pk); 617 618 Some(Timeline::new( 619 TimelineKind::notifications(pk), 620 FilterState::ready(vec![notifications_filter]), 621 TimelineTab::notifications(), 622 )) 623 } 624 625 TimelineKind::Hashtag(hashtag) => Some(Timeline::hashtag(hashtag)), 626 627 TimelineKind::List(ListKind::Contact(pk)) => Some(Timeline::new( 628 TimelineKind::contact_list(pk), 629 contact_filter_state(txn, ndb, &pk), 630 TimelineTab::full_tabs(), 631 )), 632 633 TimelineKind::List(ListKind::PeopleList(plr)) => Some(Timeline::new( 634 TimelineKind::List(ListKind::PeopleList(plr.clone())), 635 people_list_filter_state(txn, ndb, &plr), 636 TimelineTab::full_tabs(), 637 )), 638 639 TimelineKind::Algo(AlgoTimeline::LastPerPubkey(ListKind::PeopleList(plr))) => { 640 let list_filter = people_list_note_filter(&plr); 641 let results = ndb 642 .query(txn, std::slice::from_ref(&list_filter), 1) 643 .expect("people list query failed?"); 644 645 let list_kind = ListKind::PeopleList(plr); 646 let kind_fn = TimelineKind::last_per_pubkey; 647 let tabs = TimelineTab::only_notes_and_replies(); 648 649 if results.is_empty() { 650 return Some(Timeline::new( 651 kind_fn(list_kind), 652 FilterState::needs_remote(), 653 tabs, 654 )); 655 } 656 657 match Timeline::last_per_pubkey(&results[0].note, &list_kind) { 658 Err(Error::App(notedeck::Error::Filter( 659 FilterError::EmptyContactList | FilterError::EmptyList, 660 ))) => Some(Timeline::new( 661 kind_fn(list_kind), 662 FilterState::needs_remote(), 663 tabs, 664 )), 665 Err(e) => { 666 error!("Unexpected error: {e}"); 667 None 668 } 669 Ok(tl) => Some(tl), 670 } 671 } 672 } 673 } 674 675 pub fn to_title(&self, i18n: &mut Localization) -> ColumnTitle<'_> { 676 match self { 677 TimelineKind::Search(query) => { 678 ColumnTitle::formatted(format!("Search \"{}\"", query.search)) 679 } 680 TimelineKind::List(list_kind) => match list_kind { 681 ListKind::Contact(_pubkey_source) => { 682 ColumnTitle::formatted(tr!(i18n, "Contacts", "Column title for contact lists")) 683 } 684 ListKind::PeopleList(plr) => ColumnTitle::formatted(plr.identifier.clone()), 685 }, 686 TimelineKind::Algo(AlgoTimeline::LastPerPubkey(list_kind)) => match list_kind { 687 ListKind::Contact(_pubkey_source) => ColumnTitle::formatted(tr!( 688 i18n, 689 "Contacts (last notes)", 690 "Column title for last notes per contact" 691 )), 692 ListKind::PeopleList(plr) => { 693 ColumnTitle::formatted(format!("{} (last notes)", plr.identifier)) 694 } 695 }, 696 TimelineKind::Notifications(_pubkey_source) => { 697 ColumnTitle::formatted(tr!(i18n, "Notifications", "Column title for notifications")) 698 } 699 TimelineKind::Profile(_pubkey_source) => ColumnTitle::needs_db(self), 700 TimelineKind::Universe => { 701 ColumnTitle::formatted(tr!(i18n, "Universe", "Column title for universe feed")) 702 } 703 TimelineKind::Generic(_) => { 704 ColumnTitle::formatted(tr!(i18n, "Custom", "Column title for custom timelines")) 705 } 706 TimelineKind::Hashtag(hashtag) => ColumnTitle::formatted(hashtag.join(" ").to_string()), 707 } 708 } 709 } 710 711 pub fn notifications_filter(pk: &Pubkey) -> Filter { 712 Filter::new() 713 .pubkeys([pk.bytes()]) 714 .kinds(notification_kinds()) 715 .limit(default_limit()) 716 .build() 717 } 718 719 pub fn notification_kinds() -> [u64; 3] { 720 [1, 7, 6] 721 } 722 723 #[derive(Debug)] 724 pub struct TitleNeedsDb<'a> { 725 kind: &'a TimelineKind, 726 } 727 728 impl<'a> TitleNeedsDb<'a> { 729 pub fn new(kind: &'a TimelineKind) -> Self { 730 TitleNeedsDb { kind } 731 } 732 733 pub fn title<'txn>(&self, txn: &'txn Transaction, ndb: &Ndb) -> &'txn str { 734 if let TimelineKind::Profile(pubkey) = self.kind { 735 let profile = ndb.get_profile_by_pubkey(txn, pubkey); 736 let m_name = profile 737 .as_ref() 738 .ok() 739 .map(|p| notedeck::name::get_display_name(Some(p)).name()); 740 741 m_name.unwrap_or("Profile") 742 } else { 743 "Unknown" 744 } 745 } 746 } 747 748 /// This saves us from having to construct a transaction if we don't need to 749 /// for a particular column when rendering the title 750 #[derive(Debug)] 751 pub enum ColumnTitle<'a> { 752 Simple(Cow<'static, str>), 753 NeedsDb(TitleNeedsDb<'a>), 754 } 755 756 impl<'a> ColumnTitle<'a> { 757 pub fn simple(title: &'static str) -> Self { 758 Self::Simple(Cow::Borrowed(title)) 759 } 760 761 pub fn formatted(title: String) -> Self { 762 Self::Simple(Cow::Owned(title)) 763 } 764 765 pub fn needs_db(kind: &'a TimelineKind) -> ColumnTitle<'a> { 766 Self::NeedsDb(TitleNeedsDb::new(kind)) 767 } 768 } 769 770 /// Build the filter state for a contact list timeline. 771 fn contact_filter_state(txn: &Transaction, ndb: &Ndb, pk: &Pubkey) -> FilterState { 772 let contact_filter = contacts_filter(pk); 773 774 let results = match ndb.query(txn, std::slice::from_ref(&contact_filter), 1) { 775 Ok(results) => results, 776 Err(err) => { 777 error!("contact query failed: {err}"); 778 return FilterState::Broken(FilterError::EmptyContactList); 779 } 780 }; 781 782 if results.is_empty() { 783 FilterState::needs_remote() 784 } else { 785 let with_hashtags = false; 786 match hybrid_contacts_filter(&results[0].note, Some(pk.bytes()), with_hashtags) { 787 Err(notedeck::Error::Filter(FilterError::EmptyContactList)) => { 788 FilterState::needs_remote() 789 } 790 Err(err) => { 791 error!("Error getting contact filter state: {err}"); 792 FilterState::Broken(FilterError::EmptyContactList) 793 } 794 Ok(filter) => FilterState::ready_hybrid(filter), 795 } 796 } 797 } 798 799 /// Build the filter state for a last-per-pubkey timeline. 800 fn last_per_pubkey_filter_state(txn: &Transaction, ndb: &Ndb, pk: &Pubkey) -> FilterState { 801 let contact_filter = contacts_filter(pk.bytes()); 802 803 let results = match ndb.query(txn, std::slice::from_ref(&contact_filter), 1) { 804 Ok(results) => results, 805 Err(err) => { 806 error!("contact query failed: {err}"); 807 return FilterState::Broken(FilterError::EmptyContactList); 808 } 809 }; 810 811 if results.is_empty() { 812 FilterState::needs_remote() 813 } else { 814 let kind = 1; 815 let notes_per_pk = 1; 816 match filter::last_n_per_pubkey_from_tags(&results[0].note, kind, notes_per_pk) { 817 Err(notedeck::Error::Filter(FilterError::EmptyContactList)) => { 818 FilterState::needs_remote() 819 } 820 Err(err) => { 821 error!("Error getting contact filter state: {err}"); 822 FilterState::Broken(FilterError::EmptyContactList) 823 } 824 Ok(filter) => FilterState::ready(filter), 825 } 826 } 827 } 828 829 fn profile_filter(pk: &[u8; 32]) -> HybridFilter { 830 let local = vec![ 831 NdbQueryPackage { 832 filters: vec![Filter::new() 833 .authors([pk]) 834 .kinds([1]) 835 .limit(default_limit()) 836 .build()], 837 kind: ValidKind::One, 838 }, 839 NdbQueryPackage { 840 filters: vec![Filter::new() 841 .authors([pk]) 842 .kinds([6]) 843 .limit(default_limit()) 844 .build()], 845 kind: ValidKind::Six, 846 }, 847 ]; 848 849 let remote = vec![Filter::new() 850 .authors([pk]) 851 .kinds([1, 6, 0, 3]) 852 .limit(default_remote_limit()) 853 .build()]; 854 855 HybridFilter::split(local, remote) 856 } 857 858 fn search_filter(s: &SearchQuery) -> Vec<Filter> { 859 vec![s.filter().limit(default_limit()).build()] 860 } 861 862 fn universe_filter() -> Vec<Filter> { 863 vec![Filter::new().kinds([1]).limit(default_limit()).build()] 864 } 865 866 /// Filter to fetch a kind 30000 people list event by author + d tag 867 pub fn people_list_note_filter(plr: &PeopleListRef) -> Filter { 868 Filter::new() 869 .authors([plr.author.bytes()]) 870 .kinds([30000]) 871 .tags([plr.identifier.as_str()], 'd') 872 .limit(1) 873 .build() 874 } 875 876 /// Build the filter state for a people list timeline. 877 fn people_list_filter_state(txn: &Transaction, ndb: &Ndb, plr: &PeopleListRef) -> FilterState { 878 let list_filter = people_list_note_filter(plr); 879 880 let results = match ndb.query(txn, std::slice::from_ref(&list_filter), 1) { 881 Ok(results) => results, 882 Err(err) => { 883 error!("people list query failed: {err}"); 884 return FilterState::Broken(FilterError::EmptyList); 885 } 886 }; 887 888 if results.is_empty() { 889 FilterState::needs_remote() 890 } else { 891 let with_hashtags = false; 892 match hybrid_contacts_filter(&results[0].note, None, with_hashtags) { 893 Err(notedeck::Error::Filter(FilterError::EmptyContactList)) => { 894 FilterState::needs_remote() 895 } 896 Err(err) => { 897 error!("Error getting people list filter state: {err}"); 898 FilterState::Broken(FilterError::EmptyList) 899 } 900 Ok(filter) => FilterState::ready_hybrid(filter), 901 } 902 } 903 } 904 905 /// Build the filter state for a last-per-pubkey timeline backed by a people list. 906 fn people_list_last_per_pubkey_filter_state( 907 txn: &Transaction, 908 ndb: &Ndb, 909 plr: &PeopleListRef, 910 ) -> FilterState { 911 let list_filter = people_list_note_filter(plr); 912 913 let results = match ndb.query(txn, std::slice::from_ref(&list_filter), 1) { 914 Ok(results) => results, 915 Err(err) => { 916 error!("people list query failed: {err}"); 917 return FilterState::Broken(FilterError::EmptyList); 918 } 919 }; 920 921 if results.is_empty() { 922 FilterState::needs_remote() 923 } else { 924 let kind = 1; 925 let notes_per_pk = 1; 926 match filter::last_n_per_pubkey_from_tags(&results[0].note, kind, notes_per_pk) { 927 Err(notedeck::Error::Filter(FilterError::EmptyContactList)) => { 928 FilterState::needs_remote() 929 } 930 Err(err) => { 931 error!("Error getting people list filter state: {err}"); 932 FilterState::Broken(FilterError::EmptyList) 933 } 934 Ok(filter) => FilterState::ready(filter), 935 } 936 } 937 }