aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndy Wortman <ixineeringeverywhere@gmail.com>2017-10-01 20:55:15 -0700
committerAndy Wortman <ixineeringeverywhere@gmail.com>2017-10-01 20:55:15 -0700
commite6ebf2c99a70bd5ee4e8d07097e6b128c3630714 (patch)
tree476947a6f2937737bc69ca073a2519bd9f15b1fe
parentaf67981a2a1c28b3b5598f74d48bfd3a7490c91a (diff)
extract commands and twitter model into modules
-rw-r--r--commands/del.rs0
-rw-r--r--commands/fav.rs35
-rw-r--r--commands/look_up.rs32
-rw-r--r--commands/mod.rs46
-rw-r--r--commands/quit.rs18
-rw-r--r--commands/show_cache.rs31
-rw-r--r--commands/twete.rs184
-rw-r--r--commands/view.rs20
-rw-r--r--main.rs914
-rw-r--r--tw/mod.rs620
10 files changed, 996 insertions, 904 deletions
diff --git a/commands/del.rs b/commands/del.rs
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/commands/del.rs
diff --git a/commands/fav.rs b/commands/fav.rs
new file mode 100644
index 0000000..3e2b00d
--- /dev/null
+++ b/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/commands/look_up.rs b/commands/look_up.rs
new file mode 100644
index 0000000..d04f984
--- /dev/null
+++ b/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/commands/mod.rs b/commands/mod.rs
new file mode 100644
index 0000000..fc66bec
--- /dev/null
+++ b/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/commands/quit.rs b/commands/quit.rs
new file mode 100644
index 0000000..982c48f
--- /dev/null
+++ b/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/commands/show_cache.rs b/commands/show_cache.rs
new file mode 100644
index 0000000..3c31697
--- /dev/null
+++ b/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/commands/twete.rs b/commands/twete.rs
new file mode 100644
index 0000000..ecc3f98
--- /dev/null
+++ b/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/commands/view.rs b/commands/view.rs
new file mode 100644
index 0000000..1470f90
--- /dev/null
+++ b/commands/view.rs
@@ -0,0 +1,20 @@
+use tw;
+use ::Queryer;
+
+use commands::Command;
+
+use std::str::FromStr;
+
+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();
+ ::render_twete(&twete.id, tweeter);
+ println!("link: https://twitter.com/i/web/status/{}", twete.id);
+}
diff --git a/main.rs b/main.rs
index 8be302c..5b5f493 100644
--- a/main.rs
+++ b/main.rs
@@ -2,7 +2,6 @@
extern crate serde_json;
use std::str;
-use std::str::FromStr;
//use std::io::BufRead;
#[macro_use] extern crate chan;
@@ -24,6 +23,8 @@ use futures::Stream;
use hyper_tls::HttpsConnector;
//use json_streamer::JsonObjectStreamer;
+mod tw;
+
//Change these values to your real Twitter API credentials
static consumer_key: &str = "T879tHWDzd6LvKWdYVfbJL4Su";
static consumer_secret: &str = "OAXXYYIozAZ4vWSmDziI1EMJCKXPmWPFgLbJpB896iIAMIAdpb";
@@ -34,11 +35,6 @@ 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 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";
-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";
static ACCOUNT_SETTINGS_URL: &str = "https://api.twitter.com/1.1/account/settings.json";
header! { (Authorization, "Authorization") => [String] }
@@ -46,628 +42,6 @@ header! { (Accept, "Accept") => [String] }
header! { (ContentType, "Content-Type") => [String] }
header! { (Cookie, "cookie") => [String] }
-mod tw {
- 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;
-
- #[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()
- }
- }
- }
-
- pub mod events {
- extern crate termion;
- use self::termion::color;
-
- extern crate serde_json;
-
- pub struct Deleted {
- user_id: String,
- twete_id: String
- }
-
- pub struct RT_RT {
- user_id: String,
- twete_id: String
- }
-
- pub struct Fav_RT {
- user_id: String,
- twete_id: String
- }
-
- pub struct Fav {
- user_id: String,
- twete_id: String
- }
-
- pub struct Unfav {
- user_id: String,
- twete_id: String
- }
-
- pub struct Followed {
- user_id: String
- }
-
- pub struct Unfollowed {
- user_id: String
- }
-
- impl Event for Deleted {
- fn render(self: Box<Self>, _tweeter: &::tw::TwitterCache) { }
- }
- impl Event for RT_RT {
- fn render(self: Box<Self>, tweeter: &::tw::TwitterCache) {
- println!("---------------------------------");
- {
- let user = tweeter.retrieve_user(&self.user_id).unwrap();
- println!(" +rt_rt : {} (@{})", user.name, user.handle);
- }
- {
- ::render_twete(&self.twete_id, tweeter);
- }
- println!("");
- }
- }
- impl Event for Fav_RT {
- fn render(self: Box<Self>, tweeter: &::tw::TwitterCache) {
- println!("---------------------------------");
- {
- let user = tweeter.retrieve_user(&self.user_id).unwrap();
- println!(" +rt_fav : {} (@{})", user.name, user.handle);
- }
- {
- ::render_twete(&self.twete_id, tweeter);
- }
- println!("");
- }
- }
- impl Event for Fav {
- fn render(self: Box<Self>, tweeter: &::tw::TwitterCache) {
- println!("---------------------------------");
- {
- let user = tweeter.retrieve_user(&self.user_id).unwrap();
- println!("{} +fav : {} (@{}){}", color::Fg(color::Yellow), user.name, user.handle, color::Fg(color::Reset));
- }
- {
- ::render_twete(&self.twete_id, tweeter);
- }
- println!("");
- }
- }
- impl Event for Unfav {
- fn render(self: Box<Self>, tweeter: &::tw::TwitterCache) {
- println!("---------------------------------");
- {
- let user = tweeter.retrieve_user(&self.user_id).unwrap();
- println!("{} -fav : {} (@{}){}", color::Fg(color::Yellow), user.name, user.handle, color::Fg(color::Reset));
- }
- {
- ::render_twete(&self.twete_id, tweeter);
- }
- println!("");
- }
- }
- impl Event for Followed {
- fn render(self: Box<Self>, tweeter: &::tw::TwitterCache) {
- let user = tweeter.retrieve_user(&self.user_id).unwrap();
- println!("---------------------------------");
- println!(" +fl : {} (@{})", user.name, user.handle);
- println!("");
- }
- }
- impl Event for Unfollowed {
- fn render(self: Box<Self>, tweeter: &::tw::TwitterCache) {
- let user = tweeter.retrieve_user(&self.user_id).unwrap();
- println!("---------------------------------");
- println!(" -fl : {} (@{})", user.name, user.handle);
- println!("");
- }
- }
-
- /*
- impl Event for Blocked {
-
- }
- */
-
- pub trait Event {
- fn render(self: Box<Self>, tweeter: &::tw::TwitterCache);
- }
-
- impl Event {
- pub fn from_json(structure: serde_json::Map<String, serde_json::Value>) -> Option<Box<Event>> {
- match &structure["event"].as_str().unwrap() {
- &"follow" => Some(Box::new(Followed {
- user_id: structure["source"]["id_str"].as_str().unwrap().to_owned()
- })),
- &"unfollow" => Some(Box::new(Unfollowed {
- user_id: structure["source"]["id_str"].as_str().unwrap().to_owned()
- })),
- &"favorite" => Some(Box::new(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(Box::new(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(Box::new(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(Box::new(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 }
- }
- }
- }
- }
-
- 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
- }
- }
-
- #[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 = 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
- }
- }
-
- 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)
- }
- }
-}
pub struct Queryer {
client: hyper::client::Client<HttpsConnector<hyper::client::HttpConnector>>,
@@ -1071,267 +445,14 @@ fn url_encode(s: &str) -> String {
.replace("]", "%5d")
}
-struct Command {
- keyword: &'static str,
- params: u8,
- exec: fn(line: String, tweeter: &mut tw::TwitterCache, queryer: &mut Queryer)
-}
-
-static SHOW_CACHE: Command = Command {
- keyword: "show_cache",
- params: 0,
- exec: |_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!(" ..."); }
- }
- }
- }
-};
-
-static QUIT: Command = Command {
- keyword: "q",
- params: 0,
- exec: |_line: String, tweeter: &mut tw::TwitterCache, _queryer: &mut Queryer| {
- println!("Bye bye!");
- tweeter.store_cache();
- std::process::exit(0);
- }
-};
-
-static LOOK_UP_USER: Command = Command {
- keyword: "look_up_user",
- params: 1,
- exec: |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);
- }
- }
-};
-
-static LOOK_UP_TWEET: Command = Command {
- keyword: "look_up_tweet",
- params: 1,
- exec: |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);
- }
- }
-};
-
-static VIEW: Command = Command {
- keyword: "view",
- params: 1,
- exec: |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();
- render_twete(&twete.id, tweeter);
- println!("link: https://twitter.com/i/web/status/{}", twete.id);
- }
-};
-
-static UNFAV: Command = Command {
- keyword: "unfav",
- params: 1,
- exec: |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));
- }
-};
-
-static FAV: Command = Command {
- keyword: "fav",
- params: 1,
- exec: |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));
- }
-};
-
-static DEL: Command = Command {
- keyword: "del",
- params: 1,
- exec: |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));
- }
-};
-
-static TWETE: Command = Command {
- keyword: "t",
- params: 1,
- exec: |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));
- }
-};
-
-static THREAD: Command = Command {
- keyword: "thread",
- params: 2,
- exec: |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");
- }
- }
-};
-
-static REP: Command = Command {
- keyword: "rep",
- params: 2,
- exec: |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");
- }
- }
-};
-
-static QUOTE: Command = Command {
- keyword: "qt",
- params: 2,
- exec: |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");
- }
- }
-};
-
-static RETWETE: Command = Command {
- keyword: "rt",
- params: 1,
- exec: |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));
- }
-};
+mod commands;
+use commands::Command;
-fn parse_word_command<'a, 'b>(line: &'b str, commands: Vec<&'a Command>) -> Option<(&'b str, &'a 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 {
@@ -1346,24 +467,9 @@ fn parse_word_command<'a, 'b>(line: &'b str, commands: Vec<&'a Command>) -> Opti
}
fn handle_user_input(line: Vec<u8>, tweeter: &mut tw::TwitterCache, mut queryer: &mut Queryer) {
- let commands = vec![
- &SHOW_CACHE,
- &QUIT,
- &LOOK_UP_USER,
- &LOOK_UP_TWEET,
- &VIEW,
- &UNFAV,
- &FAV,
- &DEL,
- &TWETE,
- &QUOTE,
- &RETWETE,
- &REP,
- &THREAD
- ];
let command_bare = String::from_utf8(line).unwrap();
let command = command_bare.trim();
- if let Some((line, cmd)) = parse_word_command(&command, commands) {
+ 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);
diff --git a/tw/mod.rs b/tw/mod.rs
new file mode 100644
index 0000000..efc070b
--- /dev/null
+++ b/tw/mod.rs
@@ -0,0 +1,620 @@
+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;
+
+#[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()
+ }
+ }
+}
+
+pub mod events {
+ extern crate termion;
+ use self::termion::color;
+
+ extern crate serde_json;
+
+ pub struct Deleted {
+ user_id: String,
+ twete_id: String
+ }
+
+ pub struct RT_RT {
+ user_id: String,
+ twete_id: String
+ }
+
+ pub struct Fav_RT {
+ user_id: String,
+ twete_id: String
+ }
+
+ pub struct Fav {
+ user_id: String,
+ twete_id: String
+ }
+
+ pub struct Unfav {
+ user_id: String,
+ twete_id: String
+ }
+
+ pub struct Followed {
+ user_id: String
+ }
+
+ pub struct Unfollowed {
+ user_id: String
+ }
+
+ impl Event for Deleted {
+ fn render(self: Box<Self>, _tweeter: &::tw::TwitterCache) { }
+ }
+ impl Event for RT_RT {
+ fn render(self: Box<Self>, tweeter: &::tw::TwitterCache) {
+ println!("---------------------------------");
+ {
+ let user = tweeter.retrieve_user(&self.user_id).unwrap();
+ println!(" +rt_rt : {} (@{})", user.name, user.handle);
+ }
+ {
+ ::render_twete(&self.twete_id, tweeter);
+ }
+ println!("");
+ }
+ }
+ impl Event for Fav_RT {
+ fn render(self: Box<Self>, tweeter: &::tw::TwitterCache) {
+ println!("---------------------------------");
+ {
+ let user = tweeter.retrieve_user(&self.user_id).unwrap();
+ println!(" +rt_fav : {} (@{})", user.name, user.handle);
+ }
+ {
+ ::render_twete(&self.twete_id, tweeter);
+ }
+ println!("");
+ }
+ }
+ impl Event for Fav {
+ fn render(self: Box<Self>, tweeter: &::tw::TwitterCache) {
+ println!("---------------------------------");
+ {
+ let user = tweeter.retrieve_user(&self.user_id).unwrap();
+ println!("{} +fav : {} (@{}){}", color::Fg(color::Yellow), user.name, user.handle, color::Fg(color::Reset));
+ }
+ {
+ ::render_twete(&self.twete_id, tweeter);
+ }
+ println!("");
+ }
+ }
+ impl Event for Unfav {
+ fn render(self: Box<Self>, tweeter: &::tw::TwitterCache) {
+ println!("---------------------------------");
+ {
+ let user = tweeter.retrieve_user(&self.user_id).unwrap();
+ println!("{} -fav : {} (@{}){}", color::Fg(color::Yellow), user.name, user.handle, color::Fg(color::Reset));
+ }
+ {
+ ::render_twete(&self.twete_id, tweeter);
+ }
+ println!("");
+ }
+ }
+ impl Event for Followed {
+ fn render(self: Box<Self>, tweeter: &::tw::TwitterCache) {
+ let user = tweeter.retrieve_user(&self.user_id).unwrap();
+ println!("---------------------------------");
+ println!(" +fl : {} (@{})", user.name, user.handle);
+ println!("");
+ }
+ }
+ impl Event for Unfollowed {
+ fn render(self: Box<Self>, tweeter: &::tw::TwitterCache) {
+ let user = tweeter.retrieve_user(&self.user_id).unwrap();
+ println!("---------------------------------");
+ println!(" -fl : {} (@{})", user.name, user.handle);
+ println!("");
+ }
+ }
+
+ /*
+ impl Event for Blocked {
+
+ }
+ */
+
+ pub trait Event {
+ fn render(self: Box<Self>, tweeter: &::tw::TwitterCache);
+ }
+
+ impl Event {
+ pub fn from_json(structure: serde_json::Map<String, serde_json::Value>) -> Option<Box<Event>> {
+ match &structure["event"].as_str().unwrap() {
+ &"follow" => Some(Box::new(Followed {
+ user_id: structure["source"]["id_str"].as_str().unwrap().to_owned()
+ })),
+ &"unfollow" => Some(Box::new(Unfollowed {
+ user_id: structure["source"]["id_str"].as_str().unwrap().to_owned()
+ })),
+ &"favorite" => Some(Box::new(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(Box::new(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(Box::new(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(Box::new(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 }
+ }
+ }
+ }
+}
+
+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
+ }
+}
+
+#[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 = 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
+ }
+}
+
+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)
+ }
+}