diff options
| author | Andy Wortman <ixineeringeverywhere@gmail.com> | 2017-10-02 01:27:08 -0700 | 
|---|---|---|
| committer | Andy Wortman <ixineeringeverywhere@gmail.com> | 2017-10-02 01:27:18 -0700 | 
| commit | ea4e93f01d9e4ef17effae1e9a807bb1977865fe (patch) | |
| tree | 08cfbcc32b9fbea5f13fd3447026090f51402274 /src | |
| parent | afd61ae0822690f30d37859c806a8d8d843b8c1a (diff) | |
move everything to src/
Diffstat (limited to 'src')
| -rw-r--r-- | src/commands/del.rs | 0 | ||||
| -rw-r--r-- | src/commands/fav.rs | 35 | ||||
| -rw-r--r-- | src/commands/look_up.rs | 32 | ||||
| -rw-r--r-- | src/commands/mod.rs | 46 | ||||
| -rw-r--r-- | src/commands/quit.rs | 18 | ||||
| -rw-r--r-- | src/commands/show_cache.rs | 31 | ||||
| -rw-r--r-- | src/commands/twete.rs | 184 | ||||
| -rw-r--r-- | src/commands/view.rs | 22 | ||||
| -rw-r--r-- | src/display/mod.rs | 147 | ||||
| -rw-r--r-- | src/linestream.rs | 42 | ||||
| -rw-r--r-- | src/main.rs | 382 | ||||
| -rw-r--r-- | src/tw/events.rs | 44 | ||||
| -rw-r--r-- | src/tw/mod.rs | 440 | ||||
| -rw-r--r-- | src/tw/tweet.rs | 78 | ||||
| -rw-r--r-- | src/tw/user.rs | 46 | 
15 files changed, 1547 insertions, 0 deletions
diff --git a/src/commands/del.rs b/src/commands/del.rs new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/commands/del.rs 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 <id> your sik reply"); +        } +    } else { +        println!("thread <id> 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<String> = 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<String> = 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 <id> your sik reply"); +        } +    } else { +        println!("rep <id> 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 <id> your sik reply"); +        } +    } else { +        println!("rep <id> 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::<Vec<&str>>().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::<Vec<&str>>().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<S, E> where S: Stream<Item=u8, Error=E> { +  stream: S, +  progress: Vec<u8> +} + +impl<S,E> LineStream<S, E> where S: Stream<Item=u8, Error=E> + Sized { +  pub fn new(stream: S) -> LineStream<S, E> { +    LineStream { +      stream: stream, +      progress: vec![] +    } +  } +} + +impl<S, E> Stream for LineStream<S, E> where S: Stream<Item=u8, Error=E> { +  type Item = Vec<u8>; +  type Error = E; +   +  fn poll(&mut self) -> Poll<Option<Self::Item>, 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<HttpsConnector<hyper::client::HttpConnector>>, +    core: Core +} + +impl Queryer { +    fn do_api_get(&mut self, url: &str) -> Option<serde_json::Value> { +        self.issue_request(signed_api_get(url)) +    } +    fn do_api_post(&mut self, url: &str) -> Option<serde_json::Value> { +        self.issue_request(signed_api_post(url)) +    } +    /* +    fn do_web_req(&mut self, url: &str) -> Option<serde_json::Value> { +        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<serde_json::Value> { +        let lookup = self.client.request(req); + +        let resp: hyper::Response = self.core.run(lookup).unwrap(); +        let status = resp.status().clone(); + +        let chunks: Vec<hyper::Chunk> = self.core.run(resp.body().collect()).unwrap(); + +        let resp_body: Vec<u8> = 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::<Vec<String>>().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::<Vec<String>>().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::<Vec<u8>>(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<Vec<u8>>, twete_rx: chan::Receiver<Vec<u8>>, mut tweeter: &mut tw::TwitterCache, mut queryer: &mut ::Queryer) -> Option<(chan::Receiver<Vec<u8>>, chan::Receiver<Vec<u8>>)> { +    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::<Vec<&str>>()[1]).unwrap(); +            return Some((line.get((cmd.keyword.len() + 1)..).unwrap().trim(), &cmd)); +        } +    } +    return None +} + +fn handle_user_input(line: Vec<u8>, 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<Vec<u8>> { +    let (twete_tx, twete_rx) = chan::sync::<Vec<u8>>(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<String, serde_json::Value>) -> Option<Event> { +        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, serde_json::Value>) -> 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<String, User>, +    #[serde(skip)] +    pub tweets: HashMap<String, Tweet>, +    following: HashSet<String>, +    following_history: HashMap<String, (String, i64)>, // userid:date?? +    pub followers: HashSet<String>, +    lost_followers: HashSet<String>, +    follower_history: HashMap<String, (String, i64)>, // userid:date?? +    #[serde(skip)] +    id_to_tweet_id: HashMap<u64, String>, +    #[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<String, serde_json::Value>, 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<String>) { +        let uid_set = user_ids.into_iter().collect::<HashSet<String>>(); + +        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<String>) { +        let uid_set = user_ids.into_iter().collect::<HashSet<String>>(); + +        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<serde_json::Value> { +        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<serde_json::Value> { +        let url = &format!("{}?id={}", ::TWEET_LOOKUP_URL, id); +        queryer.do_api_get(url) +    } + +    pub fn get_settings(&self, queryer: &mut ::Queryer) -> Option<serde_json::Value> { +        queryer.do_api_get(::ACCOUNT_SETTINGS_URL) +    } +} + +fn handle_twitter_event( +    structure: serde_json::Map<String, serde_json::Value>, +    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<String, serde_json::Value>, +    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<String, serde_json::Value>, +    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<String, serde_json::Value>, +    _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<String, serde_json::Value>, +    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<String>, +    #[serde(skip_serializing_if="Option::is_none")] +    #[serde(default = "Option::default")] +    pub rt_tweet: Option<String>, +    #[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<Tweet> { +        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<User> { +        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 +    } +} +  | 
