use models::common::watch_mode::WatchMode; use models::repo::repo_watch; use models::repo::repos; use sea_orm::{ ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect, Set, TransactionTrait, Unchanged, }; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; use utoipa::ToSchema; use uuid::Uuid; use crate::context::facade::GitContext; use crate::service::GitService; use crate::share::errors::GitError; #[derive(Debug, Clone, Serialize, ToSchema)] pub struct RepoWatchDto { pub user_id: i64, pub repo_id: Uuid, pub mode: WatchMode, pub created_at: OffsetDateTime, pub updated_at: OffsetDateTime, } #[derive(Debug, Clone, Deserialize, ToSchema)] pub struct UpsertWatchCmd { pub mode: WatchMode, } #[derive(Debug, Clone, Serialize, ToSchema)] pub struct WatcherDto { pub user_id: i64, pub username: String, pub display_name: String, pub avatar_url: String, pub mode: WatchMode, pub watching_since: OffsetDateTime, } impl GitService { pub async fn upsert_watch( &self, ctx: &GitContext, user_id: i64, cmd: UpsertWatchCmd, ) -> Result { let db = self.db_write(); let repo_id = ctx.model.id; let now = OffsetDateTime::now_utc(); let dto = TransactionTrait::transaction(db, |txn| { Box::pin(async move { let existing = repo_watch::Entity::find() .filter(repo_watch::Column::UserId.eq(user_id)) .filter(repo_watch::Column::RepoId.eq(repo_id)) .one(txn) .await?; let saved = if let Some(row) = existing { let mut am: repo_watch::ActiveModel = row.into(); am.mode = Set(cmd.mode); am.updated_at = Set(now); am.update(txn).await? } else { let am = repo_watch::ActiveModel { id: Set(Uuid::now_v7()), user_id: Set(user_id), repo_id: Set(repo_id), mode: Set(cmd.mode), created_at: Set(now), updated_at: Set(now), }; am.insert(txn).await? }; Ok::(saved) }) }) .await?; let after = repo_watch::Entity::find() .filter(repo_watch::Column::RepoId.eq(repo_id)) .all(db) .await? .len() as i32; let am = repos::ActiveModel { id: Unchanged(repo_id), nums_watch: Set(after), updated_at: Set(now), ..Default::default() }; am.update(db).await?; Ok(RepoWatchDto { user_id: dto.user_id, repo_id: dto.repo_id, mode: dto.mode, created_at: dto.created_at, updated_at: dto.updated_at, }) } pub async fn unwatch(&self, ctx: &GitContext, user_id: i64) -> Result<(), GitError> { let db = self.db_write(); let repo_id = ctx.model.id; let now = OffsetDateTime::now_utc(); TransactionTrait::transaction(db, |txn| { Box::pin(async move { repo_watch::Entity::delete_many() .filter(repo_watch::Column::UserId.eq(user_id)) .filter(repo_watch::Column::RepoId.eq(repo_id)) .exec(txn) .await?; Ok::<(), sea_orm::DbErr>(()) }) }) .await?; let after = repo_watch::Entity::find() .filter(repo_watch::Column::RepoId.eq(repo_id)) .all(db) .await? .len() as i32; let am = repos::ActiveModel { id: Unchanged(repo_id), nums_watch: Set(after), updated_at: Set(now), ..Default::default() }; am.update(db).await?; Ok(()) } pub async fn get_watch_mode( &self, ctx: &GitContext, user_id: i64, ) -> Result, GitError> { let row = repo_watch::Entity::find() .filter(repo_watch::Column::UserId.eq(user_id)) .filter(repo_watch::Column::RepoId.eq(ctx.model.id)) .one(self.db_read()) .await?; Ok(row.map(|r| r.mode)) } pub async fn list_watchers( &self, ctx: &GitContext, mode: Option, limit: u64, offset: u64, ) -> Result, GitError> { let mut q = repo_watch::Entity::find() .filter(repo_watch::Column::RepoId.eq(ctx.model.id)) .order_by_desc(repo_watch::Column::CreatedAt) .limit(limit) .offset(offset); if let Some(m) = mode { q = q.filter(repo_watch::Column::Mode.eq(m)); } let rows = q.all(self.db_read()).await?; if rows.is_empty() { return Ok(Vec::new()); } let user_ids: Vec = rows.iter().map(|r| r.user_id).collect(); let users = models::users::user::Entity::find() .filter(models::users::user::Column::Uid.is_in(user_ids.iter().copied())) .all(self.db_read()) .await?; let by_id: std::collections::HashMap = users.into_iter().map(|u| (u.uid, u)).collect(); Ok(rows .into_iter() .filter_map(|row| { by_id.get(&row.user_id).map(|u| WatcherDto { user_id: u.uid, username: u.username.clone(), display_name: u.display_name.clone(), avatar_url: export::resolve_user_avatar_url(u.uid, &u.avatar_url), mode: row.mode, watching_since: row.created_at, }) }) .collect()) } }