472 lines
14 KiB
Go
472 lines
14 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"github.com/patbritton/sojorn-backend/internal/models"
|
|
"github.com/patbritton/sojorn-backend/internal/repository"
|
|
"github.com/patbritton/sojorn-backend/internal/services"
|
|
"github.com/patbritton/sojorn-backend/pkg/utils"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
type UserHandler struct {
|
|
repo *repository.UserRepository
|
|
postRepo *repository.PostRepository
|
|
notificationService *services.NotificationService
|
|
assetService *services.AssetService
|
|
}
|
|
|
|
func NewUserHandler(repo *repository.UserRepository, postRepo *repository.PostRepository, notificationService *services.NotificationService, assetService *services.AssetService) *UserHandler {
|
|
return &UserHandler{
|
|
repo: repo,
|
|
postRepo: postRepo,
|
|
notificationService: notificationService,
|
|
assetService: assetService,
|
|
}
|
|
}
|
|
|
|
func (h *UserHandler) GetProfile(c *gin.Context) {
|
|
userID := c.Param("id")
|
|
handle, handleExists := c.GetQuery("handle")
|
|
|
|
var profile *models.Profile
|
|
var err error
|
|
|
|
if userID != "" {
|
|
profile, err = h.repo.GetProfileByID(c.Request.Context(), userID)
|
|
} else if handleExists {
|
|
if handle == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Handle cannot be empty"})
|
|
return
|
|
}
|
|
profile, err = h.repo.GetProfileByHandle(c.Request.Context(), handle)
|
|
} else {
|
|
// Fallback to current authenticated user
|
|
if val, exists := c.Get("user_id"); exists {
|
|
userID = val.(string)
|
|
profile, err = h.repo.GetProfileByID(c.Request.Context(), userID)
|
|
}
|
|
}
|
|
|
|
if err != nil || profile == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Profile not found"})
|
|
return
|
|
}
|
|
|
|
// Use the profile ID for subsequent lookups
|
|
actualUserID := profile.ID.String()
|
|
|
|
// Get stats
|
|
stats, _ := h.repo.GetProfileStats(c.Request.Context(), actualUserID)
|
|
|
|
// Check follow status if authenticated
|
|
isFollowing := false
|
|
isFollowedBy := false
|
|
followStatus := ""
|
|
if currentUserID, exists := c.Get("user_id"); exists && currentUserID.(string) != actualUserID {
|
|
var err error
|
|
isFollowing, err = h.repo.IsFollowing(c.Request.Context(), currentUserID.(string), actualUserID)
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to check isFollowing")
|
|
}
|
|
isFollowedBy, err = h.repo.IsFollowing(c.Request.Context(), actualUserID, currentUserID.(string))
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to check isFollowedBy")
|
|
}
|
|
followStatus, _ = h.repo.GetFollowStatus(c.Request.Context(), currentUserID.(string), actualUserID)
|
|
}
|
|
|
|
// Sign URLs
|
|
if profile.AvatarURL != nil {
|
|
signed := h.assetService.SignImageURL(*profile.AvatarURL)
|
|
profile.AvatarURL = &signed
|
|
}
|
|
if profile.CoverURL != nil {
|
|
signed := h.assetService.SignImageURL(*profile.CoverURL)
|
|
profile.CoverURL = &signed
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"profile": profile,
|
|
"stats": stats,
|
|
"is_following": isFollowing,
|
|
"is_followed_by": isFollowedBy,
|
|
"is_friend": isFollowing && isFollowedBy,
|
|
"follow_status": followStatus,
|
|
"is_private": profile.IsPrivate,
|
|
})
|
|
}
|
|
|
|
func (h *UserHandler) Follow(c *gin.Context) {
|
|
followerID, exists := c.Get("user_id")
|
|
if !exists {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
|
return
|
|
}
|
|
|
|
followingID := c.Param("id")
|
|
if followingID == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "User ID required"})
|
|
return
|
|
}
|
|
|
|
status, err := h.repo.FollowUser(c.Request.Context(), followerID.(string), followingID)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "cannot follow self") {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot follow self"})
|
|
return
|
|
}
|
|
if strings.Contains(err.Error(), "23503") || strings.Contains(err.Error(), "target profile not found") { // FK Violation or custom error
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "User to follow not found"})
|
|
return
|
|
}
|
|
log.Error().Err(err).Msg("Failed to follow user")
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to follow user", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Send Notification
|
|
if h.notificationService != nil {
|
|
go func(targetID string, actorID string, isPending bool) {
|
|
_ = h.notificationService.NotifyFollow(context.Background(), targetID, actorID, isPending)
|
|
}(followingID, followerID.(string), status == "pending")
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "Follow update successful", "status": status})
|
|
}
|
|
|
|
func (h *UserHandler) Unfollow(c *gin.Context) {
|
|
followerID, exists := c.Get("user_id")
|
|
if !exists {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
|
return
|
|
}
|
|
|
|
followingID := c.Param("id")
|
|
if followingID == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "User ID required"})
|
|
return
|
|
}
|
|
|
|
if err := h.repo.UnfollowUser(c.Request.Context(), followerID.(string), followingID); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to unfollow user"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "Unfollowed successfully"})
|
|
}
|
|
func (h *UserHandler) UpdateProfile(c *gin.Context) {
|
|
userIDStr, _ := c.Get("user_id")
|
|
userID, _ := uuid.Parse(userIDStr.(string))
|
|
|
|
var req struct {
|
|
Handle *string `json:"handle"`
|
|
DisplayName *string `json:"display_name"`
|
|
Bio *string `json:"bio"`
|
|
AvatarURL *string `json:"avatar_url"`
|
|
CoverURL *string `json:"cover_url"`
|
|
Location *string `json:"location"`
|
|
Website *string `json:"website"`
|
|
Interests []string `json:"interests"`
|
|
IdentityKey *string `json:"identity_key"`
|
|
RegistrationID *int `json:"registration_id"`
|
|
EncryptedPrivateKey *string `json:"encrypted_private_key"`
|
|
IsPrivate *bool `json:"is_private"`
|
|
IsOfficial *bool `json:"is_official"`
|
|
}
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
profile := &models.Profile{
|
|
ID: userID,
|
|
Handle: req.Handle,
|
|
DisplayName: req.DisplayName,
|
|
Bio: req.Bio,
|
|
AvatarURL: req.AvatarURL,
|
|
CoverURL: req.CoverURL,
|
|
Location: req.Location,
|
|
Website: req.Website,
|
|
Interests: req.Interests,
|
|
IdentityKey: req.IdentityKey,
|
|
RegistrationID: req.RegistrationID,
|
|
EncryptedPrivateKey: req.EncryptedPrivateKey,
|
|
IsPrivate: req.IsPrivate,
|
|
IsOfficial: req.IsOfficial,
|
|
}
|
|
|
|
err := h.repo.UpdateProfile(c.Request.Context(), profile)
|
|
if err != nil {
|
|
// Log error
|
|
log.Error().Err(err).Msg("Failed to update profile")
|
|
|
|
// Check for duplicate handle
|
|
if strings.Contains(err.Error(), "23505") {
|
|
c.JSON(http.StatusConflict, gin.H{"error": "Handle already taken"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update profile", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
updated, _ := h.repo.GetProfileByID(c.Request.Context(), userID.String())
|
|
c.JSON(http.StatusOK, gin.H{"profile": updated})
|
|
}
|
|
|
|
func (h *UserHandler) GetSavedPosts(c *gin.Context) {
|
|
currentUserID := c.GetString("user_id") // Authenticated user
|
|
targetID := c.Param("id")
|
|
|
|
if targetID == "" || targetID == "me" {
|
|
targetID = currentUserID
|
|
}
|
|
|
|
// TODO: Add privacy check here if viewing another user's saved posts
|
|
|
|
limit := utils.GetQueryInt(c, "limit", 20)
|
|
offset := utils.GetQueryInt(c, "offset", 0)
|
|
|
|
posts, err := h.postRepo.GetSavedPosts(c.Request.Context(), targetID, limit, offset)
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to fetch saved posts")
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch saved posts"})
|
|
return
|
|
}
|
|
// Sign URLs
|
|
for i := range posts {
|
|
if posts[i].ImageURL != nil {
|
|
signed := h.assetService.SignImageURL(*posts[i].ImageURL)
|
|
posts[i].ImageURL = &signed
|
|
}
|
|
if posts[i].VideoURL != nil {
|
|
signed := h.assetService.SignVideoURL(*posts[i].VideoURL)
|
|
posts[i].VideoURL = &signed
|
|
}
|
|
if posts[i].ThumbnailURL != nil {
|
|
signed := h.assetService.SignImageURL(*posts[i].ThumbnailURL)
|
|
posts[i].ThumbnailURL = &signed
|
|
}
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"posts": posts})
|
|
}
|
|
|
|
func (h *UserHandler) GetLikedPosts(c *gin.Context) {
|
|
userID := c.GetString("user_id")
|
|
limit := 20
|
|
offset := 0
|
|
|
|
posts, err := h.postRepo.GetLikedPosts(c.Request.Context(), userID, limit, offset)
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to fetch liked posts")
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch liked posts"})
|
|
return
|
|
}
|
|
// Sign URLs
|
|
for i := range posts {
|
|
if posts[i].ImageURL != nil {
|
|
signed := h.assetService.SignImageURL(*posts[i].ImageURL)
|
|
posts[i].ImageURL = &signed
|
|
}
|
|
if posts[i].VideoURL != nil {
|
|
signed := h.assetService.SignVideoURL(*posts[i].VideoURL)
|
|
posts[i].VideoURL = &signed
|
|
}
|
|
if posts[i].ThumbnailURL != nil {
|
|
signed := h.assetService.SignImageURL(*posts[i].ThumbnailURL)
|
|
posts[i].ThumbnailURL = &signed
|
|
}
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"posts": posts})
|
|
}
|
|
|
|
func (h *UserHandler) AcceptFollowRequest(c *gin.Context) {
|
|
userIdStr, _ := c.Get("user_id")
|
|
requesterId := c.Param("id")
|
|
|
|
if err := h.repo.AcceptFollowRequest(c.Request.Context(), userIdStr.(string), requesterId); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to accept follow request"})
|
|
return
|
|
}
|
|
|
|
// Harmony & Notifications
|
|
if h.notificationService != nil {
|
|
go func(targetID, actorID string) {
|
|
// 1. Update Harmony Scores (Mutual gain)
|
|
_ = h.repo.UpdateHarmonyScore(context.Background(), targetID, 2)
|
|
_ = h.repo.UpdateHarmonyScore(context.Background(), actorID, 2)
|
|
|
|
// 2. Send Notification to requester
|
|
_ = h.notificationService.NotifyFollowAccepted(context.Background(), actorID, targetID)
|
|
}(userIdStr.(string), requesterId)
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "Follow request accepted"})
|
|
}
|
|
|
|
func (h *UserHandler) GetPendingFollowRequests(c *gin.Context) {
|
|
userIdStr, _ := c.Get("user_id")
|
|
|
|
requests, err := h.repo.GetPendingFollowRequests(c.Request.Context(), userIdStr.(string))
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch pending follow requests"})
|
|
return
|
|
}
|
|
|
|
// Sign URLs for avatars in requests
|
|
for i := range requests {
|
|
if avatar, ok := requests[i]["avatar_url"].(string); ok && avatar != "" {
|
|
requests[i]["avatar_url"] = h.assetService.SignImageURL(avatar)
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"requests": requests})
|
|
}
|
|
|
|
func (h *UserHandler) RejectFollowRequest(c *gin.Context) {
|
|
userIdStr, _ := c.Get("user_id")
|
|
requesterId := c.Param("id")
|
|
|
|
if err := h.repo.RejectFollowRequest(c.Request.Context(), userIdStr.(string), requesterId); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to reject follow request"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "Follow request rejected"})
|
|
}
|
|
|
|
func (h *UserHandler) BlockUser(c *gin.Context) {
|
|
blockerID, _ := c.Get("user_id")
|
|
blockedID := c.Param("id")
|
|
actorIP := c.ClientIP()
|
|
|
|
if err := h.repo.BlockUser(c.Request.Context(), blockerID.(string), blockedID, actorIP); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to block user"})
|
|
return
|
|
}
|
|
|
|
// Also unfollow automatically
|
|
_ = h.repo.UnfollowUser(c.Request.Context(), blockerID.(string), blockedID)
|
|
_ = h.repo.UnfollowUser(c.Request.Context(), blockedID, blockerID.(string))
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "User blocked"})
|
|
}
|
|
|
|
func (h *UserHandler) ReportUser(c *gin.Context) {
|
|
reporterID, _ := c.Get("user_id")
|
|
|
|
var input struct {
|
|
TargetUserID string `json:"target_user_id" binding:"required"`
|
|
PostID string `json:"post_id"`
|
|
CommentID string `json:"comment_id"`
|
|
ViolationType string `json:"violation_type" binding:"required"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
if err := c.ShouldBindJSON(&input); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
rID, _ := uuid.Parse(reporterID.(string))
|
|
tID, _ := uuid.Parse(input.TargetUserID)
|
|
|
|
report := &models.Report{
|
|
ReporterID: rID,
|
|
TargetUserID: tID,
|
|
ViolationType: input.ViolationType,
|
|
Description: input.Description,
|
|
}
|
|
|
|
if input.PostID != "" {
|
|
pID, _ := uuid.Parse(input.PostID)
|
|
report.PostID = &pID
|
|
}
|
|
if input.CommentID != "" {
|
|
cID, _ := uuid.Parse(input.CommentID)
|
|
report.CommentID = &cID
|
|
}
|
|
|
|
if err := h.repo.CreateReport(c.Request.Context(), report); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create report"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "Report submitted successfully"})
|
|
}
|
|
|
|
func (h *UserHandler) UnblockUser(c *gin.Context) {
|
|
blockerID, _ := c.Get("user_id")
|
|
blockedID := c.Param("id")
|
|
|
|
if err := h.repo.UnblockUser(c.Request.Context(), blockerID.(string), blockedID); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to unblock user"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "User unblocked"})
|
|
}
|
|
|
|
func (h *UserHandler) GetBlockedUsers(c *gin.Context) {
|
|
userID, _ := c.Get("user_id")
|
|
|
|
blocked, err := h.repo.GetBlockedUsers(c.Request.Context(), userID.(string))
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch blocked users"})
|
|
return
|
|
}
|
|
|
|
// Sign URLs
|
|
for i := range blocked {
|
|
if blocked[i].AvatarURL != nil {
|
|
signed := h.assetService.SignImageURL(*blocked[i].AvatarURL)
|
|
blocked[i].AvatarURL = &signed
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"users": blocked})
|
|
}
|
|
|
|
func (h *UserHandler) BlockUserByHandle(c *gin.Context) {
|
|
actorID, _ := c.Get("user_id")
|
|
actorIP := c.ClientIP()
|
|
|
|
var input struct {
|
|
Handle string `json:"handle" binding:"required"`
|
|
}
|
|
|
|
if err := c.ShouldBindJSON(&input); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Handle is required"})
|
|
return
|
|
}
|
|
|
|
if err := h.repo.BlockUserByHandle(c.Request.Context(), actorID.(string), input.Handle, actorIP); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to block user"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "User blocked by handle"})
|
|
}
|
|
|
|
func (h *UserHandler) GetTrustState(c *gin.Context) {
|
|
userIDStr, exists := c.Get("user_id")
|
|
if !exists {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
|
return
|
|
}
|
|
|
|
state, err := h.repo.GetTrustState(c.Request.Context(), userIDStr.(string))
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch trust state"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, state)
|
|
}
|