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
204 /// true if this post is saved by the logged in user
205 pub saved: bool,
206
207 /// the net-score of the comment
208 pub score: i64,
209
210 /// Whether the comment's score is currently hidden.
211 pub score_hidden: bool,
212
213 /// subreddit of thing excluding the /r/ prefix. "pics"
214 pub subreddit: Box<str>,
215
216 /// the id of the subreddit in which the thing is locatedss
217 pub subreddit_id: Box<str>,
218
219 /// to allow determining whether they have been distinguished by moderators/admins.
220 /// null = not distinguished.
221 /// moderator = the green \[M\].
222 /// admin = the red \[A\].
223 /// special = various other special distinguishes http://redd.it/19ak1b
224 pub distinguished: Option<Box<str>>,
225
226 /// Voting Implementation
227 #[serde(flatten)]
228 pub votable: Votable,
229
230 /// Created Implementation
231 #[serde(flatten)]
232 pub created: Created,
233 // Experimentally determined fields
234 // TODO: These are VERY best-effort, but i should still try to document what i can
235}
236
237/// Implements votable | created
238/// kind == "t3"
239/// See https://github.com/reddit-archive/reddit/wiki/JSON#link-implements-votable--created
240#[derive(Debug, serde::Deserialize)]
241pub struct Link {
242 /// the account name of the poster. null if this is a promotional link
243 pub author: Box<str>,
244
245 /// the CSS class of the author's flair. subreddit specific
246 pub author_flair_css_class: Option<Box<str>>,
247
248 /// the text of the author's flair. subreddit specific
249 pub author_flair_text: Option<Box<str>>,
250
251 /// probably always returns false
252 pub clicked: bool,
253
254 /// the domain of this link.
255 /// Self posts will be self.<subreddit> while other examples include en.wikipedia.org and s3.amazon.com
256 pub domain: Box<str>,
257
258 /// true if the post is hidden by the logged in user. false if not logged in or not hidden.
259 pub hidden: bool,
260
261 /// true if this link is a selfpost
262 pub is_self: bool,
263
264 /// how the logged-in user has voted on the link - True = upvoted, False = downvoted, null = no vote
265 pub likes: Option<bool>,
266
267 /// the CSS class of the link's flair.
268 pub link_flair_css_class: Option<Box<str>>,
269
270 /// the text of the link's flair.
271 pub link_flair_text: Option<Box<str>>,
272
273 /// whether the link is locked (closed to new comments) or not.
274 pub locked: bool,
275
276 // TODO: Finish type
277 /// Used for streaming video. Detailed information about the video and it's origins are placed here
278 pub media: serde_json::Value,
279
280 // TODO: Finish type
281 /// Used for streaming video. Technical embed specific information is found here.
282 pub media_embed: serde_json::Value,
283
284 /// the number of comments that belong to this link. includes removed comments.
285 pub num_comments: u64,
286
287 /// true if the post is tagged as NSFW. False if otherwise
288 pub over_18: bool,
289
290 /// relative URL of the permanent link for this link
291 pub permalink: Box<str>,
292
293 /// true if this post is saved by the logged in user
294 pub saved: bool,
295
296 /// the net-score of the link.
297 /// note: A submission's score is simply the number of upvotes minus the number of downvotes.
298 /// If five users like the submission and three users don't it will have a score of 2.
299 /// Please note that the vote numbers are not "real" numbers, they have been "fuzzed" to prevent spam bots etc.
300 /// So taking the above example, if five users upvoted the submission, and three users downvote it,
301 /// the upvote/downvote numbers may say 23 upvotes and 21 downvotes, or 12 upvotes, and 10 downvotes.
302 /// The points score is correct, but the vote totals are "fuzzed".
303 pub score: i64,
304
305 /// the raw text.
306 /// this is the unformatted text which includes the raw markup characters such as ** for bold.
307 /// <, >, and & are escaped.
308 /// Empty if not present.
309 pub selftext: Box<str>,
310
311 /// the formatted escaped HTML text.
312 /// this is the HTML formatted version of the marked up text.
313 /// Items that are boldened by ** or *** will now have <em> or *** tags on them.
314 /// Additionally, bullets and numbered lists will now be in HTML list format.
315 /// NOTE: The HTML string will be escaped. You must unescape to get the raw HTML. Null if not present.
316 pub selftext_html: Option<Box<str>>,
317
318 /// subreddit of thing excluding the /r/ prefix. "pics"
319 pub subreddit: Box<str>,
320
321 /// the id of the subreddit in which the thing is located
322 pub subreddit_id: Box<str>,
323
324 /// full URL to the thumbnail for this link;
325 /// "self" if this is a self post;
326 /// "image" if this is a link to an image but has no thumbnail;
327 /// "default" if a thumbnail is not available
328 pub thumbnail: Box<str>,
329
330 /// the title of the link. may contain newlines for some reason
331 pub title: Box<str>,
332
333 /// the link of this post. the permalink if this is a self-post. May be a relative Url.
334 pub url: Box<str>,
335
336 /// Indicates if link has been edited.
337 /// Will be the edit timestamp if the link has been edited and return false otherwise.
338 /// https://github.com/reddit/reddit/issues/581
339 pub edited: serde_json::Value,
340
341 /// to allow determining whether they have been distinguished by moderators/admins.
342 /// null = not distinguished.
343 /// moderator = the green \[M\].
344 /// admin = the red \[A\].
345 /// special = various other special distinguishes
346 /// http://bit.ly/ZYI47B
347 pub distinguished: Option<Box<str>>,
348
349 /// true if the post is set as the sticky in its subreddit.
350 pub stickied: bool,
351
352 /// Voting Implementation
353 #[serde(flatten)]
354 pub votable: Votable,
355
356 /// Created Implementation
357 #[serde(flatten)]
358 pub created: Created,
359
360 // Experimentally determined fields
361 // TODO: These are VERY best-effort, but i should still try to document what i can
362 pub archived: bool,
363 pub author_flair_template_id: Option<Box<str>>,
364 pub author_flair_text_color: Option<Box<str>>,
365 pub author_flair_type: Option<Box<str>>,
366 pub author_fullname: Option<Box<str>>,
367 pub author_patreon_flair: Option<bool>,
368 pub can_gild: bool,
369 pub can_mod_post: bool,
370 pub contest_mode: bool,
371
372 /// I believe that this is included for crossposted posts to get the data for the main post
373 pub crosspost_parent_list: Option<Vec<Link>>,
374
375 pub gilded: u64,
376 pub hide_score: bool,
377 pub id: Box<str>,
378 pub is_crosspostable: bool,
379 pub is_meta: bool,
380 pub is_original_content: bool,
381 pub is_reddit_media_domain: bool,
382 pub is_robot_indexable: bool,
383
384 /// Returns true if its a video
385 pub is_video: bool,
386
387 pub link_flair_text_color: Option<Box<str>>,
388 pub link_flair_type: Box<str>,
389 pub media_only: bool,
390 pub name: Box<str>,
391 pub no_follow: bool,
392 pub num_crossposts: u64,
393 pub parent_whitelist_status: Option<Box<str>>,
394
395 /// Whether this post is pinned
396 pub pinned: bool,
397
398 /// A "hint" about what this post may be
399 pub post_hint: Option<PostHint>,
400
401 pub pwls: Option<u64>,
402 pub quarantine: bool,
403 pub send_replies: bool,
404
405 /// Whether this post has a spoiler
406 pub spoiler: bool,
407
408 pub subreddit_name_prefixed: Box<str>,
409 pub subreddit_subscribers: u64,
410 pub subreddit_type: Box<str>,
411 pub suggested_sort: Option<Box<str>>,
412 pub thumbnail_height: Option<u32>,
413 pub thumbnail_width: Option<u32>,
414 pub visited: bool,
415 pub whitelist_status: Option<Box<str>>,
416 pub wls: Option<u32>,
417}
418
419/// kind == "more"
420/// See https://github.com/reddit-archive/reddit/wiki/JSON#more
421#[derive(Debug, serde::Deserialize)]
422pub struct More {
423 /// A list of String ids that are the additional things that can be downloaded but are not because there are too many to list.
424 pub children: Vec<Box<str>>,
425}
426
427/// Info on what the post may contain
428#[derive(Debug, serde::Deserialize, PartialEq, Eq)]
429pub enum PostHint {
430 /// The post is an image
431 #[serde(rename = "image")]
432 Image,
433
434 /// The post is a link
435 #[serde(rename = "link")]
436 Link,
437
438 /// A video hosted on reddit
439 #[serde(rename = "hosted:video")]
440 HostedVideo,
441
442 #[serde(rename = "rich:video")]
443 RichVideo,
444
445 #[serde(rename = "self")]
446 DataSelf,
447
448 #[serde(rename = "gallery")]
449 Gallery,
450}
451
452#[cfg(test)]
453mod test {
454 use super::*;
455
456 const SUBREDDIT_SAMPLE_1: &str = include_str!("../test_data/subreddit_dankmemes.json");
457 const SUBREDDIT_SAMPLE_2: &str = include_str!("../test_data/subreddit_cromch.json");
458 const SUBREDDIT_SAMPLE_3: &str = include_str!("../test_data/subreddit_cuddleroll.json");
459 const SUBREDDIT_SAMPLE_4: &str = include_str!("../test_data/subreddit_cursed_images.json");
460
461 const COMMENT_SAMPLE_1: &str = include_str!("../test_data/comment_h966lq.json");
462 const COMMENT_SAMPLE_2: &str = include_str!("../test_data/comment_h8p0py.json");
463
464 #[test]
465 fn parse_subreddit_1() {
466 let res = serde_json::from_str::<Thing>(SUBREDDIT_SAMPLE_1).unwrap();
467 dbg!(res);
468 }
469
470 #[test]
471 fn parse_subreddit_2() {
472 let res = serde_json::from_str::<Thing>(SUBREDDIT_SAMPLE_2).unwrap();
473 dbg!(res);
474 }
475
476 #[test]
477 fn parse_subreddit_3() {
478 let res = serde_json::from_str::<Thing>(SUBREDDIT_SAMPLE_3).unwrap();
479 dbg!(res);
480 }
481
482 #[test]
483 fn parse_subreddit_4() {
484 let res = serde_json::from_str::<Thing>(SUBREDDIT_SAMPLE_4).unwrap();
485 dbg!(res);
486 }
487
488 #[test]
489 fn parse_comments_1() {
490 let res = serde_json::from_str::<Vec<Thing>>(COMMENT_SAMPLE_1).unwrap();
491 dbg!(res);
492 }
493
494 #[test]
495 fn parse_comments_2() {
496 let res = serde_json::from_str::<Vec<Thing>>(COMMENT_SAMPLE_2).unwrap();
497 dbg!(res);
498 }
499}