twixter

A twtxt command line client in Rust

git clone git://git.shimmy1996.com/twixter.git
commit db06926c3e4d650003df6329f9fcd9de9581931a
parent 19cdd92b7978b60864c5d353050560053cb74371
Author: Shimmy Xu <shimmy.xu@shimmy1996.com>
Date:   Sun, 15 Sep 2019 00:29:40 -0400

Implement timeline

Diffstat:
MCargo.toml | 3++-
Msrc/config.rs | 2++
Msrc/main.rs | 4+++-
Asrc/timeline.rs | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 65 insertions(+), 2 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
@@ -8,5 +8,6 @@ edition = "2018"
 chrono = "~0.4.8"
 clap = "~2.33.0"
 dirs = "~2.0.2"
+reqwest = "~0.9.20"
 rust-ini = "~0.13.0"
-strfmt = "0.1.6"
+strfmt = "~0.1.6"
diff --git a/src/config.rs b/src/config.rs
@@ -8,6 +8,7 @@ pub struct Config {
     pub twturl: String,
     pub pre_tweet_hook: String,
     pub post_tweet_hook: String,
+    pub limit_timeline: i32,
     pub following: HashMap<String, String>,
 }
 
@@ -37,6 +38,7 @@ impl Config {
             twturl: twtxt_config["twturl"].to_owned(),
             pre_tweet_hook: pre_tweet_hook,
             post_tweet_hook: post_tweet_hook,
+            limit_timeline: twtxt_config["limit_timeline"].parse::<i32>().unwrap(),
             following: following,
         }
     }
diff --git a/src/main.rs b/src/main.rs
@@ -1,8 +1,9 @@
-use clap::{crate_version, App, Arg, ArgMatches, SubCommand};
+use clap::{crate_version, App, Arg, SubCommand};
 
 use std::path::Path;
 
 mod config;
+mod timeline;
 mod tweet;
 use crate::config::Config;
 
@@ -67,6 +68,7 @@ fn main() {
     // Parse subcommands.
     match command.subcommand() {
         ("tweet", Some(subcommand)) => tweet::tweet(&config, &subcommand),
+        ("timeline", Some(subcommand)) => timeline::timeline(&config, &subcommand),
         _ => {}
     }
 }
diff --git a/src/timeline.rs b/src/timeline.rs
@@ -0,0 +1,58 @@
+use chrono::{DateTime, FixedOffset, Local, SecondsFormat};
+use clap::ArgMatches;
+use reqwest::Client;
+
+use std::collections::BinaryHeap;
+
+use crate::config::Config;
+
+/// Print timeline from following.
+pub fn timeline(config: &Config, _subcommand: &ArgMatches) {
+    // Store (post_time, nick, content).
+    let mut all_tweets = BinaryHeap::<(DateTime<FixedOffset>, String, String)>::new();
+
+    for (nick, twturl) in config.following.iter() {
+        let tweets = parse_twtxt(twturl);
+        for (post_time, content) in tweets {
+            all_tweets.push((post_time, nick.to_owned(), content));
+        }
+    }
+
+    // Print the most recent tweets.
+    let now = Local::now();
+    for _ in 0..config.limit_timeline {
+        if let Some(tweet) = all_tweets.pop() {
+            println!("{}", format_tweet(&tweet, &now));
+        }
+    }
+}
+
+/// Parses given twtxt url, returns a Vec of (post_time, content).
+fn parse_twtxt(twturl: &str) -> Vec<(DateTime<FixedOffset>, String)> {
+    let client = Client::new();
+    let resp = client.get(twturl).send().unwrap().text().unwrap();
+    let mut tweets = Vec::new();
+
+    for line in resp.lines() {
+        if let Some(seperator_idx) = line.find('\t') {
+            if let Ok(tweet_time) = DateTime::parse_from_rfc3339(&line[..seperator_idx]) {
+                tweets.push((tweet_time, line[seperator_idx + 1..].to_owned()));
+            };
+        };
+    }
+
+    tweets
+}
+
+/// Formats a tweet for display in terminal.
+fn format_tweet(tweet: &(DateTime<FixedOffset>, String, String), now: &DateTime<Local>) -> String {
+    format!(
+        "\n@{} {}\n{}",
+        &tweet.1,
+        &tweet
+            .0
+            .with_timezone(&now.timezone())
+            .to_rfc3339_opts(SecondsFormat::Secs, true),
+        &tweet.2
+    )
+}