use serde::{Deserialize, Serialize};
use crate::raw::RawPost;
#[derive(Debug, Clone, Serialize)]
pub struct Post {
pub no: u64,
pub resto: u64,
pub time: i64,
pub now: String,
pub name: String,
pub sub: Option<String>,
pub com: Option<String>,
pub trip: Option<String>,
pub id: Option<String>,
pub capcode: Option<String>,
pub country: Option<String>,
pub country_name: Option<String>,
pub board_flag: Option<String>,
pub flag_name: Option<String>,
pub since4pass: Option<u32>,
pub sticky: bool,
pub closed: bool,
pub archived: bool,
pub archived_on: Option<i64>,
pub bumplimit: bool,
pub imagelimit: bool,
pub replies: Option<u32>,
pub images: Option<u32>,
pub unique_ips: Option<u32>,
pub semantic_url: Option<String>,
pub tag: Option<String>,
pub attachment: Option<Attachment>,
}
#[derive(Debug, Clone, Serialize)]
pub struct Attachment {
pub id: u64, pub filename: String, pub ext: String, pub size: u64, pub md5: String, pub width: i32,
pub height: i32,
pub thumbnail_width: i32,
pub thumbnail_height: i32,
pub spoiler: bool,
pub custom_spoiler: Option<u8>,
pub deleted: bool,
pub mobile_optimized: bool,
}
impl Attachment {
pub fn url(&self, board: &str) -> String {
format!("https://i.4cdn.org/{}/{}{}", board, self.id, self.ext)
}
pub fn thumbnail_url(&self, board: &str) -> String {
format!("https://i.4cdn.org/{}/{}s.jpg", board, self.id)
}
pub fn is_video(&self) -> bool {
matches!(
self.ext.to_lowercase().as_str(),
".webm" | ".mp4" | ".mov" | ".mkv"
)
}
pub fn is_animated(&self) -> bool {
self.is_video() || self.ext.eq_ignore_ascii_case(".gif")
}
pub fn is_image(&self) -> bool {
matches!(
self.ext.to_lowercase().as_str(),
".jpg" | ".jpeg" | ".png" | ".gif" | ".webp"
)
}
}
impl Post {
pub fn is_op(&self) -> bool {
self.resto == 0
}
}
impl Post {
pub(crate) fn from_raw(raw: RawPost) -> Self {
let attachment = raw.tim.and_then(|id| {
let ext = raw.ext?;
Some(Attachment {
id,
filename: raw.filename.unwrap_or_default(),
ext,
size: raw.fsize.unwrap_or(0),
md5: raw.md5.unwrap_or_default(),
width: raw.w.unwrap_or(0),
height: raw.h.unwrap_or(0),
thumbnail_width: raw.tn_w.unwrap_or(0),
thumbnail_height: raw.tn_h.unwrap_or(0),
spoiler: raw.spoiler.unwrap_or(0) == 1,
custom_spoiler: raw.custom_spoiler,
deleted: raw.filedeleted.unwrap_or(0) == 1,
mobile_optimized: raw.m_img.unwrap_or(0) == 1,
})
});
Post {
no: raw.no,
resto: raw.resto,
time: raw.time,
now: raw.now,
name: raw.name.unwrap_or_else(|| "Anonymous".to_string()),
sub: raw.sub,
com: raw.com,
trip: raw.trip,
id: raw.id,
capcode: raw.capcode,
country: raw.country,
country_name: raw.country_name,
board_flag: raw.board_flag,
flag_name: raw.flag_name,
since4pass: raw.since4pass,
sticky: raw.sticky.unwrap_or(0) == 1,
closed: raw.closed.unwrap_or(0) == 1,
archived: raw.archived.unwrap_or(0) == 1,
archived_on: raw.archived_on,
bumplimit: raw.bumplimit.unwrap_or(0) == 1,
imagelimit: raw.imagelimit.unwrap_or(0) == 1,
replies: raw.replies,
images: raw.images,
unique_ips: raw.unique_ips,
semantic_url: raw.semantic_url,
tag: raw.tag,
attachment,
}
}
}
impl<'de> Deserialize<'de> for Post {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
RawPost::deserialize(deserializer).map(Post::from_raw)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn op_without_file_normalizes() {
let json = r#"{"no":1,"resto":0,"time":0,"now":"x"}"#;
let p: Post = serde_json::from_str(json).unwrap();
assert!(p.is_op());
assert_eq!(p.name, "Anonymous"); assert!(p.attachment.is_none());
}
#[test]
fn reply_attachment_urls_and_kind() {
let json = r#"{"no":2,"resto":1,"time":0,"now":"x","tim":1234,"ext":".png",
"filename":"f","fsize":10,"md5":"abc","w":800,"h":600,
"tn_w":250,"tn_h":187}"#;
let p: Post = serde_json::from_str(json).unwrap();
assert!(!p.is_op());
let a = p.attachment.as_ref().expect("attachment present when tim+ext set");
assert_eq!(a.url("g"), "https://i.4cdn.org/g/1234.png");
assert_eq!(a.thumbnail_url("g"), "https://i.4cdn.org/g/1234s.jpg");
assert!(a.is_image());
assert!(!a.is_video());
}
#[test]
fn webm_is_video_not_image() {
let json = r#"{"no":3,"resto":1,"time":0,"now":"x","tim":9,"ext":".webm"}"#;
let p: Post = serde_json::from_str(json).unwrap();
let a = p.attachment.unwrap();
assert!(a.is_video());
assert!(a.is_animated());
assert!(!a.is_image());
}
}