diff --git a/crates/notedeck_columns/src/storage/decks.rs b/crates/notedeck_columns/src/storage/decks.rs index ee819500..8033814c 100644 --- a/crates/notedeck_columns/src/storage/decks.rs +++ b/crates/notedeck_columns/src/storage/decks.rs @@ -1,19 +1,15 @@ use std::{collections::HashMap, fmt, str::FromStr}; -use enostr::{NoteId, Pubkey}; +use enostr::Pubkey; use nostrdb::Ndb; use serde::{Deserialize, Serialize}; -use strum::IntoEnumIterator; -use strum_macros::EnumIter; use tracing::{error, info}; use crate::{ - accounts::AccountsRoute, column::{Columns, IntermediaryRoute}, decks::{Deck, Decks, DecksCache}, route::Route, - timeline::{kind::ListKind, AlgoTimeline, PubkeySource, TimelineKind, TimelineRoute}, - ui::add_column::{AddAlgoRoute, AddColumnRoute}, + timeline::TimelineKind, Error, }; @@ -286,9 +282,9 @@ fn serialize_columns(columns: &Columns) -> Vec> { for column in columns.columns() { let mut column_routes = Vec::new(); for route in column.router().routes() { - if let Some(route_str) = serialize_route(route, columns) { - column_routes.push(route_str); - } + let mut writer = TokenWriter::default(); + route.serialize_tokens(&mut writer); + column_routes.push(writer.str().to_string()); } cols_serialized.push(column_routes); } @@ -296,27 +292,26 @@ fn serialize_columns(columns: &Columns) -> Vec> { cols_serialized } -fn deserialize_columns(ndb: &Ndb, deck_user: &[u8; 32], serialized: Vec>) -> Columns { +fn deserialize_columns(ndb: &Ndb, deck_user: &[u8; 32], columns: Vec>) -> Columns { let mut cols = Columns::new(); - for serialized_routes in serialized { + for column in columns { let mut cur_routes = Vec::new(); - for serialized_route in serialized_routes { - let selections = Selection::from_serialized(&serialized_route); - if let Some(route_intermediary) = selections_to_route(&selections) { - if let Some(ir) = route_intermediary.intermediary_route(ndb, Some(deck_user)) { - match &ir { - IntermediaryRoute::Route(Route::Timeline(TimelineRoute::Thread(_))) - | IntermediaryRoute::Route(Route::Timeline(TimelineRoute::Profile(_))) => { - // Do nothing. TimelineRoute Threads & Profiles not yet supported for deserialization - } - _ => cur_routes.push(ir), + + for route in column { + let tokens: Vec<&str> = route.split(":").collect(); + let mut parser = TokenParser::new(&tokens); + + match CleanIntermediaryRoute::parse_from_tokens(&mut parser) { + Ok(route_intermediary) => { + if let Some(ir) = + route_intermediary.into_intermediary_route(ndb, Some(deck_user)) + { + cur_routes.push(ir); } } - } else { - error!( - "could not turn selections to RouteIntermediary: {:?}", - selections - ); + Err(err) => { + error!("could not turn tokens to RouteIntermediary: {:?}", err); + } } } @@ -328,223 +323,17 @@ fn deserialize_columns(ndb: &Ndb, deck_user: &[u8; 32], serialized: Vec Result { - Ok(parse_selection(serialized)) - } -} - -#[derive(Clone, PartialEq, Eq, Debug, EnumIter)] -enum AlgoKeyword { - LastPerPubkey, -} - -impl AlgoKeyword { - #[inline] - pub fn name(&self) -> &'static str { - match self { - AlgoKeyword::LastPerPubkey => "last_per_pubkey", - } - } -} - -#[derive(Clone, PartialEq, Eq, Debug, EnumIter)] -enum ListKeyword { - Contact, -} - -impl ListKeyword { - #[inline] - pub fn name(&self) -> &'static str { - match self { - ListKeyword::Contact => "contact", - } - } -} - -#[derive(Clone, PartialEq, Eq, Debug, EnumIter)] -enum PubkeySourceKeyword { - Explicit, - DeckAuthor, -} - -impl PubkeySourceKeyword { - #[inline] - pub fn name(&self) -> &'static str { - match self { - PubkeySourceKeyword::Explicit => "explicit", - PubkeySourceKeyword::DeckAuthor => "deck_author", - } - } -} - -#[derive(Clone, PartialEq, Eq, Debug, EnumIter)] -enum Keyword { - Notifs, - Universe, - Profile, - Hashtag, - Generic, - Thread, - Reply, - Quote, - Account, - Show, - New, - Relay, - Compose, - Column, - AlgoSelection, - NotificationSelection, - ExternalNotifSelection, - HashtagSelection, - Support, - Deck, - Edit, - IndividualSelection, - ExternalIndividualSelection, -} - -impl Keyword { - fn name(&self) -> &'static str { - match self { - Keyword::Notifs => "notifs", - Keyword::Universe => "universe", - Keyword::Profile => "profile", - Keyword::Hashtag => "hashtag", - Keyword::Generic => "generic", - Keyword::Thread => "thread", - Keyword::Reply => "reply", - Keyword::Quote => "quote", - Keyword::Account => "account", - Keyword::Show => "show", - Keyword::New => "new", - Keyword::Relay => "relay", - Keyword::Compose => "compose", - Keyword::Column => "column", - Keyword::AlgoSelection => "algo_selection", - Keyword::NotificationSelection => "notification_selection", - Keyword::ExternalNotifSelection => "external_notif_selection", - Keyword::IndividualSelection => "individual_selection", - Keyword::ExternalIndividualSelection => "external_individual_selection", - Keyword::HashtagSelection => "hashtag_selection", - Keyword::Support => "support", - Keyword::Deck => "deck", - Keyword::Edit => "edit", - } - } -} - -impl fmt::Display for Keyword { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.name()) - } -} - -impl fmt::Display for AlgoKeyword { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.name()) - } -} - -impl fmt::Display for ListKeyword { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.name()) - } -} - -impl FromStr for PubkeySourceKeyword { - type Err = Error; - - fn from_str(serialized: &str) -> Result { - for keyword in Self::iter() { - if serialized == keyword.name() { - return Ok(keyword); - } - } - - Err(Error::Generic( - "Could not convert string to Keyword enum".to_owned(), - )) - } -} - -impl FromStr for ListKeyword { - type Err = Error; - - fn from_str(serialized: &str) -> Result { - for keyword in Self::iter() { - if serialized == keyword.name() { - return Ok(keyword); - } - } - - Err(Error::Generic( - "Could not convert string to Keyword enum".to_owned(), - )) - } -} - -impl fmt::Display for PubkeySourceKeyword { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.name()) - } -} - -impl FromStr for AlgoKeyword { - type Err = Error; - - fn from_str(serialized: &str) -> Result { - for keyword in Self::iter() { - if serialized == keyword.name() { - return Ok(keyword); - } - } - - Err(Error::Generic( - "Could not convert string to Keyword enum".to_owned(), - )) - } -} - -impl FromStr for Keyword { - type Err = Error; - - fn from_str(serialized: &str) -> Result { - for keyword in Self::iter() { - if serialized == keyword.name() { - return Ok(keyword); - } - } - - Err(Error::Generic( - "Could not convert string to Keyword enum".to_owned(), - )) - } -} - enum CleanIntermediaryRoute { ToTimeline(TimelineKind), ToRoute(Route), } impl CleanIntermediaryRoute { - fn intermediary_route(self, ndb: &Ndb, user: Option<&[u8; 32]>) -> Option { + fn into_intermediary_route( + self, + ndb: &Ndb, + user: Option<&[u8; 32]>, + ) -> Option { match self { CleanIntermediaryRoute::ToTimeline(timeline_kind) => Some(IntermediaryRoute::Timeline( timeline_kind.into_timeline(ndb, user)?, @@ -554,411 +343,35 @@ impl CleanIntermediaryRoute { } } -// TODO: The public-accessible version will be a subset of this -fn serialize_route(route: &Route, columns: &Columns) -> Option { - let mut selections: Vec = Vec::new(); - match route { - Route::Timeline(timeline_route) => match timeline_route { - TimelineRoute::Timeline(timeline_id) => { - if let Some(timeline) = columns.find_timeline(*timeline_id) { - match &timeline.kind { - TimelineKind::List(list_kind) => match list_kind { - ListKind::Contact(pubkey_source) => { - selections.push(Selection::List(ListKeyword::Contact)); - selections.extend(generate_pubkey_selections(pubkey_source)); - } - }, - TimelineKind::Algo(AlgoTimeline::LastPerPubkey(list_kind)) => { - match list_kind { - ListKind::Contact(pk_src) => { - selections.push(Selection::Algo(AlgoKeyword::LastPerPubkey)); - selections.push(Selection::List(ListKeyword::Contact)); - selections.extend(generate_pubkey_selections(pk_src)); - } - } - } - TimelineKind::Notifications(pubkey_source) => { - selections.push(Selection::Keyword(Keyword::Notifs)); - selections.extend(generate_pubkey_selections(pubkey_source)); - } - TimelineKind::Profile(pubkey_source) => { - selections.push(Selection::Keyword(Keyword::Profile)); - selections.extend(generate_pubkey_selections(pubkey_source)); - } - TimelineKind::Universe => { - selections.push(Selection::Keyword(Keyword::Universe)) - } - TimelineKind::Thread(root_id) => { - selections.push(Selection::Keyword(Keyword::Thread)); - selections.push(Selection::Payload(hex::encode(root_id.bytes()))); - } - TimelineKind::Generic => { - selections.push(Selection::Keyword(Keyword::Generic)) - } - TimelineKind::Hashtag(hashtag) => { - selections.push(Selection::Keyword(Keyword::Hashtag)); - selections.push(Selection::Payload(hashtag.to_string())); - } - } - } - } - TimelineRoute::Thread(note_id) => { - selections.push(Selection::Keyword(Keyword::Thread)); - selections.push(Selection::Payload(note_id.hex())); - } - TimelineRoute::Profile(pubkey) => { - selections.push(Selection::Keyword(Keyword::Profile)); - selections.push(Selection::PubkeySource(PubkeySourceKeyword::Explicit)); - selections.push(Selection::Payload(pubkey.hex())); - } - TimelineRoute::Reply(note_id) => { - selections.push(Selection::Keyword(Keyword::Reply)); - selections.push(Selection::Payload(note_id.hex())); - } - TimelineRoute::Quote(note_id) => { - selections.push(Selection::Keyword(Keyword::Quote)); - selections.push(Selection::Payload(note_id.hex())); - } - }, - Route::Accounts(accounts_route) => { - selections.push(Selection::Keyword(Keyword::Account)); - match accounts_route { - AccountsRoute::Accounts => selections.push(Selection::Keyword(Keyword::Show)), - AccountsRoute::AddAccount => selections.push(Selection::Keyword(Keyword::New)), - } - } - Route::Relays => selections.push(Selection::Keyword(Keyword::Relay)), - Route::ComposeNote => selections.push(Selection::Keyword(Keyword::Compose)), - Route::AddColumn(add_column_route) => { - selections.push(Selection::Keyword(Keyword::Column)); - match add_column_route { - AddColumnRoute::Base => (), - AddColumnRoute::Algo(algo_route) => match algo_route { - AddAlgoRoute::Base => { - selections.push(Selection::Keyword(Keyword::AlgoSelection)) - } - - AddAlgoRoute::LastPerPubkey => { - selections.push(Selection::Keyword(Keyword::AlgoSelection)); - selections.push(Selection::Algo(AlgoKeyword::LastPerPubkey)); - } - }, - AddColumnRoute::UndecidedNotification => { - selections.push(Selection::Keyword(Keyword::NotificationSelection)) - } - AddColumnRoute::ExternalNotification => { - selections.push(Selection::Keyword(Keyword::ExternalNotifSelection)) - } - AddColumnRoute::Hashtag => { - selections.push(Selection::Keyword(Keyword::HashtagSelection)) - } - AddColumnRoute::UndecidedIndividual => { - selections.push(Selection::Keyword(Keyword::IndividualSelection)) - } - AddColumnRoute::ExternalIndividual => { - selections.push(Selection::Keyword(Keyword::ExternalIndividualSelection)) - } - } - } - Route::Support => selections.push(Selection::Keyword(Keyword::Support)), - Route::NewDeck => { - selections.push(Selection::Keyword(Keyword::Deck)); - selections.push(Selection::Keyword(Keyword::New)); - } - Route::EditDeck(index) => { - selections.push(Selection::Keyword(Keyword::Deck)); - selections.push(Selection::Keyword(Keyword::Edit)); - selections.push(Selection::Payload(index.to_string())); - } - Route::EditProfile(pubkey) => { - selections.push(Selection::Keyword(Keyword::Profile)); - selections.push(Selection::Keyword(Keyword::Edit)); - selections.push(Selection::Payload(pubkey.hex())); - } - } - - if selections.is_empty() { - None - } else { - Some( - selections - .iter() - .map(|k| k.to_string()) - .collect::>() - .join(":"), - ) - } -} - -fn generate_pubkey_selections(source: &PubkeySource) -> Vec { - let mut selections = Vec::new(); - match source { - PubkeySource::Explicit(pubkey) => { - selections.push(Selection::PubkeySource(PubkeySourceKeyword::Explicit)); - selections.push(Selection::Payload(pubkey.hex())); - } - PubkeySource::DeckAuthor => { - selections.push(Selection::PubkeySource(PubkeySourceKeyword::DeckAuthor)); - } - } - selections -} - -/// Parses a selection -fn parse_selection(token: &str) -> Selection { - AlgoKeyword::from_str(token) - .map(Selection::Algo) - .or_else(|_| ListKeyword::from_str(token).map(Selection::List)) - .or_else(|_| PubkeySourceKeyword::from_str(token).map(Selection::PubkeySource)) - .or_else(|_| Keyword::from_str(token).map(Selection::Keyword)) - .unwrap_or_else(|_| Selection::Payload(token.to_owned())) -} - -impl Selection { - fn from_serialized(buffer: &str) -> Vec { - let mut selections = Vec::new(); - let seperator = ":"; - let sep_len = seperator.len(); - let mut pos = 0; - - while let Some(offset) = buffer[pos..].find(seperator) { - selections.push(parse_selection(&buffer[pos..pos + offset])); - pos = pos + offset + sep_len; - } - - selections.push(parse_selection(&buffer[pos..])); - - selections - } -} - -/// Parse an explicit:abdef... or deck_author from a Selection token stream. -/// -/// Also handle the case where there is nothing. We assume this means deck_author. -fn parse_pubkey_src_selection(tokens: &[Selection]) -> Option { - match tokens.first() { - // we handle bare payloads and assume they are explicit pubkey sources - Some(Selection::Payload(hex)) => { - let pk = Pubkey::from_hex(hex.as_str()).ok()?; - Some(PubkeySource::Explicit(pk)) - } - - Some(Selection::PubkeySource(PubkeySourceKeyword::Explicit)) => { - if let Selection::Payload(hex) = tokens.get(1)? { - let pk = Pubkey::from_hex(hex.as_str()).ok()?; - Some(PubkeySource::Explicit(pk)) - } else { - None - } - } - - None | Some(Selection::PubkeySource(PubkeySourceKeyword::DeckAuthor)) => { - Some(PubkeySource::DeckAuthor) - } - - Some(Selection::Keyword(_kw)) => None, - Some(Selection::Algo(_kw)) => None, - Some(Selection::List(_kw)) => None, - } -} - -/// Parse ListKinds from Selections -fn parse_list_kind_selections(tokens: &[Selection]) -> Option { - // only list selections are valid in this position - let list_kw = if let Selection::List(list_kw) = tokens.first()? { - list_kw - } else { - return None; - }; - - let pubkey_src = parse_pubkey_src_selection(&tokens[1..])?; - - Some(match list_kw { - ListKeyword::Contact => ListKind::contact_list(pubkey_src), - }) -} - -fn selections_to_route(selections: &[Selection]) -> Option { - match selections.first()? { - Selection::Keyword(Keyword::AlgoSelection) => { - let r = match selections.get(1) { - None => AddColumnRoute::Algo(AddAlgoRoute::Base), - Some(Selection::Algo(algo_kw)) => match algo_kw { - AlgoKeyword::LastPerPubkey => AddColumnRoute::Algo(AddAlgoRoute::LastPerPubkey), - }, - // other keywords are invalid here - Some(_) => { - return None; - } - }; - - Some(CleanIntermediaryRoute::ToRoute(Route::AddColumn(r))) - } - - // Algorithm timelines - Selection::Algo(algo_kw) => { - let timeline_kind = match algo_kw { - AlgoKeyword::LastPerPubkey => { - let list_kind = parse_list_kind_selections(&selections[1..])?; - TimelineKind::last_per_pubkey(list_kind) - } - }; - - Some(CleanIntermediaryRoute::ToTimeline(timeline_kind)) - } - - // We never have PubkeySource keywords at the top level - Selection::PubkeySource(_pk_src) => None, - - Selection::List(ListKeyword::Contact) => { - // only pubkey/src is allowed in this position - let pubkey_src = parse_pubkey_src_selection(&selections[1..])?; - Some(CleanIntermediaryRoute::ToTimeline( - TimelineKind::contact_list(pubkey_src), - )) - } - - Selection::Keyword(Keyword::Notifs) => { - let pubkey_src = parse_pubkey_src_selection(&selections[1..])?; - Some(CleanIntermediaryRoute::ToTimeline( - TimelineKind::notifications(pubkey_src), - )) - } - - Selection::Keyword(Keyword::Profile) => { - // we only expect PubkeySource in this position - let pubkey_src = parse_pubkey_src_selection(&selections[1..])?; - Some(CleanIntermediaryRoute::ToTimeline(TimelineKind::profile( - pubkey_src, - ))) - } - - Selection::Keyword(Keyword::Universe) => { - Some(CleanIntermediaryRoute::ToTimeline(TimelineKind::Universe)) - } - - Selection::Keyword(Keyword::Hashtag) => { - if let Selection::Payload(hashtag) = selections.get(1)? { - Some(CleanIntermediaryRoute::ToTimeline(TimelineKind::Hashtag( - hashtag.to_string(), - ))) - } else { - None - } - } - - Selection::Keyword(Keyword::Generic) => { - Some(CleanIntermediaryRoute::ToTimeline(TimelineKind::Generic)) - } - - Selection::Keyword(Keyword::Thread) => { - if let Selection::Payload(hex) = selections.get(1)? { - Some(CleanIntermediaryRoute::ToRoute(Route::thread( - NoteId::from_hex(hex.as_str()).ok()?, - ))) - } else { - None - } - } - - Selection::Keyword(Keyword::Reply) => { - if let Selection::Payload(hex) = selections.get(1)? { - Some(CleanIntermediaryRoute::ToRoute(Route::reply( - NoteId::from_hex(hex.as_str()).ok()?, - ))) - } else { - None - } - } - Selection::Keyword(Keyword::Quote) => { - if let Selection::Payload(hex) = selections.get(1)? { - Some(CleanIntermediaryRoute::ToRoute(Route::quote( - NoteId::from_hex(hex.as_str()).ok()?, - ))) - } else { - None - } - } - Selection::Keyword(Keyword::Account) => match selections.get(1)? { - Selection::Keyword(Keyword::Show) => Some(CleanIntermediaryRoute::ToRoute( - Route::Accounts(AccountsRoute::Accounts), - )), - Selection::Keyword(Keyword::New) => Some(CleanIntermediaryRoute::ToRoute( - Route::Accounts(AccountsRoute::AddAccount), - )), - _ => None, - }, - Selection::Keyword(Keyword::Relay) => Some(CleanIntermediaryRoute::ToRoute(Route::Relays)), - Selection::Keyword(Keyword::Compose) => { - Some(CleanIntermediaryRoute::ToRoute(Route::ComposeNote)) - } - Selection::Keyword(Keyword::Column) => match selections.get(1)? { - Selection::Keyword(Keyword::NotificationSelection) => { - Some(CleanIntermediaryRoute::ToRoute(Route::AddColumn( - AddColumnRoute::UndecidedNotification, - ))) - } - Selection::Keyword(Keyword::ExternalNotifSelection) => { - Some(CleanIntermediaryRoute::ToRoute(Route::AddColumn( - AddColumnRoute::ExternalNotification, - ))) - } - Selection::Keyword(Keyword::HashtagSelection) => Some(CleanIntermediaryRoute::ToRoute( - Route::AddColumn(AddColumnRoute::Hashtag), - )), - Selection::Keyword(Keyword::IndividualSelection) => { - Some(CleanIntermediaryRoute::ToRoute(Route::AddColumn( - AddColumnRoute::UndecidedIndividual, - ))) - } - Selection::Keyword(Keyword::ExternalIndividualSelection) => { - Some(CleanIntermediaryRoute::ToRoute(Route::AddColumn( - AddColumnRoute::ExternalIndividual, - ))) - } - _ => None, - }, - Selection::Keyword(Keyword::Support) => { - Some(CleanIntermediaryRoute::ToRoute(Route::Support)) - } - Selection::Keyword(Keyword::Deck) => match selections.get(1)? { - Selection::Keyword(Keyword::New) => { - Some(CleanIntermediaryRoute::ToRoute(Route::NewDeck)) - } - Selection::Keyword(Keyword::Edit) => { - if let Selection::Payload(index_str) = selections.get(2)? { - let parsed_index = index_str.parse::().ok()?; - Some(CleanIntermediaryRoute::ToRoute(Route::EditDeck( - parsed_index, - ))) - } else { - None - } - } - _ => None, - }, - Selection::Payload(_) - | Selection::Keyword(Keyword::New) - | Selection::Keyword(Keyword::Show) - | Selection::Keyword(Keyword::NotificationSelection) - | Selection::Keyword(Keyword::ExternalNotifSelection) - | Selection::Keyword(Keyword::HashtagSelection) - | Selection::Keyword(Keyword::IndividualSelection) - | Selection::Keyword(Keyword::ExternalIndividualSelection) - | Selection::Keyword(Keyword::Edit) => None, - } -} - -impl fmt::Display for Selection { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +impl TokenSerializable for CleanIntermediaryRoute { + fn serialize_tokens(&self, writer: &mut TokenWriter) { match self { - Selection::Keyword(keyword) => write!(f, "{}", keyword), - Selection::Payload(payload) => write!(f, "{}", payload), - Selection::Algo(algo_kw) => write!(f, "{}", algo_kw), - Selection::List(list_kw) => write!(f, "{}", list_kw), - Selection::PubkeySource(pk_src_kw) => write!(f, "{}", pk_src_kw), + CleanIntermediaryRoute::ToTimeline(tlk) => { + tlk.serialize_tokens(writer); + } + CleanIntermediaryRoute::ToRoute(route) => { + route.serialize_tokens(writer); + } } } + + fn parse_from_tokens<'a>(parser: &mut TokenParser<'a>) -> Result> { + TokenParser::alt( + parser, + &[ + |p| { + Ok(CleanIntermediaryRoute::ToTimeline( + TimelineKind::parse_from_tokens(p)?, + )) + }, + |p| { + Ok(CleanIntermediaryRoute::ToRoute(Route::parse_from_tokens( + p, + )?)) + }, + ], + ) + } } #[cfg(test)]