1use serde::{Deserialize, Serialize};
2
3use crate::raw::RawPost;
4
5#[derive(Debug, Clone, Serialize)]
11pub struct Post {
12 pub no: u64,
14 pub resto: u64,
16 pub time: i64,
18 pub now: String,
20 pub name: String,
22 pub sub: Option<String>,
24 pub com: Option<String>,
26 pub trip: Option<String>,
28 pub id: Option<String>,
30 pub capcode: Option<String>,
32 pub country: Option<String>,
34 pub country_name: Option<String>,
35 pub board_flag: Option<String>,
36 pub flag_name: Option<String>,
37 pub since4pass: Option<u32>,
39
40 pub sticky: bool,
42 pub closed: bool,
43 pub archived: bool,
44 pub archived_on: Option<i64>,
45 pub bumplimit: bool,
46 pub imagelimit: bool,
47 pub replies: Option<u32>,
48 pub images: Option<u32>,
49 pub unique_ips: Option<u32>,
50 pub semantic_url: Option<String>,
51 pub tag: Option<String>,
52
53 pub attachment: Option<Attachment>,
54}
55
56#[derive(Debug, Clone, Serialize)]
58pub struct Attachment {
59 pub id: u64, pub filename: String, pub ext: String, pub size: u64, pub md5: String, pub width: i32,
65 pub height: i32,
66 pub thumbnail_width: i32,
67 pub thumbnail_height: i32,
68 pub spoiler: bool,
69 pub custom_spoiler: Option<u8>,
70 pub deleted: bool,
71 pub mobile_optimized: bool,
72}
73
74impl Attachment {
75 pub fn url(&self, board: &str) -> String {
77 format!("https://i.4cdn.org/{}/{}{}", board, self.id, self.ext)
78 }
79
80 pub fn thumbnail_url(&self, board: &str) -> String {
82 format!("https://i.4cdn.org/{}/{}s.jpg", board, self.id)
83 }
84
85 pub fn is_video(&self) -> bool {
86 matches!(
87 self.ext.to_lowercase().as_str(),
88 ".webm" | ".mp4" | ".mov" | ".mkv"
89 )
90 }
91
92 pub fn is_animated(&self) -> bool {
93 self.is_video() || self.ext.eq_ignore_ascii_case(".gif")
94 }
95
96 pub fn is_image(&self) -> bool {
98 matches!(
99 self.ext.to_lowercase().as_str(),
100 ".jpg" | ".jpeg" | ".png" | ".gif" | ".webp"
101 )
102 }
103}
104
105impl Post {
106 pub fn is_op(&self) -> bool {
108 self.resto == 0
109 }
110}
111
112impl Post {
113 pub(crate) fn from_raw(raw: RawPost) -> Self {
114 let attachment = raw.tim.and_then(|id| {
115 let ext = raw.ext?;
116 Some(Attachment {
117 id,
118 filename: raw.filename.unwrap_or_default(),
119 ext,
120 size: raw.fsize.unwrap_or(0),
121 md5: raw.md5.unwrap_or_default(),
122 width: raw.w.unwrap_or(0),
123 height: raw.h.unwrap_or(0),
124 thumbnail_width: raw.tn_w.unwrap_or(0),
125 thumbnail_height: raw.tn_h.unwrap_or(0),
126 spoiler: raw.spoiler.unwrap_or(0) == 1,
127 custom_spoiler: raw.custom_spoiler,
128 deleted: raw.filedeleted.unwrap_or(0) == 1,
129 mobile_optimized: raw.m_img.unwrap_or(0) == 1,
130 })
131 });
132
133 Post {
134 no: raw.no,
135 resto: raw.resto,
136 time: raw.time,
137 now: raw.now,
138 name: raw.name.unwrap_or_else(|| "Anonymous".to_string()),
139 sub: raw.sub,
140 com: raw.com,
141 trip: raw.trip,
142 id: raw.id,
143 capcode: raw.capcode,
144 country: raw.country,
145 country_name: raw.country_name,
146 board_flag: raw.board_flag,
147 flag_name: raw.flag_name,
148 since4pass: raw.since4pass,
149 sticky: raw.sticky.unwrap_or(0) == 1,
150 closed: raw.closed.unwrap_or(0) == 1,
151 archived: raw.archived.unwrap_or(0) == 1,
152 archived_on: raw.archived_on,
153 bumplimit: raw.bumplimit.unwrap_or(0) == 1,
154 imagelimit: raw.imagelimit.unwrap_or(0) == 1,
155 replies: raw.replies,
156 images: raw.images,
157 unique_ips: raw.unique_ips,
158 semantic_url: raw.semantic_url,
159 tag: raw.tag,
160 attachment,
161 }
162 }
163}
164
165impl<'de> Deserialize<'de> for Post {
166 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
167 where
168 D: serde::Deserializer<'de>,
169 {
170 RawPost::deserialize(deserializer).map(Post::from_raw)
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn op_without_file_normalizes() {
180 let json = r#"{"no":1,"resto":0,"time":0,"now":"x"}"#;
181 let p: Post = serde_json::from_str(json).unwrap();
182 assert!(p.is_op());
183 assert_eq!(p.name, "Anonymous"); assert!(p.attachment.is_none());
185 }
186
187 #[test]
188 fn reply_attachment_urls_and_kind() {
189 let json = r#"{"no":2,"resto":1,"time":0,"now":"x","tim":1234,"ext":".png",
190 "filename":"f","fsize":10,"md5":"abc","w":800,"h":600,
191 "tn_w":250,"tn_h":187}"#;
192 let p: Post = serde_json::from_str(json).unwrap();
193 assert!(!p.is_op());
194 let a = p.attachment.as_ref().expect("attachment present when tim+ext set");
195 assert_eq!(a.url("g"), "https://i.4cdn.org/g/1234.png");
196 assert_eq!(a.thumbnail_url("g"), "https://i.4cdn.org/g/1234s.jpg");
197 assert!(a.is_image());
198 assert!(!a.is_video());
199 }
200
201 #[test]
202 fn webm_is_video_not_image() {
203 let json = r#"{"no":3,"resto":1,"time":0,"now":"x","tim":9,"ext":".webm"}"#;
204 let p: Post = serde_json::from_str(json).unwrap();
205 let a = p.attachment.unwrap();
206 assert!(a.is_video());
207 assert!(a.is_animated());
208 assert!(!a.is_image());
209 }
210}