use db::conn::sea_orm::{ ActiveModelTrait, EntityTrait, ExprTrait, QueryFilter, QueryOrder, QuerySelect, TransactionTrait, }; use db::user::user_blocks as blocks; use db::user::user_followers as followers; use db::user::users as user_entity; use uuid::Uuid; use crate::error::{UserError, UserResult}; use crate::features::UserService; use super::types::{ BlockedSummary, FollowerSummary, ListBlocksQuery, ListFollowersQuery, ListFollowingQuery, }; pub async fn follow(svc: &UserService, follower: Uuid, target: Uuid) -> UserResult<()> { if follower == target { return Err(UserError::BadRequest("cannot follow self".into())); } let writer = svc.db.writer(); if user_entity::Entity::find_by_id(target) .one(&writer) .await? .is_none() { return Err(UserError::NotFound(format!("user {target}"))); } let existing = followers::Entity::find() .filter(followers::COLUMN.follower_id.eq(follower)) .filter(followers::COLUMN.followed_id.eq(target)) .one(&writer) .await?; if existing.is_some() { return Ok(()); } let active: followers::ActiveModel = followers::ActiveModel::builder() .set_id(Uuid::new_v4()) .set_follower_id(follower) .set_followed_id(target) .set_created_at(time::OffsetDateTime::now_utc()) .into(); active.insert(&writer).await?; Ok(()) } pub async fn unfollow(svc: &UserService, follower: Uuid, target: Uuid) -> UserResult<()> { let writer = svc.db.writer(); followers::Entity::delete_many() .filter(followers::COLUMN.follower_id.eq(follower)) .filter(followers::COLUMN.followed_id.eq(target)) .exec(&writer) .await?; Ok(()) } pub async fn list_followers( svc: &UserService, q: ListFollowersQuery, ) -> UserResult> { let limit = q.limit.min(svc.config.max_profile_pagination).max(1) as u64; let writer = svc.db.writer(); let mut sel = followers::Entity::find() .filter(followers::COLUMN.followed_id.eq(q.target_uid)) .order_by_asc(followers::COLUMN.id) .limit(Some(limit)); if let Some(cursor) = q.cursor { sel = sel.filter(followers::COLUMN.id.gt(cursor)); } let edges: Vec = sel.all(&writer).await?; if edges.is_empty() { return Ok(Vec::new()); } let follower_ids: Vec = edges.iter().map(|e| e.follower_id).collect(); let users: Vec = user_entity::Entity::find() .filter(user_entity::COLUMN.uid.is_in(follower_ids)) .all(&writer) .await?; let map: std::collections::HashMap = users.into_iter().map(|u| (u.uid, u)).collect(); let out = edges .into_iter() .filter_map(|e| { map.get(&e.follower_id).map(|u| FollowerSummary { user: super::super::profile::UserPublic::from(u.clone()), since: e .created_at .format(&time::format_description::well_known::Rfc3339) .unwrap_or_default(), }) }) .collect(); Ok(out) } pub async fn list_following( svc: &UserService, q: ListFollowingQuery, ) -> UserResult> { let limit = q.limit.min(svc.config.max_profile_pagination).max(1) as u64; let writer = svc.db.writer(); let mut sel = followers::Entity::find() .filter(followers::COLUMN.follower_id.eq(q.user_uid)) .order_by_asc(followers::COLUMN.id) .limit(Some(limit)); if let Some(cursor) = q.cursor { sel = sel.filter(followers::COLUMN.id.gt(cursor)); } let edges: Vec = sel.all(&writer).await?; if edges.is_empty() { return Ok(Vec::new()); } let followed_ids: Vec = edges.iter().map(|e| e.followed_id).collect(); let users: Vec = user_entity::Entity::find() .filter(user_entity::COLUMN.uid.is_in(followed_ids)) .all(&writer) .await?; let map: std::collections::HashMap = users.into_iter().map(|u| (u.uid, u)).collect(); let out = edges .into_iter() .filter_map(|e| { map.get(&e.followed_id).map(|u| FollowerSummary { user: super::super::profile::UserPublic::from(u.clone()), since: e .created_at .format(&time::format_description::well_known::Rfc3339) .unwrap_or_default(), }) }) .collect(); Ok(out) } pub async fn block(svc: &UserService, blocker: Uuid, target: Uuid) -> UserResult<()> { if blocker == target { return Err(UserError::BadRequest("cannot block self".into())); } let writer = svc.db.writer(); if user_entity::Entity::find_by_id(target) .one(&writer) .await? .is_none() { return Err(UserError::NotFound(format!("user {target}"))); } let txn = writer.begin().await?; let existing = blocks::Entity::find() .filter(blocks::COLUMN.blocker_id.eq(blocker)) .filter(blocks::COLUMN.blocked_id.eq(target)) .one(&txn) .await?; if existing.is_none() { let active: blocks::ActiveModel = blocks::ActiveModel::builder() .set_id(Uuid::new_v4()) .set_blocker_id(blocker) .set_blocked_id(target) .set_created_at(time::OffsetDateTime::now_utc()) .into(); active.insert(&txn).await?; } let a = followers::COLUMN.follower_id .eq(blocker) .and(followers::COLUMN.followed_id.eq(target)); let b = followers::COLUMN.follower_id .eq(target) .and(followers::COLUMN.followed_id.eq(blocker)); followers::Entity::delete_many() .filter(a.or(b)) .exec(&txn) .await?; txn.commit().await?; Ok(()) } pub async fn unblock(svc: &UserService, blocker: Uuid, target: Uuid) -> UserResult<()> { let writer = svc.db.writer(); blocks::Entity::delete_many() .filter(blocks::COLUMN.blocker_id.eq(blocker)) .filter(blocks::COLUMN.blocked_id.eq(target)) .exec(&writer) .await?; Ok(()) } pub async fn list_blocks( svc: &UserService, caller: Uuid, q: ListBlocksQuery, ) -> UserResult> { let limit = q.limit.min(svc.config.max_profile_pagination).max(1) as u64; let writer = svc.db.writer(); let mut sel = blocks::Entity::find() .filter(blocks::COLUMN.blocker_id.eq(caller)) .order_by_asc(blocks::COLUMN.id) .limit(Some(limit)); if let Some(cursor) = q.cursor { sel = sel.filter(blocks::COLUMN.id.gt(cursor)); } let edges: Vec = sel.all(&writer).await?; if edges.is_empty() { return Ok(Vec::new()); } let blocked_ids: Vec = edges.iter().map(|e| e.blocked_id).collect(); let users: Vec = user_entity::Entity::find() .filter(user_entity::COLUMN.uid.is_in(blocked_ids)) .all(&writer) .await?; let map: std::collections::HashMap = users.into_iter().map(|u| (u.uid, u)).collect(); let out = edges .into_iter() .filter_map(|e| { map.get(&e.blocked_id).map(|u| BlockedSummary { user: super::super::profile::UserPublic::from(u.clone()), since: e .created_at .format(&time::format_description::well_known::Rfc3339) .unwrap_or_default(), }) }) .collect(); Ok(out) }