From ea4e93f01d9e4ef17effae1e9a807bb1977865fe Mon Sep 17 00:00:00 2001 From: Andy Wortman Date: Mon, 2 Oct 2017 01:27:08 -0700 Subject: move everything to src/ --- src/tw/events.rs | 44 ++++++ src/tw/mod.rs | 440 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/tw/tweet.rs | 78 ++++++++++ src/tw/user.rs | 46 ++++++ 4 files changed, 608 insertions(+) create mode 100644 src/tw/events.rs create mode 100644 src/tw/mod.rs create mode 100644 src/tw/tweet.rs create mode 100644 src/tw/user.rs (limited to 'src/tw') diff --git a/src/tw/events.rs b/src/tw/events.rs new file mode 100644 index 0000000..0541b0a --- /dev/null +++ b/src/tw/events.rs @@ -0,0 +1,44 @@ +extern crate serde_json; + +pub enum Event { + Deleted { user_id: String, twete_id: String }, + RT_RT { user_id: String, twete_id: String }, + Fav_RT { user_id: String, twete_id: String }, + Fav { user_id: String, twete_id: String }, + Unfav { user_id: String, twete_id: String }, + Followed { user_id: String }, + Unfollowed { user_id: String } +} + +impl Event { + pub fn from_json(structure: serde_json::Map) -> Option { + match &structure["event"].as_str().unwrap() { + &"follow" => Some(Event::Followed { + user_id: structure["source"]["id_str"].as_str().unwrap().to_owned() + }), + &"unfollow" => Some(Event::Unfollowed { + user_id: structure["source"]["id_str"].as_str().unwrap().to_owned() + }), + &"favorite" => Some(Event::Fav { + user_id: structure["source"]["id_str"].as_str().unwrap().to_owned(), + twete_id: structure["target_object"]["id_str"].as_str().unwrap().to_owned() + }), + &"unfavorite" => Some(Event::Unfav { + user_id: structure["source"]["id_str"].as_str().unwrap().to_owned(), + twete_id: structure["target_object"]["id_str"].as_str().unwrap().to_owned() + }), + &"favorited_retweet" => Some(Event::Fav_RT { + user_id: structure["source"]["id_str"].as_str().unwrap().to_owned(), + twete_id: structure["target_object"]["id_str"].as_str().unwrap().to_owned() + }), + &"retweeted_retweet" => Some(Event::RT_RT { + user_id: structure["source"]["id_str"].as_str().unwrap().to_owned(), + twete_id: structure["target_object"]["id_str"].as_str().unwrap().to_owned() + }), +// &"blocked" => Blocked { }, +// &"unblocked" => Unblocked { }, +// &"quoted_tweet" => ???, + e => { println!("unrecognized event: {}", e); None } + } + } +} diff --git a/src/tw/mod.rs b/src/tw/mod.rs new file mode 100644 index 0000000..eff38e7 --- /dev/null +++ b/src/tw/mod.rs @@ -0,0 +1,440 @@ +use std::path::Path; +use std::fs::File; +use std::io::{BufRead, BufReader, Read}; +extern crate chrono; + +use self::chrono::prelude::*; + +use std::collections::{HashMap, HashSet}; +extern crate serde_json; +use std::io::Write; + +use std::fs::OpenOptions; + +pub mod events; + +use display::Render; +use display; + +pub mod tweet; +use self::tweet::Tweet; +pub mod user; +use self::user::User; + +pub fn full_twete_text(twete: &serde_json::map::Map) -> String { + if twete.contains_key("retweeted_status") { + return full_twete_text(twete["retweeted_status"].as_object().unwrap()) + } + let mut twete_text: String; + twete_text = if twete["truncated"].as_bool().unwrap() { + twete["extended_tweet"]["full_text"].as_str().unwrap().to_string() + } else { + twete["text"].as_str().unwrap().to_string() + }; + + let quoted_tweet_id = twete.get("quoted_tweet_id_str").and_then(|x| x.as_str()); + + twete_text = twete_text + .replace("&", "&") + .replace(">", ">") + .replace("<", "<"); + + for url in twete["entities"]["urls"].as_array().unwrap() { + let display_url = url["url"].as_str().unwrap(); + let expanded_url = url["expanded_url"].as_str().unwrap(); + if expanded_url.len() < 200 { + if let Some(twid) = quoted_tweet_id { + if expanded_url.ends_with(twid) { + twete_text = twete_text.replace(display_url, ""); + continue; + } + } + twete_text = twete_text.replace(display_url, expanded_url); + } + } + + twete_text +} + +#[derive(Serialize, Deserialize)] +pub struct TwitterCache { + #[serde(skip)] + pub users: HashMap, + #[serde(skip)] + pub tweets: HashMap, + following: HashSet, + following_history: HashMap, // userid:date?? + pub followers: HashSet, + lost_followers: HashSet, + follower_history: HashMap, // userid:date?? + #[serde(skip)] + id_to_tweet_id: HashMap, + #[serde(skip)] + pub needs_save: bool, + #[serde(skip)] + pub caching_permitted: bool, + #[serde(skip)] + pub current_user: User +} + +impl TwitterCache { + const PROFILE_DIR: &'static str = "cache/"; + const TWEET_CACHE: &'static str = "cache/tweets.json"; + const USERS_CACHE: &'static str = "cache/users.json"; + const PROFILE_CACHE: &'static str = "cache/profile.json"; // this should involve MY user id.. + + fn new() -> TwitterCache { + TwitterCache { + users: HashMap::new(), + tweets: HashMap::new(), + following: HashSet::new(), + following_history: HashMap::new(), + followers: HashSet::new(), + lost_followers: HashSet::new(), + follower_history: HashMap::new(), + id_to_tweet_id: HashMap::new(), + needs_save: false, + caching_permitted: true, + current_user: User::default() + } + } + fn new_without_caching() -> TwitterCache { + let mut cache = TwitterCache::new(); + cache.caching_permitted = false; + cache + } + fn cache_user(&mut self, user: User) { + if !self.users.contains_key(&user.id) { + let mut file = + OpenOptions::new() + .create(true) + .append(true) + .open(TwitterCache::USERS_CACHE) + .unwrap(); + writeln!(file, "{}", serde_json::to_string(&user).unwrap()).unwrap(); + self.users.insert(user.id.to_owned(), user); + } + } + + fn cache_tweet(&mut self, tweet: Tweet) { + if !self.tweets.contains_key(&tweet.id) { + + let mut file = + OpenOptions::new() + .create(true) + .append(true) + .open(TwitterCache::TWEET_CACHE) + .unwrap(); + writeln!(file, "{}", serde_json::to_string(&tweet).unwrap()).unwrap(); + self.number_and_insert_tweet(tweet); + } + } + pub fn store_cache(&self) { + if Path::new(TwitterCache::PROFILE_DIR).is_dir() { + let profile = OpenOptions::new() + .write(true) + .create(true) + .append(false) + .open(TwitterCache::PROFILE_CACHE) + .unwrap(); + serde_json::to_writer(profile, self).unwrap(); + } else { + println!("No cache dir exists..."); + } + // store cache + } + fn number_and_insert_tweet(&mut self, mut tw: Tweet) { + if !self.tweets.contains_key(&tw.id.to_owned()) { + if tw.internal_id == 0 { + tw.internal_id = (self.tweets.len() as u64) + 1; + self.id_to_tweet_id.insert(tw.internal_id, tw.id.to_owned()); + self.tweets.insert(tw.id.to_owned(), tw); + } + } + } + pub fn load_cache() -> TwitterCache { + if Path::new(TwitterCache::PROFILE_CACHE).is_file() { + let mut buf = vec![]; + let mut profile = File::open(TwitterCache::PROFILE_CACHE).unwrap(); + match profile.read_to_end(&mut buf) { + Ok(_sz) => { + match serde_json::from_slice(&buf) { + Ok(result) => { + let mut cache: TwitterCache = result; + cache.tweets = HashMap::new(); + for line in BufReader::new(File::open(TwitterCache::TWEET_CACHE).unwrap()).lines() { + let t: Tweet = serde_json::from_str(&line.unwrap()).unwrap(); + cache.number_and_insert_tweet(t); + } + for line in BufReader::new(File::open(TwitterCache::USERS_CACHE).unwrap()).lines() { + let u: User = serde_json::from_str(&line.unwrap()).unwrap(); + cache.users.insert(u.id.to_owned(), u); + } + cache.caching_permitted = true; + cache.needs_save = false; + cache + } + Err(e) => { + // TODO! should be able to un-frick profile after startup. + println!("Error reading profile, profile caching disabled... {}", e); + TwitterCache::new_without_caching() + } + } + } + Err(e) => { + println!("Error reading cached profile: {}. Profile caching disabled.", e); + TwitterCache::new_without_caching() + } + } + } else { + println!("Hello! First time setup?"); + TwitterCache::new() + } + } + pub fn cache_api_tweet(&mut self, json: serde_json::Value) { + if let Some((rt, rt_user)) = json.get("retweeted_status").and_then(|x| Tweet::from_api_json(x.to_owned())) { + self.cache_user(rt_user); + self.cache_tweet(rt); + } + + if let Some((qt, qt_user)) = json.get("quoted_status").and_then(|x| Tweet::from_api_json(x.to_owned())) { + self.cache_user(qt_user); + self.cache_tweet(qt); + } + + if let Some((twete, user)) = Tweet::from_api_json(json) { + self.cache_user(user); + self.cache_tweet(twete); + } + } + pub fn cache_api_user(&mut self, json: serde_json::Value) { + if let Some(user) = User::from_json(json) { + self.cache_user(user); + } + } + pub fn cache_api_event(&mut self, json: serde_json::Map, mut queryer: &mut ::Queryer) { + /* don't really care to hold on to who fav, unfav, ... when, just pick targets out. */ + match json.get("event").and_then(|x| x.as_str()) { + Some("favorite") => { + self.cache_api_tweet(json["target_object"].clone()); + self.cache_api_user(json["source"].clone()); + self.cache_api_user(json["target"].clone()); + }, + Some("unfavorite") => { + self.cache_api_tweet(json["target_object"].clone()); + self.cache_api_user(json["source"].clone()); + self.cache_api_user(json["target"].clone()); + }, + Some("retweeted_retweet") => ()/* cache rt */, + Some("favorited_retweet") => ()/* cache rt */, + Some("delete") => { + let user_id = json["delete"]["status"]["user_id_str"].as_str().unwrap().to_string(); + self.fetch_user(&user_id, &mut queryer); + }, + Some("follow") => { + let follower = json["source"]["id_str"].as_str().unwrap().to_string(); + let followed = json["target"]["id_str"].as_str().unwrap().to_string(); + self.cache_api_user(json["target"].clone()); + self.cache_api_user(json["source"].clone()); + if follower == "iximeow" { + // self.add_follow( + } else { + self.add_follower(&follower); + } + }, + Some("unfollow") => { + let follower = json["source"]["id_str"].as_str().unwrap().to_string(); + let followed = json["target"]["id_str"].as_str().unwrap().to_string(); + self.cache_api_user(json["target"].clone()); + self.cache_api_user(json["source"].clone()); + if follower == "iximeow" { + // self.add_follow( + } else { + self.remove_follower(&follower); + } + }, + Some(_) => () /* an uninteresting event */, + None => () // not really an event? should we log something? + /* nothing else to care about now, i think? */ + } + } + pub fn tweet_by_innerid(&self, inner_id: u64) -> Option<&Tweet> { + let id = &self.id_to_tweet_id[&inner_id]; + self.retrieve_tweet(id) + } + pub fn retrieve_tweet(&self, tweet_id: &String) -> Option<&Tweet> { + self.tweets.get(tweet_id) + } + pub fn retrieve_user(&self, user_id: &String) -> Option<&User> { + self.users.get(user_id) + } + pub fn fetch_tweet(&mut self, tweet_id: &String, mut queryer: &mut ::Queryer) -> Option<&Tweet> { + if !self.tweets.contains_key(tweet_id) { + match self.look_up_tweet(tweet_id, &mut queryer) { + Some(json) => self.cache_api_tweet(json), + None => println!("Unable to retrieve tweet {}", tweet_id) + }; + } + self.tweets.get(tweet_id) + } + pub fn fetch_user(&mut self, user_id: &String, mut queryer: &mut ::Queryer) -> Option<&User> { + if !self.users.contains_key(user_id) { + let maybe_parsed = self.look_up_user(user_id, &mut queryer).and_then(|x| User::from_json(x)); + match maybe_parsed { + Some(tw) => self.cache_user(tw), + None => println!("Unable to retrieve user {}", user_id) + }; + } + self.users.get(user_id) + } + pub fn set_following(&mut self, user_ids: Vec) { + let uid_set = user_ids.into_iter().collect::>(); + + let new_uids = &uid_set - &self.following; + for user in &new_uids { + println!("New following! {}", user); + self.add_following(user); + } + + let lost_uids = &self.following - &uid_set; + for user in &lost_uids { + println!("Bye, friend! {}", user); + self.remove_following(user); + } + } + pub fn set_followers(&mut self, user_ids: Vec) { + let uid_set = user_ids.into_iter().collect::>(); + + let new_uids = &uid_set - &self.followers; + for user in &new_uids { + println!("New follower! {}", user); + self.add_follower(user); + } + + let lost_uids = &self.followers - &uid_set; + for user in &lost_uids { + println!("Bye, friend! {}", user); + self.remove_follower(user); + } + } + pub fn add_following(&mut self, user_id: &String) { + self.needs_save = true; + self.following.insert(user_id.to_owned()); + self.following_history.insert(user_id.to_owned(), ("following".to_string(), Utc::now().timestamp())); + } + pub fn remove_following(&mut self, user_id: &String) { + self.needs_save = true; + self.following.remove(user_id); + self.following_history.insert(user_id.to_owned(), ("unfollowing".to_string(), Utc::now().timestamp())); + } + pub fn add_follower(&mut self, user_id: &String) { + self.needs_save = true; + self.followers.insert(user_id.to_owned()); + self.lost_followers.remove(user_id); + self.follower_history.insert(user_id.to_owned(), ("follow".to_string(), Utc::now().timestamp())); + } + pub fn remove_follower(&mut self, user_id: &String) { + self.needs_save = true; + self.followers.remove(user_id); + self.lost_followers.insert(user_id.to_owned()); + self.follower_history.insert(user_id.to_owned(), ("unfollow".to_string(), Utc::now().timestamp())); + } + + fn look_up_user(&mut self, id: &str, queryer: &mut ::Queryer) -> Option { + let url = &format!("{}?user_id={}", ::USER_LOOKUP_URL, id); + queryer.do_api_get(url) + } + + fn look_up_tweet(&mut self, id: &str, queryer: &mut ::Queryer) -> Option { + let url = &format!("{}?id={}", ::TWEET_LOOKUP_URL, id); + queryer.do_api_get(url) + } + + pub fn get_settings(&self, queryer: &mut ::Queryer) -> Option { + queryer.do_api_get(::ACCOUNT_SETTINGS_URL) + } +} + +fn handle_twitter_event( + structure: serde_json::Map, + tweeter: &mut TwitterCache, + mut queryer: &mut ::Queryer) { + tweeter.cache_api_event(structure.clone(), &mut queryer); + if let Some(event) = events::Event::from_json(structure) { + event.render(&tweeter); + }; +} + +fn handle_twitter_delete( + structure: serde_json::Map, + tweeter: &mut TwitterCache, + _queryer: &mut ::Queryer) { + events::Event::Deleted { + user_id: structure["delete"]["status"]["user_id_str"].as_str().unwrap().to_string(), + twete_id: structure["delete"]["status"]["id_str"].as_str().unwrap().to_string() + }.render(tweeter); +} + +fn handle_twitter_twete( + structure: serde_json::Map, + tweeter: &mut TwitterCache, + _queryer: &mut ::Queryer) { + let twete_id = structure["id_str"].as_str().unwrap().to_string(); + tweeter.cache_api_tweet(serde_json::Value::Object(structure)); + display::render_twete(&twete_id, tweeter); +} + +fn handle_twitter_dm( + structure: serde_json::Map, + _tweeter: &mut TwitterCache, + _queryer: &mut ::Queryer) { + // show DM + println!("{}", structure["direct_message"]["text"].as_str().unwrap()); + println!("Unknown struture {:?}", structure); +} + +fn handle_twitter_welcome( + structure: serde_json::Map, + tweeter: &mut TwitterCache, + queryer: &mut ::Queryer) { +// println!("welcome: {:?}", structure); + let user_id_nums = structure["friends"].as_array().unwrap(); + let user_id_strs = user_id_nums.into_iter().map(|x| x.as_u64().unwrap().to_string()); + tweeter.set_following(user_id_strs.collect()); + let settings = tweeter.get_settings(queryer).unwrap(); + let maybe_my_name = settings["screen_name"].as_str(); + if let Some(my_name) = maybe_my_name { + tweeter.current_user = User { + id: "".to_string(), + handle: my_name.to_owned(), + name: my_name.to_owned() + }; + println!("You are {}", tweeter.current_user.handle); + } else { + println!("Unable to make API call to figure out who you are..."); + } +} + +pub fn handle_message( + twete: serde_json::Value, + tweeter: &mut TwitterCache, + queryer: &mut ::Queryer +) { + match twete { + serde_json::Value::Object(objmap) => { + if objmap.contains_key("event") { + handle_twitter_event(objmap, tweeter, queryer); + } else if objmap.contains_key("friends") { + handle_twitter_welcome(objmap, tweeter, queryer); + } else if objmap.contains_key("delete") { + handle_twitter_delete(objmap, tweeter, queryer); + } else if objmap.contains_key("user") && objmap.contains_key("id") { + handle_twitter_twete(objmap, tweeter, queryer); + } else if objmap.contains_key("direct_message") { + handle_twitter_dm(objmap, tweeter, queryer); + } + println!(""); + }, + _ => () + }; +} diff --git a/src/tw/tweet.rs b/src/tw/tweet.rs new file mode 100644 index 0000000..778461a --- /dev/null +++ b/src/tw/tweet.rs @@ -0,0 +1,78 @@ +extern crate serde_json; + +use tw::user::User; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Tweet { + pub id: String, + pub author_id: String, + pub text: String, + pub created_at: String, // lol + #[serde(skip_serializing_if="Option::is_none")] + #[serde(default = "Option::default")] + pub quoted_tweet_id: Option, + #[serde(skip_serializing_if="Option::is_none")] + #[serde(default = "Option::default")] + pub rt_tweet: Option, + #[serde(skip)] + pub internal_id: u64 +} + +impl Tweet { + pub fn get_mentions(&self) -> Vec<&str> { + self.text.split(&[ + ',', '.', '/', ';', '\'', + '[', ']', '\\', '~', '!', + '@', '#', '$', '%', '^', + '&', '*', '(', ')', '-', + '=', '{', '}', '|', ':', + '"', '<', '>', '?', '`', + ' ' // forgot this initially. awkward. + ][..]) + .filter(|x| x.starts_with("@") && x.len() > 1) + .collect() + } + + pub fn from_api_json(json: serde_json::Value) -> Option<(Tweet, User)> { + Tweet::from_json(json.clone()).and_then(|tw| { + json.get("user").and_then(|user_json| + User::from_json(user_json.to_owned()).map(|u| (tw, u)) + ) + }) + } + pub fn from_json(json: serde_json::Value) -> Option { + if let serde_json::Value::Object(json_map) = json { + let text = ::tw::full_twete_text(&json_map); + let rt_twete = json_map.get("retweeted_status") + .and_then(|x| x.get("id_str")) + .and_then(|x| x.as_str()) + .map(|x| x.to_owned()); + if json_map.contains_key("id_str") && + json_map.contains_key("user") && + json_map.contains_key("created_at") { + if let ( + Some(id_str), + Some(author_id), + Some(created_at) + ) = ( + json_map["id_str"].as_str(), + json_map["user"]["id_str"].as_str(), + json_map["created_at"].as_str() + ) { + return Some(Tweet { + id: id_str.to_owned(), + author_id: author_id.to_owned(), + text: text, + created_at: created_at.to_owned(), + quoted_tweet_id: json_map.get("quoted_status_id_str") + .and_then(|x| x.as_str()) + .map(|x| x.to_owned()), + rt_tweet: rt_twete, + internal_id: 0 + }) + } + } + } + None + } +} diff --git a/src/tw/user.rs b/src/tw/user.rs new file mode 100644 index 0000000..1da82f0 --- /dev/null +++ b/src/tw/user.rs @@ -0,0 +1,46 @@ +extern crate serde_json; + +#[derive(Debug, Serialize, Deserialize)] +pub struct User { + pub id: String, + pub name: String, + pub handle: String +} + +impl Default for User { + fn default() -> User { + User { + id: "".to_owned(), + name: "_default_".to_owned(), + handle: "_default_".to_owned() + } + } +} + +impl User { + pub fn from_json(json: serde_json::Value) -> Option { + if let serde_json::Value::Object(json_map) = json { + if json_map.contains_key("id_str") && + json_map.contains_key("name") && + json_map.contains_key("screen_name") { + if let ( + Some(id_str), + Some(name), + Some(screen_name) + ) = ( + json_map["id_str"].as_str(), + json_map["name"].as_str(), + json_map["screen_name"].as_str() + ) { + return Some(User { + id: id_str.to_owned(), + name: name.to_owned(), + handle: screen_name.to_owned() + }) + } + } + } + None + } +} + -- cgit v1.1