Rust AppView - highly experimental!
at experiments 208 lines 7.2 kB view raw
1//! Post Entity - Content created by actors in the AT Protocol 2//! 3//! A Post represents a piece of content (text, images, etc.) created by an actor. 4//! Posts are identified by their actor_id + rkey combination. 5 6use diesel::prelude::*; 7use serde::{Deserialize, Serialize}; 8use crate::infrastructure::persistence::arrays; 9use crate::infrastructure::persistence::composite::{ExtEmbed, VideoEmbed, ImageEmbed, FacetEmbed, PostLabelRecord}; 10use crate::infrastructure::persistence::types::*; 11use crate::domain::value_objects::tid; 12use crate::domain::value_objects::tid_traits::HasTidRkey; 13use crate::{impl_tid_rkey, impl_tid_created_at}; 14 15/// Post Entity - Represents content in the AT Protocol 16/// 17/// # Invariants 18/// - actor_id + rkey combination must be unique 19/// - Parent/root references must form valid thread structures 20/// - Engagement counts must match array lengths when arrays are present 21#[derive(Clone, Debug, Serialize, Deserialize, Queryable, Selectable, Identifiable)] 22#[diesel(table_name = crate::infrastructure::schema::posts)] 23#[diesel(primary_key(actor_id, rkey))] 24#[diesel(check_for_backend(diesel::pg::Pg))] 25pub struct Post { 26 // === Identity === 27 pub actor_id: i32, // Author of the post 28 pub rkey: i64, // Record key (TID as i64) 29 pub cid: Vec<u8>, // Content identifier (32-byte digest) 30 31 // === Content === 32 pub content: Option<Vec<u8>>, // Zstd-compressed post content 33 pub langs: arrays::LanguageCodeArray, // Language codes (ISO 639-1) 34 pub tags: arrays::TextArray, // Hashtags 35 pub tokens: Option<arrays::TextArray>, // Search tokens 36 37 // === Embeds === 38 pub embed_type: Option<EmbedType>, 39 pub embed_subtype: Option<EmbedType>, 40 pub ext_embed: Option<ExtEmbed>, 41 pub video_embed: Option<VideoEmbed>, 42 pub image_1: Option<ImageEmbed>, 43 pub image_2: Option<ImageEmbed>, 44 pub image_3: Option<ImageEmbed>, 45 pub image_4: Option<ImageEmbed>, 46 47 // === Facets (rich text) === 48 pub facet_1: Option<FacetEmbed>, 49 pub facet_2: Option<FacetEmbed>, 50 pub facet_3: Option<FacetEmbed>, 51 pub facet_4: Option<FacetEmbed>, 52 pub facet_5: Option<FacetEmbed>, 53 pub facet_6: Option<FacetEmbed>, 54 pub facet_7: Option<FacetEmbed>, 55 pub facet_8: Option<FacetEmbed>, 56 pub mentions: Option<Vec<Option<i32>>>, 57 58 // === Thread Structure === 59 pub parent_post_actor_id: Option<i32>, 60 pub parent_post_rkey: Option<i64>, 61 pub root_post_actor_id: Option<i32>, 62 pub root_post_rkey: Option<i64>, 63 pub embedded_post_actor_id: Option<i32>, // Quote post 64 pub embedded_post_rkey: Option<i64>, 65 66 // === Status === 67 pub status: PostStatus, 68 pub violates_threadgate: bool, 69 pub record_detached: Option<bool>, 70 71 // === Engagement Arrays (Denormalized) === 72 pub like_actor_ids: Option<Vec<Option<i32>>>, 73 pub like_rkeys: Option<Vec<Option<i64>>>, 74 pub like_via_repost_data: Option<serde_json::Value>, 75 pub reply_actor_ids: Option<Vec<Option<i32>>>, 76 pub reply_rkeys: Option<Vec<Option<i64>>>, 77 pub quote_actor_ids: Option<Vec<Option<i32>>>, 78 pub quote_rkeys: Option<Vec<Option<i64>>>, 79 pub repost_actor_ids: Option<Vec<Option<i32>>>, 80 pub repost_rkeys: Option<Vec<Option<i64>>>, 81 82 // === Moderation === 83 pub threadgate_allow: Option<arrays::ThreadgateRuleArray>, 84 pub threadgate_hidden_actor_ids: Option<Vec<Option<i32>>>, 85 pub threadgate_hidden_rkeys: Option<Vec<Option<i64>>>, 86 pub postgate_rules: Option<arrays::PostgateRuleArray>, 87 pub postgate_detached_actor_ids: Option<Vec<Option<i32>>>, 88 pub postgate_detached_rkeys: Option<Vec<Option<i64>>>, 89 pub labels: Option<Vec<Option<PostLabelRecord>>>, 90 91 // === Engagement Counts === 92 pub like_count: i32, 93 pub repost_count: i32, 94 pub reply_count: i32, 95 pub quote_count: i32, 96} 97 98/// Information about a like on a post 99#[derive(Debug, Clone)] 100pub struct PostLikeInfo { 101 pub actor_id: i32, 102 pub rkey: i64, 103 pub via_repost_actor_id: Option<i32>, 104 pub via_repost_rkey: Option<i64>, 105} 106 107// === Domain Logic === 108 109impl Post { 110 /// Check if this is a reply 111 pub fn is_reply(&self) -> bool { 112 self.parent_post_actor_id.is_some() 113 } 114 115 /// Check if this is a quote post 116 pub fn is_quote(&self) -> bool { 117 self.embedded_post_actor_id.is_some() 118 } 119 120 /// Check if this post has images 121 pub fn has_images(&self) -> bool { 122 self.image_1.is_some() || 123 matches!(self.embed_type, Some(EmbedType::Images)) 124 } 125 126 /// Check if this post has video 127 pub fn has_video(&self) -> bool { 128 self.video_embed.is_some() || 129 matches!(self.embed_type, Some(EmbedType::Video)) 130 } 131 132 /// Count the number of images 133 pub fn image_count(&self) -> usize { 134 let mut count = 0; 135 if self.image_1.is_some() { count += 1; } 136 if self.image_2.is_some() { count += 1; } 137 if self.image_3.is_some() { count += 1; } 138 if self.image_4.is_some() { count += 1; } 139 count 140 } 141 142 /// Get a specific like by index 143 pub fn get_like(&self, idx: usize) -> Option<PostLikeInfo> { 144 let actor_ids = self.like_actor_ids.as_ref()?; 145 let rkeys = self.like_rkeys.as_ref()?; 146 147 let actor_id = *actor_ids.get(idx)?.as_ref()?; 148 let rkey = *rkeys.get(idx)?.as_ref()?; 149 150 // Extract via_repost from JSONB if it exists 151 let (via_repost_actor_id, via_repost_rkey) = self 152 .like_via_repost_data 153 .as_ref() 154 .and_then(|json| json.get(actor_id.to_string())) 155 .and_then(|data| { 156 let actor = data.get("actor_id")?.as_i64()? as i32; 157 let rkey = data.get("rkey")?.as_i64()?; 158 Some((Some(actor), Some(rkey))) 159 }) 160 .unwrap_or((None, None)); 161 162 Some(PostLikeInfo { 163 actor_id, 164 rkey, 165 via_repost_actor_id, 166 via_repost_rkey, 167 }) 168 } 169 170 /// Check if a specific actor has liked this post 171 pub fn is_liked_by(&self, actor_id: i32) -> bool { 172 if let Some(ref like_actors) = self.like_actor_ids { 173 like_actors.iter().any(|a| a == &Some(actor_id)) 174 } else { 175 false 176 } 177 } 178 179 /// Check if a specific actor has reposted this post 180 pub fn is_reposted_by(&self, actor_id: i32) -> bool { 181 if let Some(ref repost_actors) = self.repost_actor_ids { 182 repost_actors.iter().any(|a| a == &Some(actor_id)) 183 } else { 184 false 185 } 186 } 187 188 /// Check if this post is available (not deleted/forbidden) 189 pub fn is_available(&self) -> bool { 190 matches!(self.status, PostStatus::Complete | PostStatus::Stub) 191 } 192 193 /// Check if threadgate allows a specific actor to reply 194 pub fn can_reply(&self, _actor_id: i32) -> bool { 195 // If no threadgate, anyone can reply 196 if self.threadgate_allow.is_none() { 197 return true; 198 } 199 200 // TODO: Implement threadgate rule checking 201 // This would need to check the rules array 202 true 203 } 204} 205 206// Implement TID traits for Post 207impl_tid_rkey!(Post); 208impl_tid_created_at!(Post);