forked from
parakeet.at/parakeet
Rust AppView - highly experimental!
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}