From 781981f333345482e93ed35453e98e519bb7cc5e Mon Sep 17 00:00:00 2001 From: iximeow Date: Mon, 2 Oct 2017 01:27:08 -0700 Subject: move everything to src/ --- Cargo.toml | 2 +- commands/del.rs | 0 commands/fav.rs | 35 ---- commands/look_up.rs | 32 ---- commands/mod.rs | 46 ----- commands/quit.rs | 18 -- commands/show_cache.rs | 31 ---- commands/twete.rs | 184 ------------------- commands/view.rs | 22 --- display/mod.rs | 147 --------------- linestream.rs | 42 ----- main.rs | 382 --------------------------------------- src/commands/del.rs | 0 src/commands/fav.rs | 35 ++++ src/commands/look_up.rs | 32 ++++ src/commands/mod.rs | 46 +++++ src/commands/quit.rs | 18 ++ src/commands/show_cache.rs | 31 ++++ src/commands/twete.rs | 184 +++++++++++++++++++ src/commands/view.rs | 22 +++ src/display/mod.rs | 147 +++++++++++++++ src/linestream.rs | 42 +++++ src/main.rs | 382 +++++++++++++++++++++++++++++++++++++++ src/tw/events.rs | 44 +++++ src/tw/mod.rs | 440 +++++++++++++++++++++++++++++++++++++++++++++ src/tw/tweet.rs | 78 ++++++++ src/tw/user.rs | 46 +++++ tw/events.rs | 44 ----- tw/mod.rs | 440 --------------------------------------------- tw/tweet.rs | 78 -------- tw/user.rs | 46 ----- 31 files changed, 1548 insertions(+), 1548 deletions(-) delete mode 100644 commands/del.rs delete mode 100644 commands/fav.rs delete mode 100644 commands/look_up.rs delete mode 100644 commands/mod.rs delete mode 100644 commands/quit.rs delete mode 100644 commands/show_cache.rs delete mode 100644 commands/twete.rs delete mode 100644 commands/view.rs delete mode 100644 display/mod.rs delete mode 100644 linestream.rs delete mode 100644 main.rs create mode 100644 src/commands/del.rs create mode 100644 src/commands/fav.rs create mode 100644 src/commands/look_up.rs create mode 100644 src/commands/mod.rs create mode 100644 src/commands/quit.rs create mode 100644 src/commands/show_cache.rs create mode 100644 src/commands/twete.rs create mode 100644 src/commands/view.rs create mode 100644 src/display/mod.rs create mode 100644 src/linestream.rs create mode 100644 src/main.rs create mode 100644 src/tw/events.rs create mode 100644 src/tw/mod.rs create mode 100644 src/tw/tweet.rs create mode 100644 src/tw/user.rs delete mode 100644 tw/events.rs delete mode 100644 tw/mod.rs delete mode 100644 tw/tweet.rs delete mode 100644 tw/user.rs diff --git a/Cargo.toml b/Cargo.toml index 6a0653c..78a38c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ a nice twitter [[bin]] name = "twidder" -path = "main.rs" +# path = "main.rs" test = false bench = false diff --git a/commands/del.rs b/commands/del.rs deleted file mode 100644 index e69de29..0000000 diff --git a/commands/fav.rs b/commands/fav.rs deleted file mode 100644 index 3e2b00d..0000000 --- a/commands/fav.rs +++ /dev/null @@ -1,35 +0,0 @@ -use tw; -use ::Queryer; - -use commands::Command; - -use std::str::FromStr; - -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 UNFAV: Command = Command { - keyword: "unfav", - params: 1, - exec: unfav -}; - -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)); -} - -pub static FAV: Command = Command { - keyword: "fav", - params: 1, - exec: fav -}; - -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)); -} diff --git a/commands/look_up.rs b/commands/look_up.rs deleted file mode 100644 index d04f984..0000000 --- a/commands/look_up.rs +++ /dev/null @@ -1,32 +0,0 @@ -use tw; -use ::Queryer; - -use commands::Command; - -pub static LOOK_UP_USER: Command = Command { - keyword: "look_up_user", - params: 1, - exec: look_up_user -}; - -fn look_up_user(line: String, tweeter: &mut tw::TwitterCache, mut queryer: &mut Queryer) { - if let Some(user) = tweeter.fetch_user(&line, &mut queryer) { - println!("{:?}", user); - } else { -// println!("Couldn't retrieve {}", userid); - } -} - -pub static LOOK_UP_TWEET: Command = Command { - keyword: "look_up_tweet", - params: 1, - exec: look_up_tweet -}; - -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); - } else { -// println!("Couldn't retrieve {}", tweetid); - } -} diff --git a/commands/mod.rs b/commands/mod.rs deleted file mode 100644 index fc66bec..0000000 --- a/commands/mod.rs +++ /dev/null @@ -1,46 +0,0 @@ -use tw; -use ::Queryer; - -pub struct Command { - pub keyword: &'static str, - pub params: u8, - pub exec: fn(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) -} - -mod show_cache; -mod twete; -mod look_up; -mod view; -mod quit; -mod fav; - -pub static COMMANDS: &[&Command] = &[ - &show_cache::SHOW_CACHE, - &quit::QUIT, - &look_up::LOOK_UP_USER, - &look_up::LOOK_UP_TWEET, - &view::VIEW, - &fav::UNFAV, - &fav::FAV, - &twete::DEL, - &twete::TWETE, - &twete::QUOTE, - &twete::RETWETE, - &twete::REP, - &twete::THREAD - /* - &QUIT, - &LOOK_UP_USER, - &LOOK_UP_TWEET, - &VIEW, - &UNFAV, - &FAV, - &DEL, - &TWETE, - "E, - &RETWETE, - &REP, - &THREAD - ]; - */ -]; diff --git a/commands/quit.rs b/commands/quit.rs deleted file mode 100644 index 982c48f..0000000 --- a/commands/quit.rs +++ /dev/null @@ -1,18 +0,0 @@ -use tw; -use ::Queryer; - -use commands::Command; - -use std::process::exit; - -pub static QUIT: Command = Command { - keyword: "q", - params: 0, - exec: quit -}; - -fn quit(_line: String, tweeter: &mut tw::TwitterCache, _queryer: &mut Queryer) { - println!("Bye bye!"); - tweeter.store_cache(); - exit(0); -} diff --git a/commands/show_cache.rs b/commands/show_cache.rs deleted file mode 100644 index 3c31697..0000000 --- a/commands/show_cache.rs +++ /dev/null @@ -1,31 +0,0 @@ -use tw; -use ::Queryer; - -use commands::Command; - -pub static SHOW_CACHE: Command = Command { - keyword: "show_cache", - params: 0, - exec: show_cache -}; - -fn show_cache(line: String, tweeter: &mut tw::TwitterCache, mut queryer: &mut Queryer) { - println!("----* USERS *----"); - for (uid, user) in &tweeter.users { - println!("User: {} -> {:?}", uid, user); - } - println!("----* TWEETS *----"); - for (tid, tweet) in &tweeter.tweets { - println!("Tweet: {} -> {:?}", tid, tweet); - } - println!("----* FOLLOWERS *----"); - for uid in &tweeter.followers.clone() { - let user_res = tweeter.fetch_user(uid, &mut queryer); - match user_res { - Some(user) => { - println!("Follower: {} - {:?}", uid, user); - } - None => { println!(" ..."); } - } - } -} diff --git a/commands/twete.rs b/commands/twete.rs deleted file mode 100644 index ecc3f98..0000000 --- a/commands/twete.rs +++ /dev/null @@ -1,184 +0,0 @@ -use tw; -use ::Queryer; - -use commands::Command; - -use std::str::FromStr; - -static DEL_TWEET_URL: &str = "https://api.twitter.com/1.1/statuses/destroy"; -static RT_TWEET_URL: &str = "https://api.twitter.com/1.1/statuses/retweet"; -static CREATE_TWEET_URL: &str = "https://api.twitter.com/1.1/statuses/update.json"; - -pub static DEL: Command = Command { - keyword: "del", - params: 1, - exec: del -}; - -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(); - queryer.do_api_post(&format!("{}/{}.json", DEL_TWEET_URL, twete.id)); -} - -pub static TWETE: Command = Command { - keyword: "t", - params: 1, - exec: twete -}; - -fn twete(line: String, _tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { - let text = line.trim(); - let substituted = ::url_encode(text); - println!("msg len: {}", text.len()); - println!("excessively long? {}", text.len() > 140); - if text.len() > 140 { - queryer.do_api_post(&format!("{}?status={}", CREATE_TWEET_URL, substituted)); - } else { - queryer.do_api_post(&format!("{}?status={}&weighted_character_count=true", CREATE_TWEET_URL, substituted)); - } -// println!("{}", &format!("{}?status={}", CREATE_TWEET_URL, substituted)); -} - -pub static THREAD: Command = Command { - keyword: "thread", - params: 2, - exec: thread -}; - -fn thread(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { - 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); - let reply = reply_bare.trim(); - 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) { - 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 { - let substituted = ::url_encode(reply); - queryer.do_api_post(&format!("{}?status={}&in_reply_to_status_id={}", CREATE_TWEET_URL, substituted, twete.id)); - } else { - println!("you can only thread your own tweets"); - // ask if it should .@ instead? - } - let substituted = ::url_encode(reply); - queryer.do_api_post(&format!("{}?status={}&in_reply_to_status_id={}", CREATE_TWEET_URL, substituted, twete.id)); - } - } - } else { - println!("thread your sik reply"); - } - } else { - println!("thread your sik reply"); - } -} - -pub static REP: Command = Command { - keyword: "rep", - params: 2, - exec: rep -}; - -fn rep(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { - 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); - let reply = reply_bare.trim(); - 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) { - // 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(); - /* - for handle in twete.get_mentions() { - ats.insert(handle); - } - */ - 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)) { - 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)) { - 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); - } - //let ats_vec: Vec<&str> = ats.into_iter().collect(); - //let full_reply = format!("{} {}", ats_vec.join(" "), reply); - let decorated_ats: Vec = ats.into_iter().map(|x| format!("@{}", x)).collect(); - let full_reply = format!("{} {}", decorated_ats.join(" "), reply); - let substituted = ::url_encode(&full_reply); -// println!("{}", (&format!("{}?status={}&in_reply_to_status_id={}", CREATE_TWEET_URL, substituted, twete.id))); - queryer.do_api_post(&format!("{}?status={}&in_reply_to_status_id={}", CREATE_TWEET_URL, substituted, twete.id)); - } - } - } else { - println!("rep your sik reply"); - } - } else { - println!("rep your sik reply"); - } -} - -pub static QUOTE: Command = Command { - keyword: "qt", - params: 2, - exec: quote -}; - -fn quote(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { - 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); - let reply = reply_bare.trim(); - 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) { - let substituted = ::url_encode(reply); - let attachment_url = ::url_encode( - &format!( - "https://www.twitter.com/{}/status/{}", - tweeter.retrieve_user(&twete.author_id).unwrap().handle, - twete.id - ) - ); - println!("{}", substituted); - queryer.do_api_post( - &format!("{}?status={}&attachment_url={}", - CREATE_TWEET_URL, - substituted, - attachment_url - ) - ); - } - } - } else { - println!("rep your sik reply"); - } - } else { - println!("rep your sik reply"); - } -} - -pub static RETWETE: Command = Command { - keyword: "rt", - params: 1, - exec: retwete -}; - -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(); - queryer.do_api_post(&format!("{}/{}.json", RT_TWEET_URL, twete.id)); -} - diff --git a/commands/view.rs b/commands/view.rs deleted file mode 100644 index d01ff1b..0000000 --- a/commands/view.rs +++ /dev/null @@ -1,22 +0,0 @@ -use tw; -use ::Queryer; - -use commands::Command; - -use std::str::FromStr; - -use display; - -pub static VIEW: Command = Command { - keyword: "view", - params: 1, - exec: view -}; - -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(); - display::render_twete(&twete.id, tweeter); - println!("link: https://twitter.com/i/web/status/{}", twete.id); -} diff --git a/display/mod.rs b/display/mod.rs deleted file mode 100644 index 24f7e33..0000000 --- a/display/mod.rs +++ /dev/null @@ -1,147 +0,0 @@ -extern crate termion; - -use self::termion::color; - -use ::tw; - -use std; - -fn color_for(handle: &String) -> termion::color::Fg<&color::Color> { - let color_map: Vec<&color::Color> = vec![ - &color::Blue, - &color::Cyan, - &color::Green, - &color::LightBlue, - &color::LightCyan, - &color::LightGreen, - &color::LightMagenta, - &color::LightYellow, - &color::Magenta, - &color::Yellow - ]; - - let mut quot_hash_quot = std::num::Wrapping(0); - for b in handle.as_bytes().iter() { - quot_hash_quot = quot_hash_quot + std::num::Wrapping(*b); - } - color::Fg(color_map[quot_hash_quot.0 as usize % color_map.len()]) -} - -pub trait Render { - fn render(self, tweeter: &::tw::TwitterCache); -} - -impl Render for tw::events::Event { - fn render(self, tweeter: &::tw::TwitterCache) { - match self { - 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) { - println!("-------------DELETED------------------"); - render_twete(&twete_id, tweeter); - println!("-------------DELETED------------------"); - } else { - println!("dunno what, but do know who: {} - {}", user_id, handle); - } - } else { - println!("delete..."); - println!("dunno who..."); - } - }, - tw::events::Event::RT_RT { user_id, twete_id } => { - println!("---------------------------------"); - let user = tweeter.retrieve_user(&user_id).unwrap(); - println!(" +rt_rt : {} (@{})", user.name, user.handle); - render_twete(&twete_id, tweeter); - }, - tw::events::Event::Fav_RT { user_id, twete_id } => { - println!("---------------------------------"); - let user = tweeter.retrieve_user(&user_id).unwrap(); - println!(" +rt_fav : {} (@{})", user.name, user.handle); - render_twete(&twete_id, tweeter); - }, - tw::events::Event::Fav { user_id, twete_id } => { - println!("---------------------------------"); - let user = tweeter.retrieve_user(&user_id).unwrap(); - println!("{} +fav : {} (@{}){}", color::Fg(color::Yellow), user.name, user.handle, color::Fg(color::Reset)); - render_twete(&twete_id, tweeter); - }, - tw::events::Event::Unfav { user_id, twete_id } => { - println!("---------------------------------"); - let user = tweeter.retrieve_user(&user_id).unwrap(); - println!("{} -fav : {} (@{}){}", color::Fg(color::Yellow), user.name, user.handle, color::Fg(color::Reset)); - render_twete(&twete_id, tweeter); - }, - tw::events::Event::Followed { user_id } => { - println!("---------------------------------"); - let user = tweeter.retrieve_user(&user_id).unwrap(); - println!(" +fl : {} (@{})", user.name, user.handle); - }, - tw::events::Event::Unfollowed { user_id } => { - println!("---------------------------------"); - let user = tweeter.retrieve_user(&user_id).unwrap(); - println!(" -fl : {} (@{})", user.name, user.handle); - } - /* - Blocked(user_id) => { - }, - */ - } - println!(""); - } -} - -pub fn render_twete(twete_id: &String, tweeter: &tw::TwitterCache) { - let id_color = color::Fg(color::Rgb(180, 80, 40)); - let twete = tweeter.retrieve_tweet(twete_id).unwrap(); - // if we got the tweet, the API gave us the user too - let user = tweeter.retrieve_user(&twete.author_id).unwrap(); - match twete.rt_tweet { - Some(ref rt_id) => { - // same for a retweet - let rt = tweeter.retrieve_tweet(rt_id).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, color::Fg(color::Reset) - ); - println!(" {}{}{} ({}@{}{}) via {}{}{} ({}@{}{}) RT:", - color_for(&rt_author.handle), rt_author.name, color::Fg(color::Reset), - color_for(&rt_author.handle), rt_author.handle, color::Fg(color::Reset), - color_for(&user.handle), user.name, color::Fg(color::Reset), - color_for(&user.handle), user.handle, color::Fg(color::Reset) - ); - } - None => { - println!("{} id:{}{}", - id_color, twete.internal_id, color::Fg(color::Reset) - ); - println!(" {}{}{} ({}@{}{})", - color_for(&user.handle), user.name, color::Fg(color::Reset), - color_for(&user.handle), user.handle, color::Fg(color::Reset) - ); - } - } - - 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) { - let qt_author = tweeter.retrieve_user(&qt.author_id).unwrap(); - println!("{} id:{}{}", - id_color, qt.internal_id, color::Fg(color::Reset) - ); - println!( - " {}{}{} ({}@{}{})", - color_for(&qt_author.handle), qt_author.name, color::Fg(color::Reset), - color_for(&qt_author.handle), qt_author.handle, color::Fg(color::Reset) - ); - println!( - " {}", - qt.text.replace("\r", "\\r").split("\n").collect::>().join("\n ") - ); - } else { - println!(" << don't have quoted tweet! >>"); - } - } -} diff --git a/linestream.rs b/linestream.rs deleted file mode 100644 index 5106af3..0000000 --- a/linestream.rs +++ /dev/null @@ -1,42 +0,0 @@ -use std; -use futures::stream::Stream; -use futures::{Poll, Async}; - -pub struct LineStream where S: Stream { - stream: S, - progress: Vec -} - -impl LineStream where S: Stream + Sized { - pub fn new(stream: S) -> LineStream { - LineStream { - stream: stream, - progress: vec![] - } - } -} - -impl Stream for LineStream where S: Stream { - type Item = Vec; - type Error = E; - - fn poll(&mut self) -> Poll, Self::Error> { - loop { - match self.stream.poll() { - Ok(Async::Ready(Some(byte))) => { - if byte == 0x0a { - let mut new_vec = vec![]; - std::mem::swap(&mut self.progress, &mut new_vec); - return Ok(Async::Ready(Some(new_vec))) - } else { - self.progress.push(byte) - } - }, - Ok(Async::Ready(None)) => return Ok(Async::Ready(None)), - Ok(Async::NotReady) => return Ok(Async::NotReady), - Err(e) => return Err(e) - } - } - } -} - diff --git a/main.rs b/main.rs deleted file mode 100644 index 37082a1..0000000 --- a/main.rs +++ /dev/null @@ -1,382 +0,0 @@ -#![feature(vec_remove_item)] -extern crate serde_json; - -use std::str; -//use std::io::BufRead; - -#[macro_use] extern crate chan; - -extern crate url; -#[macro_use] extern crate hyper; -#[macro_use] extern crate serde_derive; -extern crate oauthcli; -extern crate tokio_core; -extern crate futures; -extern crate hyper_tls; - -use hyper::{Client, Method, Request}; -//use std::collections::{HashMap, HashSet}; -use tokio_core::reactor::Core; -use futures::future::Future; -use futures::Stream; -//use hyper::client::FutureResponse; -use hyper_tls::HttpsConnector; -//use json_streamer::JsonObjectStreamer; - -mod linestream; -use linestream::LineStream; - -mod tw; -mod display; - -//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"; -static ACCOUNT_SETTINGS_URL: &str = "https://api.twitter.com/1.1/account/settings.json"; - -header! { (Authorization, "Authorization") => [String] } -header! { (Accept, "Accept") => [String] } -header! { (ContentType, "Content-Type") => [String] } -header! { (Cookie, "cookie") => [String] } - - -pub struct Queryer { - client: hyper::client::Client>, - core: Core -} - -impl Queryer { - fn do_api_get(&mut self, url: &str) -> Option { - self.issue_request(signed_api_get(url)) - } - fn do_api_post(&mut self, url: &str) -> Option { - self.issue_request(signed_api_post(url)) - } - /* - fn do_web_req(&mut self, url: &str) -> Option { - self.issue_request(signed_web_get(url)) - }*/ - // TODO: make this return the status as well! - fn issue_request(&mut self, req: hyper::client::Request) -> Option { - let lookup = self.client.request(req); - - let resp: hyper::Response = self.core.run(lookup).unwrap(); - let status = resp.status().clone(); - - 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 { - println!("!! Requests returned status: {}", status); - println!("{}", value); - None - } else { - Some(value) - } - } - Err(e) => { - if status != hyper::StatusCode::Ok { - println!("!! Requests returned status: {}", status); - } - println!("error deserializing json: {}", e); - None - } - } - } -} - -/* -fn signed_web_get(url: &str) -> hyper::client::Request { -// let params: Vec<(String, String)> = vec![("track".to_string(), "london".to_string())]; - 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::authorization_header( - "GET", - url::Url::parse(url).unwrap(), - None, // Realm - consumer_key, - consumer_secret, - Some(token), - Some(token_secret), - oauthcli::SignatureMethod::HmacSha1, - &oauthcli::timestamp(), - &oauthcli::nonce(), - None, // oauth_callback - None, // oauth_verifier - params.clone().into_iter() - ); - - let mut req = Request::new(Method::Get, url.parse().unwrap()); - - req.set_body(param_string); - - { - let mut headers = req.headers_mut(); - headers.set(Cookie(format!("auth_token={}", lol_auth_token))); - headers.set(Accept("* / *".to_owned())); - headers.set(ContentType("application/x-www-form-urlencoded".to_owned())); - }; - - req -} -*/ - -fn signed_api_post(url: &str) -> hyper::client::Request { - signed_api_req(url, Method::Post) -} - -fn signed_api_get(url: &str) -> hyper::client::Request { - signed_api_req(url, Method::Get) -} - -fn signed_api_req(url: &str, method: Method) -> hyper::client::Request { -// let params: Vec<(String, String)> = vec![("track".to_string(), "london".to_string())]; - let method_string = match method { - Method::Get => "GET", - Method::Post => "POST", - _ => panic!(format!("unsupported method {}", method)) - }; - - 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( - method_string, - &url::Url::parse(url).unwrap(), - consumer_key, - consumer_secret, - oauthcli::SignatureMethod::HmacSha1, - ) - .token(token, token_secret) - .finish(); - - let mut req = Request::new(method, url.parse().unwrap()); - - { - let headers = req.headers_mut(); - headers.set(Authorization(header.to_string())); - headers.set(Accept("*/*".to_owned())); - }; - -// println!("Request built: {:?}", req); - req -} - -fn main() { - - //Track words -// let url = "https://stream.twitter.com/1.1/statuses/filter.json"; -// let url = "https://stream.twitter.com/1.1/statuses/sample.json"; - - println!("starting!"); - - let (ui_tx, mut ui_rx) = chan::sync::>(0); - - let mut twete_rx = connect_twitter_stream(); - - std::thread::spawn(move || { - loop { - let mut line = String::new(); - std::io::stdin().read_line(&mut line).unwrap(); - ui_tx.send(line.into_bytes()); - } - }); - - // 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 - // and rustc says nooooo - // - // even though it's not ever present before here - println!("Loading cache..."); - - let mut tweeter = tw::TwitterCache::load_cache(); - - println!("Loaded cache!"); - - 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(); - - let secondary_client = Client::configure() - .connector(secondary_connector) - .build(handle); - - let mut queryer = Queryer { - client: secondary_client, - core: c2 - }; - - loop { - match do_ui(ui_rx, twete_rx, &mut tweeter, &mut queryer) { - Some((new_ui_rx, new_twete_rx)) => { - ui_rx = new_ui_rx; - twete_rx = new_twete_rx; - }, - None => { - break; - } - } - } - - println!("Bye bye"); -} - -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>)> { - 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(); -// println!("{}", jsonstr); - /* 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 => { - println!("Twitter stream hung up..."); - chan_select! { - ui_rx_b.recv() -> input => match input { - Some(line) => { - if line == "reconnect\n".as_bytes() { - return Some((ui_rx_orig.clone(), connect_twitter_stream())); - } else { - handle_user_input(line, &mut tweeter, &mut queryer); - } - } - None => std::process::exit(0) - } - } - } - }, - ui_rx_a.recv() -> user_input => match user_input { - Some(line) => { - handle_user_input(line, &mut tweeter, &mut queryer); - }, - None => println!("UI thread hung up...") - } - } - } -} - -fn url_encode(s: &str) -> String { - s - .replace("%", "%25") - .replace("+", "%2b") - .replace(" ", "+") - .replace("\\n", "%0a") - .replace("\\r", "%0d") - .replace("\\esc", "%1b") - .replace("!", "%21") - .replace("#", "%23") - .replace("&", "%26") - .replace("'", "%27") - .replace("(", "%28") - .replace(")", "%29") - .replace("*", "%2a") - .replace(",", "%2c") - .replace("-", "%2d") - .replace(".", "%2e") - .replace("/", "%2f") - .replace(":", "%3a") - .replace(";", "%3b") - .replace(">", "%3e") - .replace("<", "%3c") - .replace("?", "%3f") - .replace("@", "%40") - .replace("[", "%5b") - .replace("\\", "%5c") - .replace("]", "%5d") -} - -mod commands; -use commands::Command; - -// is there a nice way to make this accept commands: Iterable<&'a Command>? eg either a Vec or an -// array or whatever? -// (extra: WITHOUT having to build an iterator?) -// ((extra 2: when compiled with -O3, how does `commands` iteration look? same as array?)) -fn parse_word_command<'a, 'b>(line: &'b str, commands: &[&'a Command]) -> Option<(&'b str, &'a Command)> { - for cmd in commands.into_iter() { - if cmd.params == 0 { - if line == cmd.keyword { - return Some(("", &cmd)); - } - } else if line.starts_with(cmd.keyword) { - // let inner_twid = u64::from_str(&linestr.split(" ").collect::>()[1]).unwrap(); - return Some((line.get((cmd.keyword.len() + 1)..).unwrap().trim(), &cmd)); - } - } - return None -} - -fn handle_user_input(line: Vec, tweeter: &mut tw::TwitterCache, mut queryer: &mut Queryer) { - let command_bare = String::from_utf8(line).unwrap(); - let command = command_bare.trim(); - if let Some((line, cmd)) = parse_word_command(&command, commands::COMMANDS) { - (cmd.exec)(line.to_owned(), tweeter, &mut queryer); - } else { - println!("I don't know what {} means", command); - } - println!(""); // temporaryish because there's no visual distinction between output atm -} - -fn connect_twitter_stream() -> chan::Receiver> { - let (twete_tx, twete_rx) = chan::sync::>(0); - - std::thread::spawn(move || { - let mut core = Core::new().unwrap(); - - let connector = HttpsConnector::new(1, &core.handle()).unwrap(); - - let client = Client::configure() - .keep_alive(true) - .connector(connector) - .build(&core.handle()); - - // println!("{}", do_web_req("https://caps.twitter.com/v2/capi/passthrough/1?twitter:string:card_uri=card://887655800482787328&twitter:long:original_tweet_id=887655800981925888&twitter:string:response_card_name=poll3choice_text_only&twitter:string:cards_platform=Web-12", &client, &mut core).unwrap()); - // println!("{}", look_up_tweet("887655800981925888", &client, &mut core).unwrap()); - - let req = signed_api_get(STREAMURL); - let work = client.request(req).and_then(|res| { - let status = res.status(); - if status != hyper::StatusCode::Ok { - println!("Twitter stream connect was abnormal: {}", status); - println!("result: {:?}", res); - } - LineStream::new(res.body() - .map(|chunk| futures::stream::iter_ok(chunk.into_iter())) - .flatten()) - .for_each(|s| { - if s.len() != 1 { - twete_tx.send(s); - }; - Ok(()) - }) - }); - - let resp = core.run(work); - match resp { - Ok(_good) => (), - Err(e) => println!("Error in setting up: {}", e) - } - }); - - twete_rx -} diff --git a/src/commands/del.rs b/src/commands/del.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/commands/fav.rs b/src/commands/fav.rs new file mode 100644 index 0000000..3e2b00d --- /dev/null +++ b/src/commands/fav.rs @@ -0,0 +1,35 @@ +use tw; +use ::Queryer; + +use commands::Command; + +use std::str::FromStr; + +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 UNFAV: Command = Command { + keyword: "unfav", + params: 1, + exec: unfav +}; + +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)); +} + +pub static FAV: Command = Command { + keyword: "fav", + params: 1, + exec: fav +}; + +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)); +} diff --git a/src/commands/look_up.rs b/src/commands/look_up.rs new file mode 100644 index 0000000..d04f984 --- /dev/null +++ b/src/commands/look_up.rs @@ -0,0 +1,32 @@ +use tw; +use ::Queryer; + +use commands::Command; + +pub static LOOK_UP_USER: Command = Command { + keyword: "look_up_user", + params: 1, + exec: look_up_user +}; + +fn look_up_user(line: String, tweeter: &mut tw::TwitterCache, mut queryer: &mut Queryer) { + if let Some(user) = tweeter.fetch_user(&line, &mut queryer) { + println!("{:?}", user); + } else { +// println!("Couldn't retrieve {}", userid); + } +} + +pub static LOOK_UP_TWEET: Command = Command { + keyword: "look_up_tweet", + params: 1, + exec: look_up_tweet +}; + +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); + } else { +// println!("Couldn't retrieve {}", tweetid); + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 0000000..fc66bec --- /dev/null +++ b/src/commands/mod.rs @@ -0,0 +1,46 @@ +use tw; +use ::Queryer; + +pub struct Command { + pub keyword: &'static str, + pub params: u8, + pub exec: fn(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) +} + +mod show_cache; +mod twete; +mod look_up; +mod view; +mod quit; +mod fav; + +pub static COMMANDS: &[&Command] = &[ + &show_cache::SHOW_CACHE, + &quit::QUIT, + &look_up::LOOK_UP_USER, + &look_up::LOOK_UP_TWEET, + &view::VIEW, + &fav::UNFAV, + &fav::FAV, + &twete::DEL, + &twete::TWETE, + &twete::QUOTE, + &twete::RETWETE, + &twete::REP, + &twete::THREAD + /* + &QUIT, + &LOOK_UP_USER, + &LOOK_UP_TWEET, + &VIEW, + &UNFAV, + &FAV, + &DEL, + &TWETE, + "E, + &RETWETE, + &REP, + &THREAD + ]; + */ +]; diff --git a/src/commands/quit.rs b/src/commands/quit.rs new file mode 100644 index 0000000..982c48f --- /dev/null +++ b/src/commands/quit.rs @@ -0,0 +1,18 @@ +use tw; +use ::Queryer; + +use commands::Command; + +use std::process::exit; + +pub static QUIT: Command = Command { + keyword: "q", + params: 0, + exec: quit +}; + +fn quit(_line: String, tweeter: &mut tw::TwitterCache, _queryer: &mut Queryer) { + println!("Bye bye!"); + tweeter.store_cache(); + exit(0); +} diff --git a/src/commands/show_cache.rs b/src/commands/show_cache.rs new file mode 100644 index 0000000..3c31697 --- /dev/null +++ b/src/commands/show_cache.rs @@ -0,0 +1,31 @@ +use tw; +use ::Queryer; + +use commands::Command; + +pub static SHOW_CACHE: Command = Command { + keyword: "show_cache", + params: 0, + exec: show_cache +}; + +fn show_cache(line: String, tweeter: &mut tw::TwitterCache, mut queryer: &mut Queryer) { + println!("----* USERS *----"); + for (uid, user) in &tweeter.users { + println!("User: {} -> {:?}", uid, user); + } + println!("----* TWEETS *----"); + for (tid, tweet) in &tweeter.tweets { + println!("Tweet: {} -> {:?}", tid, tweet); + } + println!("----* FOLLOWERS *----"); + for uid in &tweeter.followers.clone() { + let user_res = tweeter.fetch_user(uid, &mut queryer); + match user_res { + Some(user) => { + println!("Follower: {} - {:?}", uid, user); + } + None => { println!(" ..."); } + } + } +} diff --git a/src/commands/twete.rs b/src/commands/twete.rs new file mode 100644 index 0000000..ecc3f98 --- /dev/null +++ b/src/commands/twete.rs @@ -0,0 +1,184 @@ +use tw; +use ::Queryer; + +use commands::Command; + +use std::str::FromStr; + +static DEL_TWEET_URL: &str = "https://api.twitter.com/1.1/statuses/destroy"; +static RT_TWEET_URL: &str = "https://api.twitter.com/1.1/statuses/retweet"; +static CREATE_TWEET_URL: &str = "https://api.twitter.com/1.1/statuses/update.json"; + +pub static DEL: Command = Command { + keyword: "del", + params: 1, + exec: del +}; + +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(); + queryer.do_api_post(&format!("{}/{}.json", DEL_TWEET_URL, twete.id)); +} + +pub static TWETE: Command = Command { + keyword: "t", + params: 1, + exec: twete +}; + +fn twete(line: String, _tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { + let text = line.trim(); + let substituted = ::url_encode(text); + println!("msg len: {}", text.len()); + println!("excessively long? {}", text.len() > 140); + if text.len() > 140 { + queryer.do_api_post(&format!("{}?status={}", CREATE_TWEET_URL, substituted)); + } else { + queryer.do_api_post(&format!("{}?status={}&weighted_character_count=true", CREATE_TWEET_URL, substituted)); + } +// println!("{}", &format!("{}?status={}", CREATE_TWEET_URL, substituted)); +} + +pub static THREAD: Command = Command { + keyword: "thread", + params: 2, + exec: thread +}; + +fn thread(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { + 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); + let reply = reply_bare.trim(); + 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) { + 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 { + let substituted = ::url_encode(reply); + queryer.do_api_post(&format!("{}?status={}&in_reply_to_status_id={}", CREATE_TWEET_URL, substituted, twete.id)); + } else { + println!("you can only thread your own tweets"); + // ask if it should .@ instead? + } + let substituted = ::url_encode(reply); + queryer.do_api_post(&format!("{}?status={}&in_reply_to_status_id={}", CREATE_TWEET_URL, substituted, twete.id)); + } + } + } else { + println!("thread your sik reply"); + } + } else { + println!("thread your sik reply"); + } +} + +pub static REP: Command = Command { + keyword: "rep", + params: 2, + exec: rep +}; + +fn rep(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { + 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); + let reply = reply_bare.trim(); + 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) { + // 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(); + /* + for handle in twete.get_mentions() { + ats.insert(handle); + } + */ + 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)) { + 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)) { + 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); + } + //let ats_vec: Vec<&str> = ats.into_iter().collect(); + //let full_reply = format!("{} {}", ats_vec.join(" "), reply); + let decorated_ats: Vec = ats.into_iter().map(|x| format!("@{}", x)).collect(); + let full_reply = format!("{} {}", decorated_ats.join(" "), reply); + let substituted = ::url_encode(&full_reply); +// println!("{}", (&format!("{}?status={}&in_reply_to_status_id={}", CREATE_TWEET_URL, substituted, twete.id))); + queryer.do_api_post(&format!("{}?status={}&in_reply_to_status_id={}", CREATE_TWEET_URL, substituted, twete.id)); + } + } + } else { + println!("rep your sik reply"); + } + } else { + println!("rep your sik reply"); + } +} + +pub static QUOTE: Command = Command { + keyword: "qt", + params: 2, + exec: quote +}; + +fn quote(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer) { + 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); + let reply = reply_bare.trim(); + 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) { + let substituted = ::url_encode(reply); + let attachment_url = ::url_encode( + &format!( + "https://www.twitter.com/{}/status/{}", + tweeter.retrieve_user(&twete.author_id).unwrap().handle, + twete.id + ) + ); + println!("{}", substituted); + queryer.do_api_post( + &format!("{}?status={}&attachment_url={}", + CREATE_TWEET_URL, + substituted, + attachment_url + ) + ); + } + } + } else { + println!("rep your sik reply"); + } + } else { + println!("rep your sik reply"); + } +} + +pub static RETWETE: Command = Command { + keyword: "rt", + params: 1, + exec: retwete +}; + +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(); + queryer.do_api_post(&format!("{}/{}.json", RT_TWEET_URL, twete.id)); +} + diff --git a/src/commands/view.rs b/src/commands/view.rs new file mode 100644 index 0000000..d01ff1b --- /dev/null +++ b/src/commands/view.rs @@ -0,0 +1,22 @@ +use tw; +use ::Queryer; + +use commands::Command; + +use std::str::FromStr; + +use display; + +pub static VIEW: Command = Command { + keyword: "view", + params: 1, + exec: view +}; + +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(); + display::render_twete(&twete.id, tweeter); + println!("link: https://twitter.com/i/web/status/{}", twete.id); +} diff --git a/src/display/mod.rs b/src/display/mod.rs new file mode 100644 index 0000000..24f7e33 --- /dev/null +++ b/src/display/mod.rs @@ -0,0 +1,147 @@ +extern crate termion; + +use self::termion::color; + +use ::tw; + +use std; + +fn color_for(handle: &String) -> termion::color::Fg<&color::Color> { + let color_map: Vec<&color::Color> = vec![ + &color::Blue, + &color::Cyan, + &color::Green, + &color::LightBlue, + &color::LightCyan, + &color::LightGreen, + &color::LightMagenta, + &color::LightYellow, + &color::Magenta, + &color::Yellow + ]; + + let mut quot_hash_quot = std::num::Wrapping(0); + for b in handle.as_bytes().iter() { + quot_hash_quot = quot_hash_quot + std::num::Wrapping(*b); + } + color::Fg(color_map[quot_hash_quot.0 as usize % color_map.len()]) +} + +pub trait Render { + fn render(self, tweeter: &::tw::TwitterCache); +} + +impl Render for tw::events::Event { + fn render(self, tweeter: &::tw::TwitterCache) { + match self { + 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) { + println!("-------------DELETED------------------"); + render_twete(&twete_id, tweeter); + println!("-------------DELETED------------------"); + } else { + println!("dunno what, but do know who: {} - {}", user_id, handle); + } + } else { + println!("delete..."); + println!("dunno who..."); + } + }, + tw::events::Event::RT_RT { user_id, twete_id } => { + println!("---------------------------------"); + let user = tweeter.retrieve_user(&user_id).unwrap(); + println!(" +rt_rt : {} (@{})", user.name, user.handle); + render_twete(&twete_id, tweeter); + }, + tw::events::Event::Fav_RT { user_id, twete_id } => { + println!("---------------------------------"); + let user = tweeter.retrieve_user(&user_id).unwrap(); + println!(" +rt_fav : {} (@{})", user.name, user.handle); + render_twete(&twete_id, tweeter); + }, + tw::events::Event::Fav { user_id, twete_id } => { + println!("---------------------------------"); + let user = tweeter.retrieve_user(&user_id).unwrap(); + println!("{} +fav : {} (@{}){}", color::Fg(color::Yellow), user.name, user.handle, color::Fg(color::Reset)); + render_twete(&twete_id, tweeter); + }, + tw::events::Event::Unfav { user_id, twete_id } => { + println!("---------------------------------"); + let user = tweeter.retrieve_user(&user_id).unwrap(); + println!("{} -fav : {} (@{}){}", color::Fg(color::Yellow), user.name, user.handle, color::Fg(color::Reset)); + render_twete(&twete_id, tweeter); + }, + tw::events::Event::Followed { user_id } => { + println!("---------------------------------"); + let user = tweeter.retrieve_user(&user_id).unwrap(); + println!(" +fl : {} (@{})", user.name, user.handle); + }, + tw::events::Event::Unfollowed { user_id } => { + println!("---------------------------------"); + let user = tweeter.retrieve_user(&user_id).unwrap(); + println!(" -fl : {} (@{})", user.name, user.handle); + } + /* + Blocked(user_id) => { + }, + */ + } + println!(""); + } +} + +pub fn render_twete(twete_id: &String, tweeter: &tw::TwitterCache) { + let id_color = color::Fg(color::Rgb(180, 80, 40)); + let twete = tweeter.retrieve_tweet(twete_id).unwrap(); + // if we got the tweet, the API gave us the user too + let user = tweeter.retrieve_user(&twete.author_id).unwrap(); + match twete.rt_tweet { + Some(ref rt_id) => { + // same for a retweet + let rt = tweeter.retrieve_tweet(rt_id).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, color::Fg(color::Reset) + ); + println!(" {}{}{} ({}@{}{}) via {}{}{} ({}@{}{}) RT:", + color_for(&rt_author.handle), rt_author.name, color::Fg(color::Reset), + color_for(&rt_author.handle), rt_author.handle, color::Fg(color::Reset), + color_for(&user.handle), user.name, color::Fg(color::Reset), + color_for(&user.handle), user.handle, color::Fg(color::Reset) + ); + } + None => { + println!("{} id:{}{}", + id_color, twete.internal_id, color::Fg(color::Reset) + ); + println!(" {}{}{} ({}@{}{})", + color_for(&user.handle), user.name, color::Fg(color::Reset), + color_for(&user.handle), user.handle, color::Fg(color::Reset) + ); + } + } + + 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) { + let qt_author = tweeter.retrieve_user(&qt.author_id).unwrap(); + println!("{} id:{}{}", + id_color, qt.internal_id, color::Fg(color::Reset) + ); + println!( + " {}{}{} ({}@{}{})", + color_for(&qt_author.handle), qt_author.name, color::Fg(color::Reset), + color_for(&qt_author.handle), qt_author.handle, color::Fg(color::Reset) + ); + println!( + " {}", + qt.text.replace("\r", "\\r").split("\n").collect::>().join("\n ") + ); + } else { + println!(" << don't have quoted tweet! >>"); + } + } +} diff --git a/src/linestream.rs b/src/linestream.rs new file mode 100644 index 0000000..5106af3 --- /dev/null +++ b/src/linestream.rs @@ -0,0 +1,42 @@ +use std; +use futures::stream::Stream; +use futures::{Poll, Async}; + +pub struct LineStream where S: Stream { + stream: S, + progress: Vec +} + +impl LineStream where S: Stream + Sized { + pub fn new(stream: S) -> LineStream { + LineStream { + stream: stream, + progress: vec![] + } + } +} + +impl Stream for LineStream where S: Stream { + type Item = Vec; + type Error = E; + + fn poll(&mut self) -> Poll, Self::Error> { + loop { + match self.stream.poll() { + Ok(Async::Ready(Some(byte))) => { + if byte == 0x0a { + let mut new_vec = vec![]; + std::mem::swap(&mut self.progress, &mut new_vec); + return Ok(Async::Ready(Some(new_vec))) + } else { + self.progress.push(byte) + } + }, + Ok(Async::Ready(None)) => return Ok(Async::Ready(None)), + Ok(Async::NotReady) => return Ok(Async::NotReady), + Err(e) => return Err(e) + } + } + } +} + diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..37082a1 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,382 @@ +#![feature(vec_remove_item)] +extern crate serde_json; + +use std::str; +//use std::io::BufRead; + +#[macro_use] extern crate chan; + +extern crate url; +#[macro_use] extern crate hyper; +#[macro_use] extern crate serde_derive; +extern crate oauthcli; +extern crate tokio_core; +extern crate futures; +extern crate hyper_tls; + +use hyper::{Client, Method, Request}; +//use std::collections::{HashMap, HashSet}; +use tokio_core::reactor::Core; +use futures::future::Future; +use futures::Stream; +//use hyper::client::FutureResponse; +use hyper_tls::HttpsConnector; +//use json_streamer::JsonObjectStreamer; + +mod linestream; +use linestream::LineStream; + +mod tw; +mod display; + +//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"; +static ACCOUNT_SETTINGS_URL: &str = "https://api.twitter.com/1.1/account/settings.json"; + +header! { (Authorization, "Authorization") => [String] } +header! { (Accept, "Accept") => [String] } +header! { (ContentType, "Content-Type") => [String] } +header! { (Cookie, "cookie") => [String] } + + +pub struct Queryer { + client: hyper::client::Client>, + core: Core +} + +impl Queryer { + fn do_api_get(&mut self, url: &str) -> Option { + self.issue_request(signed_api_get(url)) + } + fn do_api_post(&mut self, url: &str) -> Option { + self.issue_request(signed_api_post(url)) + } + /* + fn do_web_req(&mut self, url: &str) -> Option { + self.issue_request(signed_web_get(url)) + }*/ + // TODO: make this return the status as well! + fn issue_request(&mut self, req: hyper::client::Request) -> Option { + let lookup = self.client.request(req); + + let resp: hyper::Response = self.core.run(lookup).unwrap(); + let status = resp.status().clone(); + + 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 { + println!("!! Requests returned status: {}", status); + println!("{}", value); + None + } else { + Some(value) + } + } + Err(e) => { + if status != hyper::StatusCode::Ok { + println!("!! Requests returned status: {}", status); + } + println!("error deserializing json: {}", e); + None + } + } + } +} + +/* +fn signed_web_get(url: &str) -> hyper::client::Request { +// let params: Vec<(String, String)> = vec![("track".to_string(), "london".to_string())]; + 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::authorization_header( + "GET", + url::Url::parse(url).unwrap(), + None, // Realm + consumer_key, + consumer_secret, + Some(token), + Some(token_secret), + oauthcli::SignatureMethod::HmacSha1, + &oauthcli::timestamp(), + &oauthcli::nonce(), + None, // oauth_callback + None, // oauth_verifier + params.clone().into_iter() + ); + + let mut req = Request::new(Method::Get, url.parse().unwrap()); + + req.set_body(param_string); + + { + let mut headers = req.headers_mut(); + headers.set(Cookie(format!("auth_token={}", lol_auth_token))); + headers.set(Accept("* / *".to_owned())); + headers.set(ContentType("application/x-www-form-urlencoded".to_owned())); + }; + + req +} +*/ + +fn signed_api_post(url: &str) -> hyper::client::Request { + signed_api_req(url, Method::Post) +} + +fn signed_api_get(url: &str) -> hyper::client::Request { + signed_api_req(url, Method::Get) +} + +fn signed_api_req(url: &str, method: Method) -> hyper::client::Request { +// let params: Vec<(String, String)> = vec![("track".to_string(), "london".to_string())]; + let method_string = match method { + Method::Get => "GET", + Method::Post => "POST", + _ => panic!(format!("unsupported method {}", method)) + }; + + 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( + method_string, + &url::Url::parse(url).unwrap(), + consumer_key, + consumer_secret, + oauthcli::SignatureMethod::HmacSha1, + ) + .token(token, token_secret) + .finish(); + + let mut req = Request::new(method, url.parse().unwrap()); + + { + let headers = req.headers_mut(); + headers.set(Authorization(header.to_string())); + headers.set(Accept("*/*".to_owned())); + }; + +// println!("Request built: {:?}", req); + req +} + +fn main() { + + //Track words +// let url = "https://stream.twitter.com/1.1/statuses/filter.json"; +// let url = "https://stream.twitter.com/1.1/statuses/sample.json"; + + println!("starting!"); + + let (ui_tx, mut ui_rx) = chan::sync::>(0); + + let mut twete_rx = connect_twitter_stream(); + + std::thread::spawn(move || { + loop { + let mut line = String::new(); + std::io::stdin().read_line(&mut line).unwrap(); + ui_tx.send(line.into_bytes()); + } + }); + + // 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 + // and rustc says nooooo + // + // even though it's not ever present before here + println!("Loading cache..."); + + let mut tweeter = tw::TwitterCache::load_cache(); + + println!("Loaded cache!"); + + 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(); + + let secondary_client = Client::configure() + .connector(secondary_connector) + .build(handle); + + let mut queryer = Queryer { + client: secondary_client, + core: c2 + }; + + loop { + match do_ui(ui_rx, twete_rx, &mut tweeter, &mut queryer) { + Some((new_ui_rx, new_twete_rx)) => { + ui_rx = new_ui_rx; + twete_rx = new_twete_rx; + }, + None => { + break; + } + } + } + + println!("Bye bye"); +} + +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>)> { + 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(); +// println!("{}", jsonstr); + /* 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 => { + println!("Twitter stream hung up..."); + chan_select! { + ui_rx_b.recv() -> input => match input { + Some(line) => { + if line == "reconnect\n".as_bytes() { + return Some((ui_rx_orig.clone(), connect_twitter_stream())); + } else { + handle_user_input(line, &mut tweeter, &mut queryer); + } + } + None => std::process::exit(0) + } + } + } + }, + ui_rx_a.recv() -> user_input => match user_input { + Some(line) => { + handle_user_input(line, &mut tweeter, &mut queryer); + }, + None => println!("UI thread hung up...") + } + } + } +} + +fn url_encode(s: &str) -> String { + s + .replace("%", "%25") + .replace("+", "%2b") + .replace(" ", "+") + .replace("\\n", "%0a") + .replace("\\r", "%0d") + .replace("\\esc", "%1b") + .replace("!", "%21") + .replace("#", "%23") + .replace("&", "%26") + .replace("'", "%27") + .replace("(", "%28") + .replace(")", "%29") + .replace("*", "%2a") + .replace(",", "%2c") + .replace("-", "%2d") + .replace(".", "%2e") + .replace("/", "%2f") + .replace(":", "%3a") + .replace(";", "%3b") + .replace(">", "%3e") + .replace("<", "%3c") + .replace("?", "%3f") + .replace("@", "%40") + .replace("[", "%5b") + .replace("\\", "%5c") + .replace("]", "%5d") +} + +mod commands; +use commands::Command; + +// is there a nice way to make this accept commands: Iterable<&'a Command>? eg either a Vec or an +// array or whatever? +// (extra: WITHOUT having to build an iterator?) +// ((extra 2: when compiled with -O3, how does `commands` iteration look? same as array?)) +fn parse_word_command<'a, 'b>(line: &'b str, commands: &[&'a Command]) -> Option<(&'b str, &'a Command)> { + for cmd in commands.into_iter() { + if cmd.params == 0 { + if line == cmd.keyword { + return Some(("", &cmd)); + } + } else if line.starts_with(cmd.keyword) { + // let inner_twid = u64::from_str(&linestr.split(" ").collect::>()[1]).unwrap(); + return Some((line.get((cmd.keyword.len() + 1)..).unwrap().trim(), &cmd)); + } + } + return None +} + +fn handle_user_input(line: Vec, tweeter: &mut tw::TwitterCache, mut queryer: &mut Queryer) { + let command_bare = String::from_utf8(line).unwrap(); + let command = command_bare.trim(); + if let Some((line, cmd)) = parse_word_command(&command, commands::COMMANDS) { + (cmd.exec)(line.to_owned(), tweeter, &mut queryer); + } else { + println!("I don't know what {} means", command); + } + println!(""); // temporaryish because there's no visual distinction between output atm +} + +fn connect_twitter_stream() -> chan::Receiver> { + let (twete_tx, twete_rx) = chan::sync::>(0); + + std::thread::spawn(move || { + let mut core = Core::new().unwrap(); + + let connector = HttpsConnector::new(1, &core.handle()).unwrap(); + + let client = Client::configure() + .keep_alive(true) + .connector(connector) + .build(&core.handle()); + + // println!("{}", do_web_req("https://caps.twitter.com/v2/capi/passthrough/1?twitter:string:card_uri=card://887655800482787328&twitter:long:original_tweet_id=887655800981925888&twitter:string:response_card_name=poll3choice_text_only&twitter:string:cards_platform=Web-12", &client, &mut core).unwrap()); + // println!("{}", look_up_tweet("887655800981925888", &client, &mut core).unwrap()); + + let req = signed_api_get(STREAMURL); + let work = client.request(req).and_then(|res| { + let status = res.status(); + if status != hyper::StatusCode::Ok { + println!("Twitter stream connect was abnormal: {}", status); + println!("result: {:?}", res); + } + LineStream::new(res.body() + .map(|chunk| futures::stream::iter_ok(chunk.into_iter())) + .flatten()) + .for_each(|s| { + if s.len() != 1 { + twete_tx.send(s); + }; + Ok(()) + }) + }); + + let resp = core.run(work); + match resp { + Ok(_good) => (), + Err(e) => println!("Error in setting up: {}", e) + } + }); + + twete_rx +} diff --git a/src/tw/events.rs b/src/tw/events.rs new file mode 100644 index 0000000..0541b0a --- /dev/null +++ b/src/tw/events.rs @@ -0,0 +1,44 @@ +extern crate serde_json; + +pub enum Event { + Deleted { user_id: String, twete_id: String }, + RT_RT { user_id: String, twete_id: String }, + Fav_RT { user_id: String, twete_id: String }, + Fav { user_id: String, twete_id: String }, + Unfav { user_id: String, twete_id: String }, + Followed { user_id: String }, + Unfollowed { user_id: String } +} + +impl Event { + pub fn from_json(structure: serde_json::Map) -> Option { + match &structure["event"].as_str().unwrap() { + &"follow" => Some(Event::Followed { + user_id: structure["source"]["id_str"].as_str().unwrap().to_owned() + }), + &"unfollow" => Some(Event::Unfollowed { + user_id: structure["source"]["id_str"].as_str().unwrap().to_owned() + }), + &"favorite" => Some(Event::Fav { + user_id: structure["source"]["id_str"].as_str().unwrap().to_owned(), + twete_id: structure["target_object"]["id_str"].as_str().unwrap().to_owned() + }), + &"unfavorite" => Some(Event::Unfav { + user_id: structure["source"]["id_str"].as_str().unwrap().to_owned(), + twete_id: structure["target_object"]["id_str"].as_str().unwrap().to_owned() + }), + &"favorited_retweet" => Some(Event::Fav_RT { + user_id: structure["source"]["id_str"].as_str().unwrap().to_owned(), + twete_id: structure["target_object"]["id_str"].as_str().unwrap().to_owned() + }), + &"retweeted_retweet" => Some(Event::RT_RT { + user_id: structure["source"]["id_str"].as_str().unwrap().to_owned(), + twete_id: structure["target_object"]["id_str"].as_str().unwrap().to_owned() + }), +// &"blocked" => Blocked { }, +// &"unblocked" => Unblocked { }, +// &"quoted_tweet" => ???, + e => { println!("unrecognized event: {}", e); None } + } + } +} diff --git a/src/tw/mod.rs b/src/tw/mod.rs new file mode 100644 index 0000000..eff38e7 --- /dev/null +++ b/src/tw/mod.rs @@ -0,0 +1,440 @@ +use std::path::Path; +use std::fs::File; +use std::io::{BufRead, BufReader, Read}; +extern crate chrono; + +use self::chrono::prelude::*; + +use std::collections::{HashMap, HashSet}; +extern crate serde_json; +use std::io::Write; + +use std::fs::OpenOptions; + +pub mod events; + +use display::Render; +use display; + +pub mod tweet; +use self::tweet::Tweet; +pub mod user; +use self::user::User; + +pub fn full_twete_text(twete: &serde_json::map::Map) -> String { + if twete.contains_key("retweeted_status") { + return full_twete_text(twete["retweeted_status"].as_object().unwrap()) + } + let mut twete_text: String; + twete_text = if twete["truncated"].as_bool().unwrap() { + twete["extended_tweet"]["full_text"].as_str().unwrap().to_string() + } else { + twete["text"].as_str().unwrap().to_string() + }; + + let quoted_tweet_id = twete.get("quoted_tweet_id_str").and_then(|x| x.as_str()); + + twete_text = twete_text + .replace("&", "&") + .replace(">", ">") + .replace("<", "<"); + + for url in twete["entities"]["urls"].as_array().unwrap() { + let display_url = url["url"].as_str().unwrap(); + let expanded_url = url["expanded_url"].as_str().unwrap(); + if expanded_url.len() < 200 { + if let Some(twid) = quoted_tweet_id { + if expanded_url.ends_with(twid) { + twete_text = twete_text.replace(display_url, ""); + continue; + } + } + twete_text = twete_text.replace(display_url, expanded_url); + } + } + + twete_text +} + +#[derive(Serialize, Deserialize)] +pub struct TwitterCache { + #[serde(skip)] + pub users: HashMap, + #[serde(skip)] + pub tweets: HashMap, + following: HashSet, + following_history: HashMap, // userid:date?? + pub followers: HashSet, + lost_followers: HashSet, + follower_history: HashMap, // userid:date?? + #[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 +} + +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.. + + fn new() -> TwitterCache { + TwitterCache { + users: HashMap::new(), + tweets: HashMap::new(), + following: HashSet::new(), + following_history: HashMap::new(), + 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() + } + } + fn new_without_caching() -> TwitterCache { + let mut cache = TwitterCache::new(); + cache.caching_permitted = false; + cache + } + fn cache_user(&mut self, user: User) { + if !self.users.contains_key(&user.id) { + let mut file = + OpenOptions::new() + .create(true) + .append(true) + .open(TwitterCache::USERS_CACHE) + .unwrap(); + writeln!(file, "{}", serde_json::to_string(&user).unwrap()).unwrap(); + self.users.insert(user.id.to_owned(), user); + } + } + + fn cache_tweet(&mut self, tweet: Tweet) { + if !self.tweets.contains_key(&tweet.id) { + + let mut file = + OpenOptions::new() + .create(true) + .append(true) + .open(TwitterCache::TWEET_CACHE) + .unwrap(); + writeln!(file, "{}", serde_json::to_string(&tweet).unwrap()).unwrap(); + self.number_and_insert_tweet(tweet); + } + } + pub fn store_cache(&self) { + if Path::new(TwitterCache::PROFILE_DIR).is_dir() { + let profile = OpenOptions::new() + .write(true) + .create(true) + .append(false) + .open(TwitterCache::PROFILE_CACHE) + .unwrap(); + serde_json::to_writer(profile, self).unwrap(); + } else { + println!("No cache dir exists..."); + } + // store cache + } + fn number_and_insert_tweet(&mut self, mut tw: Tweet) { + 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.tweets.insert(tw.id.to_owned(), tw); + } + } + } + pub fn load_cache() -> TwitterCache { + if Path::new(TwitterCache::PROFILE_CACHE).is_file() { + let mut buf = vec![]; + let mut profile = File::open(TwitterCache::PROFILE_CACHE).unwrap(); + match profile.read_to_end(&mut buf) { + Ok(_sz) => { + match serde_json::from_slice(&buf) { + Ok(result) => { + let mut cache: TwitterCache = result; + cache.tweets = HashMap::new(); + for line in BufReader::new(File::open(TwitterCache::TWEET_CACHE).unwrap()).lines() { + let t: Tweet = serde_json::from_str(&line.unwrap()).unwrap(); + cache.number_and_insert_tweet(t); + } + for line in BufReader::new(File::open(TwitterCache::USERS_CACHE).unwrap()).lines() { + let u: User = serde_json::from_str(&line.unwrap()).unwrap(); + cache.users.insert(u.id.to_owned(), u); + } + cache.caching_permitted = true; + cache.needs_save = false; + cache + } + Err(e) => { + // TODO! should be able to un-frick profile after startup. + println!("Error reading profile, profile caching disabled... {}", e); + TwitterCache::new_without_caching() + } + } + } + Err(e) => { + println!("Error reading cached profile: {}. Profile caching disabled.", e); + TwitterCache::new_without_caching() + } + } + } else { + println!("Hello! First time setup?"); + TwitterCache::new() + } + } + pub fn cache_api_tweet(&mut self, json: serde_json::Value) { + if let Some((rt, rt_user)) = json.get("retweeted_status").and_then(|x| Tweet::from_api_json(x.to_owned())) { + self.cache_user(rt_user); + self.cache_tweet(rt); + } + + if let Some((qt, qt_user)) = json.get("quoted_status").and_then(|x| Tweet::from_api_json(x.to_owned())) { + self.cache_user(qt_user); + self.cache_tweet(qt); + } + + if let Some((twete, user)) = Tweet::from_api_json(json) { + self.cache_user(user); + self.cache_tweet(twete); + } + } + pub fn cache_api_user(&mut self, json: serde_json::Value) { + if let Some(user) = User::from_json(json) { + self.cache_user(user); + } + } + pub fn cache_api_event(&mut self, json: serde_json::Map, mut queryer: &mut ::Queryer) { + /* don't really care to hold on to who fav, unfav, ... when, just pick targets out. */ + match json.get("event").and_then(|x| x.as_str()) { + Some("favorite") => { + self.cache_api_tweet(json["target_object"].clone()); + self.cache_api_user(json["source"].clone()); + self.cache_api_user(json["target"].clone()); + }, + Some("unfavorite") => { + self.cache_api_tweet(json["target_object"].clone()); + self.cache_api_user(json["source"].clone()); + self.cache_api_user(json["target"].clone()); + }, + Some("retweeted_retweet") => ()/* cache rt */, + Some("favorited_retweet") => ()/* cache rt */, + Some("delete") => { + let user_id = json["delete"]["status"]["user_id_str"].as_str().unwrap().to_string(); + self.fetch_user(&user_id, &mut queryer); + }, + Some("follow") => { + let follower = json["source"]["id_str"].as_str().unwrap().to_string(); + let followed = json["target"]["id_str"].as_str().unwrap().to_string(); + self.cache_api_user(json["target"].clone()); + self.cache_api_user(json["source"].clone()); + if follower == "iximeow" { + // self.add_follow( + } else { + self.add_follower(&follower); + } + }, + Some("unfollow") => { + let follower = json["source"]["id_str"].as_str().unwrap().to_string(); + let followed = json["target"]["id_str"].as_str().unwrap().to_string(); + self.cache_api_user(json["target"].clone()); + self.cache_api_user(json["source"].clone()); + if follower == "iximeow" { + // self.add_follow( + } else { + self.remove_follower(&follower); + } + }, + Some(_) => () /* an uninteresting event */, + None => () // not really an event? should we log something? + /* 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_user(&self, user_id: &String) -> Option<&User> { + self.users.get(user_id) + } + pub fn fetch_tweet(&mut self, tweet_id: &String, mut queryer: &mut ::Queryer) -> Option<&Tweet> { + if !self.tweets.contains_key(tweet_id) { + match self.look_up_tweet(tweet_id, &mut queryer) { + Some(json) => self.cache_api_tweet(json), + None => println!("Unable to retrieve tweet {}", tweet_id) + }; + } + self.tweets.get(tweet_id) + } + pub fn fetch_user(&mut self, user_id: &String, mut queryer: &mut ::Queryer) -> Option<&User> { + if !self.users.contains_key(user_id) { + let maybe_parsed = self.look_up_user(user_id, &mut queryer).and_then(|x| User::from_json(x)); + match maybe_parsed { + Some(tw) => self.cache_user(tw), + None => println!("Unable to retrieve user {}", user_id) + }; + } + self.users.get(user_id) + } + pub fn set_following(&mut self, user_ids: Vec) { + let uid_set = user_ids.into_iter().collect::>(); + + let new_uids = &uid_set - &self.following; + for user in &new_uids { + println!("New following! {}", user); + self.add_following(user); + } + + let lost_uids = &self.following - &uid_set; + for user in &lost_uids { + println!("Bye, friend! {}", user); + self.remove_following(user); + } + } + pub fn set_followers(&mut self, user_ids: Vec) { + let uid_set = user_ids.into_iter().collect::>(); + + let new_uids = &uid_set - &self.followers; + for user in &new_uids { + println!("New follower! {}", user); + self.add_follower(user); + } + + let lost_uids = &self.followers - &uid_set; + for user in &lost_uids { + println!("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())); + } + 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())); + } + 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())); + } + 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())); + } + + fn look_up_user(&mut self, id: &str, queryer: &mut ::Queryer) -> Option { + let url = &format!("{}?user_id={}", ::USER_LOOKUP_URL, id); + queryer.do_api_get(url) + } + + fn look_up_tweet(&mut self, id: &str, queryer: &mut ::Queryer) -> Option { + let url = &format!("{}?id={}", ::TWEET_LOOKUP_URL, id); + queryer.do_api_get(url) + } + + pub fn get_settings(&self, queryer: &mut ::Queryer) -> Option { + queryer.do_api_get(::ACCOUNT_SETTINGS_URL) + } +} + +fn handle_twitter_event( + structure: serde_json::Map, + tweeter: &mut TwitterCache, + mut queryer: &mut ::Queryer) { + tweeter.cache_api_event(structure.clone(), &mut queryer); + if let Some(event) = events::Event::from_json(structure) { + event.render(&tweeter); + }; +} + +fn handle_twitter_delete( + structure: serde_json::Map, + tweeter: &mut TwitterCache, + _queryer: &mut ::Queryer) { + events::Event::Deleted { + user_id: structure["delete"]["status"]["user_id_str"].as_str().unwrap().to_string(), + twete_id: structure["delete"]["status"]["id_str"].as_str().unwrap().to_string() + }.render(tweeter); +} + +fn handle_twitter_twete( + structure: serde_json::Map, + tweeter: &mut TwitterCache, + _queryer: &mut ::Queryer) { + let twete_id = structure["id_str"].as_str().unwrap().to_string(); + tweeter.cache_api_tweet(serde_json::Value::Object(structure)); + display::render_twete(&twete_id, tweeter); +} + +fn handle_twitter_dm( + structure: serde_json::Map, + _tweeter: &mut TwitterCache, + _queryer: &mut ::Queryer) { + // show DM + println!("{}", structure["direct_message"]["text"].as_str().unwrap()); + println!("Unknown struture {:?}", structure); +} + +fn handle_twitter_welcome( + structure: serde_json::Map, + tweeter: &mut TwitterCache, + queryer: &mut ::Queryer) { +// println!("welcome: {:?}", structure); + let user_id_nums = structure["friends"].as_array().unwrap(); + let user_id_strs = user_id_nums.into_iter().map(|x| x.as_u64().unwrap().to_string()); + tweeter.set_following(user_id_strs.collect()); + 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() + }; + println!("You are {}", tweeter.current_user.handle); + } else { + println!("Unable to make API call to figure out who you are..."); + } +} + +pub fn handle_message( + twete: serde_json::Value, + tweeter: &mut TwitterCache, + queryer: &mut ::Queryer +) { + match twete { + serde_json::Value::Object(objmap) => { + if objmap.contains_key("event") { + handle_twitter_event(objmap, tweeter, queryer); + } else if objmap.contains_key("friends") { + handle_twitter_welcome(objmap, tweeter, queryer); + } else if objmap.contains_key("delete") { + handle_twitter_delete(objmap, tweeter, queryer); + } else if objmap.contains_key("user") && objmap.contains_key("id") { + handle_twitter_twete(objmap, tweeter, queryer); + } else if objmap.contains_key("direct_message") { + handle_twitter_dm(objmap, tweeter, queryer); + } + println!(""); + }, + _ => () + }; +} diff --git a/src/tw/tweet.rs b/src/tw/tweet.rs new file mode 100644 index 0000000..778461a --- /dev/null +++ b/src/tw/tweet.rs @@ -0,0 +1,78 @@ +extern crate serde_json; + +use tw::user::User; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Tweet { + pub id: String, + pub author_id: String, + pub text: String, + pub created_at: String, // lol + #[serde(skip_serializing_if="Option::is_none")] + #[serde(default = "Option::default")] + pub quoted_tweet_id: Option, + #[serde(skip_serializing_if="Option::is_none")] + #[serde(default = "Option::default")] + pub rt_tweet: Option, + #[serde(skip)] + pub internal_id: u64 +} + +impl Tweet { + pub fn get_mentions(&self) -> Vec<&str> { + self.text.split(&[ + ',', '.', '/', ';', '\'', + '[', ']', '\\', '~', '!', + '@', '#', '$', '%', '^', + '&', '*', '(', ')', '-', + '=', '{', '}', '|', ':', + '"', '<', '>', '?', '`', + ' ' // forgot this initially. awkward. + ][..]) + .filter(|x| x.starts_with("@") && x.len() > 1) + .collect() + } + + pub fn from_api_json(json: serde_json::Value) -> Option<(Tweet, User)> { + Tweet::from_json(json.clone()).and_then(|tw| { + json.get("user").and_then(|user_json| + User::from_json(user_json.to_owned()).map(|u| (tw, u)) + ) + }) + } + pub fn from_json(json: serde_json::Value) -> Option { + if let serde_json::Value::Object(json_map) = json { + let text = ::tw::full_twete_text(&json_map); + let rt_twete = json_map.get("retweeted_status") + .and_then(|x| x.get("id_str")) + .and_then(|x| x.as_str()) + .map(|x| x.to_owned()); + if json_map.contains_key("id_str") && + json_map.contains_key("user") && + json_map.contains_key("created_at") { + if let ( + Some(id_str), + Some(author_id), + Some(created_at) + ) = ( + json_map["id_str"].as_str(), + json_map["user"]["id_str"].as_str(), + json_map["created_at"].as_str() + ) { + return Some(Tweet { + id: id_str.to_owned(), + author_id: author_id.to_owned(), + text: text, + created_at: created_at.to_owned(), + quoted_tweet_id: json_map.get("quoted_status_id_str") + .and_then(|x| x.as_str()) + .map(|x| x.to_owned()), + rt_tweet: rt_twete, + internal_id: 0 + }) + } + } + } + None + } +} diff --git a/src/tw/user.rs b/src/tw/user.rs new file mode 100644 index 0000000..1da82f0 --- /dev/null +++ b/src/tw/user.rs @@ -0,0 +1,46 @@ +extern crate serde_json; + +#[derive(Debug, Serialize, Deserialize)] +pub struct User { + pub id: String, + pub name: String, + pub handle: String +} + +impl Default for User { + fn default() -> User { + User { + id: "".to_owned(), + name: "_default_".to_owned(), + handle: "_default_".to_owned() + } + } +} + +impl User { + pub fn from_json(json: serde_json::Value) -> Option { + if let serde_json::Value::Object(json_map) = json { + if json_map.contains_key("id_str") && + json_map.contains_key("name") && + json_map.contains_key("screen_name") { + if let ( + Some(id_str), + Some(name), + Some(screen_name) + ) = ( + json_map["id_str"].as_str(), + json_map["name"].as_str(), + json_map["screen_name"].as_str() + ) { + return Some(User { + id: id_str.to_owned(), + name: name.to_owned(), + handle: screen_name.to_owned() + }) + } + } + } + None + } +} + diff --git a/tw/events.rs b/tw/events.rs deleted file mode 100644 index 0541b0a..0000000 --- a/tw/events.rs +++ /dev/null @@ -1,44 +0,0 @@ -extern crate serde_json; - -pub enum Event { - Deleted { user_id: String, twete_id: String }, - RT_RT { user_id: String, twete_id: String }, - Fav_RT { user_id: String, twete_id: String }, - Fav { user_id: String, twete_id: String }, - Unfav { user_id: String, twete_id: String }, - Followed { user_id: String }, - Unfollowed { user_id: String } -} - -impl Event { - pub fn from_json(structure: serde_json::Map) -> Option { - match &structure["event"].as_str().unwrap() { - &"follow" => Some(Event::Followed { - user_id: structure["source"]["id_str"].as_str().unwrap().to_owned() - }), - &"unfollow" => Some(Event::Unfollowed { - user_id: structure["source"]["id_str"].as_str().unwrap().to_owned() - }), - &"favorite" => Some(Event::Fav { - user_id: structure["source"]["id_str"].as_str().unwrap().to_owned(), - twete_id: structure["target_object"]["id_str"].as_str().unwrap().to_owned() - }), - &"unfavorite" => Some(Event::Unfav { - user_id: structure["source"]["id_str"].as_str().unwrap().to_owned(), - twete_id: structure["target_object"]["id_str"].as_str().unwrap().to_owned() - }), - &"favorited_retweet" => Some(Event::Fav_RT { - user_id: structure["source"]["id_str"].as_str().unwrap().to_owned(), - twete_id: structure["target_object"]["id_str"].as_str().unwrap().to_owned() - }), - &"retweeted_retweet" => Some(Event::RT_RT { - user_id: structure["source"]["id_str"].as_str().unwrap().to_owned(), - twete_id: structure["target_object"]["id_str"].as_str().unwrap().to_owned() - }), -// &"blocked" => Blocked { }, -// &"unblocked" => Unblocked { }, -// &"quoted_tweet" => ???, - e => { println!("unrecognized event: {}", e); None } - } - } -} diff --git a/tw/mod.rs b/tw/mod.rs deleted file mode 100644 index eff38e7..0000000 --- a/tw/mod.rs +++ /dev/null @@ -1,440 +0,0 @@ -use std::path::Path; -use std::fs::File; -use std::io::{BufRead, BufReader, Read}; -extern crate chrono; - -use self::chrono::prelude::*; - -use std::collections::{HashMap, HashSet}; -extern crate serde_json; -use std::io::Write; - -use std::fs::OpenOptions; - -pub mod events; - -use display::Render; -use display; - -pub mod tweet; -use self::tweet::Tweet; -pub mod user; -use self::user::User; - -pub fn full_twete_text(twete: &serde_json::map::Map) -> String { - if twete.contains_key("retweeted_status") { - return full_twete_text(twete["retweeted_status"].as_object().unwrap()) - } - let mut twete_text: String; - twete_text = if twete["truncated"].as_bool().unwrap() { - twete["extended_tweet"]["full_text"].as_str().unwrap().to_string() - } else { - twete["text"].as_str().unwrap().to_string() - }; - - let quoted_tweet_id = twete.get("quoted_tweet_id_str").and_then(|x| x.as_str()); - - twete_text = twete_text - .replace("&", "&") - .replace(">", ">") - .replace("<", "<"); - - for url in twete["entities"]["urls"].as_array().unwrap() { - let display_url = url["url"].as_str().unwrap(); - let expanded_url = url["expanded_url"].as_str().unwrap(); - if expanded_url.len() < 200 { - if let Some(twid) = quoted_tweet_id { - if expanded_url.ends_with(twid) { - twete_text = twete_text.replace(display_url, ""); - continue; - } - } - twete_text = twete_text.replace(display_url, expanded_url); - } - } - - twete_text -} - -#[derive(Serialize, Deserialize)] -pub struct TwitterCache { - #[serde(skip)] - pub users: HashMap, - #[serde(skip)] - pub tweets: HashMap, - following: HashSet, - following_history: HashMap, // userid:date?? - pub followers: HashSet, - lost_followers: HashSet, - follower_history: HashMap, // userid:date?? - #[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 -} - -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.. - - fn new() -> TwitterCache { - TwitterCache { - users: HashMap::new(), - tweets: HashMap::new(), - following: HashSet::new(), - following_history: HashMap::new(), - 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() - } - } - fn new_without_caching() -> TwitterCache { - let mut cache = TwitterCache::new(); - cache.caching_permitted = false; - cache - } - fn cache_user(&mut self, user: User) { - if !self.users.contains_key(&user.id) { - let mut file = - OpenOptions::new() - .create(true) - .append(true) - .open(TwitterCache::USERS_CACHE) - .unwrap(); - writeln!(file, "{}", serde_json::to_string(&user).unwrap()).unwrap(); - self.users.insert(user.id.to_owned(), user); - } - } - - fn cache_tweet(&mut self, tweet: Tweet) { - if !self.tweets.contains_key(&tweet.id) { - - let mut file = - OpenOptions::new() - .create(true) - .append(true) - .open(TwitterCache::TWEET_CACHE) - .unwrap(); - writeln!(file, "{}", serde_json::to_string(&tweet).unwrap()).unwrap(); - self.number_and_insert_tweet(tweet); - } - } - pub fn store_cache(&self) { - if Path::new(TwitterCache::PROFILE_DIR).is_dir() { - let profile = OpenOptions::new() - .write(true) - .create(true) - .append(false) - .open(TwitterCache::PROFILE_CACHE) - .unwrap(); - serde_json::to_writer(profile, self).unwrap(); - } else { - println!("No cache dir exists..."); - } - // store cache - } - fn number_and_insert_tweet(&mut self, mut tw: Tweet) { - 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.tweets.insert(tw.id.to_owned(), tw); - } - } - } - pub fn load_cache() -> TwitterCache { - if Path::new(TwitterCache::PROFILE_CACHE).is_file() { - let mut buf = vec![]; - let mut profile = File::open(TwitterCache::PROFILE_CACHE).unwrap(); - match profile.read_to_end(&mut buf) { - Ok(_sz) => { - match serde_json::from_slice(&buf) { - Ok(result) => { - let mut cache: TwitterCache = result; - cache.tweets = HashMap::new(); - for line in BufReader::new(File::open(TwitterCache::TWEET_CACHE).unwrap()).lines() { - let t: Tweet = serde_json::from_str(&line.unwrap()).unwrap(); - cache.number_and_insert_tweet(t); - } - for line in BufReader::new(File::open(TwitterCache::USERS_CACHE).unwrap()).lines() { - let u: User = serde_json::from_str(&line.unwrap()).unwrap(); - cache.users.insert(u.id.to_owned(), u); - } - cache.caching_permitted = true; - cache.needs_save = false; - cache - } - Err(e) => { - // TODO! should be able to un-frick profile after startup. - println!("Error reading profile, profile caching disabled... {}", e); - TwitterCache::new_without_caching() - } - } - } - Err(e) => { - println!("Error reading cached profile: {}. Profile caching disabled.", e); - TwitterCache::new_without_caching() - } - } - } else { - println!("Hello! First time setup?"); - TwitterCache::new() - } - } - pub fn cache_api_tweet(&mut self, json: serde_json::Value) { - if let Some((rt, rt_user)) = json.get("retweeted_status").and_then(|x| Tweet::from_api_json(x.to_owned())) { - self.cache_user(rt_user); - self.cache_tweet(rt); - } - - if let Some((qt, qt_user)) = json.get("quoted_status").and_then(|x| Tweet::from_api_json(x.to_owned())) { - self.cache_user(qt_user); - self.cache_tweet(qt); - } - - if let Some((twete, user)) = Tweet::from_api_json(json) { - self.cache_user(user); - self.cache_tweet(twete); - } - } - pub fn cache_api_user(&mut self, json: serde_json::Value) { - if let Some(user) = User::from_json(json) { - self.cache_user(user); - } - } - pub fn cache_api_event(&mut self, json: serde_json::Map, mut queryer: &mut ::Queryer) { - /* don't really care to hold on to who fav, unfav, ... when, just pick targets out. */ - match json.get("event").and_then(|x| x.as_str()) { - Some("favorite") => { - self.cache_api_tweet(json["target_object"].clone()); - self.cache_api_user(json["source"].clone()); - self.cache_api_user(json["target"].clone()); - }, - Some("unfavorite") => { - self.cache_api_tweet(json["target_object"].clone()); - self.cache_api_user(json["source"].clone()); - self.cache_api_user(json["target"].clone()); - }, - Some("retweeted_retweet") => ()/* cache rt */, - Some("favorited_retweet") => ()/* cache rt */, - Some("delete") => { - let user_id = json["delete"]["status"]["user_id_str"].as_str().unwrap().to_string(); - self.fetch_user(&user_id, &mut queryer); - }, - Some("follow") => { - let follower = json["source"]["id_str"].as_str().unwrap().to_string(); - let followed = json["target"]["id_str"].as_str().unwrap().to_string(); - self.cache_api_user(json["target"].clone()); - self.cache_api_user(json["source"].clone()); - if follower == "iximeow" { - // self.add_follow( - } else { - self.add_follower(&follower); - } - }, - Some("unfollow") => { - let follower = json["source"]["id_str"].as_str().unwrap().to_string(); - let followed = json["target"]["id_str"].as_str().unwrap().to_string(); - self.cache_api_user(json["target"].clone()); - self.cache_api_user(json["source"].clone()); - if follower == "iximeow" { - // self.add_follow( - } else { - self.remove_follower(&follower); - } - }, - Some(_) => () /* an uninteresting event */, - None => () // not really an event? should we log something? - /* 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_user(&self, user_id: &String) -> Option<&User> { - self.users.get(user_id) - } - pub fn fetch_tweet(&mut self, tweet_id: &String, mut queryer: &mut ::Queryer) -> Option<&Tweet> { - if !self.tweets.contains_key(tweet_id) { - match self.look_up_tweet(tweet_id, &mut queryer) { - Some(json) => self.cache_api_tweet(json), - None => println!("Unable to retrieve tweet {}", tweet_id) - }; - } - self.tweets.get(tweet_id) - } - pub fn fetch_user(&mut self, user_id: &String, mut queryer: &mut ::Queryer) -> Option<&User> { - if !self.users.contains_key(user_id) { - let maybe_parsed = self.look_up_user(user_id, &mut queryer).and_then(|x| User::from_json(x)); - match maybe_parsed { - Some(tw) => self.cache_user(tw), - None => println!("Unable to retrieve user {}", user_id) - }; - } - self.users.get(user_id) - } - pub fn set_following(&mut self, user_ids: Vec) { - let uid_set = user_ids.into_iter().collect::>(); - - let new_uids = &uid_set - &self.following; - for user in &new_uids { - println!("New following! {}", user); - self.add_following(user); - } - - let lost_uids = &self.following - &uid_set; - for user in &lost_uids { - println!("Bye, friend! {}", user); - self.remove_following(user); - } - } - pub fn set_followers(&mut self, user_ids: Vec) { - let uid_set = user_ids.into_iter().collect::>(); - - let new_uids = &uid_set - &self.followers; - for user in &new_uids { - println!("New follower! {}", user); - self.add_follower(user); - } - - let lost_uids = &self.followers - &uid_set; - for user in &lost_uids { - println!("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())); - } - 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())); - } - 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())); - } - 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())); - } - - fn look_up_user(&mut self, id: &str, queryer: &mut ::Queryer) -> Option { - let url = &format!("{}?user_id={}", ::USER_LOOKUP_URL, id); - queryer.do_api_get(url) - } - - fn look_up_tweet(&mut self, id: &str, queryer: &mut ::Queryer) -> Option { - let url = &format!("{}?id={}", ::TWEET_LOOKUP_URL, id); - queryer.do_api_get(url) - } - - pub fn get_settings(&self, queryer: &mut ::Queryer) -> Option { - queryer.do_api_get(::ACCOUNT_SETTINGS_URL) - } -} - -fn handle_twitter_event( - structure: serde_json::Map, - tweeter: &mut TwitterCache, - mut queryer: &mut ::Queryer) { - tweeter.cache_api_event(structure.clone(), &mut queryer); - if let Some(event) = events::Event::from_json(structure) { - event.render(&tweeter); - }; -} - -fn handle_twitter_delete( - structure: serde_json::Map, - tweeter: &mut TwitterCache, - _queryer: &mut ::Queryer) { - events::Event::Deleted { - user_id: structure["delete"]["status"]["user_id_str"].as_str().unwrap().to_string(), - twete_id: structure["delete"]["status"]["id_str"].as_str().unwrap().to_string() - }.render(tweeter); -} - -fn handle_twitter_twete( - structure: serde_json::Map, - tweeter: &mut TwitterCache, - _queryer: &mut ::Queryer) { - let twete_id = structure["id_str"].as_str().unwrap().to_string(); - tweeter.cache_api_tweet(serde_json::Value::Object(structure)); - display::render_twete(&twete_id, tweeter); -} - -fn handle_twitter_dm( - structure: serde_json::Map, - _tweeter: &mut TwitterCache, - _queryer: &mut ::Queryer) { - // show DM - println!("{}", structure["direct_message"]["text"].as_str().unwrap()); - println!("Unknown struture {:?}", structure); -} - -fn handle_twitter_welcome( - structure: serde_json::Map, - tweeter: &mut TwitterCache, - queryer: &mut ::Queryer) { -// println!("welcome: {:?}", structure); - let user_id_nums = structure["friends"].as_array().unwrap(); - let user_id_strs = user_id_nums.into_iter().map(|x| x.as_u64().unwrap().to_string()); - tweeter.set_following(user_id_strs.collect()); - 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() - }; - println!("You are {}", tweeter.current_user.handle); - } else { - println!("Unable to make API call to figure out who you are..."); - } -} - -pub fn handle_message( - twete: serde_json::Value, - tweeter: &mut TwitterCache, - queryer: &mut ::Queryer -) { - match twete { - serde_json::Value::Object(objmap) => { - if objmap.contains_key("event") { - handle_twitter_event(objmap, tweeter, queryer); - } else if objmap.contains_key("friends") { - handle_twitter_welcome(objmap, tweeter, queryer); - } else if objmap.contains_key("delete") { - handle_twitter_delete(objmap, tweeter, queryer); - } else if objmap.contains_key("user") && objmap.contains_key("id") { - handle_twitter_twete(objmap, tweeter, queryer); - } else if objmap.contains_key("direct_message") { - handle_twitter_dm(objmap, tweeter, queryer); - } - println!(""); - }, - _ => () - }; -} diff --git a/tw/tweet.rs b/tw/tweet.rs deleted file mode 100644 index 778461a..0000000 --- a/tw/tweet.rs +++ /dev/null @@ -1,78 +0,0 @@ -extern crate serde_json; - -use tw::user::User; - -#[derive(Debug, Serialize, Deserialize)] -pub struct Tweet { - pub id: String, - pub author_id: String, - pub text: String, - pub created_at: String, // lol - #[serde(skip_serializing_if="Option::is_none")] - #[serde(default = "Option::default")] - pub quoted_tweet_id: Option, - #[serde(skip_serializing_if="Option::is_none")] - #[serde(default = "Option::default")] - pub rt_tweet: Option, - #[serde(skip)] - pub internal_id: u64 -} - -impl Tweet { - pub fn get_mentions(&self) -> Vec<&str> { - self.text.split(&[ - ',', '.', '/', ';', '\'', - '[', ']', '\\', '~', '!', - '@', '#', '$', '%', '^', - '&', '*', '(', ')', '-', - '=', '{', '}', '|', ':', - '"', '<', '>', '?', '`', - ' ' // forgot this initially. awkward. - ][..]) - .filter(|x| x.starts_with("@") && x.len() > 1) - .collect() - } - - pub fn from_api_json(json: serde_json::Value) -> Option<(Tweet, User)> { - Tweet::from_json(json.clone()).and_then(|tw| { - json.get("user").and_then(|user_json| - User::from_json(user_json.to_owned()).map(|u| (tw, u)) - ) - }) - } - pub fn from_json(json: serde_json::Value) -> Option { - if let serde_json::Value::Object(json_map) = json { - let text = ::tw::full_twete_text(&json_map); - let rt_twete = json_map.get("retweeted_status") - .and_then(|x| x.get("id_str")) - .and_then(|x| x.as_str()) - .map(|x| x.to_owned()); - if json_map.contains_key("id_str") && - json_map.contains_key("user") && - json_map.contains_key("created_at") { - if let ( - Some(id_str), - Some(author_id), - Some(created_at) - ) = ( - json_map["id_str"].as_str(), - json_map["user"]["id_str"].as_str(), - json_map["created_at"].as_str() - ) { - return Some(Tweet { - id: id_str.to_owned(), - author_id: author_id.to_owned(), - text: text, - created_at: created_at.to_owned(), - quoted_tweet_id: json_map.get("quoted_status_id_str") - .and_then(|x| x.as_str()) - .map(|x| x.to_owned()), - rt_tweet: rt_twete, - internal_id: 0 - }) - } - } - } - None - } -} diff --git a/tw/user.rs b/tw/user.rs deleted file mode 100644 index 1da82f0..0000000 --- a/tw/user.rs +++ /dev/null @@ -1,46 +0,0 @@ -extern crate serde_json; - -#[derive(Debug, Serialize, Deserialize)] -pub struct User { - pub id: String, - pub name: String, - pub handle: String -} - -impl Default for User { - fn default() -> User { - User { - id: "".to_owned(), - name: "_default_".to_owned(), - handle: "_default_".to_owned() - } - } -} - -impl User { - pub fn from_json(json: serde_json::Value) -> Option { - if let serde_json::Value::Object(json_map) = json { - if json_map.contains_key("id_str") && - json_map.contains_key("name") && - json_map.contains_key("screen_name") { - if let ( - Some(id_str), - Some(name), - Some(screen_name) - ) = ( - json_map["id_str"].as_str(), - json_map["name"].as_str(), - json_map["screen_name"].as_str() - ) { - return Some(User { - id: id_str.to_owned(), - name: name.to_owned(), - handle: screen_name.to_owned() - }) - } - } - } - None - } -} - -- cgit v1.1