Rust AppView - highly experimental!
at experiments 210 lines 7.7 kB view raw
1//! Actor Entity - Core identity in the AT Protocol 2//! 3//! An Actor represents a user account in the AT Protocol network. 4//! Actors are identified by their DID (Decentralized Identifier) which never changes. 5 6use crate::infrastructure::persistence::composite::{Block, Bookmark, Follow, LabelerDef, Mute}; 7use crate::infrastructure::persistence::types::*; 8use chrono::{DateTime, Utc}; 9use diesel::prelude::*; 10 11/// Actor Entity - Represents a user account in the AT Protocol 12/// 13/// # Invariants 14/// - DID must be valid and never changes 15/// - Handle can change but must be a valid domain 16/// - Status transitions must follow AT Protocol rules 17#[derive(Debug, Clone, Queryable, Selectable, Identifiable)] 18#[diesel(table_name = crate::infrastructure::schema::actors)] 19#[diesel(primary_key(id))] 20#[diesel(check_for_backend(diesel::pg::Pg))] 21pub struct Actor { 22 // === Identity (Immutable) === 23 pub id: i32, // Internal database ID 24 pub did: String, // Decentralized Identifier (never changes) 25 26 // === Mutable Attributes === 27 pub handle: Option<String>, // Handle (can change) 28 pub status: ActorStatus, // active | takendown | suspended | deleted | deactivated 29 pub sync_state: ActorSyncState, // synced | dirty | partial | processing 30 31 // === Repository State === 32 pub repo_rev: Option<String>, // Repository revision 33 pub repo_cid: Option<Vec<u8>>, // Repository root CID (32 bytes) 34 pub last_indexed: Option<DateTime<Utc>>, 35 pub account_created_at: Option<DateTime<Utc>>, 36 37 // === Profile (app.bsky.actor.profile) === 38 pub profile_cid: Option<Vec<u8>>, 39 pub profile_created_at: Option<DateTime<Utc>>, 40 pub profile_avatar_cid: Option<Vec<u8>>, 41 pub profile_banner_cid: Option<Vec<u8>>, 42 pub profile_display_name: Option<String>, 43 pub profile_description: Option<String>, 44 pub profile_pinned_post_rkey: Option<i64>, 45 pub profile_joined_sp_id: Option<i64>, 46 pub profile_pronouns: Option<String>, 47 pub profile_website: Option<String>, 48 49 // === Status (app.bsky.actor.status) === 50 pub status_cid: Option<Vec<u8>>, 51 pub status_created_at: Option<DateTime<Utc>>, 52 pub status_type: Option<StatusType>, 53 pub status_duration: Option<i32>, 54 pub status_embed_post_actor_id: Option<i32>, 55 pub status_embed_post_rkey: Option<i64>, 56 pub status_thumb_mime_type: Option<ImageMimeType>, 57 pub status_thumb_cid: Option<Vec<u8>>, 58 59 // === Chat Settings === 60 pub chat_allow_incoming: Option<ChatAllowIncoming>, 61 pub chat_created_at: Option<DateTime<Utc>>, 62 63 // === Notification Settings === 64 pub notif_decl_allow_subscriptions: Option<NotifAllowSubscriptions>, 65 pub notif_decl_created_at: Option<DateTime<Utc>>, 66 pub notif_seen_at: Option<DateTime<Utc>>, 67 pub notif_unread_count: Option<i32>, 68 69 // === Statistics (Denormalized) === 70 pub followers_count: Option<i32>, 71 pub following_count: Option<i32>, 72 pub posts_count: Option<i32>, 73 pub lists_count: Option<i16>, 74 pub feeds_count: Option<i16>, 75 pub starterpacks_count: Option<i16>, 76 77 // === Labeler Fields (NULL for non-labelers) === 78 pub labeler_cid: Option<Vec<u8>>, 79 pub labeler_created_at: Option<DateTime<Utc>>, 80 pub labeler_reasons: Option<Vec<Option<ReasonType>>>, 81 pub labeler_subject_types: Option<Vec<Option<SubjectType>>>, 82 pub labeler_subject_collections: Option<Vec<Option<String>>>, 83 pub labeler_status: Option<LabelerStatus>, 84 pub labeler_like_count: Option<i32>, 85 pub labeler_defs: Option<Vec<Option<LabelerDef>>>, 86 87 // === Social Graph (Denormalized Arrays) === 88 pub following: Option<Vec<Option<Follow>>>, // Who this actor follows 89 pub followers: Option<Vec<Option<Follow>>>, // Who follows this actor 90 91 // === User Preferences (Denormalized Arrays) === 92 pub mutes: Option<Vec<Option<Mute>>>, // Muted actors 93 pub blocks: Option<Vec<Option<Block>>>, // Blocked actors 94 pub bookmarks: Option<Vec<Option<Bookmark>>>, // Bookmarked posts 95 96 // === List Moderation (Denormalized Arrays) === 97 pub thread_mutes: Option<Vec<Option<crate::infrastructure::composite::ThreadMute>>>, 98 pub list_mutes: Option<Vec<Option<crate::infrastructure::composite::ListMute>>>, 99 pub list_blocks: Option<Vec<Option<crate::infrastructure::composite::ListBlock>>>, 100 101 // === Content References === 102 pub post_rkeys: Option<Vec<Option<i64>>>, // All post rkeys owned by this actor 103 pub repost_rkeys: Option<Vec<Option<i64>>>, // All repost rkeys owned by this actor 104 105 // === Labels === 106 pub labels: Option<Vec<Option<crate::infrastructure::composite::ActorLabelRecord>>>, 107} 108 109// === Domain Logic === 110 111impl Actor { 112 /// Check if this actor is active 113 pub fn is_active(&self) -> bool { 114 matches!(self.status, ActorStatus::Active) 115 } 116 117 /// Check if this actor is a labeler 118 pub fn is_labeler(&self) -> bool { 119 self.labeler_status.is_some() 120 && matches!(self.labeler_status, Some(LabelerStatus::Complete)) 121 } 122 123 /// Check if this actor follows another actor 124 pub fn follows(&self, actor_id: i32) -> bool { 125 if let Some(ref following) = self.following { 126 following.iter().any(|f| { 127 if let Some(ref follow) = f { 128 follow.subject_actor_id == actor_id 129 } else { 130 false 131 } 132 }) 133 } else { 134 false 135 } 136 } 137 138 /// Check if this actor is followed by another actor 139 pub fn is_followed_by(&self, actor_id: i32) -> bool { 140 if let Some(ref followers) = self.followers { 141 followers.iter().any(|f| { 142 if let Some(ref follow) = f { 143 // In followers array, the subject is the one following this actor 144 // So we need to check if any follower's actor_id matches 145 false // This needs the proper follow structure 146 } else { 147 false 148 } 149 }) 150 } else { 151 false 152 } 153 } 154 155 /// Check if this actor has blocked another actor 156 pub fn has_blocked(&self, actor_id: i32) -> bool { 157 if let Some(ref blocks) = self.blocks { 158 blocks.iter().any(|b| { 159 if let Some(ref block) = b { 160 block.subject_actor_id == actor_id 161 } else { 162 false 163 } 164 }) 165 } else { 166 false 167 } 168 } 169 170 /// Check if this actor has muted another actor 171 pub fn has_muted(&self, actor_id: i32) -> bool { 172 if let Some(ref mutes) = self.mutes { 173 mutes.iter().any(|m| { 174 if let Some(ref mute) = m { 175 mute.subject_actor_id == actor_id 176 } else { 177 false 178 } 179 }) 180 } else { 181 false 182 } 183 } 184 185 /// Get the display name or fall back to handle 186 pub fn display_name_or_handle(&self) -> &str { 187 self.profile_display_name 188 .as_deref() 189 .filter(|s| !s.is_empty()) 190 .or(self.handle.as_deref()) 191 .unwrap_or(&self.did) 192 } 193 194 /// Check if the actor can be synced 195 pub fn can_sync(&self) -> bool { 196 self.is_active() 197 && matches!( 198 self.sync_state, 199 ActorSyncState::Synced | ActorSyncState::Dirty 200 ) 201 } 202 203 /// Check if the actor is on the allowlist (fully synced) 204 pub fn is_allowlisted(&self) -> bool { 205 matches!( 206 self.sync_state, 207 ActorSyncState::Synced | ActorSyncState::Dirty | ActorSyncState::Processing 208 ) 209 } 210}