package handlers import ( "context" "net/http" "github.com/gin-gonic/gin" "github.com/jackc/pgx/v5/pgxpool" ) type FollowHandler struct { db *pgxpool.Pool } func NewFollowHandler(db *pgxpool.Pool) *FollowHandler { return &FollowHandler{db: db} } // FollowUser — POST /users/:userId/follow func (h *FollowHandler) FollowUser(c *gin.Context) { userID := c.GetString("user_id") targetUserID := c.Param("id") if userID == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) return } if userID == targetUserID { c.JSON(http.StatusBadRequest, gin.H{"error": "cannot follow yourself"}) return } _, err := h.db.Exec(context.Background(), ` INSERT INTO follows (follower_id, following_id) VALUES ($1, $2) ON CONFLICT (follower_id, following_id) DO NOTHING `, userID, targetUserID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to follow user"}) return } c.JSON(http.StatusOK, gin.H{"message": "followed"}) } // UnfollowUser — POST /users/:userId/unfollow func (h *FollowHandler) UnfollowUser(c *gin.Context) { userID := c.GetString("user_id") targetUserID := c.Param("id") if userID == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) return } _, err := h.db.Exec(context.Background(), ` DELETE FROM follows WHERE follower_id = $1 AND following_id = $2 `, userID, targetUserID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to unfollow user"}) return } c.JSON(http.StatusOK, gin.H{"message": "unfollowed"}) } // IsFollowing — GET /users/:userId/is-following func (h *FollowHandler) IsFollowing(c *gin.Context) { userID := c.GetString("user_id") targetUserID := c.Param("id") if userID == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) return } var isFollowing bool err := h.db.QueryRow(context.Background(), ` SELECT EXISTS( SELECT 1 FROM follows WHERE follower_id = $1 AND following_id = $2 ) `, userID, targetUserID).Scan(&isFollowing) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to check follow status"}) return } c.JSON(http.StatusOK, gin.H{"is_following": isFollowing}) } // GetMutualFollowers — GET /users/:userId/mutual-followers func (h *FollowHandler) GetMutualFollowers(c *gin.Context) { userID := c.GetString("user_id") targetUserID := c.Param("id") if userID == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) return } rows, err := h.db.Query(context.Background(), ` SELECT p.id, p.handle, p.display_name, p.avatar_url FROM profiles p WHERE p.id IN ( SELECT following_id FROM follows WHERE follower_id = $1 ) AND p.id IN ( SELECT following_id FROM follows WHERE follower_id = $2 ) LIMIT 50 `, userID, targetUserID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get mutual followers"}) return } defer rows.Close() type mutualUser struct { ID string `json:"id"` Handle string `json:"handle"` DisplayName string `json:"display_name"` AvatarURL *string `json:"avatar_url"` } users := []mutualUser{} for rows.Next() { var u mutualUser if err := rows.Scan(&u.ID, &u.Handle, &u.DisplayName, &u.AvatarURL); err == nil { users = append(users, u) } } c.JSON(http.StatusOK, gin.H{"mutual_followers": users}) } // GetSuggestedUsers — GET /users/suggested func (h *FollowHandler) GetSuggestedUsers(c *gin.Context) { userID := c.GetString("user_id") if userID == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) return } // Suggest users followed by people the current user follows, excluding already-followed rows, err := h.db.Query(context.Background(), ` SELECT DISTINCT p.id, p.handle, p.display_name, p.avatar_url, COUNT(f2.follower_id) AS mutual_count FROM follows f1 JOIN follows f2 ON f2.follower_id = f1.following_id JOIN profiles p ON p.id = f2.following_id WHERE f1.follower_id = $1 AND f2.following_id != $1 AND f2.following_id NOT IN ( SELECT following_id FROM follows WHERE follower_id = $1 ) GROUP BY p.id, p.handle, p.display_name, p.avatar_url ORDER BY mutual_count DESC LIMIT 10 `, userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get suggestions"}) return } defer rows.Close() type suggestedUser struct { ID string `json:"id"` Handle string `json:"handle"` DisplayName string `json:"display_name"` AvatarURL *string `json:"avatar_url"` MutualCount int `json:"mutual_count"` } suggestions := []suggestedUser{} for rows.Next() { var u suggestedUser if err := rows.Scan(&u.ID, &u.Handle, &u.DisplayName, &u.AvatarURL, &u.MutualCount); err == nil { suggestions = append(suggestions, u) } } c.JSON(http.StatusOK, gin.H{"suggestions": suggestions}) } // GetFollowers — GET /users/:userId/followers func (h *FollowHandler) GetFollowers(c *gin.Context) { targetUserID := c.Param("id") rows, err := h.db.Query(context.Background(), ` SELECT p.id, p.handle, p.display_name, p.avatar_url, f.created_at FROM follows f JOIN profiles p ON f.follower_id = p.id WHERE f.following_id = $1 ORDER BY f.created_at DESC LIMIT 100 `, targetUserID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get followers"}) return } defer rows.Close() type follower struct { ID string `json:"id"` Handle string `json:"handle"` DisplayName string `json:"display_name"` AvatarURL *string `json:"avatar_url"` FollowedAt string `json:"followed_at"` } followers := []follower{} for rows.Next() { var f follower var followedAt interface{} if err := rows.Scan(&f.ID, &f.Handle, &f.DisplayName, &f.AvatarURL, &followedAt); err == nil { followers = append(followers, f) } } c.JSON(http.StatusOK, gin.H{"followers": followers}) } // GetFollowing — GET /users/:userId/following func (h *FollowHandler) GetFollowing(c *gin.Context) { targetUserID := c.Param("id") rows, err := h.db.Query(context.Background(), ` SELECT p.id, p.handle, p.display_name, p.avatar_url, f.created_at FROM follows f JOIN profiles p ON f.following_id = p.id WHERE f.follower_id = $1 ORDER BY f.created_at DESC LIMIT 100 `, targetUserID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get following"}) return } defer rows.Close() type followingUser struct { ID string `json:"id"` Handle string `json:"handle"` DisplayName string `json:"display_name"` AvatarURL *string `json:"avatar_url"` FollowedAt string `json:"followed_at"` } following := []followingUser{} for rows.Next() { var f followingUser var followedAt interface{} if err := rows.Scan(&f.ID, &f.Handle, &f.DisplayName, &f.AvatarURL, &followedAt); err == nil { following = append(following, f) } } c.JSON(http.StatusOK, gin.H{"following": following}) }