1mod client;
2mod model;
3mod serde;
4
5pub use self::client::Client;
6pub use self::client::CreatePostBuilder;
7pub use self::client::ListPostsBuilder;
8pub use self::client::SortOrder;
9pub use self::client::UpdatePostBuilder;
10pub use self::client::UploadPostFile;
11use self::model::ApiCompletedResponse;
12use self::model::ApiResponse;
13use self::model::ApiUpdateFilesBulkRequest;
14pub use self::model::FileUpdate;
15pub use self::model::InvalidScrapedPostError;
16pub use self::model::InvalidScrapedUserError;
17pub use self::model::ListPostsPost;
18pub use self::model::Post;
19pub use self::model::PostFile;
20pub use self::model::PostPrivacy;
21pub use self::model::ScrapedPost;
22pub use self::model::ScrapedPostFile;
23pub use self::model::ScrapedUser;
24pub use self::model::User;
25pub use reqwest::Body;
26pub use reqwest::Url;
27pub use reqwest_cookie_store::CookieStore;
28pub use reqwest_cookie_store::CookieStoreMutex;
29pub use reqwest_cookie_store::RawCookie;
30
31#[derive(Debug, thiserror::Error)]
33pub enum Error {
34 #[error("reqwest http error")]
36 Reqwest(#[from] reqwest::Error),
37
38 #[error("failed to join tokio task")]
40 TokioJoin(#[from] tokio::task::JoinError),
41
42 #[error("invalid scraped post")]
44 InvalidScrapedPost(#[from] InvalidScrapedPostError),
45
46 #[error("invalid scraped user")]
48 InvalidScrapedUser(#[from] InvalidScrapedUserError),
49
50 #[error("missing token")]
52 MissingToken,
53
54 #[error("need at least 1 image")]
56 MissingImages,
57
58 #[error("api operation was not successful")]
60 ApiOperationFailed,
61
62 #[error("api response missing messsage")]
64 ApiResponseMissingMessage,
65
66 #[error("api response had unknown message \"{message}\"")]
68 ApiResponseUnknownMessage {
69 message: Box<str>,
71 },
72
73 #[error("missing description")]
75 MissingDescription,
76
77 #[error("title too short, must be at least 3 characters")]
79 TitleTooShort,
80}
81
82#[cfg(test)]
83mod test {
84 use super::*;
85 use std::sync::OnceLock;
86 use time::format_description::well_known::Iso8601;
87 use time::OffsetDateTime;
88
89 const POST_ID: &str = "3qe4gdvj4j2";
90 const GIF_POST_ID: &str = "pwl7lgepyx2";
91 const VIDEO_POST_ID: &str = "ej7mko58jyd";
92
93 const USER_NAME: &str = "LunarLandr";
94
95 fn get_token() -> &'static str {
96 static TOKEN: OnceLock<String> = OnceLock::new();
97 TOKEN.get_or_init(|| {
98 let token_env = std::env::var_os("IMGCHEST_TOKEN").map(|token| {
99 token
100 .into_string()
101 .expect("\"IMGCHEST_TOKEN\" env var value is not valid unicode")
102 });
103
104 if let Some(token) = token_env {
105 return token;
106 }
107
108 let token_file = match std::fs::read_to_string("token.txt") {
109 Ok(token) => Some(token),
110 Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
111 Err(error) => panic!("failed to read token from file: {error}"),
112 };
113
114 if let Some(token) = token_file {
115 return token;
116 }
117
118 panic!("missing token");
119 })
120 }
121
122 #[tokio::test]
123 async fn get_scraped_post() {
124 let client = Client::new();
125 let post = client
126 .get_scraped_post(POST_ID)
127 .await
128 .expect("failed to get scraped post");
129 assert!(&*post.id == "3qe4gdvj4j2");
130 assert!(&*post.title == "Donkey Kong - Video Game From The Mid 80's");
131 assert!(&*post.username == "LunarLandr");
132 assert!(post.views >= 198);
135 assert!(!post.nsfw);
136 assert!(post.image_count == 4);
137 assert!(&*post.images[0].id == "nw7w6cmlvye");
140 assert!(post.images[0]
141 .description
142 .as_ref()
143 .expect("missing description")
144 .starts_with("**Description** \nReleased in the arcades in 1981, Donkey Kong"));
145 assert!(&*post.images[0].link == "https://cdn.imgchest.com/files/nw7w6cmlvye.png");
146
147 assert!(&*post.images[1].id == "kwye3cpag4b");
148 assert!(post.images[1].description.as_deref() == Some("amstrad - apple ii - atari - colecovision - c64 - msx\nnes - pc - vic-20 - spectrum - tI-99 4A - arcade"));
149 assert!(&*post.images[1].link == "https://cdn.imgchest.com/files/kwye3cpag4b.png");
150
151 assert!(&*post.images[2].id == "5g4z9c8ok72");
152 assert!(post.images[2].description.as_deref() == Some(""));
153 assert!(&*post.images[2].link == "https://cdn.imgchest.com/files/5g4z9c8ok72.png");
154
155 assert!(&*post.images[3].id == "we4gdcv5j4r");
156 assert!(post.images[3].description.as_deref() == Some(""));
157 assert!(&*post.images[3].link == "https://cdn.imgchest.com/files/we4gdcv5j4r.jpg");
158
159 dbg!(&post);
160 }
161
162 #[tokio::test]
163 async fn get_scraped_gif_post() {
164 let client = Client::new();
165 let post = client
166 .get_scraped_post(GIF_POST_ID)
167 .await
168 .expect("failed to get post");
169
170 assert!(&*post.id == "pwl7lgepyx2");
171 assert!(&*post.title == "PDN AGIF Issue #1");
172 assert!(&*post.username == "Jacob");
173 assert!(post.views >= 2537);
174 assert!(post.image_count == 1);
175
176 assert!(&*post.images[0].id == "6yxkcz5ml7w");
177 assert!(post.images[0].description.as_deref() == Some("Notice how inserting an AGIF is now supported, but does not want to be moved from its initial position."));
178 assert!(&*post.images[0].link == "https://cdn.imgchest.com/files/6yxkcz5ml7w.gif");
179
180 dbg!(&post);
181 }
182
183 #[tokio::test]
184 async fn get_scraped_video_post() {
185 let client = Client::new();
186 let post = client
187 .get_scraped_post(VIDEO_POST_ID)
188 .await
189 .expect("failed to get post");
190
191 assert!(&*post.id == "ej7mko58jyd");
192 assert!(&*post.title == "Better with sound");
193 assert!(&*post.username == "moods");
194 assert!(post.views >= 336);
195 assert!(post.image_count == 1);
196
197 assert!(&*post.images[0].id == "e4gdcbqe294");
198 assert!(post.images[0].description.is_none());
199 assert!(&*post.images[0].link == "https://cdn.imgchest.com/files/e4gdcbqe294.mp4");
200
201 dbg!(&post);
202 }
203
204 #[tokio::test]
205 async fn get_scraped_user() {
206 let client = Client::new();
207 let user = client
208 .get_scraped_user(USER_NAME)
209 .await
210 .expect("failed to get scraped user");
211 assert!(&*user.name == USER_NAME);
212 assert!(user.posts >= 268);
213 assert!(user.comments >= 1);
214 assert!(user.created == time::macros::datetime!(2019-09-25 0:00 UTC));
215
216 assert!(user.post_views >= 1867537);
217 assert!(user.experience >= 12871);
218 assert!(user.favorites == 0);
219 }
220
221 #[tokio::test]
222 async fn list_posts_home() {
223 let client = Client::new();
224 let builder = ListPostsBuilder::new();
225 let posts = client
226 .list_posts(builder)
227 .await
228 .expect("failed to list posts");
229
230 dbg!(posts);
231
232 let mut builder = ListPostsBuilder::new();
233 builder.sort(SortOrder::Old);
234 let posts_old = client
235 .list_posts(builder)
236 .await
237 .expect("failed to list posts");
238
239 dbg!(posts_old);
240
241 let mut builder = ListPostsBuilder::new();
242 builder.sort(SortOrder::Popular);
243 builder.username("LunarLandr".to_string());
244 let posts = client
245 .list_posts(builder)
246 .await
247 .expect("failed to list posts");
248
249 dbg!(posts);
250 }
251
252 #[tokio::test]
253 async fn get_post_no_token() {
254 let client = Client::new();
255
256 let err = client
257 .get_post("3qe4gdvj4j2")
258 .await
259 .expect_err("succeeded getting post with no token");
260 assert!(matches!(err, Error::MissingToken));
261 }
262
263 #[tokio::test]
264 async fn get_post() {
265 let client = Client::new();
266 client.set_token(get_token());
267
268 let post = client
269 .get_post("3qe4gdvj4j2")
270 .await
271 .expect("failed to get post");
272
273 assert!(&*post.id == "3qe4gdvj4j2");
274 assert!(post.title.as_deref() == Some("Donkey Kong - Video Game From The Mid 80's"));
275 assert!(&*post.username == "LunarLandr");
276 assert!(post.privacy == PostPrivacy::Public);
277 assert!(post.report_status == 1);
278 assert!(post.views >= 198);
279 assert!(!post.nsfw);
280 assert!(post.image_count == 4);
281 assert!(
282 post.created
283 == OffsetDateTime::parse("2019-11-03T00:36:00.000000Z", &Iso8601::DEFAULT).unwrap()
284 );
285 assert!(post.delete_url.is_none());
286
287 assert!(&*post.images[0].id == "nw7w6cmlvye");
288 assert!(post.images[0]
289 .description
290 .as_ref()
291 .expect("missing description")
292 .starts_with("**Description** \nReleased in the arcades in 1981, Donkey Kong"));
293 assert!(&*post.images[0].link == "https://cdn.imgchest.com/files/nw7w6cmlvye.png");
294 assert!(post.images[0].position.get() == 1);
295 assert!(
296 post.images[0].created
297 == OffsetDateTime::parse("2019-11-03T00:36:00.000000Z", &Iso8601::DEFAULT).unwrap()
298 );
299 assert!(post.images[0].original_name.is_none());
300
301 assert!(&*post.images[1].id == "kwye3cpag4b");
302 assert!(post.images[1].description.as_deref() == Some("amstrad - apple ii - atari - colecovision - c64 - msx\nnes - pc - vic-20 - spectrum - tI-99 4A - arcade"));
303 assert!(&*post.images[1].link == "https://cdn.imgchest.com/files/kwye3cpag4b.png");
304 assert!(post.images[1].position.get() == 2);
305 assert!(
306 post.images[1].created
307 == OffsetDateTime::parse("2019-11-03T00:36:00.000000Z", &Iso8601::DEFAULT).unwrap()
308 );
309 assert!(post.images[1].original_name.is_none());
310
311 assert!(&*post.images[2].id == "5g4z9c8ok72");
312 assert!(post.images[2].description.as_deref() == Some(""));
313 assert!(&*post.images[2].link == "https://cdn.imgchest.com/files/5g4z9c8ok72.png");
314 assert!(post.images[2].position.get() == 3);
315 assert!(
316 post.images[2].created
317 == OffsetDateTime::parse("2019-11-03T00:36:00.000000Z", &Iso8601::DEFAULT).unwrap()
318 );
319 assert!(post.images[2].original_name.is_none());
320
321 assert!(&*post.images[3].id == "we4gdcv5j4r");
322 assert!(post.images[3].description.as_deref() == Some(""));
323 assert!(&*post.images[3].link == "https://cdn.imgchest.com/files/we4gdcv5j4r.jpg");
324 assert!(post.images[3].position.get() == 4);
325 assert!(
326 post.images[3].created
327 == OffsetDateTime::parse("2019-11-03T00:36:00.000000Z", &Iso8601::DEFAULT).unwrap()
328 );
329 assert!(post.images[3].original_name.is_none());
330
331 dbg!(&post);
332 }
333
334 #[tokio::test]
335 async fn get_user() {
336 let client = Client::new();
337 client.set_token(get_token());
338
339 let user = client
340 .get_user("LunarLandr")
341 .await
342 .expect("failed to get user");
343
344 assert!(&*user.name == "LunarLandr");
345 assert!(
346 user.created
347 == OffsetDateTime::parse("2019-09-25T01:00:45.000000Z", &Iso8601::DEFAULT).unwrap()
348 );
349
350 dbg!(&user);
351 }
352
353 #[tokio::test]
384 async fn create_post_too_short_title() {
385 let client = Client::new();
386 client.set_token(get_token());
387
388 let mut builder = CreatePostBuilder::new();
389 builder.title("");
390
391 let err = client
392 .create_post(builder)
393 .await
394 .expect_err("title should have been too short");
395
396 assert!(matches!(err, Error::TitleTooShort));
397 }
398
399 #[tokio::test]
400 async fn update_post_too_short_title() {
401 let client = Client::new();
402 client.set_token(get_token());
403
404 let mut builder = UpdatePostBuilder::new();
405 builder.title("");
406
407 let err = client
408 .update_post("3qe4gdvj4j2", builder)
409 .await
410 .expect_err("title should have been too short");
411
412 assert!(matches!(err, Error::TitleTooShort));
413 }
414
415 #[tokio::test]
416 async fn add_post_images_missing_images() {
417 let client = Client::new();
418 client.set_token(get_token());
419
420 let err = client
421 .add_post_images("3qe4gdvj4j2", Vec::new())
422 .await
423 .expect_err("should be missing images");
424
425 assert!(matches!(err, Error::MissingImages));
426 }
427
428 #[tokio::test]
429 async fn create_post_missing_images() {
430 let client = Client::new();
431 client.set_token(get_token());
432
433 let builder = CreatePostBuilder::new();
434
435 let err = client
436 .create_post(builder)
437 .await
438 .expect_err("should be missing images");
439
440 assert!(matches!(err, Error::MissingImages));
441 }
442
443 #[tokio::test]
444 async fn update_file_missing_description() {
445 let client = Client::new();
446 client.set_token(get_token());
447
448 let err = client
449 .update_file("pwl7lgepyx2", "")
450 .await
451 .expect_err("should be missing description");
452
453 assert!(matches!(err, Error::MissingDescription));
454 }
455
456 #[tokio::test]
457 async fn update_files_bulk_missing_description() {
458 let client = Client::new();
459 client.set_token(get_token());
460
461 let err = client
462 .update_files_bulk(vec![FileUpdate {
463 id: "pwl7lgepyx2".into(),
464 description: "".into(),
465 }])
466 .await
467 .expect_err("should be missing description");
468
469 assert!(matches!(err, Error::MissingDescription));
470 }
471}