1use crate::{
2 error::Error,
3 types::Thing,
4};
5
6const DEFAULT_PLATFORM: &str = "pc";
10
11const DEFAULT_APP_ID: &str = env!("CARGO_PKG_NAME");
12const DEFAULT_APP_VERSION: &str = env!("CARGO_PKG_VERSION");
13
14const DEFAULT_REDDIT_USERNAME: &str = "deleted";
16
17#[derive(Clone)]
19pub struct Client {
20 pub client: reqwest::Client,
25}
26
27impl Client {
28 pub fn new() -> Self {
30 Self::new_with_user_agent(
31 DEFAULT_PLATFORM,
32 DEFAULT_APP_ID,
33 DEFAULT_APP_VERSION,
34 DEFAULT_REDDIT_USERNAME,
35 )
36 }
37
38 pub fn new_with_user_agent(
42 platform: &str,
43 app_id: &str,
44 app_version: &str,
45 reddit_username: &str,
46 ) -> Self {
47 let user_agent = format!("{platform}:{app_id}:v{app_version} (by /u/{reddit_username})");
48
49 let mut client_builder = reqwest::Client::builder();
50 client_builder = client_builder.user_agent(user_agent);
51
52 let client = client_builder
53 .build()
54 .expect("failed to build reddit client");
55
56 Self { client }
57 }
58
59 pub async fn get_subreddit(&self, subreddit: &str, num_posts: usize) -> Result<Thing, Error> {
61 let url = format!("https://www.reddit.com/r/{subreddit}.json?limit={num_posts}");
62 let res = self.client.get(&url).send().await?.error_for_status()?;
63
64 const SEARCH_URL: &str = "https://www.reddit.com/subreddits/search.json?";
66 if res.url().as_str().starts_with(SEARCH_URL) {
67 return Err(Error::SubredditNotFound);
68 }
69
70 let text = res.text().await?;
71 serde_json::from_str(&text).map_err(|error| Error::Json {
72 data: text.into(),
73 error,
74 })
75 }
76
77 pub async fn get_post(&self, subreddit: &str, post_id: &str) -> Result<Vec<Thing>, Error> {
79 let url = format!("https://www.reddit.com/r/{subreddit}/comments/{post_id}.json");
80 Ok(self
81 .client
82 .get(&url)
83 .send()
84 .await?
85 .error_for_status()?
86 .json()
87 .await?)
88 }
89}
90
91impl Default for Client {
92 fn default() -> Self {
93 Self::new()
94 }
95}
96
97#[cfg(test)]
98mod test {
99 use super::*;
100 use crate::Listing;
101
102 async fn get_subreddit(name: &str) -> Result<(), Error> {
103 let client = Client::new();
104 let subreddit = client.get_subreddit(name, 100).await?;
106 println!(
107 "# of children: {}",
108 subreddit.data.as_listing().unwrap().children.len()
109 );
110 Ok(())
111 }
112
113 #[tokio::test]
114 #[ignore]
115 async fn get_post_works() {
116 let post_data = [
117 ("dankmemes", "h966lq"),
118 ("oddlysatisfying", "ha7obv"),
120 ];
121 let client = Client::new();
122
123 for (subreddit, post_id) in post_data.iter() {
124 let post = client
125 .get_post(subreddit, post_id)
126 .await
127 .expect("failed to get post");
128 dbg!(&post);
129 }
130 }
131
132 #[tokio::test]
133 #[ignore]
134 async fn get_subreddit_works() {
135 let subreddits = [
136 "forbiddensnacks",
137 "dankmemes",
138 "cursedimages",
139 "MEOW_IRL",
140 "cuddleroll",
141 "cromch",
142 "cats",
143 "cursed_images",
144 "aww",
145 "dogpictures",
146 ];
147
148 for subreddit in subreddits.iter() {
149 match get_subreddit(subreddit).await {
150 Ok(()) => {}
151 Err(Error::Json { data, .. }) => {
152 #[derive(Debug, serde::Deserialize)]
154 struct Subreddit {
155 #[expect(dead_code)]
156 data: Listing,
157 }
158
159 let jd = &mut serde_json::Deserializer::from_str(&data);
160 let error = serde_path_to_error::deserialize::<_, Subreddit>(jd)
161 .expect_err("deserializing with serde_path_to_error should error too");
162
163 tokio::fs::write("subreddit-error.json", data.as_bytes())
164 .await
165 .expect("failed to save error");
166
167 panic!("failed to get subreddit \"{subreddit}\": {error:#?}");
168 }
169 Err(error) => {
170 panic!("failed to get subreddit \"{subreddit}\": {error:#?}");
171 }
172 }
173 }
174 }
175
176 #[tokio::test]
177 #[ignore]
178 async fn invalid_subreddit() {
179 let client = Client::new();
180 let error = client.get_subreddit("gfdghfj", 25).await.unwrap_err();
181 assert!(error.is_subreddit_not_found(), "error = {error:#?}");
182 }
183}