sojorn/_tmp_server/post_handler.go
Patrick Britton b76154be3a Fix UUID casting issues in post, notification, and category repositories
- Replace NULLIF with CASE WHEN for proper UUID casting
- Fix missing ::uuid casting in WHERE clauses
- Resolve 'operator does not exist: uuid = text' errors
- Focus on post_repository.go, notification_repository.go, and category_repository.go
2026-01-31 13:55:59 -06:00

495 lines
13 KiB
Go

package handlers
import (
"net/http"
"strings"
"time"
"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"
)
type PostHandler struct {
postRepo *repository.PostRepository
userRepo *repository.UserRepository
feedService *services.FeedService
assetService *services.AssetService
}
func NewPostHandler(postRepo *repository.PostRepository, userRepo *repository.UserRepository, feedService *services.FeedService, assetService *services.AssetService) *PostHandler {
return &PostHandler{
postRepo: postRepo,
userRepo: userRepo,
feedService: feedService,
assetService: assetService,
}
}
func (h *PostHandler) CreateComment(c *gin.Context) {
userIDStr, _ := c.Get("user_id")
userID, _ := uuid.Parse(userIDStr.(string))
postID := c.Param("id")
var req struct {
Body string `json:"body" binding:"required,max=500"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
comment := &models.Comment{
PostID: postID,
AuthorID: userID,
Body: req.Body,
Status: "active",
}
if err := h.postRepo.CreateComment(c.Request.Context(), comment); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create comment", "details": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"comment": comment})
}
func (h *PostHandler) GetNearbyBeacons(c *gin.Context) {
lat := utils.GetQueryFloat(c, "lat", 0)
long := utils.GetQueryFloat(c, "long", 0)
radius := utils.GetQueryInt(c, "radius", 16000)
beacons, err := h.postRepo.GetNearbyBeacons(c.Request.Context(), lat, long, radius)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch nearby beacons", "details": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"beacons": beacons})
}
func (h *PostHandler) CreatePost(c *gin.Context) {
userIDStr, _ := c.Get("user_id")
userID, _ := uuid.Parse(userIDStr.(string))
var req struct {
CategoryID *string `json:"category_id"`
Body string `json:"body" binding:"required,max=500"`
ImageURL *string `json:"image_url"`
VideoURL *string `json:"video_url"`
Thumbnail *string `json:"thumbnail_url"`
DurationMS *int `json:"duration_ms"`
IsBeacon bool `json:"is_beacon"`
BeaconType *string `json:"beacon_type"`
BeaconLat *float64 `json:"beacon_lat"`
BeaconLong *float64 `json:"beacon_long"`
TTLHours *int `json:"ttl_hours"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 1. Check rate limit (Simplification)
trustState, err := h.userRepo.GetTrustState(c.Request.Context(), userID.String())
if err == nil && trustState.PostsToday >= 50 { // Example hard limit
c.JSON(http.StatusTooManyRequests, gin.H{"error": "Daily post limit reached"})
return
}
// 2. Extract tags
tags := utils.ExtractHashtags(req.Body)
// 3. Mock Tone Check (In production, this would call a service or AI model)
tone := "neutral"
cis := 0.8
// 4. Resolve TTL
var expiresAt *time.Time
if req.TTLHours != nil && *req.TTLHours > 0 {
t := time.Now().Add(time.Duration(*req.TTLHours) * time.Hour)
expiresAt = &t
}
duration := 0
if req.DurationMS != nil {
duration = *req.DurationMS
}
post := &models.Post{
AuthorID: userID,
Body: req.Body,
Status: "active",
ToneLabel: &tone,
CISScore: &cis,
ImageURL: req.ImageURL,
VideoURL: req.VideoURL,
ThumbnailURL: req.Thumbnail,
DurationMS: duration,
BodyFormat: "plain",
Tags: tags,
IsBeacon: req.IsBeacon,
BeaconType: req.BeaconType,
Confidence: 0.5, // Initial confidence
IsActiveBeacon: req.IsBeacon,
AllowChain: !req.IsBeacon,
Visibility: "public",
ExpiresAt: expiresAt,
Lat: req.BeaconLat,
Long: req.BeaconLong,
}
if req.CategoryID != nil {
catID, _ := uuid.Parse(*req.CategoryID)
post.CategoryID = &catID
}
// Create post
err = h.postRepo.CreatePost(c.Request.Context(), post)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create post", "details": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{
"post": post,
"tags": tags,
"tone_analysis": gin.H{
"tone": tone,
"cis": cis,
},
})
}
func (h *PostHandler) GetFeed(c *gin.Context) {
userIDStr, _ := c.Get("user_id")
limit := utils.GetQueryInt(c, "limit", 20)
offset := utils.GetQueryInt(c, "offset", 0)
category := c.Query("category")
hasVideo := c.Query("has_video") == "true"
posts, err := h.feedService.GetFeed(c.Request.Context(), userIDStr.(string), category, hasVideo, limit, offset)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch feed", "details": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"posts": posts})
}
func (h *PostHandler) GetProfilePosts(c *gin.Context) {
authorID := c.Param("id")
if authorID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Author ID required"})
return
}
limit := utils.GetQueryInt(c, "limit", 20)
offset := utils.GetQueryInt(c, "offset", 0)
viewerID := ""
if val, exists := c.Get("user_id"); exists {
viewerID = val.(string)
}
posts, err := h.postRepo.GetPostsByAuthor(c.Request.Context(), authorID, viewerID, limit, offset)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch profile posts", "details": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"posts": posts})
}
func (h *PostHandler) GetPost(c *gin.Context) {
postID := c.Param("id")
userIDStr, _ := c.Get("user_id")
post, err := h.postRepo.GetPostByID(c.Request.Context(), postID, userIDStr.(string))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Post not found"})
return
}
// Sign URL
if post.ImageURL != nil {
signed := h.assetService.SignImageURL(*post.ImageURL)
post.ImageURL = &signed
}
if post.VideoURL != nil {
signed := h.assetService.SignVideoURL(*post.VideoURL)
post.VideoURL = &signed
}
if post.ThumbnailURL != nil {
signed := h.assetService.SignImageURL(*post.ThumbnailURL)
post.ThumbnailURL = &signed
}
c.JSON(http.StatusOK, gin.H{"post": post})
}
func (h *PostHandler) UpdatePost(c *gin.Context) {
postID := c.Param("id")
userIDStr, _ := c.Get("user_id")
var req struct {
Body string `json:"body" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
err := h.postRepo.UpdatePost(c.Request.Context(), postID, userIDStr.(string), req.Body)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update post", "details": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Post updated"})
}
func (h *PostHandler) DeletePost(c *gin.Context) {
postID := c.Param("id")
userIDStr, _ := c.Get("user_id")
err := h.postRepo.DeletePost(c.Request.Context(), postID, userIDStr.(string))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete post", "details": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Post deleted"})
}
func (h *PostHandler) PinPost(c *gin.Context) {
postID := c.Param("id")
userIDStr, _ := c.Get("user_id")
var req struct {
Pinned bool `json:"pinned"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
err := h.postRepo.PinPost(c.Request.Context(), postID, userIDStr.(string), req.Pinned)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to pin/unpin post", "details": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Post pin status updated"})
}
func (h *PostHandler) UpdateVisibility(c *gin.Context) {
postID := c.Param("id")
userIDStr, _ := c.Get("user_id")
var req struct {
Visibility string `json:"visibility" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
err := h.postRepo.UpdateVisibility(c.Request.Context(), postID, userIDStr.(string), req.Visibility)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update visibility", "details": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Post visibility updated"})
}
func (h *PostHandler) LikePost(c *gin.Context) {
postID := c.Param("id")
userIDStr, _ := c.Get("user_id")
err := h.postRepo.LikePost(c.Request.Context(), postID, userIDStr.(string))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to like post", "details": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Post liked"})
}
func (h *PostHandler) UnlikePost(c *gin.Context) {
postID := c.Param("id")
userIDStr, _ := c.Get("user_id")
err := h.postRepo.UnlikePost(c.Request.Context(), postID, userIDStr.(string))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to unlike post", "details": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Post unliked"})
}
func (h *PostHandler) SavePost(c *gin.Context) {
postID := c.Param("id")
userIDStr, _ := c.Get("user_id")
err := h.postRepo.SavePost(c.Request.Context(), postID, userIDStr.(string))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save post", "details": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Post saved"})
}
func (h *PostHandler) UnsavePost(c *gin.Context) {
postID := c.Param("id")
userIDStr, _ := c.Get("user_id")
err := h.postRepo.UnsavePost(c.Request.Context(), postID, userIDStr.(string))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to unsave post", "details": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Post unsaved"})
}
func (h *PostHandler) ToggleReaction(c *gin.Context) {
postID := c.Param("id")
userIDStr, _ := c.Get("user_id")
var req struct {
Emoji string `json:"emoji" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
emoji := strings.TrimSpace(req.Emoji)
if emoji == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Emoji is required"})
return
}
counts, myReactions, err := h.postRepo.ToggleReaction(c.Request.Context(), postID, userIDStr.(string), emoji)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to toggle reaction", "details": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"reactions": counts,
"my_reactions": myReactions,
})
}
func (h *PostHandler) GetSavedPosts(c *gin.Context) {
userID := c.Param("id")
if userID == "" || userID == "me" {
userIDStr, exists := c.Get("user_id")
if exists {
userID = userIDStr.(string)
}
}
if userID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "User ID required"})
return
}
limit := utils.GetQueryInt(c, "limit", 20)
offset := utils.GetQueryInt(c, "offset", 0)
posts, err := h.postRepo.GetSavedPosts(c.Request.Context(), userID, limit, offset)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch saved posts", "details": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"posts": posts})
}
func (h *PostHandler) GetLikedPosts(c *gin.Context) {
userID := c.Param("id")
if userID == "" || userID == "me" {
userIDStr, exists := c.Get("user_id")
if exists {
userID = userIDStr.(string)
}
}
if userID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "User ID required"})
return
}
limit := utils.GetQueryInt(c, "limit", 20)
offset := utils.GetQueryInt(c, "offset", 0)
posts, err := h.postRepo.GetLikedPosts(c.Request.Context(), userID, limit, offset)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch liked posts", "details": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"posts": posts})
}
func (h *PostHandler) GetPostChain(c *gin.Context) {
postID := c.Param("id")
posts, err := h.postRepo.GetPostChain(c.Request.Context(), postID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch post chain", "details": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"chain": posts})
}
func (h *PostHandler) GetPostFocusContext(c *gin.Context) {
postID := c.Param("id")
userIDStr, _ := c.Get("user_id")
focusContext, err := h.postRepo.GetPostFocusContext(c.Request.Context(), postID, userIDStr.(string))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch focus context", "details": err.Error()})
return
}
h.signPostMedia(focusContext.TargetPost)
h.signPostMedia(focusContext.ParentPost)
for i := range focusContext.Children {
h.signPostMedia(&focusContext.Children[i])
}
c.JSON(http.StatusOK, focusContext)
}
func (h *PostHandler) signPostMedia(post *models.Post) {
if post == nil {
return
}
if post.ImageURL != nil {
signed := h.assetService.SignImageURL(*post.ImageURL)
post.ImageURL = &signed
}
if post.VideoURL != nil {
signed := h.assetService.SignVideoURL(*post.VideoURL)
post.VideoURL = &signed
}
if post.ThumbnailURL != nil {
signed := h.assetService.SignImageURL(*post.ThumbnailURL)
post.ThumbnailURL = &signed
}
}