roux/models/subreddit/
mod.rs

1//! # Subreddit
2//! A read-only module to read data from a specific subreddit.
3//!
4//! # Basic Usage
5//! ```no_run
6//! use roux::Subreddit;
7//! # #[cfg(not(feature = "blocking"))]
8//! # use tokio;
9//!
10//! # #[cfg_attr(not(feature = "blocking"), tokio::main)]
11//! # #[maybe_async::maybe_async]
12//! # async fn main() {
13//! let subreddit = Subreddit::new("rust");
14//! // Now you are able to:
15//!
16//! // Get moderators.
17//! let moderators = subreddit.moderators().await;
18//!
19//! // Get hot posts with limit = 25.
20//! let hot = subreddit.hot(25, None).await;
21//!
22//! // Get rising posts with limit = 30.
23//! let rising = subreddit.rising(30, None).await;
24//!
25//! // Get top posts with limit = 10.
26//! let top = subreddit.top(10, None).await;
27//!
28//! // Get latest comments.
29//! // `depth` and `limit` are optional.
30//! let latest_comments = subreddit.latest_comments(None, Some(25)).await;
31//!
32//! // Get comments from a submission.
33//! let article_id = &hot.unwrap().data.children.first().unwrap().data.id.clone();
34//! let article_comments = subreddit.article_comments(article_id, None, Some(25));
35//! # }
36//! ```
37//!
38//! # Usage with feed options
39//!
40//! ```no_run
41//! use roux::Subreddit;
42//! use roux::util::{FeedOption, TimePeriod};
43//! # #[cfg(not(feature = "blocking"))]
44//! # use tokio;
45//!
46//! # #[cfg_attr(not(feature = "blocking"), tokio::main)]
47//! # #[maybe_async::maybe_async]
48//! # async fn main() {
49//! let subreddit = Subreddit::new("astolfo");
50//!
51//! // Gets top 10 posts from this month
52//! let options = FeedOption::new().period(TimePeriod::ThisMonth);
53//! let top = subreddit.top(25, Some(options)).await;
54//!
55//! // Gets hot 10
56//! let hot = subreddit.hot(25, None).await;
57//!
58//! // Get after param from `hot`
59//! let after = hot.unwrap().data.after.unwrap();
60//! let after_options = FeedOption::new().after(&after);
61//!
62//! // Gets next 25
63//! let next_hot = subreddit.hot(25, Some(after_options)).await;
64//! # }
65//! ```
66pub mod response;
67extern crate serde_json;
68
69use crate::models::subreddit::response::{SubredditData, SubredditResponse, SubredditsData};
70
71use crate::client::Client;
72use crate::util::defaults::default_client;
73use crate::util::{FeedOption, RouxError};
74
75use crate::models::{Comments, Moderators, Submissions};
76
77/// Access subreddits API
78pub struct Subreddits;
79
80impl Subreddits {
81    /// Search subreddits
82    #[maybe_async::maybe_async]
83    pub async fn search(
84        name: &str,
85        limit: Option<u32>,
86        options: Option<FeedOption>,
87    ) -> Result<SubredditsData, RouxError> {
88        let url = &mut format!("https://www.reddit.com/subreddits/search.json?q={}", name);
89
90        if let Some(limit) = limit {
91            url.push_str(&format!("&limit={}", limit));
92        }
93
94        if let Some(options) = options {
95            options.build_url(url);
96        }
97
98        let client = default_client();
99
100        Ok(client
101            .get(&url.to_owned())
102            .send()
103            .await?
104            .json::<SubredditsData>()
105            .await?)
106    }
107}
108
109/// Subreddit
110pub struct Subreddit {
111    /// Name of subreddit.
112    pub name: String,
113    url: String,
114    client: Client,
115    is_oauth: bool,
116}
117
118impl Subreddit {
119    /// Create a new `Subreddit` instance.
120    pub fn new(name: &str) -> Subreddit {
121        let subreddit_url = format!("https://www.reddit.com/r/{}", name);
122
123        Subreddit {
124            name: name.to_owned(),
125            url: subreddit_url,
126            client: default_client(),
127            is_oauth: false,
128        }
129    }
130
131    /// Create a new authenticated `Subreddit` instance using an oauth client
132    /// from the `Reddit` module.
133    pub fn new_oauth(name: &str, client: &Client) -> Subreddit {
134        let subreddit_url = format!("https://oauth.reddit.com/r/{}", name);
135
136        Subreddit {
137            name: name.to_owned(),
138            url: subreddit_url,
139            client: client.to_owned(),
140            is_oauth: true,
141        }
142    }
143
144    /// Get moderators (requires authentication)
145    #[maybe_async::maybe_async]
146    pub async fn moderators(&self) -> Result<Moderators, RouxError> {
147        if self.is_oauth {
148            Ok(self
149                .client
150                .get(&format!("{}/about/moderators/.json", self.url))
151                .send()
152                .await?
153                .json::<Moderators>()
154                .await?)
155        } else {
156            Err(RouxError::OAuthClientRequired)
157        }
158    }
159
160    /// Get subreddit data.
161    #[maybe_async::maybe_async]
162    pub async fn about(&self) -> Result<SubredditData, RouxError> {
163        Ok(self
164            .client
165            .get(&format!("{}/about/.json", self.url))
166            .send()
167            .await?
168            .json::<SubredditResponse>()
169            .await?
170            .data)
171    }
172
173    #[maybe_async::maybe_async]
174    async fn get_feed(
175        &self,
176        ty: &str,
177        limit: u32,
178        options: Option<FeedOption>,
179    ) -> Result<Submissions, RouxError> {
180        let url = &mut format!("{}/{}.json?limit={}", self.url, ty, limit);
181
182        if let Some(options) = options {
183            options.build_url(url);
184        }
185
186        Ok(self
187            .client
188            .get(&url.to_owned())
189            .send()
190            .await?
191            .json::<Submissions>()
192            .await?)
193    }
194
195    #[maybe_async::maybe_async]
196    async fn get_comment_feed(
197        &self,
198        ty: &str,
199        depth: Option<u32>,
200        limit: Option<u32>,
201    ) -> Result<Comments, RouxError> {
202        let url = &mut format!("{}/{}.json?", self.url, ty);
203
204        if let Some(depth) = depth {
205            url.push_str(&format!("&depth={}", depth));
206        }
207
208        if let Some(limit) = limit {
209            url.push_str(&format!("&limit={}", limit));
210        }
211
212        // This is one of the dumbest APIs I've ever seen.
213        // The comments for a subreddit are stored in a normal hash map
214        // but for posts the comments are in an array with the ONLY item
215        // being same hash map as the one for subreddits...
216        if url.contains("comments/") {
217            let mut comments = self
218                .client
219                .get(&url.to_owned())
220                .send()
221                .await?
222                .json::<Vec<Comments>>()
223                .await?;
224
225            Ok(comments.pop().unwrap())
226        } else {
227            Ok(self
228                .client
229                .get(&url.to_owned())
230                .send()
231                .await?
232                .json::<Comments>()
233                .await?)
234        }
235    }
236
237    /// Get hot posts.
238    #[maybe_async::maybe_async]
239    pub async fn hot(
240        &self,
241        limit: u32,
242        options: Option<FeedOption>,
243    ) -> Result<Submissions, RouxError> {
244        self.get_feed("hot", limit, options).await
245    }
246
247    /// Get rising posts.
248    #[maybe_async::maybe_async]
249    pub async fn rising(
250        &self,
251        limit: u32,
252        options: Option<FeedOption>,
253    ) -> Result<Submissions, RouxError> {
254        self.get_feed("rising", limit, options).await
255    }
256
257    /// Get top posts.
258    #[maybe_async::maybe_async]
259    pub async fn top(
260        &self,
261        limit: u32,
262        options: Option<FeedOption>,
263    ) -> Result<Submissions, RouxError> {
264        self.get_feed("top", limit, options).await
265    }
266
267    /// Get latest posts.
268    #[maybe_async::maybe_async]
269    pub async fn latest(
270        &self,
271        limit: u32,
272        options: Option<FeedOption>,
273    ) -> Result<Submissions, RouxError> {
274        self.get_feed("new", limit, options).await
275    }
276
277    /// Get latest comments.
278    #[maybe_async::maybe_async]
279    pub async fn latest_comments(
280        &self,
281        depth: Option<u32>,
282        limit: Option<u32>,
283    ) -> Result<Comments, RouxError> {
284        self.get_comment_feed("comments", depth, limit).await
285    }
286
287    /// Get comments from article.
288    #[maybe_async::maybe_async]
289    pub async fn article_comments(
290        &self,
291        article: &str,
292        depth: Option<u32>,
293        limit: Option<u32>,
294    ) -> Result<Comments, RouxError> {
295        self.get_comment_feed(&format!("comments/{}", article), depth, limit)
296            .await
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use super::Subreddit;
303    use super::Subreddits;
304
305    #[maybe_async::async_impl]
306    #[tokio::test]
307    async fn test_no_auth() {
308        let subreddit = Subreddit::new("astolfo");
309
310        // Test feeds
311        let hot = subreddit.hot(25, None).await;
312        assert!(hot.is_ok());
313
314        let rising = subreddit.rising(25, None).await;
315        assert!(rising.is_ok());
316
317        let top = subreddit.top(25, None).await;
318        assert!(top.is_ok());
319
320        let latest_comments = subreddit.latest_comments(None, Some(25)).await;
321        assert!(latest_comments.is_ok());
322
323        let article_id = &hot.unwrap().data.children.first().unwrap().data.id.clone();
324        let article_comments = subreddit.article_comments(article_id, None, Some(25)).await;
325        assert!(article_comments.is_ok());
326
327        // Test subreddit data.
328        let data_res = subreddit.about().await;
329        assert!(data_res.is_ok());
330
331        let data = data_res.unwrap();
332        assert!(data.title == Some(String::from("Rider of Black, Astolfo")));
333        assert!(data.subscribers.is_some());
334        assert!(data.subscribers.unwrap() > 1000);
335
336        assert!(subreddit.moderators().await.is_err());
337
338        // Test subreddit search
339        let subreddits_limit = 3u32;
340        let subreddits = Subreddits::search("rust", Some(subreddits_limit), None).await;
341        assert!(subreddits.is_ok());
342        assert!(subreddits.unwrap().data.children.len() == subreddits_limit as usize);
343    }
344}