- 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)
226 lines
7.3 KiB
Rust
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)
|
|
}
|
|
}
|