reddit/types.rs
1/// Reddit base class.
2/// Listing things have neither name nor id because they are indefinite objects.
3/// That is, they are system generated, not user submitted, and are subject to change quickly and expire.
4/// See https://github.com/reddit-archive/reddit/wiki/JSON#thing-reddit-base-class
5#[derive(Debug, serde::Deserialize)]
6pub struct Thing {
7 /// this item's identifier, e.g. "8xwlg"
8 pub id: Option<Box<str>>,
9
10 /// Fullname of comment, e.g. "t1_c3v7f8u"
11 pub name: Option<Box<str>>,
12
13 /// Data
14 #[serde(flatten)]
15 pub data: ThingData,
16}
17
18/// kind:
19/// All things have a kind. The kind is a String identifier that denotes the object's type.
20/// Some examples: Listing, more, t1, t2
21/// data:
22/// A custom data structure used to hold valuable information.
23/// This object's format will follow the data structure respective of its kind. See below for specific structures.
24/// See https://www.reddit.com/dev/api#fullnames
25#[derive(Debug, serde::Deserialize)]
26#[serde(tag = "kind", content = "data")]
27pub enum ThingData {
28 #[serde(rename = "Listing")]
29 Listing(Box<Listing>),
30
31 // More is small + it already has a vector of things as a vec
32 #[serde(rename = "more")]
33 More(More),
34
35 #[serde(rename = "t1")]
36 Comment(Box<Comment>),
37
38 // TODO: Finish type
39 // #[serde(rename = "t2")]
40 // Account(serde_json::Value),
41 #[serde(rename = "t3")]
42 Link(Box<Link>),
43}
44
45impl ThingData {
46 /// Tries to get this ThingData as a listing
47 pub fn as_listing(&self) -> Option<&Listing> {
48 match self {
49 ThingData::Listing(listing) => Some(listing),
50 _ => None,
51 }
52 }
53
54 /// Tries to get this ThingData as a mutable listing
55 pub fn as_listing_mut(&mut self) -> Option<&mut Listing> {
56 match self {
57 ThingData::Listing(listing) => Some(listing),
58 _ => None,
59 }
60 }
61
62 /// Tries to turn this ThingData into a listing
63 pub fn into_listing(self) -> Option<Box<Listing>> {
64 match self {
65 ThingData::Listing(listing) => Some(listing),
66 _ => None,
67 }
68 }
69
70 /// Tries to get this ThingData as a link
71 pub fn as_link(&self) -> Option<&Link> {
72 match self {
73 ThingData::Link(link) => Some(link),
74 _ => None,
75 }
76 }
77
78 /// Tries to turn this ThingData into a link
79 pub fn into_link(self) -> Option<Box<Link>> {
80 match self {
81 ThingData::Link(link) => Some(link),
82 _ => None,
83 }
84 }
85}
86
87/// Used to paginate content that is too long to display in one go.
88/// Add the query argument before or after with the value given to get the previous or next page.
89/// This is usually used in conjunction with a count argument.
90/// Exception: Unlike the other classes documented on this page, Listing is not a thing subclass, as it inherits directly from the Python base class, Object.
91/// Although it does have data and kind as parameters, it does not have id and name.
92/// A listing's kind will always be Listing and its data will be a List of things.
93/// kind == "Listing"
94/// See https://github.com/reddit-archive/reddit/wiki/JSON#listing
95#[derive(Debug, serde::Deserialize)]
96pub struct Listing {
97 /// The fullname of the listing that follows before this page. null if there is no previous page.
98 pub before: Option<Box<str>>,
99
100 /// The fullname of the listing that follows after this page. null if there is no next page.
101 pub after: Option<Box<str>>,
102
103 /// This modhash is not the same modhash provided upon login.
104 /// You do not need to update your user's modhash everytime you get a new modhash.
105 /// You can reuse the modhash given upon login.
106 pub modhash: Box<str>,
107
108 /// A list of things that this Listing wraps.
109 pub children: Vec<Thing>,
110}
111
112/// Implementation
113/// See: https://github.com/reddit-archive/reddit/wiki/JSON#votable-implementation
114#[derive(Debug, serde::Deserialize)]
115pub struct Votable {
116 /// the number of upvotes. (includes own)
117 pub ups: i64,
118
119 /// the number of downvotes. (includes own)
120 pub downs: u64,
121
122 /// true if thing is liked by the user, false if thing is disliked, null if the user has not voted or you are not logged in.
123 /// Certain languages such as Java may need to use a boolean wrapper that supports null assignment.
124 pub likes: Option<bool>,
125}
126
127/// Implementation
128/// See https://github.com/reddit-archive/reddit/wiki/JSON#created-implementation
129#[derive(Debug, serde::Deserialize)]
130pub struct Created {
131 // TODO: use chrono?
132 /// the time of creation in local epoch-second format. ex: 1331042771.0
133 pub created: f64,
134
135 /// the time of creation in UTC epoch-second format. Note that neither of these ever have a non-zero fraction.
136 pub created_utc: f64,
137}
138
139/// Implements votable | created
140/// kind == "t1"
141/// See https://github.com/reddit-archive/reddit/wiki/JSON#comment-implements-votable--created
142#[derive(Debug, serde::Deserialize)]
143pub struct Comment {
144 /// who approved this comment. null if nobody or you are not a mod
145 pub approved_by: Option<Box<str>>,
146
147 /// the account name of the poster
148 pub author: Box<str>,
149
150 /// the CSS class of the author's flair. subreddit specific
151 pub author_flair_css_class: Option<Box<str>>,
152
153 /// the text of the author's flair. subreddit specific
154 pub author_flair_text: Option<Box<str>>,
155
156 /// who removed this comment. null if nobody or you are not a mod
157 pub banned_by: Option<Box<str>>,
158
159 /// the raw text.
160 /// this is the unformatted text which includes the raw markup characters such as ** for bold. <, >, and & are escaped.
161 pub body: Box<str>,
162
163 /// the formatted HTML text as displayed on reddit.
164 /// For example, text that is emphasised by * will now have <em> tags wrapping it.
165 /// Additionally, bullets and numbered lists will now be in HTML list format.
166 /// NOTE: The HTML string will be escaped. You must unescape to get the raw HTML.
167 pub body_html: Box<str>,
168
169 /// false if not edited, edit date in UTC epoch-seconds otherwise.
170 /// NOTE: for some old edited comments on reddit.com, this will be set to true instead of edit date.
171 pub special: Option<serde_json::Value>,
172
173 /// the number of times this comment received reddit gold
174 pub gilded: u64,
175
176 /// how the logged-in user has voted on the comment - True = upvoted, False = downvoted, null = no vote
177 pub likes: Option<bool>,
178
179 /// present if the comment is being displayed outside its thread (user pages, /r/subreddit/comments/.json, etc.).
180 /// Contains the author of the parent link
181 pub link_author: Option<Box<str>>,
182
183 /// ID of the link this comment is in
184 pub link_id: Box<str>,
185
186 /// present if the comment is being displayed outside its thread (user pages, /r/subreddit/comments/.json, etc.).
187 /// Contains the title of the parent link
188 pub link_title: Option<Box<str>>,
189
190 /// present if the comment is being displayed outside its thread (user pages, /r/subreddit/comments/.json, etc.).
191 /// Contains the URL of the parent link
192 pub link_url: Option<Box<str>>,
193
194 /// how many times this comment has been reported, null if not a mod
195 pub num_reports: Option<u64>,
196
197 /// ID of the thing this comment is a reply to, either the link or a comment in it
198 pub parent_id: Box<str>,
199
200 // TODO: Find out why this is a string sometimes
201 // /// A list of replies to this comment
202 // pub replies: Thing,
203 /// true if this post is saved by the logged in user
204 pub saved: bool,
205
206 /// the net-score of the comment
207 pub score: i64,
208
209 /// Whether the comment's score is currently hidden.
210 pub score_hidden: bool,
211
212 /// subreddit of thing excluding the /r/ prefix. "pics"
213 pub subreddit: Box<str>,
214
215 /// the id of the subreddit in which the thing is locatedss
216 pub subreddit_id: Box<str>,
217
218 /// to allow determining whether they have been distinguished by moderators/admins.
219 /// null = not distinguished.
220 /// moderator = the green \[M\].
221 /// admin = the red \[A\].
222 /// special = various other special distinguishes http://redd.it/19ak1b
223 pub distinguished: Option<Box<str>>,
224
225 /// Voting Implementation
226 #[serde(flatten)]
227 pub votable: Votable,
228
229 /// Created Implementation
230 #[serde(flatten)]
231 pub created: Created,
232 // Experimentally determined fields
233 // TODO: These are VERY best-effort, but i should still try to document what i can
234}
235
236/// Implements votable | created
237/// kind == "t3"
238/// See https://github.com/reddit-archive/reddit/wiki/JSON#link-implements-votable--created
239#[derive(Debug, serde::Deserialize)]
240pub struct Link {
241 /// the account name of the poster. null if this is a promotional link
242 pub author: Box<str>,
243
244 /// the CSS class of the author's flair. subreddit specific
245 pub author_flair_css_class: Option<Box<str>>,
246
247 /// the text of the author's flair. subreddit specific
248 pub author_flair_text: Option<Box<str>>,
249
250 /// probably always returns false
251 pub clicked: bool,
252
253 /// the domain of this link.
254 /// Self posts will be self.<subreddit> while other examples include en.wikipedia.org and s3.amazon.com
255 pub domain: Box<str>,
256
257 /// true if the post is hidden by the logged in user. false if not logged in or not hidden.
258 pub hidden: bool,
259
260 /// true if this link is a selfpost
261 pub is_self: bool,
262
263 /// how the logged-in user has voted on the link - True = upvoted, False = downvoted, null = no vote
264 pub likes: Option<bool>,
265
266 /// the CSS class of the link's flair.
267 pub link_flair_css_class: Option<Box<str>>,
268
269 /// the text of the link's flair.
270 pub link_flair_text: Option<Box<str>>,
271
272 /// whether the link is locked (closed to new comments) or not.
273 pub locked: bool,
274
275 // TODO: Finish type
276 /// Used for streaming video. Detailed information about the video and it's origins are placed here
277 pub media: serde_json::Value,
278
279 // TODO: Finish type
280 /// Used for streaming video. Technical embed specific information is found here.
281 pub media_embed: serde_json::Value,
282
283 /// the number of comments that belong to this link. includes removed comments.
284 pub num_comments: u64,
285
286 /// true if the post is tagged as NSFW. False if otherwise
287 pub over_18: bool,
288
289 /// relative URL of the permanent link for this link
290 pub permalink: Box<str>,
291
292 /// true if this post is saved by the logged in user
293 pub saved: bool,
294
295 /// the net-score of the link.
296 /// note: A submission's score is simply the number of upvotes minus the number of downvotes.
297 /// If five users like the submission and three users don't it will have a score of 2.
298 /// Please note that the vote numbers are not "real" numbers, they have been "fuzzed" to prevent spam bots etc.
299 /// So taking the above example, if five users upvoted the submission, and three users downvote it,
300 /// the upvote/downvote numbers may say 23 upvotes and 21 downvotes, or 12 upvotes, and 10 downvotes.
301 /// The points score is correct, but the vote totals are "fuzzed".
302 pub score: i64,
303
304 /// the raw text.
305 /// this is the unformatted text which includes the raw markup characters such as ** for bold.
306 /// <, >, and & are escaped.
307 /// Empty if not present.
308 pub selftext: Box<str>,
309
310 /// the formatted escaped HTML text.
311 /// this is the HTML formatted version of the marked up text.
312 /// Items that are boldened by ** or *** will now have <em> or *** tags on them.
313 /// Additionally, bullets and numbered lists will now be in HTML list format.
314 /// NOTE: The HTML string will be escaped. You must unescape to get the raw HTML. Null if not present.
315 pub selftext_html: Option<Box<str>>,
316
317 /// subreddit of thing excluding the /r/ prefix. "pics"
318 pub subreddit: Option<Box<str>>,
319
320 /// the id of the subreddit in which the thing is located
321 pub subreddit_id: Box<str>,
322
323 /// full URL to the thumbnail for this link;
324 /// "self" if this is a self post;
325 /// "image" if this is a link to an image but has no thumbnail;
326 /// "default" if a thumbnail is not available
327 pub thumbnail: Option<Box<str>>,
328
329 /// the title of the link. may contain newlines for some reason
330 pub title: Box<str>,
331
332 /// the link of this post. the permalink if this is a self-post. May be a relative Url.
333 pub url: Box<str>,
334
335 /// Indicates if link has been edited.
336 /// Will be the edit timestamp if the link has been edited and return false otherwise.
337 /// https://github.com/reddit/reddit/issues/581
338 pub edited: serde_json::Value,
339
340 /// to allow determining whether they have been distinguished by moderators/admins.
341 /// null = not distinguished.
342 /// moderator = the green \[M\].
343 /// admin = the red \[A\].
344 /// special = various other special distinguishes
345 /// http://bit.ly/ZYI47B
346 pub distinguished: Option<Box<str>>,
347
348 /// true if the post is set as the sticky in its subreddit.
349 pub stickied: bool,
350
351 /// Voting Implementation
352 #[serde(flatten)]
353 pub votable: Votable,
354
355 /// Created Implementation
356 #[serde(flatten)]
357 pub created: Created,
358
359 // Experimentally determined fields
360 // TODO: These are VERY best-effort, but i should still try to document what i can
361 pub archived: bool,
362 pub author_flair_template_id: Option<Box<str>>,
363 pub author_flair_text_color: Option<Box<str>>,
364 pub author_flair_type: Option<Box<str>>,
365 pub author_fullname: Option<Box<str>>,
366 pub author_patreon_flair: Option<bool>,
367 pub can_gild: bool,
368 pub can_mod_post: bool,
369 pub contest_mode: bool,
370
371 /// I believe that this is included for crossposted posts to get the data for the main post
372 pub crosspost_parent_list: Option<Vec<Link>>,
373
374 pub gilded: u64,
375 pub hide_score: bool,
376 pub id: Box<str>,
377 pub is_crosspostable: bool,
378 pub is_meta: bool,
379 pub is_original_content: bool,
380 pub is_reddit_media_domain: bool,
381 pub is_robot_indexable: bool,
382
383 /// Returns true if its a video
384 pub is_video: bool,
385
386 pub link_flair_text_color: Option<Box<str>>,
387 pub link_flair_type: Box<str>,
388 pub media_only: bool,
389 pub name: Box<str>,
390 pub no_follow: bool,
391 pub num_crossposts: u64,
392 pub parent_whitelist_status: Option<Box<str>>,
393
394 /// Whether this post is pinned
395 pub pinned: bool,
396
397 /// A "hint" about what this post may be
398 pub post_hint: Option<PostHint>,
399
400 pub pwls: Option<u64>,
401 pub quarantine: bool,
402 pub send_replies: bool,
403
404 /// Whether this post has a spoiler
405 pub spoiler: bool,
406
407 pub subreddit_name_prefixed: Box<str>,
408 pub subreddit_subscribers: u64,
409 pub subreddit_type: Box<str>,
410 pub suggested_sort: Option<Box<str>>,
411 pub thumbnail_height: Option<u32>,
412 pub thumbnail_width: Option<u32>,
413 pub visited: bool,
414 pub whitelist_status: Option<Box<str>>,
415 pub wls: Option<u32>,
416}
417
418/// kind == "more"
419/// See https://github.com/reddit-archive/reddit/wiki/JSON#more
420#[derive(Debug, serde::Deserialize)]
421pub struct More {
422 /// A list of String ids that are the additional things that can be downloaded but are not because there are too many to list.
423 pub children: Vec<Box<str>>,
424}
425
426/// Info on what the post may contain
427#[derive(Debug, serde::Deserialize, PartialEq, Eq)]
428pub enum PostHint {
429 /// The post is an image
430 #[serde(rename = "image")]
431 Image,
432
433 /// The post is a link
434 #[serde(rename = "link")]
435 Link,
436
437 /// A video hosted on reddit
438 #[serde(rename = "hosted:video")]
439 HostedVideo,
440
441 #[serde(rename = "rich:video")]
442 RichVideo,
443
444 #[serde(rename = "self")]
445 DataSelf,
446
447 #[serde(rename = "gallery")]
448 Gallery,
449}
450
451#[cfg(test)]
452mod test {
453 use super::*;
454
455 const SUBREDDIT_SAMPLE_1: &str = include_str!("../test_data/subreddit_dankmemes.json");
456 const SUBREDDIT_SAMPLE_2: &str = include_str!("../test_data/subreddit_cromch.json");
457 const SUBREDDIT_SAMPLE_3: &str = include_str!("../test_data/subreddit_cuddleroll.json");
458 const SUBREDDIT_SAMPLE_4: &str = include_str!("../test_data/subreddit_cursed_images.json");
459
460 const COMMENT_SAMPLE_1: &str = include_str!("../test_data/comment_h966lq.json");
461 const COMMENT_SAMPLE_2: &str = include_str!("../test_data/comment_h8p0py.json");
462
463 #[test]
464 fn parse_subreddit_1() {
465 let res = serde_json::from_str::<Thing>(SUBREDDIT_SAMPLE_1).unwrap();
466 dbg!(res);
467 }
468
469 #[test]
470 fn parse_subreddit_2() {
471 let res = serde_json::from_str::<Thing>(SUBREDDIT_SAMPLE_2).unwrap();
472 dbg!(res);
473 }
474
475 #[test]
476 fn parse_subreddit_3() {
477 let res = serde_json::from_str::<Thing>(SUBREDDIT_SAMPLE_3).unwrap();
478 dbg!(res);
479 }
480
481 #[test]
482 fn parse_subreddit_4() {
483 let res = serde_json::from_str::<Thing>(SUBREDDIT_SAMPLE_4).unwrap();
484 dbg!(res);
485 }
486
487 #[test]
488 fn parse_comments_1() {
489 let res = serde_json::from_str::<Vec<Thing>>(COMMENT_SAMPLE_1).unwrap();
490 dbg!(res);
491 }
492
493 #[test]
494 fn parse_comments_2() {
495 let res = serde_json::from_str::<Vec<Thing>>(COMMENT_SAMPLE_2).unwrap();
496 dbg!(res);
497 }
498}