From ca0762652e293ad9d35b03b537c02d218e44a13f Mon Sep 17 00:00:00 2001 From: Andy Wortman Date: Fri, 10 Nov 2017 04:04:00 -0800 Subject: very hackily add notion of user credentials and PIN auth also fix bug where cached user info takes precedence over (possibly updated) api json user info --- src/commands/auth.rs | 91 ++++++++++++++++++++++++ src/commands/fav.rs | 12 +++- src/commands/follow.rs | 24 ++++++- src/commands/mod.rs | 3 + src/commands/twete.rs | 50 +++++++++++--- src/display/mod.rs | 2 + src/main.rs | 184 ++++++++++++++++++++++++++----------------------- src/tw/mod.rs | 53 ++++++++++++-- src/tw/user.rs | 2 +- 9 files changed, 311 insertions(+), 110 deletions(-) create mode 100644 src/commands/auth.rs (limited to 'src') diff --git a/src/commands/auth.rs b/src/commands/auth.rs new file mode 100644 index 0000000..0ed006b --- /dev/null +++ b/src/commands/auth.rs @@ -0,0 +1,91 @@ +use tw; +use std; +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"; +static UNFAV_TWEET_URL: &str = "https://api.twitter.com/1.1/favorites/destroy.json"; + +pub static AUTH: Command = Command { + keyword: "auth", + params: 0, + exec: auth +}; + +static OAUTH_REQUEST_TOKEN_URL: &str = "https://api.twitter.com/oauth/request_token"; +static OAUTH_AUTHORIZE_URL: &str = "https://api.twitter.com/oauth/authorize"; +static OAUTH_ACCESS_TOKEN_URL: &str = "https://api.twitter.com/oauth/access_token"; + +fn auth(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { + // step 0: get an oauth token. + // https://developer.twitter.com/en/docs/basics/authentication/api-reference/request_token with + // callback set to oob so the user will later get a PIN. + // step 1: now present the correect oauth/authorize URL + // this is as far as auth can get (rest depends on user PIN'ing with the right thing) + let res = queryer.raw_issue_request(::signed_api_req(&format!("{}?oauth_callback=oob", OAUTH_REQUEST_TOKEN_URL), hyper::Method::Post, &tweeter.app_key)); + match res { + Ok(bytes) => + match std::str::from_utf8(&bytes) { + Ok(url) => { + let parts: Vec> = url.split("&").map(|part| part.split("=").collect()).collect(); + let mut as_map: HashMap<&str, &str> = HashMap::new(); + for part in parts { + as_map.insert(part[0], part[1]); + } + tweeter.WIP_auth = Some(tw::Credential { + key: as_map["oauth_token"].to_owned(), + secret: as_map["oauth_token_secret"].to_owned() + }); + tweeter.display_info.status(format!("Now enter `pin` with the code at {}?oauth_token={}", OAUTH_AUTHORIZE_URL, as_map["oauth_token"])); + } + Err(_) => + tweeter.display_info.status("couldn't rebuild url".to_owned()) + }, + Err(e) => + tweeter.display_info.status(format!("request token url error: {}", e)) + }; +} + +pub static PIN: Command = Command { + keyword: "pin", + params: 1, + exec: pin +}; + +fn pin(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { + if tweeter.WIP_auth.is_none() { + tweeter.display_info.status("Begin authorizing an account with `auth` first.".to_owned()); + return; + } + + let res = queryer.raw_issue_request(::signed_api_req_with_token(&format!("{}?oauth_verifier={}", OAUTH_ACCESS_TOKEN_URL, line), hyper::Method::Post, &tweeter.app_key, &tweeter.WIP_auth.clone().unwrap())); + match res { + Ok(bytes) => + match std::str::from_utf8(&bytes) { + Ok(url) => { + let parts: Vec> = url.split("&").map(|part| part.split("=").collect()).collect(); + let mut as_map: HashMap<&str, &str> = HashMap::new(); + for part in parts { + as_map.insert(part[0], part[1]); + } + // turns out the "actual" oauth creds are different + tweeter.add_profile(tw::Credential { + key: as_map["oauth_token"].to_owned(), + secret: as_map["oauth_token_secret"].to_owned() + }); + tweeter.WIP_auth = None; + tweeter.state = tw::AppState::Reconnect; + tweeter.display_info.status("Looks like you authed! Connecting...".to_owned()); + }, + Err(_) => + tweeter.display_info.status("couldn't rebuild url".to_owned()) + }, + Err(e) => + tweeter.display_info.status(format!("request token url error: {}", e)) + }; +} diff --git a/src/commands/fav.rs b/src/commands/fav.rs index 6109310..08ad7f0 100644 --- a/src/commands/fav.rs +++ b/src/commands/fav.rs @@ -19,7 +19,11 @@ 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 - match queryer.do_api_post(&format!("{}?id={}", UNFAV_TWEET_URL, twete.id)) { + 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), + None => Err("No logged in user to unfav from".to_owned()) + }; + match result { Ok(_) => (), Err(e) => tweeter.display_info.status(e) } @@ -45,7 +49,11 @@ 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 - match queryer.do_api_post(&format!("{}?id={}", FAV_TWEET_URL, twete.id)) { + 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), + None => Err("No logged in user to fav from".to_owned()) + }; + match result { Ok(_) => (), Err(e) => tweeter.display_info.status(e) } diff --git a/src/commands/follow.rs b/src/commands/follow.rs index b0dc8a7..e9099c9 100644 --- a/src/commands/follow.rs +++ b/src/commands/follow.rs @@ -14,7 +14,13 @@ pub static UNFOLLOW: Command = Command { fn unfl(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { let screen_name = line.trim(); - match queryer.do_api_post(&format!("{}?screen_name={}", FOLLOW_URL, screen_name)) { + 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) + }, + None => Err("No logged in user to unfollow from".to_owned()) + }; + match result { Ok(_resp) => (), Err(e) => tweeter.display_info.status(format!("unfl request error: {}", e)) } @@ -28,5 +34,19 @@ pub static FOLLOW: Command = Command { fn fl(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { let screen_name = line.trim(); - tweeter.display_info.status(format!("fl resp: {:?}", queryer.do_api_post(&format!("{}?screen_name={}", UNFOLLOW_URL, screen_name)))); + match tweeter.profile.clone() { + Some(user_creds) => { + tweeter.display_info.status( + format!( + "fl resp: {:?}", + queryer.do_api_post( + &format!("{}?screen_name={}", UNFOLLOW_URL, screen_name), + &tweeter.app_key, + &user_creds + ) + ) + ) + }, + None => tweeter.display_info.status("No logged in user to follow from".to_owned()) + }; } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 9ec6c4b..f7536a0 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -7,6 +7,7 @@ pub struct Command { pub exec: fn(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) } +pub mod auth; pub mod show_cache; pub mod twete; pub mod look_up; @@ -17,6 +18,8 @@ pub mod follow; pub mod thread; pub static COMMANDS: &[&Command] = &[ + &auth::AUTH, + &auth::PIN, &show_cache::SHOW_CACHE, &quit::QUIT, &look_up::LOOK_UP_USER, diff --git a/src/commands/twete.rs b/src/commands/twete.rs index 4452df9..239e039 100644 --- a/src/commands/twete.rs +++ b/src/commands/twete.rs @@ -20,7 +20,11 @@ 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()) { - match queryer.do_api_post(&format!("{}/{}.json", DEL_TWEET_URL, twitter_id)) { + 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), + None => Err("No logged in user to delete as".to_owned()) + }; + match result { Ok(_) => (), Err(e) => tweeter.display_info.status(e) } @@ -54,7 +58,11 @@ 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); if text.len() <= 140 { - match queryer.do_api_post(&format!("{}?status={}", CREATE_TWEET_URL, substituted)) { + let result = match tweeter.profile.clone() { + Some(user_creds) => queryer.do_api_post(&format!("{}?status={}", CREATE_TWEET_URL, substituted), &tweeter.app_key, &user_creds), + None => Err("No logged in user to tweet as".to_owned()) + }; + match result { Ok(_) => (), Err(e) => tweeter.display_info.status(e) } @@ -166,7 +174,13 @@ 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) { 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); - match queryer.do_api_post(&format!("{}?status={}&in_reply_to_status_id={}", CREATE_TWEET_URL, substituted, twete.id)) { + let result = match tweeter.profile.clone() { + Some(user_creds) => { + 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()) + }; + match result { Ok(_) => (), Err(e) => tweeter.display_info.status(e) } @@ -200,13 +214,21 @@ fn quote(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { twete.id ) ); - match queryer.do_api_post( - &format!("{}?status={}&attachment_url={}", - CREATE_TWEET_URL, - substituted, - attachment_url - ) - ) { + let result = match tweeter.profile.clone() { + Some(user_creds) => { + queryer.do_api_post( + &format!("{}?status={}&attachment_url={}", + CREATE_TWEET_URL, + substituted, + attachment_url + ), + &tweeter.app_key, + &user_creds + ) + }, + None => Err("No logged in user to tweet as".to_owned()) + }; + match result { Ok(_) => (), Err(e) => tweeter.display_info.status(e) } @@ -237,7 +259,13 @@ 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()) { - match queryer.do_api_post(&format!("{}/{}.json", RT_TWEET_URL, twitter_id)) { + 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) + }, + None => Err("No logged in user to retweet as".to_owned()) + }; + match result { Ok(_) => (), Err(e) => tweeter.display_info.status(e) } diff --git a/src/display/mod.rs b/src/display/mod.rs index 4480855..92fa9dd 100644 --- a/src/display/mod.rs +++ b/src/display/mod.rs @@ -180,6 +180,8 @@ pub fn paint(tweeter: &mut ::tw::TwitterCache) -> Result<(), std::io::Error> { lines } Infos::Thread(ids) => { + // TODO: group together thread elements by the same person a little + // better.. let mut tweets: Vec> = ids.iter().rev().map(|x| into_display_lines(render_twete(x, tweeter), width)).collect(); let last = tweets.pop(); let mut lines = tweets.into_iter().fold(Vec::new(), |mut sum, lines| { diff --git a/src/main.rs b/src/main.rs index f8276ed..97d5315 100644 --- a/src/main.rs +++ b/src/main.rs @@ -39,13 +39,6 @@ mod tw; mod display; mod commands; -//Change these values to your real Twitter API credentials -static consumer_key: &str = "T879tHWDzd6LvKWdYVfbJL4Su"; -static consumer_secret: &str = "OAXXYYIozAZ4vWSmDziI1EMJCKXPmWPFgLbJpB896iIAMIAdpb"; -static token: &str = "629126745-Qt6LPq2kR7w58s7WHzSqcs4CIdiue64kkfYYB7RI"; -static token_secret: &str = "3BI3YC4WVbKW5icpHORWpsTYqYIj5oAZFkrgyIAoaoKnK"; -static lol_auth_token: &str = "641cdf3a4bbddb72c118b5821e8696aee6300a9a"; - static STREAMURL: &str = "https://userstream.twitter.com/1.1/user.json?tweet_mode=extended"; static TWEET_LOOKUP_URL: &str = "https://api.twitter.com/1.1/statuses/show.json?tweet_mode=extended"; static USER_LOOKUP_URL: &str = "https://api.twitter.com/1.1/users/show.json"; @@ -64,11 +57,11 @@ pub struct Queryer { } impl Queryer { - fn do_api_get(&mut self, url: &str) -> Result { - self.issue_request(signed_api_get(url)) + fn do_api_get(&mut self, url: &str, app_cred: &tw::Credential, user_cred: &tw::Credential) -> Result { + self.issue_request(signed_api_get(url, app_cred, user_cred)) } - fn do_api_post(&mut self, url: &str) -> Result { - self.issue_request(signed_api_post(url)) + fn do_api_post(&mut self, url: &str, app_cred: &tw::Credential, user_cred: &tw::Credential) -> Result { + self.issue_request(signed_api_post(url, app_cred, user_cred)) } /* fn do_web_req(&mut self, url: &str) -> Option { @@ -76,6 +69,10 @@ impl Queryer { }*/ // TODO: make this return the status as well! fn issue_request(&mut self, req: hyper::client::Request) -> Result { + let resp_body = self.raw_issue_request(req); + resp_body.and_then(|body| serde_json::from_slice(&body).map_err(|e| e.to_string())) + } + fn raw_issue_request(&mut self, req: hyper::client::Request) -> Result, String> { let lookup = self.client.request(req); let resp: hyper::Response = self.core.run(lookup).unwrap(); @@ -84,18 +81,10 @@ impl Queryer { let chunks: Vec = self.core.run(resp.body().collect()).unwrap(); let resp_body: Vec = chunks.into_iter().flat_map(|chunk| chunk.into_iter()).collect(); - - match serde_json::from_slice(&resp_body) { - Ok(value) => { - if status != hyper::StatusCode::Ok { - Err(format!("!! Requests returned status: {}\n{}", status, value)) - } else { - Ok(value) - } - } - Err(e) => { - Err(format!("!! Requests returned status: {}\nerror deserializing json: {}", status, e)) - } + if status != hyper::StatusCode::Ok { + Err(format!("!! Requests returned status: {} - {:?}", status, std::str::from_utf8(&resp_body))) + } else { + Ok(resp_body) } } } @@ -137,15 +126,23 @@ fn signed_web_get(url: &str) -> hyper::client::Request { } */ -fn signed_api_post(url: &str) -> hyper::client::Request { - signed_api_req(url, Method::Post) +fn signed_api_post(url: &str, app_cred: &tw::Credential, user_cred: &tw::Credential) -> hyper::client::Request { + signed_api_req_with_token(url, Method::Post, app_cred, user_cred) } -fn signed_api_get(url: &str) -> hyper::client::Request { - signed_api_req(url, Method::Get) +fn signed_api_get(url: &str, app_cred: &tw::Credential, user_cred: &tw::Credential) -> hyper::client::Request { + signed_api_req_with_token(url, Method::Get, app_cred, user_cred) } -fn signed_api_req(url: &str, method: Method) -> hyper::client::Request { +fn signed_api_req_with_token(url: &str, method: Method, app_cred: &tw::Credential, user_cred: &tw::Credential) -> hyper::client::Request { + inner_signed_api_req(url, method, app_cred, Some(user_cred)) +} + +fn signed_api_req(url: &str, method: Method, app_cred: &tw::Credential) -> hyper::client::Request { + inner_signed_api_req(url, method, app_cred, None) +} + +fn inner_signed_api_req(url: &str, method: Method, app_cred: &tw::Credential, maybe_user_cred: Option<&tw::Credential>) -> hyper::client::Request { // let params: Vec<(String, String)> = vec![("track".to_string(), "london".to_string())]; let method_string = match method { Method::Get => "GET", @@ -156,15 +153,21 @@ fn signed_api_req(url: &str, method: Method) -> hyper::client::Request { let params: Vec<(String, String)> = vec![]; let _param_string: String = params.iter().map(|p| p.0.clone() + &"=".to_string() + &p.1).collect::>().join("&"); - let header = oauthcli::OAuthAuthorizationHeaderBuilder::new( + let parsed_url = url::Url::parse(url).unwrap(); + + let mut builder = oauthcli::OAuthAuthorizationHeaderBuilder::new( method_string, - &url::Url::parse(url).unwrap(), - consumer_key, - consumer_secret, + &parsed_url, + app_cred.key.to_owned(), + app_cred.secret.to_owned(), oauthcli::SignatureMethod::HmacSha1, - ) - .token(token, token_secret) - .finish(); + ); + + if let Some(user_cred) = maybe_user_cred { + builder.token(user_cred.key.to_owned(), user_cred.secret.to_owned()); + } + + let header = builder.finish(); let mut req = Request::new(method, url.parse().unwrap()); @@ -185,14 +188,6 @@ fn main() { let (ui_tx, mut ui_rx) = chan::sync::>(0); - let mut twete_rx = connect_twitter_stream(); - - std::thread::spawn(move || { - for input in stdin().events() { - ui_tx.send(input); - } - }); - // I *would* want to load this before spawning the thread, but.. // tokio_core::reactor::Inner can't be moved between threads safely // and beacuse it's an Option-al field, it might be present @@ -205,6 +200,14 @@ fn main() { println!("Loaded cache!"); + let mut maybe_twete_rx: Option>> = tweeter.profile.clone().map(|user_creds| connect_twitter_stream(tweeter.app_key.clone(), user_creds)); + + std::thread::spawn(move || { + for input in stdin().events() { + ui_tx.send(input); + } + }); + let c2 = Core::new().unwrap(); // i swear this is not where the botnet lives let handle = &c2.handle(); let secondary_connector = HttpsConnector::new(4, handle).unwrap(); @@ -226,10 +229,10 @@ fn main() { tcsetattr(0, TCSANOW, &new_termios).unwrap(); loop { - match do_ui(ui_rx, twete_rx, &mut tweeter, &mut queryer) { - Some((new_ui_rx, new_twete_rx)) => { + match do_ui(ui_rx, maybe_twete_rx, &mut tweeter, &mut queryer) { + Some((new_ui_rx, new_maybe_twete_rx)) => { ui_rx = new_ui_rx; - twete_rx = new_twete_rx; + maybe_twete_rx = new_maybe_twete_rx; }, None => { break; @@ -266,6 +269,7 @@ fn handle_input(event: termion::event::Event, tweeter: &mut tw::TwitterCache, qu _ => {} } } + // TODO: ctrl+u, ctrl+w Event::Key(Key::Char(x)) => { match tweeter.display_info.mode.clone() { None => { @@ -311,62 +315,66 @@ fn handle_input(event: termion::event::Event, tweeter: &mut tw::TwitterCache, qu } } -fn do_ui(ui_rx_orig: chan::Receiver>, twete_rx: chan::Receiver>, mut tweeter: &mut tw::TwitterCache, mut queryer: &mut ::Queryer) -> Option<(chan::Receiver>, chan::Receiver>)> { +fn handle_twitter_line(line: Vec, mut tweeter: &mut tw::TwitterCache, mut queryer: &mut ::Queryer) { + let jsonstr = std::str::from_utf8(&line).unwrap().trim(); + /* TODO: replace from_str with from_slice */ + match serde_json::from_str(&jsonstr) { + Ok(json) => { + tw::handle_message(json, &mut tweeter, &mut queryer); + if tweeter.needs_save && tweeter.caching_permitted { + tweeter.store_cache(); + } + }, + Err(e) => + tweeter.display_info.status(format!("Error reading twitter line: {}", jsonstr)) + } +} + +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>>)> { loop { let ui_rx_a = &ui_rx_orig; let ui_rx_b = &ui_rx_orig; - chan_select! { - twete_rx.recv() -> twete => match twete { - Some(line) => { - let jsonstr = std::str::from_utf8(&line).unwrap().trim(); - /* TODO: replace from_str with from_slice */ - let json: serde_json::Value = serde_json::from_str(&jsonstr).unwrap(); - tw::handle_message(json, &mut tweeter, &mut queryer); - if tweeter.needs_save && tweeter.caching_permitted { - tweeter.store_cache(); - } - } - None => { - tweeter.display_info.status("Twitter stream hung up...".to_owned()); - chan_select! { - ui_rx_b.recv() -> input => match input { - Some(maybe_event) => { - if let Ok(event) = maybe_event { - handle_input(event, tweeter, queryer); - } else { - // stdin closed? - } - } - // twitter stream closed, ui thread closed, uhh.. - None => std::process::exit(0) + 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 } } }, - ui_rx_a.recv() -> user_input => match user_input { - Some(maybe_event) => { - if let Ok(event) = maybe_event { - handle_input(event, tweeter, queryer); // eventually DisplayInfo too, as a separate piece of data... - } else { - // dunno how we'd reach this... stdin closed? + &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 } - }, - None => tweeter.display_info.status("UI thread hung up...".to_owned()) + } } - // and then we can introduce a channel that just sends a message every 100 ms or so - // that acts as a clock! } - match tweeter.state { - tw::AppState::Reconnect => return Some((ui_rx_orig.clone(), connect_twitter_stream())), - _ => () - }; - // one day display_info should be distinct match display::paint(tweeter) { Ok(_) => (), Err(e) => println!("{}", e) // TODO: we got here because writing to stdout failed. what to do now? }; + + match tweeter.state { + tw::AppState::Reconnect => { + tweeter.state = tw::AppState::View; + return Some((ui_rx_orig.clone(), tweeter.profile.clone().map(|creds| connect_twitter_stream(tweeter.app_key.clone(), creds)))); + } + _ => () + }; } } @@ -402,7 +410,7 @@ fn url_encode(s: &str) -> String { .replace("]", "%5d") } -fn connect_twitter_stream() -> chan::Receiver> { +fn connect_twitter_stream(app_cred: tw::Credential, user_cred: tw::Credential) -> chan::Receiver> { let (twete_tx, twete_rx) = chan::sync::>(0); std::thread::spawn(move || { @@ -415,7 +423,7 @@ fn connect_twitter_stream() -> chan::Receiver> { .connector(connector) .build(&core.handle()); - let req = signed_api_get(STREAMURL); + let req = signed_api_get(STREAMURL, &app_cred, &user_cred); let work = client.request(req).and_then(|res| { let status = res.status(); if status != hyper::StatusCode::Ok { diff --git a/src/tw/mod.rs b/src/tw/mod.rs index b1bc9c2..767aae9 100644 --- a/src/tw/mod.rs +++ b/src/tw/mod.rs @@ -92,12 +92,24 @@ pub fn full_twete_text(twete: &serde_json::map::Map) twete_text } +#[derive(Clone, Serialize, Deserialize)] +pub struct Credential { + pub key: String, + pub secret: String +} + #[derive(Serialize, Deserialize)] pub struct TwitterCache { #[serde(skip)] pub users: HashMap, #[serde(skip)] pub tweets: HashMap, + #[serde(skip)] + pub WIP_auth: Option, + 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, @@ -255,12 +267,18 @@ impl TwitterCache { const PROFILE_DIR: &'static str = "cache/"; const TWEET_CACHE: &'static str = "cache/tweets.json"; const USERS_CACHE: &'static str = "cache/users.json"; - const PROFILE_CACHE: &'static str = "cache/profile.json"; // this should involve MY user id.. + const PROFILE_CACHE: &'static str = "cache/profile.json"; fn new() -> TwitterCache { TwitterCache { users: HashMap::new(), tweets: HashMap::new(), + WIP_auth: None, + app_key: Credential { + key: "".to_owned(), + secret: "".to_owned() + }, + profile: None, // this will become a HashMap when multiple profiles are supported following: HashSet::new(), following_history: HashMap::new(), followers: HashSet::new(), @@ -293,8 +311,19 @@ impl TwitterCache { cache.caching_permitted = false; cache } + pub fn add_profile(&mut self, creds: Credential) { + self.profile = Some(creds); + if self.caching_permitted { + self.store_cache(); + } + } fn cache_user(&mut self, user: User) { - if !self.users.contains_key(&user.id) { + let update_cache = match self.users.get(&user.id) { + Some(cached_user) => &user != cached_user, + None => true + }; + + if update_cache { let mut file = OpenOptions::new() .create(true) @@ -583,20 +612,32 @@ impl TwitterCache { fn look_up_user(&mut self, id: &str, queryer: &mut ::Queryer) -> Result { let url = &format!("{}?user_id={}", ::USER_LOOKUP_URL, id); - queryer.do_api_get(url) + match self.profile { + Some(ref user_creds) => queryer.do_api_get(url, &self.app_key, &user_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); - queryer.do_api_get(url) + match self.profile { + Some(ref user_creds) => queryer.do_api_get(url, &self.app_key, &user_creds), + None => Err("No authorized user to conduct lookup".to_owned()) + } } pub fn get_settings(&self, queryer: &mut ::Queryer) -> Result { - queryer.do_api_get(::ACCOUNT_SETTINGS_URL) + match self.profile { + Some(ref user_creds) => queryer.do_api_get(::ACCOUNT_SETTINGS_URL, &self.app_key, &user_creds), + None => Err("No authorized user to request settings".to_owned()) + } } pub fn get_followers(&self, queryer: &mut ::Queryer) -> Result { - queryer.do_api_get(::GET_FOLLOWER_IDS_URL) + match self.profile { + Some(ref user_creds) => queryer.do_api_get(::GET_FOLLOWER_IDS_URL, &self.app_key, &user_creds), + None => Err("No authorized user to request followers".to_owned()) + } } pub fn set_thread(&mut self, name: String, last_id: u64) -> bool { diff --git a/src/tw/user.rs b/src/tw/user.rs index 0af4eb8..86fbe3f 100644 --- a/src/tw/user.rs +++ b/src/tw/user.rs @@ -1,6 +1,6 @@ extern crate serde_json; -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct User { pub id: String, pub name: String, -- cgit v1.1