package handlers import ( "net/http" "strconv" "time" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" ) type RepostHandler struct { db *pgxpool.Pool } func NewRepostHandler(db *pgxpool.Pool) *RepostHandler { return &RepostHandler{db: db} } // CreateRepost — POST /posts/repost func (h *RepostHandler) CreateRepost(c *gin.Context) { userID, _ := c.Get("user_id") userIDStr := userID.(string) var req struct { OriginalPostID string `json:"original_post_id" binding:"required"` Type string `json:"type" binding:"required"` Comment string `json:"comment"` Metadata map[string]interface{} `json:"metadata"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } validTypes := map[string]bool{"standard": true, "quote": true, "boost": true, "amplify": true} if !validTypes[req.Type] { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid repost type"}) return } var authorHandle string var avatarURL *string err := h.db.QueryRow(c.Request.Context(), "SELECT handle, avatar_url FROM profiles WHERE id = $1", userIDStr, ).Scan(&authorHandle, &avatarURL) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get user info"}) return } id := uuid.New().String() now := time.Now() _, err = h.db.Exec(c.Request.Context(), ` INSERT INTO reposts (id, original_post_id, author_id, type, comment, metadata, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (original_post_id, author_id, type) DO NOTHING `, id, req.OriginalPostID, userIDStr, req.Type, req.Comment, req.Metadata, now) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create repost"}) return } countCol := repostCountColumn(req.Type) h.db.Exec(c.Request.Context(), "UPDATE posts SET "+countCol+" = "+countCol+" + 1 WHERE id = $1", req.OriginalPostID) c.JSON(http.StatusOK, gin.H{ "success": true, "repost": gin.H{ "id": id, "original_post_id": req.OriginalPostID, "author_id": userIDStr, "author_handle": authorHandle, "author_avatar": avatarURL, "type": req.Type, "comment": req.Comment, "created_at": now.Format(time.RFC3339), "boost_count": 0, "amplification_score": 0, "is_amplified": false, }, }) } // BoostPost — POST /posts/boost func (h *RepostHandler) BoostPost(c *gin.Context) { userID, _ := c.Get("user_id") userIDStr := userID.(string) var req struct { PostID string `json:"post_id" binding:"required"` BoostType string `json:"boost_type" binding:"required"` BoostAmount int `json:"boost_amount"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if req.BoostAmount <= 0 { req.BoostAmount = 1 } maxDaily := 5 if req.BoostType == "amplify" { maxDaily = 3 } var dailyCount int h.db.QueryRow(c.Request.Context(), ` SELECT COUNT(*) FROM reposts WHERE author_id = $1 AND type = $2 AND created_at > NOW() - INTERVAL '24 hours' `, userIDStr, req.BoostType).Scan(&dailyCount) if dailyCount >= maxDaily { c.JSON(http.StatusTooManyRequests, gin.H{"error": "daily boost limit reached", "success": false}) return } id := uuid.New().String() _, err := h.db.Exec(c.Request.Context(), ` INSERT INTO reposts (id, original_post_id, author_id, type, created_at) VALUES ($1, $2, $3, $4, NOW()) ON CONFLICT (original_post_id, author_id, type) DO NOTHING `, id, req.PostID, userIDStr, req.BoostType) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to boost post"}) return } countCol := repostCountColumn(req.BoostType) h.db.Exec(c.Request.Context(), "UPDATE posts SET "+countCol+" = "+countCol+" + 1 WHERE id = $1", req.PostID) c.JSON(http.StatusOK, gin.H{"success": true}) } // GetRepostsForPost — GET /posts/:id/reposts func (h *RepostHandler) GetRepostsForPost(c *gin.Context) { postID := c.Param("id") limit := clampInt(queryInt(c, "limit", 20), 1, 100) rows, err := h.db.Query(c.Request.Context(), ` SELECT r.id, r.original_post_id, r.author_id, p.handle, p.avatar_url, r.type, r.comment, r.created_at FROM reposts r JOIN profiles p ON p.id = r.author_id WHERE r.original_post_id = $1 ORDER BY r.created_at DESC LIMIT $2 `, postID, limit) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get reposts"}) return } defer rows.Close() reposts := buildRepostList(rows) c.JSON(http.StatusOK, gin.H{"success": true, "reposts": reposts}) } // GetUserReposts — GET /users/:id/reposts func (h *RepostHandler) GetUserReposts(c *gin.Context) { userID := c.Param("id") limit := clampInt(queryInt(c, "limit", 20), 1, 100) rows, err := h.db.Query(c.Request.Context(), ` SELECT r.id, r.original_post_id, r.author_id, p.handle, p.avatar_url, r.type, r.comment, r.created_at FROM reposts r JOIN profiles p ON p.id = r.author_id WHERE r.author_id = $1 ORDER BY r.created_at DESC LIMIT $2 `, userID, limit) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get user reposts"}) return } defer rows.Close() reposts := buildRepostList(rows) c.JSON(http.StatusOK, gin.H{"success": true, "reposts": reposts}) } // DeleteRepost — DELETE /reposts/:id func (h *RepostHandler) DeleteRepost(c *gin.Context) { userID, _ := c.Get("user_id") repostID := c.Param("id") var origPostID, repostType string err := h.db.QueryRow(c.Request.Context(), "SELECT original_post_id, type FROM reposts WHERE id = $1 AND author_id = $2", repostID, userID.(string), ).Scan(&origPostID, &repostType) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "repost not found"}) return } _, err = h.db.Exec(c.Request.Context(), "DELETE FROM reposts WHERE id = $1 AND author_id = $2", repostID, userID.(string)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete repost"}) return } countCol := repostCountColumn(repostType) h.db.Exec(c.Request.Context(), "UPDATE posts SET "+countCol+" = GREATEST("+countCol+" - 1, 0) WHERE id = $1", origPostID) c.JSON(http.StatusOK, gin.H{"success": true}) } // GetAmplificationAnalytics — GET /posts/:id/amplification func (h *RepostHandler) GetAmplificationAnalytics(c *gin.Context) { postID := c.Param("id") var totalAmplification int h.db.QueryRow(c.Request.Context(), "SELECT COUNT(*) FROM reposts WHERE original_post_id = $1", postID, ).Scan(&totalAmplification) var viewCount int h.db.QueryRow(c.Request.Context(), "SELECT COALESCE(view_count, 1) FROM posts WHERE id = $1", postID, ).Scan(&viewCount) if viewCount == 0 { viewCount = 1 } amplificationRate := float64(totalAmplification) / float64(viewCount) rows, _ := h.db.Query(c.Request.Context(), "SELECT type, COUNT(*) FROM reposts WHERE original_post_id = $1 GROUP BY type", postID) repostCounts := map[string]int{} if rows != nil { defer rows.Close() for rows.Next() { var t string var cnt int rows.Scan(&t, &cnt) repostCounts[t] = cnt } } c.JSON(http.StatusOK, gin.H{ "success": true, "analytics": gin.H{ "post_id": postID, "metrics": []gin.H{}, "reposts": []gin.H{}, "total_amplification": totalAmplification, "amplification_rate": amplificationRate, "repost_counts": repostCounts, }, }) } // GetTrendingPosts — GET /posts/trending func (h *RepostHandler) GetTrendingPosts(c *gin.Context) { limit := clampInt(queryInt(c, "limit", 10), 1, 50) category := c.Query("category") query := ` SELECT p.id FROM posts p WHERE p.status = 'active' AND p.deleted_at IS NULL ` args := []interface{}{} argIdx := 1 if category != "" { query += " AND p.category = $" + strconv.Itoa(argIdx) args = append(args, category) argIdx++ } query += ` ORDER BY ( COALESCE(p.like_count, 0) * 1 + COALESCE(p.comment_count, 0) * 3 + COALESCE(p.repost_count, 0) * 4 + COALESCE(p.boost_count, 0) * 8 + COALESCE(p.amplify_count, 0) * 10 ) DESC, p.created_at DESC LIMIT $` + strconv.Itoa(argIdx) args = append(args, limit) rows, err := h.db.Query(c.Request.Context(), query, args...) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get trending posts"}) return } defer rows.Close() var postIDs []string for rows.Next() { var id string rows.Scan(&id) postIDs = append(postIDs, id) } if postIDs == nil { postIDs = []string{} } c.JSON(http.StatusOK, gin.H{"success": true, "posts": postIDs}) } // GetAmplificationRules — GET /amplification/rules func (h *RepostHandler) GetAmplificationRules(c *gin.Context) { rules := []gin.H{ { "id": "rule-standard", "name": "Standard Repost", "description": "Share a post with your followers", "type": "standard", "weight_multiplier": 1.0, "min_boost_score": 0, "max_daily_boosts": 20, "is_active": true, "created_at": "2024-01-01T00:00:00Z", }, { "id": "rule-quote", "name": "Quote Repost", "description": "Share a post with your commentary", "type": "quote", "weight_multiplier": 1.5, "min_boost_score": 0, "max_daily_boosts": 10, "is_active": true, "created_at": "2024-01-01T00:00:00Z", }, { "id": "rule-boost", "name": "Boost", "description": "Amplify a post's reach in the feed", "type": "boost", "weight_multiplier": 8.0, "min_boost_score": 0, "max_daily_boosts": 5, "is_active": true, "created_at": "2024-01-01T00:00:00Z", }, { "id": "rule-amplify", "name": "Amplify", "description": "Maximum amplification for high-quality content", "type": "amplify", "weight_multiplier": 10.0, "min_boost_score": 100, "max_daily_boosts": 3, "is_active": true, "created_at": "2024-01-01T00:00:00Z", }, } c.JSON(http.StatusOK, gin.H{"success": true, "rules": rules}) } // CalculateAmplificationScore — POST /posts/:id/calculate-score func (h *RepostHandler) CalculateAmplificationScore(c *gin.Context) { postID := c.Param("id") var likes, comments, reposts, boosts, amplifies int h.db.QueryRow(c.Request.Context(), ` SELECT COALESCE(like_count,0), COALESCE(comment_count,0), COALESCE(repost_count,0), COALESCE(boost_count,0), COALESCE(amplify_count,0) FROM posts WHERE id = $1 `, postID).Scan(&likes, &comments, &reposts, &boosts, &lifies) score := likes*1 + comments*3 + reposts*4 + boosts*8 + amplifies*10 c.JSON(http.StatusOK, gin.H{"success": true, "score": score}) } // CanBoostPost — GET /users/:id/can-boost/:postId func (h *RepostHandler) CanBoostPost(c *gin.Context) { userID := c.Param("id") postID := c.Param("postId") boostType := c.Query("type") var alreadyBoosted int h.db.QueryRow(c.Request.Context(), "SELECT COUNT(*) FROM reposts WHERE author_id=$1 AND original_post_id=$2 AND type=$3", userID, postID, boostType, ).Scan(&alreadyBoosted) if alreadyBoosted > 0 { c.JSON(http.StatusOK, gin.H{"can_boost": false, "reason": "already_boosted"}) return } maxDaily := 5 if boostType == "amplify" { maxDaily = 3 } var dailyCount int h.db.QueryRow(c.Request.Context(), ` SELECT COUNT(*) FROM reposts WHERE author_id=$1 AND type=$2 AND created_at > NOW() - INTERVAL '24 hours' `, userID, boostType).Scan(&dailyCount) c.JSON(http.StatusOK, gin.H{"can_boost": dailyCount < maxDaily}) } // GetDailyBoostCount — GET /users/:id/daily-boosts func (h *RepostHandler) GetDailyBoostCount(c *gin.Context) { userID := c.Param("id") rows, err := h.db.Query(c.Request.Context(), ` SELECT type, COUNT(*) FROM reposts WHERE author_id=$1 AND created_at > NOW() - INTERVAL '24 hours' GROUP BY type `, userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get boost counts"}) return } defer rows.Close() boostCounts := map[string]int{} for rows.Next() { var t string var cnt int rows.Scan(&t, &cnt) boostCounts[t] = cnt } c.JSON(http.StatusOK, gin.H{"success": true, "boost_counts": boostCounts}) } // ReportRepost — POST /reposts/:id/report func (h *RepostHandler) ReportRepost(c *gin.Context) { userID, _ := c.Get("user_id") repostID := c.Param("id") var req struct { Reason string `json:"reason" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } _, err := h.db.Exec(c.Request.Context(), ` INSERT INTO repost_reports (id, repost_id, reporter_id, reason, created_at) VALUES ($1, $2, $3, $4, NOW()) ON CONFLICT (repost_id, reporter_id) DO NOTHING `, uuid.New().String(), repostID, userID.(string), req.Reason) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to report repost"}) return } c.JSON(http.StatusOK, gin.H{"success": true}) } // ─── helpers ───────────────────────────────────────────────────────────────── func repostCountColumn(repostType string) string { switch repostType { case "boost": return "boost_count" case "amplify": return "amplify_count" default: return "repost_count" } } func queryInt(c *gin.Context, key string, def int) int { if s := c.Query(key); s != "" { if n, err := strconv.Atoi(s); err == nil { return n } } return def } func clampInt(v, min, max int) int { if v < min { return min } if v > max { return max } return v } func buildRepostList(rows interface { Next() bool Scan(...interface{}) error Close() }) []gin.H { list := []gin.H{} for rows.Next() { var id, origPostID, authorID, handle, repostType string var avatarURL, comment *string var createdAt time.Time rows.Scan(&id, &origPostID, &authorID, &handle, &avatarURL, &repostType, &comment, &createdAt) list = append(list, gin.H{ "id": id, "original_post_id": origPostID, "author_id": authorID, "author_handle": handle, "author_avatar": avatarURL, "type": repostType, "comment": comment, "created_at": createdAt.Format(time.RFC3339), "boost_count": 0, "amplification_score": 0, "is_amplified": false, }) } return list }