sojorn/go-backend/internal/handlers/discover_handler.go
2026-02-15 00:33:24 -06:00

368 lines
9 KiB
Go

package handlers
import (
"net/http"
"sync"
"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"
"github.com/rs/zerolog/log"
)
type DiscoverHandler struct {
userRepo *repository.UserRepository
postRepo *repository.PostRepository
tagRepo *repository.TagRepository
categoryRepo repository.CategoryRepository
assetService *services.AssetService
}
func NewDiscoverHandler(
userRepo *repository.UserRepository,
postRepo *repository.PostRepository,
tagRepo *repository.TagRepository,
categoryRepo repository.CategoryRepository,
assetService *services.AssetService,
) *DiscoverHandler {
return &DiscoverHandler{
userRepo: userRepo,
postRepo: postRepo,
tagRepo: tagRepo,
categoryRepo: categoryRepo,
assetService: assetService,
}
}
// GetDiscover returns the discover page data
// GET /api/v1/discover
func (h *DiscoverHandler) GetDiscover(c *gin.Context) {
userIDStr, _ := c.Get("user_id")
userID := userIDStr.(string)
ctx := c.Request.Context()
start := time.Now()
var wg sync.WaitGroup
var trending []models.Hashtag
var popularPosts []models.Post
wg.Add(2)
// Get top 4 tags
go func() {
defer wg.Done()
var err error
trending, err = h.tagRepo.GetTrendingHashtags(ctx, 4)
if err != nil {
log.Warn().Err(err).Msg("Failed to get trending hashtags")
}
}()
// Get popular public posts
go func() {
defer wg.Done()
var err error
popularPosts, err = h.postRepo.GetPopularPublicPosts(ctx, userID, 20)
if err != nil {
log.Warn().Err(err).Msg("Failed to get popular posts")
}
}()
wg.Wait()
// Sign URLs
for i := range popularPosts {
h.signPostURLs(&popularPosts[i])
}
// Ensure non-nil slices
if trending == nil {
trending = []models.Hashtag{}
}
if popularPosts == nil {
popularPosts = []models.Post{}
}
response := gin.H{
"top_tags": trending,
"popular_posts": popularPosts,
}
log.Debug().Dur("duration", time.Since(start)).Msg("Discover page generated")
c.JSON(http.StatusOK, response)
}
// Search performs a combined search across users, hashtags, and posts
// GET /api/v1/search
func (h *DiscoverHandler) Search(c *gin.Context) {
query := c.Query("q")
searchType := c.Query("type") // "all", "users", "hashtags", "posts"
if query == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Query parameter 'q' is required"})
return
}
if searchType == "" {
searchType = "all"
}
ctx := c.Request.Context()
start := time.Now()
viewerID := ""
if val, exists := c.Get("user_id"); exists {
viewerID = val.(string)
}
var wg sync.WaitGroup
var users []models.Profile
var hashtags []models.Hashtag
var posts []models.Post
var tags []models.TagResult
if searchType == "all" || searchType == "users" {
wg.Add(1)
go func() {
defer wg.Done()
var err error
users, err = h.userRepo.SearchUsers(ctx, query, viewerID, 10)
if err != nil {
log.Warn().Err(err).Msg("Failed to search users")
users = []models.Profile{}
}
}()
}
if searchType == "all" || searchType == "hashtags" {
wg.Add(1)
go func() {
defer wg.Done()
var err error
hashtags, err = h.tagRepo.SearchHashtags(ctx, query, 10)
if err != nil {
log.Warn().Err(err).Msg("Failed to search hashtags")
hashtags = []models.Hashtag{}
}
// Also get legacy tag results for backward compatibility
tags, _ = h.postRepo.SearchTags(ctx, query, 5)
}()
}
if searchType == "all" || searchType == "posts" {
wg.Add(1)
go func() {
defer wg.Done()
var err error
posts, err = h.postRepo.SearchPosts(ctx, query, viewerID, 20)
if err != nil {
log.Warn().Err(err).Msg("Failed to search posts")
posts = []models.Post{}
}
}()
}
wg.Wait()
// Sign URLs
for i := range users {
if users[i].AvatarURL != nil {
signed := h.assetService.SignImageURL(*users[i].AvatarURL)
users[i].AvatarURL = &signed
}
}
for i := range posts {
h.signPostURLs(&posts[i])
}
// Ensure non-nil slices
if users == nil {
users = []models.Profile{}
}
if hashtags == nil {
hashtags = []models.Hashtag{}
}
if tags == nil {
tags = []models.TagResult{}
}
if posts == nil {
posts = []models.Post{}
}
response := gin.H{
"users": users,
"hashtags": hashtags,
"tags": tags, // Legacy format
"posts": posts,
"query": query,
}
log.Info().Str("query", query).Str("type", searchType).Dur("duration", time.Since(start)).Msg("Search completed")
c.JSON(http.StatusOK, response)
}
// GetHashtagPage returns posts for a specific hashtag
// GET /api/v1/hashtags/:name
func (h *DiscoverHandler) GetHashtagPage(c *gin.Context) {
hashtagName := c.Param("name")
if hashtagName == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Hashtag name required"})
return
}
ctx := c.Request.Context()
limit := utils.GetQueryInt(c, "limit", 20)
offset := utils.GetQueryInt(c, "offset", 0)
viewerID := ""
if val, exists := c.Get("user_id"); exists {
viewerID = val.(string)
}
// Get hashtag info
hashtag, err := h.tagRepo.GetHashtagByName(ctx, hashtagName)
if err != nil || hashtag == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Hashtag not found"})
return
}
// Check if user follows this hashtag
isFollowing := false
if viewerID != "" {
userUUID, err := uuid.Parse(viewerID)
if err == nil {
isFollowing, _ = h.tagRepo.IsFollowingHashtag(ctx, userUUID, hashtag.ID)
}
}
// Get posts
posts, err := h.tagRepo.GetPostsByHashtag(ctx, hashtagName, viewerID, limit, offset)
if err != nil {
log.Error().Err(err).Str("hashtag", hashtagName).Msg("Failed to get posts by hashtag")
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch posts"})
return
}
// Sign URLs
for i := range posts {
h.signPostURLs(&posts[i])
}
if posts == nil {
posts = []models.Post{}
}
response := models.HashtagPageResponse{
Hashtag: *hashtag,
Posts: posts,
IsFollowing: isFollowing,
TotalPosts: hashtag.UseCount,
}
c.JSON(http.StatusOK, response)
}
// FollowHashtag follows a hashtag
// POST /api/v1/hashtags/:name/follow
func (h *DiscoverHandler) FollowHashtag(c *gin.Context) {
hashtagName := c.Param("name")
userIDStr, _ := c.Get("user_id")
userUUID, _ := uuid.Parse(userIDStr.(string))
ctx := c.Request.Context()
// Get or create the hashtag
hashtag, err := h.tagRepo.GetOrCreateHashtag(ctx, hashtagName)
if err != nil || hashtag == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to process hashtag"})
return
}
if err := h.tagRepo.FollowHashtag(ctx, userUUID, hashtag.ID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to follow hashtag"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Now following #" + hashtag.Name})
}
// UnfollowHashtag unfollows a hashtag
// DELETE /api/v1/hashtags/:name/follow
func (h *DiscoverHandler) UnfollowHashtag(c *gin.Context) {
hashtagName := c.Param("name")
userIDStr, _ := c.Get("user_id")
userUUID, _ := uuid.Parse(userIDStr.(string))
ctx := c.Request.Context()
hashtag, err := h.tagRepo.GetHashtagByName(ctx, hashtagName)
if err != nil || hashtag == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Hashtag not found"})
return
}
if err := h.tagRepo.UnfollowHashtag(ctx, userUUID, hashtag.ID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to unfollow hashtag"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Unfollowed #" + hashtag.Name})
}
// GetTrendingHashtags returns trending hashtags
// GET /api/v1/hashtags/trending
func (h *DiscoverHandler) GetTrendingHashtags(c *gin.Context) {
limit := utils.GetQueryInt(c, "limit", 20)
hashtags, err := h.tagRepo.GetTrendingHashtags(c.Request.Context(), limit)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch trending hashtags"})
return
}
if hashtags == nil {
hashtags = []models.Hashtag{}
}
c.JSON(http.StatusOK, gin.H{"hashtags": hashtags})
}
// GetFollowedHashtags returns hashtags the user follows
// GET /api/v1/hashtags/following
func (h *DiscoverHandler) GetFollowedHashtags(c *gin.Context) {
userIDStr, _ := c.Get("user_id")
userUUID, _ := uuid.Parse(userIDStr.(string))
hashtags, err := h.tagRepo.GetFollowedHashtags(c.Request.Context(), userUUID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch followed hashtags"})
return
}
if hashtags == nil {
hashtags = []models.Hashtag{}
}
c.JSON(http.StatusOK, gin.H{"hashtags": hashtags})
}
// signPostURLs signs all URLs in a post
func (h *DiscoverHandler) signPostURLs(post *models.Post) {
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.Author != nil && post.Author.AvatarURL != "" {
post.Author.AvatarURL = h.assetService.SignImageURL(post.Author.AvatarURL)
}
}