imgchest/
lib.rs

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/// The error
32#[derive(Debug, thiserror::Error)]
33pub enum Error {
34    /// Reqwest error
35    #[error("reqwest http error")]
36    Reqwest(#[from] reqwest::Error),
37
38    /// Failed to join tokio task
39    #[error("failed to join tokio task")]
40    TokioJoin(#[from] tokio::task::JoinError),
41
42    /// Failed to parse post
43    #[error("invalid scraped post")]
44    InvalidScrapedPost(#[from] InvalidScrapedPostError),
45
46    /// Failed to parse user
47    #[error("invalid scraped user")]
48    InvalidScrapedUser(#[from] InvalidScrapedUserError),
49
50    /// Missing a token
51    #[error("missing token")]
52    MissingToken,
53
54    /// Missing images
55    #[error("need at least 1 image")]
56    MissingImages,
57
58    /// An api operation was not successful
59    #[error("api operation was not successful")]
60    ApiOperationFailed,
61
62    /// An api response is missing a message
63    #[error("api response missing messsage")]
64    ApiResponseMissingMessage,
65
66    /// An api response had un unknown message
67    #[error("api response had unknown message \"{message}\"")]
68    ApiResponseUnknownMessage {
69        /// The unknown message
70        message: Box<str>,
71    },
72
73    /// Missing description
74    #[error("missing description")]
75    MissingDescription,
76
77    /// The title is too short.
78    #[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.privacy == "public");
133        // assert!(post.report_status == 1);
134        assert!(post.views >= 198);
135        assert!(!post.nsfw);
136        assert!(post.image_count == 4);
137        // assert!(post.created == "2019-11-03T00:36:00.000000Z");
138
139        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    // Endpoint appears disabled
354    /*
355    #[tokio::test]
356    async fn get_file() {
357        let client = Client::new();
358        client.set_token(get_token());
359
360        let file = client
361            .get_file("nw7w6cmlvye")
362            .await
363            .expect("failed to get file");
364
365        assert!(&*file.id == "nw7w6cmlvye");
366        assert!(file
367            .description
368            .as_ref()
369            .expect("missing description")
370            .starts_with("**Description**  \nReleased in the arcades in 1981, Donkey Kong"));
371        assert!(&*file.link == "https://cdn.imgchest.com/files/nw7w6cmlvye.png");
372        assert!(file.position.get() == 1);
373        assert!(
374            file.created
375                == OffsetDateTime::parse("2019-11-03T00:36:00.000000Z", &Iso8601::DEFAULT).unwrap()
376        );
377        assert!(file.original_name.is_none());
378
379        dbg!(&file);
380    }
381    */
382
383    #[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}