//! Feed generator operations //! //! This module handles feed generator (custom algorithm) database operations: //! - Creating/updating feed generators with metadata //! - Deleting feed generators //! //! Feed generators use arbitrary string rkeys (not TIDs), so they are stored as TEXT. use crate::Result; use crate::types::records::AppBskyFeedGenerator; use crate::utils::blob_to_cid_bytes; use deadpool_postgres::GenericClient; use eyre::Context as _; use ipld_core::cid::Cid; /// Strip lexicon prefix from enum value /// /// Example: "app.bsky.feed.defs#contentModeUnspecified" -> "contentModeUnspecified" #[allow(dead_code)] fn strip_lexicon_prefix(value: &str) -> &str { value.split('#').nth(1).unwrap_or(value) } /// Insert or update a feed generator record /// /// Feed generators represent custom algorithm feeds. They include: /// - owner_actor_id: The feed's owner (from actor_id) /// - service_actor_id: The service DID that serves the feed /// - display_name, description, description_facets: Metadata /// - avatar: Optional avatar blob CID /// - content_mode: Content filtering mode (e.g., "contentModeUnspecified") /// - created_at: Timestamp (NOT derived from rkey, provided explicitly) /// /// IMPORTANT: Feedgens use arbitrary string rkeys (not TIDs), stored as TEXT. /// Unlike posts/likes/reposts, feedgen rkeys are NOT converted to INT8. /// /// Returns true if the feedgen was updated (status changed from stub to active). pub async fn feedgen_upsert( conn: &C, actor_id: i32, rkey: &str, cid: Cid, service_actor_id: i32, rec: AppBskyFeedGenerator, ) -> Result { // Acquire table-scoped advisory lock using actor_id + rkey (string form for feedgens) let (table_id, key_id) = crate::database_writer::locking::actor_record_lock_str("feedgens", actor_id, rkey); conn.execute( "SELECT pg_advisory_xact_lock($1, $2)", &[&table_id, &key_id], ) .await?; let cid_bytes = cid.to_bytes(); let cid_digest = parakeet_db::utils::cid::cid_to_digest(&cid_bytes) .expect("CID must be valid AT Protocol CID"); let description_facets = rec .description_facets .and_then(|v| serde_json::to_value(v).ok()); let avatar = blob_to_cid_bytes(rec.avatar.as_ref()); // Note: content_mode field not available in jacquard types let content_mode: Option = None; conn.query_one( include_str!("../../sql/feedgen_upsert.sql"), &[ &actor_id, &rkey, // TEXT rkey (feedgens use arbitrary strings, not TIDs) &cid_digest, &service_actor_id, &content_mode, &rec.display_name, &rec.description, &description_facets, &avatar, &rec.created_at, ], ) .await .wrap_err_with(|| format!("Failed to upsert feedgen for actor_id:{} rkey:{}", actor_id, rkey)) .map(|r| r.get::<_, i32>(0) == 0) } /// Delete a feed generator record /// /// IMPORTANT: Feedgens use arbitrary string rkeys (not TIDs), so NO conversion to INT8. pub async fn feedgen_delete( conn: &C, actor_id: i32, rkey: &str, ) -> Result { // NOTE: Feedgens use arbitrary string rkeys, not TIDs, so NO conversion to INT8 conn.execute( "DELETE FROM feedgens WHERE actor_id = $1 AND rkey = $2", &[&actor_id, &rkey], ) .await .wrap_err_with(|| format!("Failed to delete feedgen for actor_id:{} rkey:{}", actor_id, rkey)) } // NOTE: update_feedgen_like_counts() removed - like_count is now maintained automatically // by the update_feedgen_like_count() trigger (Phase 8 migration 2025-12-08-152125). // The trigger fires on INSERT or UPDATE OF like_actor_ids and sets like_count = array_length(). /// Decrement like_count for a single feedgen /// /// This is called when deleting a feedgen_like record. /// Note: This function is likely obsolete with Phase 8 triggers that maintain like_count /// from like_actor_ids[] array. Consider removing if no longer used in production. pub async fn decrement_feedgen_like_count( conn: &C, actor_id: i32, rkey: &str, ) -> Result { conn.execute( "UPDATE feedgens SET like_count = GREATEST(like_count - 1, 0) WHERE actor_id = $1 AND rkey = $2", &[&actor_id, &rkey], ) .await .wrap_err("Failed to decrement feedgen like_count") }