diff --git a/Cargo.lock b/Cargo.lock index 81d1016..3db39b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -227,6 +227,20 @@ dependencies = [ "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "curl-sys" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.36 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.54 (registry+https://github.com/rust-lang/crates.io-index)", + "libz-sys 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-sys 0.9.45 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", + "vcpkg 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "dtoa" version = "0.4.4" @@ -329,10 +343,25 @@ dependencies = [ "num_cpus 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "git2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.54 (registry+https://github.com/rust-lang/crates.io-index)", + "libgit2-sys 0.7.11 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-sys 0.9.45 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "gitredditor" version = "0.1.0" dependencies = [ + "git2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "reqwest 0.9.16 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)", @@ -472,6 +501,44 @@ name = "libc" version = "0.2.54" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "libgit2-sys" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.36 (registry+https://github.com/rust-lang/crates.io-index)", + "curl-sys 0.4.18 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.54 (registry+https://github.com/rust-lang/crates.io-index)", + "libssh2-sys 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "libz-sys 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-sys 0.9.45 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "libssh2-sys" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.36 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.54 (registry+https://github.com/rust-lang/crates.io-index)", + "libz-sys 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-sys 0.9.45 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", + "vcpkg 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "libz-sys" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.36 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.54 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", + "vcpkg 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "lock_api" version = "0.1.5" @@ -1447,6 +1514,7 @@ dependencies = [ "checksum crossbeam-epoch 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "04c9e3102cc2d69cd681412141b390abd55a362afc1540965dad0ad4d34280b4" "checksum crossbeam-queue 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7c979cd6cfe72335896575c6b5688da489e420d36a27a0b9eb0c73db574b4a4b" "checksum crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f8306fcef4a7b563b76b7dd949ca48f52bc1141aa067d2ea09565f3e2652aa5c" +"checksum curl-sys 0.4.18 (registry+https://github.com/rust-lang/crates.io-index)" = "9d91a0052d5b982887d8e829bee0faffc7218ea3c6ebd3d6c2c8f678a93c9a42" "checksum dtoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "ea57b42383d091c85abcc2706240b94ab2a8fa1fc81c10ff23c4de06e2a90b5e" "checksum encoding_rs 0.8.17 (registry+https://github.com/rust-lang/crates.io-index)" = "4155785c79f2f6701f185eb2e6b4caf0555ec03477cb4c70db67b465311620ed" "checksum error-chain 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "07e791d3be96241c77c43846b665ef1384606da2cd2a48730abe606a12906e02" @@ -1461,6 +1529,7 @@ dependencies = [ "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" "checksum futures 0.1.26 (registry+https://github.com/rust-lang/crates.io-index)" = "62941eff9507c8177d448bd83a44d9b9760856e184081d8cd79ba9f03dd24981" "checksum futures-cpupool 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "ab90cde24b3319636588d0c35fe03b1333857621051837ed769faefb4c2162e4" +"checksum git2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c7339329bfa14a00223244311560d11f8f489b453fb90092af97f267a6090ab0" "checksum h2 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)" = "85ab6286db06040ddefb71641b50017c06874614001a134b423783e2db2920bd" "checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" "checksum http 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)" = "eed324f0f0daf6ec10c474f150505af2c143f251722bf9dbd1261bd1f2ee2c1a" @@ -1475,6 +1544,9 @@ dependencies = [ "checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14" "checksum lazycell 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b294d6fa9ee409a054354afc4352b0b9ef7ca222c69b8812cbea9e7d2bf3783f" "checksum libc 0.2.54 (registry+https://github.com/rust-lang/crates.io-index)" = "c6785aa7dd976f5fbf3b71cfd9cd49d7f783c1ff565a858d71031c6c313aa5c6" +"checksum libgit2-sys 0.7.11 (registry+https://github.com/rust-lang/crates.io-index)" = "48441cb35dc255da8ae72825689a95368bf510659ae1ad55dc4aa88cb1789bf1" +"checksum libssh2-sys 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "126a1f4078368b163bfdee65fbab072af08a1b374a5551b21e87ade27b1fbf9d" +"checksum libz-sys 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)" = "2eb5e43362e38e2bca2fd5f5134c4d4564a23a5c28e9b95411652021a8675ebe" "checksum lock_api 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "62ebf1391f6acad60e5c8b43706dde4582df75c06698ab44511d15016bc2442c" "checksum log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c84ec4b527950aa83a329754b01dbe3f58361d1c5efacd1f6d68c494d08a17c6" "checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" diff --git a/Cargo.toml b/Cargo.toml index 850d130..d497e1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,4 @@ reqwest = "0.9.16" structopt = "0.2.15" serde = "1.0.90" serde_derive = "1.0.90" +git2 = "0.8.0" diff --git a/src/main.rs b/src/main.rs index c3a09fc..5b17a9c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,57 +2,132 @@ #[macro_use] extern crate serde_derive; +use git2::{Repository, Signature, Time as GitTime}; +use serde_json::to_string_pretty; use std::error::Error; use std::fmt; +use std::fs::{create_dir_all, read_to_string, write as fs_write}; use std::iter; use std::iter::Chain; +use std::path::{Path, PathBuf}; use std::thread; use std::time::Duration; +use std::time::UNIX_EPOCH; +use std::iter::IntoIterator; mod model; +mod opts; use crate::model::*; +use crate::opts::*; fn main() { - let comments = Comments::for_user("shim__"); - for comment in comments { - dbg!(comment); - thread::sleep(Duration::from_millis(100)); - } - println!("Hello, world!"); + let opts = Opts::from_args(); + let comments = Comments::for_user(&opts.redditor); + let all = comments.take(opts.fetch).filter_map(|c| c.ok()).collect::>(); + + println!( + "Hello, world! {:?}", + update( + &opts.repo.unwrap(), + &all, + &opts.redditor, + &("reddit.com/u/".to_owned() + &opts.redditor) + ) + ); } -/*fn fetch_comments<'a, T: AsRef + fmt::Display>( - redditor: &'a T, - from: Option, -) -> std::iter::Chain< - std::result::Result< - std::vec::Vec, - std::boxed::Box<(dyn std::error::Error + 'static)>, - >, - std::vec::Vec, -> { - fn request_paged(url: &str) -> Result<(Vec, Option), Box> { - let comment_json = reqwest::get(url)?.text()?; - - let continuation: Option = unimplemented!(); - let comments: Vec = unimplemented!(); - return Ok((comments, continuation)); - } - let page = request_paged(&format!("https://reddit.com/user/{}.json", redditor)); - let next: std::iter::Chain< - std::result::Result< - std::vec::Vec, - std::boxed::Box<(dyn std::error::Error + 'static)>, - >, - std::vec::Vec, - > = if let Ok((_, cont)) = page { - fetch_comments(redditor, cont) - } else { - unimplemented!() //iter::once(Vec::with_capacity(0).iter()).chain([].into_iter().map(|_| fetch_comments("never_gonna_happen", None))) +fn update( + repo: &PathBuf, + current: &Vec, + redditor: &str, + email: &str, +) -> Result> { + let comment_path = |c: &Comment| { + let mut p = repo.clone(); + for s in c.permalink.split("/") { + p.push(s); + } + p.set_extension("json"); + p }; - iter::once([()].into_iter().map(|_| page)).chain([()].into_iter().flat_map(|_| next)) -}*/ + let mut updated: usize = 0; + let git = Repository::open(&repo)?; + let sig = Signature::now(redditor, email)?; + let mut index = git.index()?; + for comment in current.into_iter() { + let path = comment_path(&comment); + let path_rel = || { + let mut p = PathBuf::from(&comment.permalink[1..(comment.permalink.len() - 1)]); + p.set_extension("json"); + p + }; + let before = updated; + let mut commit_msg = String::new(); + if (&path).exists() { + let content = read_to_string(&path)?; + let old: Comment = serde_json::from_str(&content)?; + let delta = CommentDelta::from(&old, &comment); + if delta.len() > 0 { + fs_write(&path, to_string_pretty(&comment)?)?; + commit_msg = delta.iter().map(|d| d.to_string()).collect::>()[..].join("\n"); + index.update_all(vec![&path], None)?; + + updated += 1; + } + } else { + create_dir_all((&path).parent().unwrap())?; + fs_write(&path, to_string_pretty(&comment)?)?; + index.add_all(vec![&path], git2::IndexAddOption::DEFAULT, None)?; + commit_msg = CommentDelta::New.to_string(); + updated += 1; + } + if before != updated { + index.add_path(&path_rel())?; + let tree_id = index.write_tree()?; + let tree = git.find_tree(tree_id)?; + + let parent = dbg!(git.find_commit(git.head()?.target().unwrap()))?; + + let time = { + let created = UNIX_EPOCH + Duration::from_secs(comment.created as u64); + let edited = comment + .edited + .filter(|e| e > &1) + .map(|e| UNIX_EPOCH + Duration::from_secs(e)); + if let Some(edited) = edited { + if edited > created { + edited + } else { + created + } + } else { + created + } + }; + + let sig_backdate = Signature::new( + sig.name().unwrap(), + sig.email().unwrap(), + &GitTime::new( + dbg!(time).duration_since(UNIX_EPOCH).unwrap().as_secs() as i64, + 0, + ), + )?; + + println!("Commiting: {}:\n{}", comment.id, commit_msg); + git.commit( + Some("HEAD"), + &sig_backdate, + &sig_backdate, + &commit_msg, + &tree, + &[&parent], + )?; + } + } + Ok(updated) +} fn poll(interval: Duration, count: Option) { let mut it: u32 = 0; diff --git a/src/model.rs b/src/model.rs index c7ad12e..639970a 100644 --- a/src/model.rs +++ b/src/model.rs @@ -2,17 +2,55 @@ use serde::de; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value; use std::error::Error; +use std::fmt; use std::iter; use std::iter::Chain; -#[derive(Serialize, Deserialize, Debug)] +#[derive(PartialEq, Clone, Serialize, Deserialize, Debug)] pub struct Comment { - score: u32, - id: String, - created: f64, - permalink: String, + pub score: i32, + pub id: String, + pub created: f64, + pub permalink: String, + pub body: String, #[serde(deserialize_with = "false_or_val")] - edited: Option, + pub edited: Option, +} + +pub enum CommentDelta { + Votes(i32), + Content, + New, +} + +impl fmt::Display for CommentDelta { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use CommentDelta::*; + match self { + Votes(d) if d > &0 => write!(f, "Received +{} upvotes", d), + Votes(d) if d < &0 => write!(f, "Received {} downvotes", d), + Votes(_) => write!(f, "You shouln't see this one, if you do check the source"), + Content => write!(f, "Edited"), + New => write!(f, "Created"), + } + } +} + +impl CommentDelta { + pub fn from(a: &Comment, b: &Comment) -> Vec { + assert_eq!(a.id, b.id); + //Assume that most comments don't change + let mut delta = Vec::with_capacity(0); + use CommentDelta::*; + if a.score != b.score { + delta.push(Votes(b.score - a.score)) + } + + if a.body != b.body { + delta.push(Content) + } + delta + } } fn false_or_val<'de, D>(deserializer: D) -> Result, D::Error> @@ -64,8 +102,7 @@ impl Iterator for Comments { fn next(&mut self) -> Option { fn request_paged(url: &str) -> Result<(Vec, Option), Box> { - dbg!(("Requesting", url)); - let comment_json = dbg!(reqwest::get(url)?.text()?); + let comment_json = reqwest::get(url)?.text()?; let comment_json: Value = serde_json::from_str(&comment_json)?; let data: &Value = &comment_json["data"]; let continuation: Option = match &data["after"] { diff --git a/src/opts.rs b/src/opts.rs new file mode 100644 index 0000000..75b8569 --- /dev/null +++ b/src/opts.rs @@ -0,0 +1,15 @@ +use std::path::PathBuf; +pub use structopt::StructOpt; + +#[derive(StructOpt, Debug)] +#[structopt(name = "gitredditor")] +pub struct Opts { + #[structopt(short = "f", long = "fetch", default_value = "20")] + pub fetch: usize, + + #[structopt(short = "r", long = "redditor")] + pub redditor: String, + + #[structopt(parse(from_os_str))] + pub repo: Option, +}