From a3966446bab8bf849457c24a9a6d05216f950e11 Mon Sep 17 00:00:00 2001 From: Andy Wortman Date: Sat, 18 Nov 2017 16:49:23 -0800 Subject: groundwork for multi-account use add connection state tracked per-stream, add explicit TwitterProfile mapped to names that can be used for accounts. default names are the handle of the corresponding twitter account. partition out user Credential to be per TwitterProfile, so fav, reply, etc, all come from the selected account. Multiplex twitter connections and message streams across chan (instead of earlier plan, which was to have one chan per thread) --- src/commands/auth.rs | 7 +- src/commands/fav.rs | 8 +-- src/commands/follow.rs | 12 ++-- src/commands/help.rs | 2 - src/commands/quit.rs | 2 - src/commands/show_cache.rs | 2 + src/commands/twete.rs | 50 +++++++++----- src/display/mod.rs | 9 +-- src/main.rs | 110 ++++++++++++++++++----------- src/tw/mod.rs | 168 +++++++++++++++++++++++++++------------------ 10 files changed, 223 insertions(+), 147 deletions(-) diff --git a/src/commands/auth.rs b/src/commands/auth.rs index 7d01451..65dcaf5 100644 --- a/src/commands/auth.rs +++ b/src/commands/auth.rs @@ -4,8 +4,6 @@ use std::collections::HashMap; use hyper; use ::Queryer; -use tw::TweetId; - use commands::Command; static FAV_TWEET_URL: &str = "https://api.twitter.com/1.1/favorites/create.json"; @@ -79,10 +77,11 @@ fn pin(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { as_map.insert(part[0], part[1]); } // turns out the "actual" oauth creds are different - tweeter.add_profile(tw::Credential { + // TODO: profile names? + tweeter.add_profile(tw::TwitterProfile::new(tw::Credential { key: as_map["oauth_token"].to_owned(), secret: as_map["oauth_token_secret"].to_owned() - }); + }, tw::user::User::default()), Some("iximeow".to_owned())); tweeter.WIP_auth = None; tweeter.state = tw::AppState::Reconnect; tweeter.display_info.status("Looks like you authed! Connecting...".to_owned()); diff --git a/src/commands/fav.rs b/src/commands/fav.rs index 7a4852e..5c12535 100644 --- a/src/commands/fav.rs +++ b/src/commands/fav.rs @@ -21,8 +21,8 @@ fn unfav(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { match maybe_id { Ok(twid) => { if let Some(twete) = tweeter.retrieve_tweet(&twid).map(|x| x.clone()) { // TODO: no clone when this stops taking &mut self - let result = match tweeter.profile.clone() { - Some(user_creds) => queryer.do_api_post(&format!("{}?id={}", UNFAV_TWEET_URL, twete.id), &tweeter.app_key, &user_creds), + let result = match tweeter.current_profile() { + Some(user_profile) => queryer.do_api_post(&format!("{}?id={}", UNFAV_TWEET_URL, twete.id), &tweeter.app_key, &user_profile.creds), None => Err("No logged in user to unfav from".to_owned()) }; match result { @@ -53,8 +53,8 @@ fn fav(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { Ok(twid) => { // tweeter.to_twitter_tweet_id(twid)... if let Some(twete) = tweeter.retrieve_tweet(&twid).map(|x| x.clone()) { // TODO: no clone when this stops taking &mut self - let result = match tweeter.profile.clone() { - Some(user_creds) => queryer.do_api_post(&format!("{}?id={}", FAV_TWEET_URL, twete.id), &tweeter.app_key, &user_creds), + let result = match tweeter.current_profile() { + Some(user_profile) => queryer.do_api_post(&format!("{}?id={}", FAV_TWEET_URL, twete.id), &tweeter.app_key, &user_profile.creds), None => Err("No logged in user to fav from".to_owned()) }; match result { diff --git a/src/commands/follow.rs b/src/commands/follow.rs index 3a29252..6e29788 100644 --- a/src/commands/follow.rs +++ b/src/commands/follow.rs @@ -16,9 +16,9 @@ pub static UNFOLLOW: Command = Command { fn unfl(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { let screen_name = line.trim(); - let result = match tweeter.profile.clone() { - Some(user_creds) => { - queryer.do_api_post(&format!("{}?screen_name={}", FOLLOW_URL, screen_name), &tweeter.app_key, &user_creds) + let result = match tweeter.current_profile() { + Some(user_profile) => { + queryer.do_api_post(&format!("{}?screen_name={}", FOLLOW_URL, screen_name), &tweeter.app_key, &user_profile.creds) }, None => Err("No logged in user to unfollow from".to_owned()) }; @@ -38,15 +38,15 @@ pub static FOLLOW: Command = Command { fn fl(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { let screen_name = line.trim(); - match tweeter.profile.clone() { - Some(user_creds) => { + match tweeter.current_profile().map(|profile| profile.to_owned()) { + Some(user_profile) => { tweeter.display_info.status( format!( "fl resp: {:?}", queryer.do_api_post( &format!("{}?screen_name={}", UNFOLLOW_URL, screen_name), &tweeter.app_key, - &user_creds + &user_profile.creds ) ) ) diff --git a/src/commands/help.rs b/src/commands/help.rs index c99681e..445684b 100644 --- a/src/commands/help.rs +++ b/src/commands/help.rs @@ -1,8 +1,6 @@ use tw; use ::Queryer; -use tw::TweetId; - use commands::Command; pub static HELP: Command = Command { diff --git a/src/commands/quit.rs b/src/commands/quit.rs index 6638299..0f5c582 100644 --- a/src/commands/quit.rs +++ b/src/commands/quit.rs @@ -3,8 +3,6 @@ use ::Queryer; use commands::Command; -use std::process::exit; - pub static QUIT: Command = Command { keyword: "q", params: 0, diff --git a/src/commands/show_cache.rs b/src/commands/show_cache.rs index fffcdb6..6dda8dc 100644 --- a/src/commands/show_cache.rs +++ b/src/commands/show_cache.rs @@ -12,6 +12,7 @@ pub static SHOW_CACHE: Command = Command { }; fn show_cache(_line: String, tweeter: &mut tw::TwitterCache, mut queryer: &mut Queryer) { + /* tweeter.display_info.status("----* USERS *----".to_owned()); for (uid, user) in &tweeter.users { tweeter.display_info.status(format!("User: {} -> {:?}", uid, user)); @@ -30,4 +31,5 @@ fn show_cache(_line: String, tweeter: &mut tw::TwitterCache, mut queryer: &mut Q None => { tweeter.display_info.status(" ...".to_owned()); } } } + */ } diff --git a/src/commands/twete.rs b/src/commands/twete.rs index c5c0c1a..0d82b76 100644 --- a/src/commands/twete.rs +++ b/src/commands/twete.rs @@ -22,8 +22,8 @@ fn del(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { Ok(twid) => { // TODO this really converts twid to a TweetId::Twitter if let Some(twitter_id) = tweeter.retrieve_tweet(&twid).map(|x| x.id.to_owned()) { - let result = match tweeter.profile.clone() { - Some(user_creds) => queryer.do_api_post(&format!("{}/{}.json", DEL_TWEET_URL, twitter_id), &tweeter.app_key, &user_creds), + let result = match tweeter.current_profile() { + Some(user_profile) => queryer.do_api_post(&format!("{}/{}.json", DEL_TWEET_URL, twitter_id), &tweeter.app_key, &user_profile.creds), None => Err("No logged in user to delete as".to_owned()) }; match result { @@ -61,8 +61,8 @@ fn twete(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { pub fn send_twete(text: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { let substituted = ::url_encode(&text); - let result = match tweeter.profile.clone() { - Some(user_creds) => queryer.do_api_post(&format!("{}?status={}", CREATE_TWEET_URL, substituted), &tweeter.app_key, &user_creds), + let result = match tweeter.current_profile() { + Some(user_profile) => queryer.do_api_post(&format!("{}?status={}", CREATE_TWEET_URL, substituted), &tweeter.app_key, &user_profile.creds), None => Err("No logged in user to tweet as".to_owned()) }; match result { @@ -83,6 +83,13 @@ pub static THREAD: Command = Command { // the difference between threading and replying is not including // yourself in th @'s. fn thread(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { + let user_profile = match tweeter.current_profile().map(|profile| profile.to_owned()) { + Some(profile) => profile, + None => { + tweeter.display_info.status("To reply you must be authenticated as a user.".to_owned()); + return; + } + }; let mut text: String = line.trim().to_string(); if let Some(id_end_idx) = text.find(" ") { let reply_bare = text.split_off(id_end_idx + 1); @@ -95,8 +102,8 @@ fn thread(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { if let Some(twete) = tweeter.retrieve_tweet(&twid).map(|x| x.clone()) { // TODO: no clone when this stops taking &mut self let handle = &tweeter.retrieve_user(&twete.author_id).unwrap().handle.to_owned(); // TODO: definitely breaks if you change your handle right now - if handle == &tweeter.current_user.handle { - send_reply(reply.to_owned(), twid, tweeter, queryer); + if handle == &user_profile.user.handle { + send_reply(reply.to_owned(), twid, tweeter, queryer, user_profile.creds); } else { tweeter.display_info.status("you can only thread your own tweets".to_owned()); // ask if it should .@ instead? @@ -125,6 +132,13 @@ pub static REP: Command = Command { }; fn rep(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { + let user_profile = match tweeter.current_profile().map(|profile| profile.to_owned()) { + Some(profile) => profile, + None => { + tweeter.display_info.status("To reply you must be authenticated as a user.".to_owned()); + return; + } + }; let mut text: String = line.trim().to_string(); let reply_bare = match text.find(" ") { None => "".to_owned(), @@ -152,8 +166,8 @@ fn rep(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { // if you're directly replying to yourself, i trust you know what you're doing and // want to @ yourself again (this keeps self-replies from showing up on your // profile as threaded tweets, f.ex) - if !(ats.len() > 0 && &ats[0] == &tweeter.current_user.handle) { - ats.remove_item(&tweeter.current_user.handle); + if !(ats.len() > 0 && &ats[0] == &user_profile.user.handle) { + ats.remove_item(&user_profile.user.handle); } //let ats_vec: Vec<&str> = ats.into_iter().collect(); //let full_reply = format!("{} {}", ats_vec.join(" "), reply); @@ -161,7 +175,7 @@ fn rep(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { let full_reply = format!("{} {}", decorated_ats.join(" "), reply); if reply.len() > 0 { - send_reply(full_reply, twid, tweeter, queryer); + send_reply(full_reply, twid, tweeter, queryer, user_profile.creds); } else { tweeter.display_info.mode = Some(::display::DisplayMode::Reply(twid, full_reply)); } @@ -175,11 +189,11 @@ fn rep(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { } } -pub fn send_reply(text: String, twid: TweetId, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { +pub fn send_reply(text: String, twid: TweetId, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer, user_creds: tw::Credential) { if let Some(twete) = tweeter.retrieve_tweet(&twid).map(|x| x.clone()) { // TODO: no clone when this stops taking &mut self let substituted = ::url_encode(&text); - let result = match tweeter.profile.clone() { - Some(user_creds) => { + let result = match tweeter.current_profile() { + Some(user_profile) => { queryer.do_api_post(&format!("{}?status={}&in_reply_to_status_id={}", CREATE_TWEET_URL, substituted, twete.id), &tweeter.app_key, &user_creds) }, None => Err("No logged in user to tweet as".to_owned()) @@ -220,8 +234,8 @@ fn quote(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { twete.id ) ); - let result = match tweeter.profile.clone() { - Some(user_creds) => { + let result = match tweeter.current_profile() { + Some(user_profile) => { queryer.do_api_post( &format!("{}?status={}&attachment_url={}", CREATE_TWEET_URL, @@ -229,7 +243,7 @@ fn quote(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { attachment_url ), &tweeter.app_key, - &user_creds + &user_profile.creds ) }, None => Err("No logged in user to tweet as".to_owned()) @@ -267,9 +281,9 @@ fn retwete(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) Ok(twid) => { // TODO this really converts twid to a TweetId::Twitter if let Some(twitter_id) = tweeter.retrieve_tweet(&twid).map(|x| x.id.to_owned()) { - let result = match tweeter.profile.clone() { - Some(user_creds) => { - queryer.do_api_post(&format!("{}/{}.json", RT_TWEET_URL, twitter_id), &tweeter.app_key, &user_creds) + let result = match tweeter.current_profile() { + Some(user_profile) => { + queryer.do_api_post(&format!("{}/{}.json", RT_TWEET_URL, twitter_id), &tweeter.app_key, &user_profile.creds) }, None => Err("No logged in user to retweet as".to_owned()) }; diff --git a/src/display/mod.rs b/src/display/mod.rs index 954ed38..88722cc 100644 --- a/src/display/mod.rs +++ b/src/display/mod.rs @@ -119,10 +119,11 @@ pub fn paint(tweeter: &mut ::tw::TwitterCache) -> Result<(), std::io::Error> { */ let (cursor_x, cursor_y) = match tweeter.display_info.mode.clone() { None => { + let handle = tweeter.current_profile().map(|profile| profile.user.handle.to_owned()).unwrap_or("_default_".to_owned()); print!("{}{}", cursor::Goto(1, height - 6), clear::CurrentLine); - print!("{}{}@{}>{}", cursor::Goto(1, height - 5), clear::CurrentLine, tweeter.current_user.handle, tweeter.display_info.input_buf.clone().into_iter().collect::()); + print!("{}{}@{}>{}", cursor::Goto(1, height - 5), clear::CurrentLine, handle, tweeter.display_info.input_buf.clone().into_iter().collect::()); print!("{}{}", cursor::Goto(1, height - 4), clear::CurrentLine); - ((1 + tweeter.current_user.handle.len() + 2 + tweeter.display_info.input_buf.len()) as u16, height as u16 - 5) + ((1 + handle.len() + 2 + tweeter.display_info.input_buf.len()) as u16, height as u16 - 5) } Some(DisplayMode::Compose(x)) => { let mut lines: Vec = vec![]; @@ -143,7 +144,7 @@ pub fn paint(tweeter: &mut ::tw::TwitterCache) -> Result<(), std::io::Error> { ); lines_drawn += 1; } - h += (lines_drawn - 3); + h += lines_drawn - 3; (cursor_idx as u16 + 3, height as u16 - 5) // TODO: panic on underflow } Some(DisplayMode::Reply(twid, msg)) => { @@ -166,7 +167,7 @@ pub fn paint(tweeter: &mut ::tw::TwitterCache) -> Result<(), std::io::Error> { ); lines_drawn += 1; } - h += (lines_drawn - 3); + h += lines_drawn - 3; (cursor_idx as u16 + 3, height as u16 - 5) // TODO: panic on underflow } }; diff --git a/src/main.rs b/src/main.rs index bfb60f9..cd15ea9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -180,6 +180,16 @@ fn inner_signed_api_req(url: &str, method: Method, app_cred: &tw::Credential, ma req } +static mut connection_id: u8 = 0; + +fn get_id() -> u8 { + unsafe { + let curr_id = connection_id; + connection_id += 1; + curr_id + } +} + fn main() { //Track words @@ -200,11 +210,13 @@ fn main() { tweeter.display_info.status("Cache loaded".to_owned()); - let mut maybe_twete_rx = tweeter.profile.clone() - .map(|user_creds| { - let rx = connect_twitter_stream(tweeter.app_key.clone(), user_creds); - tweeter.display_info.status("Twitter stream open".to_owned()); - rx + let (tweet_tx, mut twete_rx) = chan::sync::>(0); + let (coordination_tx, mut coordination_rx) = chan::sync::(0); + + tweeter.current_profile() + .map(|user_profile| user_profile.to_owned()) + .map(|user_profile| { + connect_twitter_stream(tweeter.app_key.clone(), user_profile.creds, tweet_tx.clone(), coordination_tx.clone(), get_id()); }); std::thread::spawn(move || { @@ -240,10 +252,11 @@ fn main() { }; loop { - match do_ui(ui_rx, maybe_twete_rx, &mut tweeter, &mut queryer) { - Some((new_ui_rx, new_maybe_twete_rx)) => { + match do_ui(ui_rx, twete_rx, coordination_rx, &mut tweeter, &mut queryer) { + Some((new_ui_rx, new_twete_rx, new_coordination_rx)) => { ui_rx = new_ui_rx; - maybe_twete_rx = new_maybe_twete_rx; + twete_rx = new_twete_rx; + coordination_rx = new_coordination_rx; }, None => { break; @@ -314,8 +327,15 @@ fn handle_input(event: termion::event::Event, tweeter: &mut tw::TwitterCache, qu } Some(display::DisplayMode::Reply(twid, msg)) => { if x == '\n' { - // TODO: move this somewhere better. - ::commands::twete::send_reply(msg, twid, tweeter, queryer); + match tweeter.current_profile().map(|profile| profile.to_owned()) { + Some(profile) => { + // TODO: move this somewhere better. + ::commands::twete::send_reply(msg, twid, tweeter, queryer, profile.creds); + }, + None => { + tweeter.display_info.status("Cannot reply when not logged in".to_owned()); + } + } tweeter.display_info.mode = None; } else { tweeter.display_info.mode = Some(display::DisplayMode::Reply(twid, format!("{}{}", msg, x))); @@ -356,35 +376,32 @@ fn handle_twitter_line(line: Vec, mut tweeter: &mut tw::TwitterCache, mut qu } } -fn do_ui(ui_rx_orig: chan::Receiver>, maybe_twete_rx: Option>>, mut tweeter: &mut tw::TwitterCache, mut queryer: &mut ::Queryer) -> Option<(chan::Receiver>, Option>>)> { +fn do_ui( + ui_rx_orig: chan::Receiver>, + twete_rx: chan::Receiver>, + coordination_rx: chan::Receiver, + mut tweeter: &mut tw::TwitterCache, + mut queryer: &mut ::Queryer +) -> Option<(chan::Receiver>, chan::Receiver>, chan::Receiver)> { loop { let ui_rx_a = &ui_rx_orig; let ui_rx_b = &ui_rx_orig; - match &maybe_twete_rx { - &Some(ref twete_rx) => { - chan_select! { - twete_rx.recv() -> twete => match twete { - Some(line) => handle_twitter_line(line, tweeter, queryer), - None => { - tweeter.display_info.status("Twitter stream hung up...".to_owned()); - return Some((ui_rx_orig.clone(), None)) - } - }, - ui_rx_a.recv() -> user_input => match user_input { - Some(Ok(event)) => handle_input(event, tweeter, queryer), - Some(Err(_)) => (), /* stdin closed? */ - None => return None // UI ded - } - } + chan_select! { + coordination_rx.recv() -> coordination => { + tweeter.display_info.status(format!("{:?}", coordination)); }, - &None => { - chan_select! { - ui_rx_a.recv() -> user_input => match user_input { - Some(Ok(event)) => handle_input(event, tweeter, queryer), - Some(Err(_)) => (), /* stdin closed? */ - None => return None // UI ded - } + twete_rx.recv() -> twete => match twete { + Some(line) => handle_twitter_line(line, tweeter, queryer), + None => { + tweeter.display_info.status("Twitter stream hung up...".to_owned()); + display::paint(tweeter).unwrap(); + return None // if the twitter channel died, something real bad happeneed? } + }, + ui_rx_a.recv() -> user_input => match user_input { + Some(Ok(event)) => handle_input(event, tweeter, queryer), + Some(Err(_)) => (), /* stdin closed? */ + None => return None // UI ded } } @@ -424,7 +441,8 @@ fn do_ui(ui_rx_orig: chan::Receiver { tweeter.state = tw::AppState::View; - return Some((ui_rx_orig.clone(), tweeter.profile.clone().map(|creds| connect_twitter_stream(tweeter.app_key.clone(), creds)))); + // TODO: reconnect *which*? + return None // Some((ui_rx_orig.clone(), tweeter.profile.clone().map(|creds| connect_twitter_stream(tweeter.app_key.clone(), creds)))); }, tw::AppState::Shutdown => { tweeter.display_info.status("Saving cache...".to_owned()); @@ -471,10 +489,22 @@ fn url_encode(s: &str) -> String { .replace("]", "%5d") } -fn connect_twitter_stream(app_cred: tw::Credential, user_cred: tw::Credential) -> chan::Receiver> { - let (twete_tx, twete_rx) = chan::sync::>(0); - +// let (twete_tx, twete_rx) = chan::sync::>(0); +#[derive(Debug)] +enum TwitterConnectionState { + Connecting(u8), + Connected(u8), + Closed(u8) +} +fn connect_twitter_stream( + app_cred: tw::Credential, + user_cred: tw::Credential, + twete_tx: chan::Sender>, + coordination_tx: chan::Sender, + conn_id: u8 +) { std::thread::spawn(move || { + coordination_tx.send(TwitterConnectionState::Connecting(conn_id)); let mut core = Core::new().unwrap(); let connector = HttpsConnector::new(1, &core.handle()).unwrap(); @@ -491,6 +521,7 @@ fn connect_twitter_stream(app_cred: tw::Credential, user_cred: tw::Credential) - println!("Twitter stream connect was abnormal: {}", status); println!("result: {:?}", res); } + coordination_tx.send(TwitterConnectionState::Connected(conn_id)); LineStream::new(res.body() .map(|chunk| futures::stream::iter_ok(chunk.into_iter())) .flatten()) @@ -507,7 +538,6 @@ fn connect_twitter_stream(app_cred: tw::Credential, user_cred: tw::Credential) - Ok(_good) => (), Err(e) => println!("Error in setting up: {}", e) } + coordination_tx.send(TwitterConnectionState::Closed(conn_id)); }); - - twete_rx } diff --git a/src/tw/mod.rs b/src/tw/mod.rs index acee0c2..52ab31c 100644 --- a/src/tw/mod.rs +++ b/src/tw/mod.rs @@ -98,20 +98,14 @@ pub struct TwitterCache { pub app_key: Credential, // right now we're stuck assuming one profile. // alts and such will be others here. - pub profile: Option, - following: HashSet, - following_history: HashMap, // userid:date?? - pub followers: HashSet, - lost_followers: HashSet, - follower_history: HashMap, // userid:date?? + pub curr_profile: Option, + pub profiles: HashMap, threads: HashMap, // thread : latest_tweet_in_thread #[serde(skip)] pub needs_save: bool, #[serde(skip)] pub caching_permitted: bool, #[serde(skip)] - pub current_user: User, - #[serde(skip)] id_conversions: IdConversions, #[serde(skip)] pub display_info: display::DisplayInfo, @@ -271,6 +265,31 @@ fn parse_word_command<'a, 'b>(line: &'b str, commands: &[&'a Command]) -> Option return None } +#[derive(Serialize, Deserialize, Clone)] +pub struct TwitterProfile { + pub creds: Credential, + pub user: User, + following: HashSet, + following_history: HashMap, // userid:date?? + pub followers: HashSet, + lost_followers: HashSet, + follower_history: HashMap // userid:date?? +} + +impl TwitterProfile { + pub fn new(creds: Credential, user: User) -> TwitterProfile { + TwitterProfile { + creds: creds, + user: user, + following: HashSet::new(), + following_history: HashMap::new(), + followers: HashSet::new(), + lost_followers: HashSet::new(), + follower_history: HashMap::new() + } + } +} + impl TwitterCache { const PROFILE_DIR: &'static str = "cache/"; const TWEET_CACHE: &'static str = "cache/tweets.json"; @@ -289,15 +308,10 @@ impl TwitterCache { // So, supporting multiple profiles will be ... interesting? // how do we support a variable number of channels? which will be necessary as we'll // have one channel up per twitter stream... - profile: None, // this will become a HashMap when multiple profiles are supported - following: HashSet::new(), - following_history: HashMap::new(), - followers: HashSet::new(), - lost_followers: HashSet::new(), - follower_history: HashMap::new(), + curr_profile: None, + profiles: HashMap::new(), needs_save: false, caching_permitted: true, - current_user: User::default(), threads: HashMap::new(), id_conversions: IdConversions::default(), display_info: display::DisplayInfo::default(), @@ -305,6 +319,17 @@ impl TwitterCache { } } + pub fn current_profile(&self) -> Option<&TwitterProfile> { + match &self.curr_profile { + &Some(ref profile_name) => self.profiles.get(profile_name), + &None => None + } + } + + pub fn current_handle(&self) -> Option { + self.current_profile().map(|profile| profile.user.handle.to_owned()) + } + // TODO: pull out the "Cache" part of TwitterCache, it can be serialized/deserialized - the // rest of the history is just for the running instance.. pub fn handle_user_input(&mut self, line: Vec, mut queryer: &mut Queryer) { @@ -322,8 +347,8 @@ impl TwitterCache { cache.caching_permitted = false; cache } - pub fn add_profile(&mut self, creds: Credential) { - self.profile = Some(creds); + pub fn add_profile(&mut self, profile: TwitterProfile, name: Option) { + self.profiles.insert(name.unwrap_or(profile.user.handle.to_owned()), profile); if self.caching_permitted { self.store_cache(); } @@ -578,84 +603,96 @@ impl TwitterCache { self.users.get(user_id) } pub fn set_following(&mut self, user_ids: Vec) { - let uid_set = user_ids.into_iter().collect::>(); + self.current_profile().map(|profile| profile.to_owned()).map(|mut profile| { + let uid_set = user_ids.into_iter().collect::>(); - let new_uids = &uid_set - &self.following; - for user in &new_uids { - self.display_info.status(format!("New following! {}", user)); - self.add_following(user); - } + let new_uids = &uid_set - &profile.following; + for user in &new_uids { + self.display_info.status(format!("New following! {}", user)); + self.add_following(user); + } - let lost_uids = &self.following - &uid_set; - for user in &lost_uids { - self.display_info.status(format!("Bye, friend! {}", user)); - self.remove_following(user); - } + let lost_uids = &profile.following - &uid_set; + for user in &lost_uids { + self.display_info.status(format!("Bye, friend! {}", user)); + self.remove_following(user); + } + }); } pub fn set_followers(&mut self, user_ids: Vec) { - let uid_set = user_ids.into_iter().collect::>(); + self.current_profile().map(|profile| profile.to_owned()).map(|mut profile| { + let uid_set = user_ids.into_iter().collect::>(); - let new_uids = &uid_set - &self.followers; - for user in &new_uids { - self.display_info.status(format!("New follower! {}", user)); - self.add_follower(user); - } + let new_uids = &uid_set - &profile.followers; + for user in &new_uids { + self.display_info.status(format!("New follower! {}", user)); + self.add_follower(user); + } - let lost_uids = &self.followers - &uid_set; - for user in &lost_uids { - self.display_info.status(format!("Bye, friend! {}", user)); - self.remove_follower(user); - } + let lost_uids = &profile.followers - &uid_set; + for user in &lost_uids { + self.display_info.status(format!("Bye, friend! {}", user)); + self.remove_follower(user); + } + }); } pub fn add_following(&mut self, user_id: &String) { - self.needs_save = true; - self.following.insert(user_id.to_owned()); - self.following_history.insert(user_id.to_owned(), ("following".to_string(), Utc::now().timestamp())); + self.current_profile().map(|profile| profile.to_owned()).map(|mut profile| { + self.needs_save = true; + profile.following.insert(user_id.to_owned()); + profile.following_history.insert(user_id.to_owned(), ("following".to_string(), Utc::now().timestamp())); + }); } pub fn remove_following(&mut self, user_id: &String) { - self.needs_save = true; - self.following.remove(user_id); - self.following_history.insert(user_id.to_owned(), ("unfollowing".to_string(), Utc::now().timestamp())); + self.current_profile().map(|profile| profile.to_owned()).map(|mut profile| { + self.needs_save = true; + profile.following.remove(user_id); + profile.following_history.insert(user_id.to_owned(), ("unfollowing".to_string(), Utc::now().timestamp())); + }); } pub fn add_follower(&mut self, user_id: &String) { - self.needs_save = true; - self.followers.insert(user_id.to_owned()); - self.lost_followers.remove(user_id); - self.follower_history.insert(user_id.to_owned(), ("follow".to_string(), Utc::now().timestamp())); + self.current_profile().map(|profile| profile.to_owned()).map(|mut profile| { + self.needs_save = true; + profile.followers.insert(user_id.to_owned()); + profile.lost_followers.remove(user_id); + profile.follower_history.insert(user_id.to_owned(), ("follow".to_string(), Utc::now().timestamp())); + }); } pub fn remove_follower(&mut self, user_id: &String) { - self.needs_save = true; - self.followers.remove(user_id); - self.lost_followers.insert(user_id.to_owned()); - self.follower_history.insert(user_id.to_owned(), ("unfollow".to_string(), Utc::now().timestamp())); + self.current_profile().map(|profile| profile.to_owned()).map(|mut profile| { + self.needs_save = true; + profile.followers.remove(user_id); + profile.lost_followers.insert(user_id.to_owned()); + profile.follower_history.insert(user_id.to_owned(), ("unfollow".to_string(), Utc::now().timestamp())); + }); } fn look_up_user(&mut self, id: &str, queryer: &mut ::Queryer) -> Result { let url = &format!("{}?user_id={}", ::USER_LOOKUP_URL, id); - match self.profile { - Some(ref user_creds) => queryer.do_api_get(url, &self.app_key, &user_creds), + match self.current_profile() { + Some(ref user_profile) => queryer.do_api_get(url, &self.app_key, &user_profile.creds), None => Err("No authorized user to conduct lookup".to_owned()) } } fn look_up_tweet(&mut self, id: &str, queryer: &mut ::Queryer) -> Result { let url = &format!("{}&id={}", ::TWEET_LOOKUP_URL, id); - match self.profile { - Some(ref user_creds) => queryer.do_api_get(url, &self.app_key, &user_creds), + match self.current_profile() { + Some(ref user_profile) => queryer.do_api_get(url, &self.app_key, &user_profile.creds), None => Err("No authorized user to conduct lookup".to_owned()) } } pub fn get_settings(&self, queryer: &mut ::Queryer) -> Result { - match self.profile { - Some(ref user_creds) => queryer.do_api_get(::ACCOUNT_SETTINGS_URL, &self.app_key, &user_creds), + match self.current_profile() { + Some(ref user_profile) => queryer.do_api_get(::ACCOUNT_SETTINGS_URL, &self.app_key, &user_profile.creds), None => Err("No authorized user to request settings".to_owned()) } } pub fn get_followers(&self, queryer: &mut ::Queryer) -> Result { - match self.profile { - Some(ref user_creds) => queryer.do_api_get(::GET_FOLLOWER_IDS_URL, &self.app_key, &user_creds), + match self.current_profile() { + Some(ref user_profile) => queryer.do_api_get(::GET_FOLLOWER_IDS_URL, &self.app_key, &user_profile.creds), None => Err("No authorized user to request followers".to_owned()) } } @@ -746,12 +783,9 @@ fn handle_twitter_welcome( let settings = tweeter.get_settings(queryer).unwrap(); let maybe_my_name = settings["screen_name"].as_str(); if let Some(my_name) = maybe_my_name { - tweeter.current_user = User { - id: "".to_string(), - handle: my_name.to_owned(), - name: my_name.to_owned() - }; - tweeter.display_info.status(format!("You are {}", tweeter.current_user.handle)) + // TODO: come back to this when custom profile names are supported? + tweeter.curr_profile = Some(my_name.to_owned()); + tweeter.display_info.status(format!("You are {}", my_name)) } else { tweeter.display_info.status("Unable to make API call to figure out who you are...".to_string()); } -- cgit v1.1