gitdataai/libs/service/user/subscribe.rs
ZhenYi 80e2201b8b feat(user): add Activity, Following, Stars, Security tabs to profile page
- Backend: user_activity service (user_activity_log + project_activity)
- Backend: stars service (repo_star + project_follow)
- Backend: user_get_following_list (with is_following_me mutual check)
- Frontend: Tab navigation on /user/{username} with Overview/Activity/Following/Stars/Security
- Frontend: UserActivity timeline, FollowingList grid, StarsList, SecurityTab (SSH keys + PATs)
2026-04-22 22:39:14 +08:00

226 lines
7.3 KiB
Rust

use crate::{AppService, error::AppError};
use chrono::Utc;
use models::users::user_relation;
use sea_orm::*;
use serde::{Deserialize, Serialize};
use session::Session;
use uuid::Uuid;
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
pub struct SubscriptionInfo {
pub id: i64,
pub user_uid: Uuid,
pub target_uid: Uuid,
pub subscribed_at: chrono::DateTime<Utc>,
pub is_active: bool,
}
#[derive(serde::Serialize, Clone, Debug, utoipa::ToSchema)]
pub struct UserCard {
pub user_uid: Uuid,
pub username: String,
pub display_name: Option<String>,
pub avatar_url: Option<String>,
pub is_following_me: bool,
}
impl From<user_relation::Model> for SubscriptionInfo {
fn from(sub: user_relation::Model) -> Self {
SubscriptionInfo {
id: sub.id,
user_uid: sub.user,
target_uid: sub.target,
subscribed_at: sub.created_at,
is_active: true, // user_relation doesn't have is_active, we treat follow as active
}
}
}
impl AppService {
pub async fn user_subscribe_target(
&self,
context: Session,
target: String,
) -> Result<(), AppError> {
let user_uid = context.user().ok_or(AppError::Unauthorized)?;
let target_user = self.utils_find_user_by_username(target).await?;
let target_uid = target_user.uid;
let existing = user_relation::Entity::find()
.filter(user_relation::Column::User.eq(user_uid))
.filter(user_relation::Column::Target.eq(target_uid))
.filter(user_relation::Column::RelationType.eq("follow"))
.one(&self.db)
.await?;
if existing.is_some() {
return Err(AppError::NotFound("Already subscribed".to_string()));
}
let subscription = user_relation::ActiveModel {
user: Set(user_uid),
target: Set(target_uid),
relation_type: Set("follow".to_string()),
created_at: Set(Utc::now()),
..Default::default()
};
subscription.insert(&self.db).await?;
Ok(())
}
pub async fn user_unsubscribe_target(
&self,
context: Session,
target: String,
) -> Result<(), AppError> {
let user_uid = context.user().ok_or(AppError::Unauthorized)?;
let target_user = self.utils_find_user_by_username(target).await?;
let target_uid = target_user.uid;
user_relation::Entity::delete_many()
.filter(user_relation::Column::User.eq(user_uid))
.filter(user_relation::Column::Target.eq(target_uid))
.filter(user_relation::Column::RelationType.eq("follow"))
.exec(&self.db)
.await?;
Ok(())
}
pub async fn user_is_subscribed_to_target(
&self,
context: Session,
target: String,
) -> Result<bool, AppError> {
let user_uid = context.user().ok_or(AppError::Unauthorized)?;
let target_user = self.utils_find_user_by_username(target).await?;
let target_uid = target_user.uid;
let subscription = user_relation::Entity::find()
.filter(user_relation::Column::User.eq(user_uid))
.filter(user_relation::Column::Target.eq(target_uid))
.filter(user_relation::Column::RelationType.eq("follow"))
.one(&self.db)
.await?;
Ok(subscription.is_some())
}
pub async fn user_get_subscribers(
&self,
_context: Session,
target: String,
) -> Result<Vec<SubscriptionInfo>, AppError> {
let target_user = self.utils_find_user_by_username(target).await?;
let target_uid = target_user.uid;
let subscribers = user_relation::Entity::find()
.filter(user_relation::Column::Target.eq(target_uid))
.filter(user_relation::Column::RelationType.eq("follow"))
.order_by_desc(user_relation::Column::CreatedAt)
.all(&self.db)
.await?;
Ok(subscribers
.into_iter()
.map(SubscriptionInfo::from)
.collect())
}
pub async fn user_get_subscription_count(
&self,
_context: Session,
username: String,
) -> Result<u64, AppError> {
let user_uid = self.utils_find_user_by_username(username).await?.uid;
let count = user_relation::Entity::find()
.filter(user_relation::Column::User.eq(user_uid))
.filter(user_relation::Column::RelationType.eq("follow"))
.count(&self.db)
.await?;
Ok(count)
}
pub async fn user_get_subscriber_count(
&self,
_context: Session,
target: String,
) -> Result<u64, AppError> {
let target_user = self.utils_find_user_by_username(target).await?;
let target_uid = target_user.uid;
let count = user_relation::Entity::find()
.filter(user_relation::Column::Target.eq(target_uid))
.filter(user_relation::Column::RelationType.eq("follow"))
.count(&self.db)
.await?;
Ok(count)
}
pub async fn user_get_following_list(
&self,
context: Session,
username: String,
) -> Result<Vec<UserCard>, AppError> {
let target_user = self.utils_find_user_by_username(username).await?;
let target_uid = target_user.uid;
let current_uid = context.user();
let following = user_relation::Entity::find()
.filter(user_relation::Column::User.eq(target_uid))
.filter(user_relation::Column::RelationType.eq("follow"))
.order_by_desc(user_relation::Column::CreatedAt)
.all(&self.db)
.await?;
let followed_uids: Vec<Uuid> = following.iter().map(|f| f.target).collect();
let followed_users: std::collections::HashMap<Uuid, models::users::user::Model> = models::users::user::Entity::find()
.filter(models::users::user::Column::Uid.is_in(followed_uids.clone()))
.all(&self.db)
.await?
.into_iter()
.map(|u| (u.uid, u))
.collect();
// If current user is logged in, check who they also follow
let current_follows: std::collections::HashSet<Uuid> = if let Some(uid) = current_uid {
user_relation::Entity::find()
.filter(user_relation::Column::User.eq(uid))
.filter(user_relation::Column::Target.is_in(followed_uids.clone()))
.filter(user_relation::Column::RelationType.eq("follow"))
.select_only()
.column(user_relation::Column::Target)
.into_tuple::<Uuid>()
.all(&self.db)
.await?
.into_iter()
.collect()
} else {
std::collections::HashSet::new()
};
let mut cards: Vec<UserCard> = Vec::new();
for rel in following {
if let Some(user) = followed_users.get(&rel.target) {
cards.push(UserCard {
user_uid: user.uid,
username: user.username.clone(),
display_name: user.display_name.clone(),
avatar_url: user.avatar_url.clone(),
is_following_me: current_follows.contains(&user.uid),
});
}
}
Ok(cards)
}
}