From aa3c4e239f4932de1d89a03bd6c9103957199dbf Mon Sep 17 00:00:00 2001 From: iximeow Date: Sat, 21 Oct 2017 14:01:56 -0700 Subject: begin conversion to typed TwitterID --- Cargo.toml | 4 +- src/commands/fav.rs | 29 +++++++-- src/commands/look_up.rs | 1 + src/commands/thread.rs | 24 +++++--- src/commands/twete.rs | 16 ++--- src/commands/view.rs | 6 +- src/display/mod.rs | 16 ++--- src/tw/mod.rs | 159 ++++++++++++++++++++++++++++++++++++++++++++---- 8 files changed, 212 insertions(+), 43 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 78a38c5..ecc2ffe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,8 +12,8 @@ a nice twitter [[bin]] name = "twidder" # path = "main.rs" -test = false -bench = false +# test = false +# bench = false [dependencies] "termios" = "0.2.2" diff --git a/src/commands/fav.rs b/src/commands/fav.rs index 3e2b00d..7b9cce6 100644 --- a/src/commands/fav.rs +++ b/src/commands/fav.rs @@ -1,6 +1,8 @@ use tw; use ::Queryer; +use tw::TweetId; + use commands::Command; use std::str::FromStr; @@ -16,9 +18,17 @@ pub static UNFAV: Command = Command { fn unfav(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { // TODO handle this unwrap - let inner_twid = u64::from_str(&line).unwrap(); - let twete = tweeter.tweet_by_innerid(inner_twid).unwrap(); - queryer.do_api_post(&format!("{}?id={}", UNFAV_TWEET_URL, twete.id)); +// let inner_twid = u64::from_str(&line).unwrap(); + let maybe_id = TweetId::parse(line.to_owned()); + match maybe_id { + Some(twid) => { + let twete = tweeter.retrieve_tweet(&twid).unwrap(); + queryer.do_api_post(&format!("{}?id={}", UNFAV_TWEET_URL, twete.id)); + } + None => { + println!("Invalid id: {}", line); + } + } } pub static FAV: Command = Command { @@ -29,7 +39,14 @@ pub static FAV: Command = Command { fn fav(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { // TODO handle this unwrap - let inner_twid = u64::from_str(&line).unwrap(); - let twete = tweeter.tweet_by_innerid(inner_twid).unwrap(); - queryer.do_api_post(&format!("{}?id={}", FAV_TWEET_URL, twete.id)); + let maybe_id = TweetId::parse(line.to_owned()); + match maybe_id { + Some(twid) => { + let twete = tweeter.retrieve_tweet(&twid).unwrap(); + queryer.do_api_post(&format!("{}?id={}", FAV_TWEET_URL, twete.id)); + } + None => { + println!("Invalid id: {}", line); + } + } } diff --git a/src/commands/look_up.rs b/src/commands/look_up.rs index d04f984..386fade 100644 --- a/src/commands/look_up.rs +++ b/src/commands/look_up.rs @@ -23,6 +23,7 @@ pub static LOOK_UP_TWEET: Command = Command { exec: look_up_tweet }; +// TODO: make this parse a proper tweet id fn look_up_tweet(line: String, tweeter: &mut tw::TwitterCache, mut queryer: &mut Queryer) { if let Some(tweet) = tweeter.fetch_tweet(&line, &mut queryer) { println!("{:?}", tweet); diff --git a/src/commands/thread.rs b/src/commands/thread.rs index 92566fc..57b410c 100644 --- a/src/commands/thread.rs +++ b/src/commands/thread.rs @@ -2,6 +2,8 @@ use tw; use ::Queryer; use ::display; +use tw::TweetId; + use commands::Command; use std::str::FromStr; @@ -30,10 +32,15 @@ fn remember(line: String, tweeter: &mut tw::TwitterCache, _queryer: &mut Queryer let name = name_bare.trim(); let id_str = text.trim(); if name.len() > 0 { - if let Some(inner_twid) = u64::from_str(&id_str).ok() { - if tweeter.tweet_by_innerid(inner_twid).is_some() { - tweeter.set_thread(name.to_string(), inner_twid); - println!("Ok! Recorded {} as thread {}", inner_twid, name); + let maybe_id = TweetId::parse(line.to_owned()); + match maybe_id { + Some(twid) => { + let twete = tweeter.retrieve_tweet(&twid).unwrap().clone(); + tweeter.set_thread(name.to_string(), twete.internal_id); + println!("Ok! Recorded {:?} as thread {}", twid, name); + } + None => { + println!("Invalid id: {}", line); } } } @@ -51,9 +58,12 @@ fn ls_threads(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Querye for k in tweeter.threads() { println!("Thread: {}", k); let latest_inner_id = tweeter.latest_in_thread(k.to_owned()).unwrap(); - let twete = tweeter.tweet_by_innerid(*latest_inner_id).unwrap(); + if let Some(twete) = tweeter.retrieve_tweet(&TweetId::Bare(*latest_inner_id)) { // gross.. - display::render_twete(&twete.id, tweeter); - println!(""); + display::render_twete(&twete.id, tweeter); + println!(""); + } else { + println!("ERROR no tweet for remembered thread."); + } } } diff --git a/src/commands/twete.rs b/src/commands/twete.rs index f1f19cf..a66d5eb 100644 --- a/src/commands/twete.rs +++ b/src/commands/twete.rs @@ -1,6 +1,8 @@ use tw; use ::Queryer; +use tw::TweetId; + use commands::Command; use std::str::FromStr; @@ -17,7 +19,7 @@ pub static DEL: Command = Command { fn del(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { let inner_twid = u64::from_str(&line).unwrap(); - let twete = tweeter.tweet_by_innerid(inner_twid).unwrap(); + let twete = tweeter.retrieve_tweet(&TweetId::Bare(inner_twid)).unwrap(); queryer.do_api_post(&format!("{}/{}.json", DEL_TWEET_URL, twete.id)); } @@ -54,7 +56,7 @@ fn thread(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { let id_str = text.trim(); if reply.len() > 0 { if let Some(inner_twid) = u64::from_str(&id_str).ok() { - if let Some(twete) = tweeter.tweet_by_innerid(inner_twid) { + if let Some(twete) = tweeter.retrieve_tweet(&TweetId::Bare(inner_twid)) { let handle = &tweeter.retrieve_user(&twete.author_id).unwrap().handle; // TODO: definitely breaks if you change your handle right now if handle == &tweeter.current_user.handle { @@ -90,7 +92,7 @@ fn rep(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { let id_str = text.trim(); if reply.len() > 0 { if let Some(inner_twid) = u64::from_str(&id_str).ok() { - if let Some(twete) = tweeter.tweet_by_innerid(inner_twid) { + if let Some(twete) = tweeter.retrieve_tweet(&TweetId::Bare(inner_twid)) { // get handles to reply to... let author_handle = tweeter.retrieve_user(&twete.author_id).unwrap().handle.to_owned(); let mut ats: Vec = twete.get_mentions().into_iter().map(|x| x.to_owned()).collect(); //std::collections::HashSet::new(); @@ -102,12 +104,12 @@ fn rep(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { ats.remove_item(&author_handle); ats.insert(0, author_handle); // no idea why i have to .to_owned() here --v-- what about twete.rt_tweet is a move? - if let Some(rt_tweet) = twete.rt_tweet.to_owned().and_then(|id| tweeter.retrieve_tweet(&id)) { + if let Some(rt_tweet) = twete.rt_tweet.to_owned().and_then(|id| tweeter.retrieve_tweet(&TweetId::Twitter(id))) { let rt_author_handle = tweeter.retrieve_user(&rt_tweet.author_id).unwrap().handle.to_owned(); ats.remove_item(&rt_author_handle); ats.insert(1, rt_author_handle); } - if let Some(qt_tweet) = twete.quoted_tweet_id.to_owned().and_then(|id| tweeter.retrieve_tweet(&id)) { + if let Some(qt_tweet) = twete.quoted_tweet_id.to_owned().and_then(|id| tweeter.retrieve_tweet(&TweetId::Twitter(id))) { // let qt_author_handle = tweeter.retrieve_user(&qt_tweet.author_id).unwrap().handle.to_owned(); // ats.remove_item(&qt_author_handle); // ats.insert(1, qt_author_handle); @@ -143,7 +145,7 @@ fn quote(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { let id_str = text.trim(); if reply.len() > 0 { if let Some(inner_twid) = u64::from_str(&id_str).ok() { - if let Some(twete) = tweeter.tweet_by_innerid(inner_twid) { + if let Some(twete) = tweeter.retrieve_tweet(&TweetId::Bare(inner_twid)) { let substituted = ::url_encode(reply); let attachment_url = ::url_encode( &format!( @@ -178,7 +180,7 @@ pub static RETWETE: Command = Command { fn retwete(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { let inner_twid = u64::from_str(&line).unwrap(); - let twete = tweeter.tweet_by_innerid(inner_twid).unwrap(); + let twete = tweeter.retrieve_tweet(&TweetId::Bare(inner_twid)).unwrap(); queryer.do_api_post(&format!("{}/{}.json", RT_TWEET_URL, twete.id)); } diff --git a/src/commands/view.rs b/src/commands/view.rs index b9cbf96..c14446a 100644 --- a/src/commands/view.rs +++ b/src/commands/view.rs @@ -1,6 +1,8 @@ use tw; use ::Queryer; +use tw::TweetId; + use commands::Command; use std::str::FromStr; @@ -16,7 +18,7 @@ pub static VIEW: Command = Command { fn view(line: String, tweeter: &mut tw::TwitterCache, _queryer: &mut Queryer) { // TODO handle this unwrap let inner_twid = u64::from_str(&line).unwrap(); - let twete = tweeter.tweet_by_innerid(inner_twid).unwrap(); + let twete = tweeter.retrieve_tweet(&TweetId::Bare(inner_twid)).unwrap(); display::render_twete(&twete.id, tweeter); println!(" link: https://twitter.com/i/web/status/{}", twete.id); } @@ -34,7 +36,7 @@ fn view_tr(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) } fn view_tr_inner(id: u64, mut tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { - let twete: tw::tweet::Tweet = tweeter.tweet_by_innerid(id).unwrap().to_owned(); + let twete: tw::tweet::Tweet = tweeter.retrieve_tweet(&TweetId::Bare(id)).unwrap().to_owned(); if let Some(reply_id) = twete.reply_to_tweet.clone() { if let Some(reply_internal_id) = tweeter.fetch_tweet(&reply_id, queryer).map(|x| x.internal_id).map(|x| x.to_owned()) { view_tr_inner(reply_internal_id, tweeter, queryer); diff --git a/src/display/mod.rs b/src/display/mod.rs index 15e23d7..e7f2454 100644 --- a/src/display/mod.rs +++ b/src/display/mod.rs @@ -4,6 +4,8 @@ use self::termion::color; use ::tw; +use ::tw::TweetId; + use std; fn color_for(handle: &String) -> termion::color::Fg<&color::Color> { @@ -42,7 +44,7 @@ impl Render for tw::events::Event { } tw::events::Event::Deleted { user_id, twete_id } => { if let Some(handle) = tweeter.retrieve_user(&user_id).map(|x| &x.handle) { - if let Some(_tweet) = tweeter.retrieve_tweet(&twete_id) { + if let Some(_tweet) = tweeter.retrieve_tweet(&TweetId::Twitter(twete_id.to_owned())) { println!("-------------DELETED------------------"); render_twete(&twete_id, tweeter); println!("-------------DELETED------------------"); @@ -102,7 +104,7 @@ impl Render for tw::events::Event { pub fn render_twete(twete_id: &String, tweeter: &tw::TwitterCache) { let id_color = color::Fg(color::Rgb(180, 80, 40)); - let maybe_twete = tweeter.retrieve_tweet(twete_id); + let maybe_twete = tweeter.retrieve_tweet(&TweetId::Twitter(twete_id.to_owned())); if maybe_twete.is_none() { println!("No such tweet: {}", twete_id); return; @@ -113,13 +115,13 @@ pub fn render_twete(twete_id: &String, tweeter: &tw::TwitterCache) { match twete.rt_tweet { Some(ref rt_id) => { // same for a retweet - let rt = tweeter.retrieve_tweet(rt_id).unwrap(); + let rt = tweeter.retrieve_tweet(&TweetId::Twitter(rt_id.to_owned())).unwrap(); // and its author let rt_author = tweeter.retrieve_user(&rt.author_id).unwrap(); println!("{} id:{} (rt_id:{}){}{}", id_color, rt.internal_id, twete.internal_id, rt.reply_to_tweet.clone() - .map(|id| tweeter.retrieve_tweet(&id) + .map(|id| tweeter.retrieve_tweet(&TweetId::Twitter(id.to_owned())) .and_then(|tw| Some(format!(" reply_to:{}", tw.internal_id))) .unwrap_or(format!(" reply_to:twitter::{}", id)) ) @@ -137,7 +139,7 @@ pub fn render_twete(twete_id: &String, tweeter: &tw::TwitterCache) { println!("{} id:{}{}{}", id_color, twete.internal_id, twete.reply_to_tweet.clone() - .map(|id| tweeter.retrieve_tweet(&id) + .map(|id| tweeter.retrieve_tweet(&TweetId::Twitter(id.to_owned())) .and_then(|tw| Some(format!(" reply_to:{}", tw.internal_id))) .unwrap_or(format!(" reply_to:twitter::{}", id)) ) @@ -154,12 +156,12 @@ pub fn render_twete(twete_id: &String, tweeter: &tw::TwitterCache) { println!(" {}", twete.text.replace("\r", "\\r").split("\n").collect::>().join("\n ")); if let Some(ref qt_id) = twete.quoted_tweet_id { - if let Some(ref qt) = tweeter.retrieve_tweet(qt_id) { + if let Some(ref qt) = tweeter.retrieve_tweet(&TweetId::Twitter(qt_id.to_owned())) { let qt_author = tweeter.retrieve_user(&qt.author_id).unwrap(); println!("{} id:{}{}{}", id_color, qt.internal_id, qt.reply_to_tweet.clone() - .map(|id| tweeter.retrieve_tweet(&id) + .map(|id| tweeter.retrieve_tweet(&TweetId::Twitter(id.to_owned())) .and_then(|tw| Some(format!(" reply_to:{}", tw.internal_id))) .unwrap_or(format!(" reply_to:twitter::{}", id)) ) diff --git a/src/tw/mod.rs b/src/tw/mod.rs index 90610f1..daf1088 100644 --- a/src/tw/mod.rs +++ b/src/tw/mod.rs @@ -1,4 +1,5 @@ use std::path::Path; +use std::str::FromStr; use std::fs::File; use std::io::{BufRead, BufReader, Read}; extern crate chrono; @@ -95,13 +96,136 @@ pub struct TwitterCache { follower_history: HashMap, // userid:date?? threads: HashMap, // thread : latest_tweet_in_thread #[serde(skip)] - id_to_tweet_id: HashMap, - #[serde(skip)] pub needs_save: bool, #[serde(skip)] pub caching_permitted: bool, #[serde(skip)] - pub current_user: User + pub current_user: User, + #[serde(skip)] + id_conversions: IdConversions +} + +// Internally, a monotonically increasin i64 is always the id used. +// This is the client's id, not the twitter id for a tweet. +// +// Id forms: +// num // implicitly today:num +// today:num // last_tweet_of_yesterday_id + num +// 20171009:num // last_tweet_of_previous_day_id + num +// open question: what about 20171009:123451234 .. does this reference a future tweet? should +// probably cause an issue if this would overflow into the next day. +// ::num // tweet id num +// twitter::num // twiter tweet id num +struct IdConversions { + // maps a day to the base id for tweets off that day. + id_per_date: HashMap, + id_to_tweet_id: HashMap + // twitter id to id is satisfied by looking up the twitter id in tweeter.tweets and getting + // .inner_id +} + +impl Default for IdConversions { + fn default() -> Self { + IdConversions { + id_per_date: HashMap::new(), + id_to_tweet_id: HashMap::new() + } + } +} + +#[derive(Debug, PartialEq)] +pub enum TweetId { + Today(u64), // just a number + Dated(String, u64), // 20171002:number + Bare(u64), // ::number + Twitter(String) // twitter::number +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn tweet_id_parse_test() { + assert_eq!(TweetId::parse("12345".to_string()), Some(TweetId::Today(12345))); + assert_eq!(TweetId::parse("20170403:12345".to_string()), Some(TweetId::Dated("20170403".to_string(), 12345))); + assert_eq!(TweetId::parse(":12345".to_string()), Some(TweetId::Bare(12345))); + assert_eq!(TweetId::parse("twitter:12345".to_string()), Some(TweetId::Twitter("12345".to_string()))); + assert_eq!(TweetId::parse("twitter:asdf".to_string()), Some(TweetId::Twitter("asdf".to_string()))); + assert_eq!(TweetId::parse("a2345".to_string()), None); + assert_eq!(TweetId::parse(":".to_string()), None); + assert_eq!(TweetId::parse("::".to_string()), None); + assert_eq!(TweetId::parse("a:13234".to_string()), None); + assert_eq!(TweetId::parse(":a34".to_string()), None); + assert_eq!(TweetId::parse("asdf:34".to_string()), None); + } +} + +impl TweetId { + pub fn parse(id_str: String) -> Option { + // ... + if id_str.starts_with("twitter:") { + Some(TweetId::Twitter(id_str.chars().skip("twitter:".len()).collect())) + } else if id_str.starts_with(":") { + let rest = id_str.chars().skip(1); + if rest.clone().all(|x| x.is_digit(10)) { + match u64::from_str(&rest.collect::()) { + Ok(num) => Some(TweetId::Bare(num)), + Err(e) => { + println!("Invalid id: {} - {}", id_str, e); + None + } + } + } else { + None + } + } else if id_str.find(":") == Some(8) { + let strings: Vec<&str> = id_str.split(":").collect(); + if strings.len() == 2 { + match (strings[0].chars().all(|x| x.is_digit(10)), u64::from_str(strings[1])) { + (true, Ok(num)) => Some(TweetId::Dated(strings[0].to_owned(), num)), + _ => { + println!("Invalid format."); + None + } + } + } else { + None + } + } else if id_str.chars().all(|x| x.is_digit(10)) { + // today + match u64::from_str(&id_str) { + Ok(num) => Some(TweetId::Today(num)), + Err(e) => { + println!("Invalid id: {} - {}", id_str, e); + None + } + } + } else { + None + } + } +} + +impl IdConversions { + // So this is twid -> Option + // elsewhere we u64 -> Option + // + // except in the TweetId::Twitter case we TweetId -> Option -> Option ... to -> + // Option in the future? + fn to_inner_id(&self, tweeter: &TwitterCache, twid: TweetId) -> Option { + match twid { + TweetId::Today(num) => { + let first_for_today: u64 = 0; + Some(first_for_today + num) + }, + TweetId::Dated(date, num) => { + let first_for_date: u64 = 0; + Some(first_for_date + num) + }, + TweetId::Bare(num) => Some(num), + twid @ TweetId::Twitter(_) => tweeter.retrieve_tweet(&twid).map(|x| x.internal_id) + } + } } impl TwitterCache { @@ -119,11 +243,11 @@ impl TwitterCache { followers: HashSet::new(), lost_followers: HashSet::new(), follower_history: HashMap::new(), - id_to_tweet_id: HashMap::new(), needs_save: false, caching_permitted: true, current_user: User::default(), - threads: HashMap::new() + threads: HashMap::new(), + id_conversions: IdConversions::default() } } fn new_without_caching() -> TwitterCache { @@ -175,7 +299,7 @@ impl TwitterCache { if !self.tweets.contains_key(&tw.id.to_owned()) { if tw.internal_id == 0 { tw.internal_id = (self.tweets.len() as u64) + 1; - self.id_to_tweet_id.insert(tw.internal_id, tw.id.to_owned()); + self.id_conversions.id_to_tweet_id.insert(tw.internal_id, tw.id.to_owned()); self.tweets.insert(tw.id.to_owned(), tw); } } @@ -298,12 +422,23 @@ impl TwitterCache { /* nothing else to care about now, i think? */ } } - pub fn tweet_by_innerid(&self, inner_id: u64) -> Option<&Tweet> { - let id = &self.id_to_tweet_id[&inner_id]; - self.retrieve_tweet(id) - } - pub fn retrieve_tweet(&self, tweet_id: &String) -> Option<&Tweet> { - self.tweets.get(tweet_id) + pub fn retrieve_tweet(&self, tweet_id: &TweetId) -> Option<&Tweet> { + match tweet_id { + &TweetId::Bare(ref id) => { + self.id_conversions.id_to_tweet_id.get(id) + .and_then(|x| self.tweets.get(x)) + }, + &TweetId::Today(ref id) => { + let inner_id = self.id_conversions.id_to_tweet_id.get(id); + panic!("Retrieving tweets with dated IDs is not yet supported."); + None + }, + &TweetId::Dated(ref date, ref id) => { + panic!("Retrieving tweets with dated IDs is not yet supported."); + None + }, + &TweetId::Twitter(ref id) => self.tweets.get(id) + } } pub fn retrieve_user(&self, user_id: &String) -> Option<&User> { self.users.get(user_id) -- cgit v1.1