diff --git a/cave/src/feed.rs b/cave/src/feed.rs index 5c26048..5038ccc 100644 --- a/cave/src/feed.rs +++ b/cave/src/feed.rs @@ -51,17 +51,29 @@ impl Mention { } } +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct MediaAttachment { + #[serde(rename = "type")] + pub media_type: String, + pub remote_url: Option, +} + #[derive(Debug, serde::Serialize, serde::Deserialize)] pub struct Post { pub created_at: String, pub uri: String, + #[serde(default = "String::new")] pub content: String, pub account: Account, + #[serde(default)] pub tags: Vec, pub application: Option, pub sensitive: Option, + #[serde(default)] pub mentions: Vec, pub language: Option, + #[serde(default)] + pub media_attachments: Vec, } impl Post { diff --git a/cave/src/store.rs b/cave/src/store.rs index 69b12bd..ac04256 100644 --- a/cave/src/store.rs +++ b/cave/src/store.rs @@ -9,6 +9,7 @@ const POST_EXPIRE: usize = 86400; const TAG_EXPIRE: u64 = 30 * 24; pub const TREND_POOL_SIZE: usize = 20; +pub const IMAGES_PER_TAG: usize = 5; pub type Error = RedisError; @@ -168,7 +169,7 @@ impl Store { let language = post.lang(); let mut cmd = redis::pipe(); - let mut store_tags = |spellings, tag_key, user_key| { + let store_tags = |cmd: &mut redis::Pipeline, spellings, tag_key, user_key| { // by spelling for spelling in spellings { cmd.hincr( @@ -186,29 +187,62 @@ impl Store { if let Some(user_id) = &user_id { // users by tag/hour cmd.sadd(&user_key, &user_id).ignore() - .expire(&user_key, TAG_EXPIRE as usize * 86400); + .expire(&user_key, TAG_EXPIRE as usize * 86400) + .ignore(); } }; + let images = if post.sensitive == Some(false) { + post.media_attachments.iter() + .filter(|a| a.media_type == "image") + .filter_map(|a| a.remote_url.as_ref()) + .take(2) + .collect::>() + } else { + // ignore disturbing porn images from sensitive posts + vec![] + }; + let mut image_keys = vec![]; for (name, spellings) in post.tags_set() { log::debug!("tag {}", name); // global - store_tags( + store_tags(&mut cmd, spellings.clone(), format!("g:{}", name), format!("u::{}:{}", hour, name), ); // by language if let Some(language) = &language { - store_tags( + store_tags(&mut cmd, spellings, format!("l:{}:{}", language, name), format!("u:{}:{}:{}", language, hour, name), ); } + + for image in &images { + let image_key = format!("i:{}", name); + cmd.sadd(&image_key, image) + .ignore() + .expire(&image_key, TAG_EXPIRE as usize * 86400) + .ignore() + .scard(&image_key); + image_keys.push(image_key); + } } - match cmd.query_async(self).await { - Ok(()) => {} + match cmd.query_async::<_, Vec>(self).await { + Ok(image_key_sizes) => { + assert_eq!(image_keys.len(), image_key_sizes.len()); + let mut cmd = redis::pipe(); + for (image_key, size) in image_keys.into_iter().zip(image_key_sizes.into_iter()) { + let excess = size.saturating_sub(IMAGES_PER_TAG); + if excess > 0 { + cmd.spop(image_key).arg(excess) + .ignore(); + } + } + let _ = cmd.query_async::<_, ()>(self).await; + } Err(e) => { log::error!("redis error: {:?}", e); }