Added upvotes and downvotes

Changes:
- Added upvotes and downvotes parsing.
- Added text to read from selftext and selftext_html.
- Added an texttype to imply what type of text is present (HTML or raw text)
- Added some documentation.
- A few minor changes (I hope).

- Removed the pinned field.

Todo:
- Remove texttype field and parse the selftext_html into raw text
This commit is contained in:
Uttarayan Mondal
2021-03-15 14:52:11 +05:30
parent 3df3e8ad00
commit c95dc097d4
4 changed files with 118 additions and 38 deletions
+2
View File
@@ -1,3 +1,5 @@
/target /target
Cargo.lock Cargo.lock
rust.json rust.json
post.json
*.json
View File
+34 -7
View File
@@ -1,17 +1,44 @@
//!# Reddit api wrapper
//!Example program
//!You need tokio to use this.
//!Make sure to use `tokio = { version = "1.3.*", features = ["full"] }`
//!```rust
//!extern crate rapr;
//!extern crate tokio;
//!use crate::rapr::{RaprClient,RaSub};
//!#[tokio::main]
//!async fn main() {
//! let client = RaprClient::new();
//! let mut sub = RaSub::new("pics");
//! client.fetch(10, &mut sub).await.unwrap();
//! for post in sub.posts {
//! println!("{}",post.title);
//! }
//!}
mod rapr; mod rapr;
pub use crate::rapr::RaprClient; pub use crate::rapr::{RaPost, RaSub, RaprClient};
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::rapr::{RaSub, RaprClient};
#[tokio::test] #[tokio::test]
async fn subreddit() { async fn subreddit() {
use crate::rapr::RaprClient;
let client = RaprClient::new(); let client = RaprClient::new();
let mut sub = RaprClient::subreddit("rust"); let mut sub = RaSub::new("rust");
client.fetch(10, &mut sub).await.unwrap(); client.fetch(10, &mut sub).await.unwrap();
println!("{:#?}", sub.posts.len()); // println!("{:#?}", sub.posts.len());
client.fetch(5, &mut sub).await.unwrap(); // client.fetch(5, &mut sub).await.unwrap();
println!("{:#?}", sub.posts.len()); for post in sub.posts {
println!("{:#?}", sub.pinned_posts()); println!("{:#?}", post);
}
}
#[tokio::test]
async fn title() {
let client = RaprClient::new();
let mut sub = RaSub::new("rust");
client.fetch(10, &mut sub).await.unwrap();
for post in sub.posts {
println!("{}", post.title);
}
} }
} }
+81 -30
View File
@@ -10,14 +10,27 @@ pub enum Error {
HttpGetError, HttpGetError,
} }
#[derive(Debug, Clone, Copy)]
pub enum TextType {
HTML,
Raw,
Empty,
}
/// Reddit post object
/// (more like repost object).
/// Keeps track of post information.
#[derive(Clone)] #[derive(Clone)]
pub struct RaPost { pub struct RaPost {
id: String, pub id: String,
datetime: DateTime<Local>, pub datetime: DateTime<Local>,
title: String, pub title: String,
text: Option<String>, pub text: Option<String>,
json: json::JsonValue, texttype: TextType,
pinned: bool, pub permalink: String,
pub upvotes: u32,
pub downvotes: u32,
pub json: json::JsonValue,
} }
impl fmt::Debug for RaPost { impl fmt::Debug for RaPost {
@@ -27,31 +40,73 @@ impl fmt::Debug for RaPost {
.field("datetime", &self.datetime) .field("datetime", &self.datetime)
.field("title", &self.title) .field("title", &self.title)
.field("text", &self.text) .field("text", &self.text)
.field("texttype", &self.texttype)
.field("upvotes", &self.upvotes)
.field("downvotes", &self.downvotes)
.finish() .finish()
} }
} }
impl RaPost { impl RaPost {
pub fn new(id: &str, title: &str, json: &json::JsonValue, pinned: bool) -> Self { /// Generate new post object (you probably don't need this).
pub fn new(
id: &str,
title: &str,
text: Option<&str>,
texttype: TextType,
permalink: &str,
upvotes: u32,
downvotes: u32,
json: &json::JsonValue,
) -> Self {
let _text = match text {
Some(text) => Some(text.to_string()),
None => None,
};
Self { Self {
id: id.to_string(), id: id.to_string(),
title: title.to_string(), title: title.to_string(),
datetime: Local::now(), // Temp datetime: Local::now(), // Temp
text: Some(String::from("")), text: _text,
texttype,
permalink: String::from(permalink),
upvotes,
downvotes,
json: json.clone(), json: json.clone(),
pinned,
} }
} }
/// Parse json from `json['data']['children']` array elements.
pub fn parse(post: &json::JsonValue) -> Result<RaPost, json::Error> { pub fn parse(post: &json::JsonValue) -> Result<RaPost, json::Error> {
let mut text: Option<&str> = post["selftext"].as_str();
let mut texttype: TextType = TextType::Empty;
// Reddit always returns an empty string on selftext
// if there is no text. So this shouldn't panic!
// Some also have both selftext and selftext_html
// I am only taking selftext from these.
if text.unwrap().is_empty() {
text = post["selftext_html"].as_str();
if text != None {
texttype = TextType::HTML;
}
} else {
texttype = TextType::Raw;
}
Ok(RaPost::new( Ok(RaPost::new(
json::stringify(post["name"].clone()).as_str(), post["name"].as_str().unwrap(),
json::stringify(post["title"].clone()).as_str(), post["title"].as_str().unwrap(),
text,
texttype,
post["permalink"].as_str().unwrap(),
post["ups"].as_u32().unwrap(),
post["downs"].as_u32().unwrap(),
post, post,
post["pinned"].as_bool().unwrap(),
)) ))
} }
} }
///# RaSub
///Subreddit object
///Keeps track of posts
#[derive(Debug)] #[derive(Debug)]
pub struct RaSub { pub struct RaSub {
pub name: String, pub name: String,
@@ -60,21 +115,19 @@ pub struct RaSub {
} }
impl RaSub { impl RaSub {
pub fn pinned_posts(&self) -> Option<Vec<RaPost>> { /// Generate subreddit object
let mut pinned_posts: Vec<RaPost> = Vec::new(); pub fn new(name: &str) -> RaSub {
for post in &self.posts { RaSub {
if post.pinned { name: String::from(name),
pinned_posts.push(post.clone()); posts: Vec::new(),
} after: None,
}
if pinned_posts.is_empty() {
return None;
} else {
return Some(pinned_posts);
} }
} }
} }
/// Reddit api client.
/// Uses a [reqwest::Client](https://docs.rs/reqwest/0.11.2/reqwest/struct.Client.html) internally.
/// Currently no authentication
#[derive(Debug)] #[derive(Debug)]
pub struct RaprClient { pub struct RaprClient {
oauth: Option<String>, oauth: Option<String>,
@@ -88,13 +141,9 @@ impl RaprClient {
rwclient: reqwest::Client::new(), rwclient: reqwest::Client::new(),
} }
} }
pub fn subreddit(name: &str) -> RaSub {
RaSub { /// Fetch posts from subreddit and store them in the subreddit object.
name: String::from(name), /// Note: First fetch always seems to pull two pinned posts which are not marked pinned in the json
posts: Vec::new(),
after: None,
}
}
pub async fn fetch(&self, count: u32, sub: &mut RaSub) -> Result<Vec<RaPost>, Error> { pub async fn fetch(&self, count: u32, sub: &mut RaSub) -> Result<Vec<RaPost>, Error> {
let url = match self.oauth { let url = match self.oauth {
None => format!("https://reddit.com/r/{}.json", sub.name), None => format!("https://reddit.com/r/{}.json", sub.name),
@@ -128,8 +177,10 @@ impl RaprClient {
let mut parsed_posts: Vec<RaPost> = Vec::new(); let mut parsed_posts: Vec<RaPost> = Vec::new();
for post in raw_posts { for post in raw_posts {
if post["kind"].as_str().unwrap() == "t3" {
parsed_posts.push(RaPost::parse(&post["data"]).unwrap()); parsed_posts.push(RaPost::parse(&post["data"]).unwrap());
} }
}
if parsed["data"]["after"].is_string() { if parsed["data"]["after"].is_string() {
sub.after = Some(parsed["data"]["after"].to_string()); sub.after = Some(parsed["data"]["after"].to_string());
} }