aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/commands/del.rs0
-rw-r--r--src/commands/fav.rs35
-rw-r--r--src/commands/look_up.rs32
-rw-r--r--src/commands/mod.rs46
-rw-r--r--src/commands/quit.rs18
-rw-r--r--src/commands/show_cache.rs31
-rw-r--r--src/commands/twete.rs184
-rw-r--r--src/commands/view.rs22
-rw-r--r--src/display/mod.rs147
-rw-r--r--src/linestream.rs42
-rw-r--r--src/main.rs382
-rw-r--r--src/tw/events.rs44
-rw-r--r--src/tw/mod.rs440
-rw-r--r--src/tw/tweet.rs78
-rw-r--r--src/tw/user.rs46
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,
+ &QUOTE,
+ &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("&amp;", "&")
+ .replace("&gt;", ">")
+ .replace("&lt;", "<");
+
+ 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
+ }
+}
+