368 lines
9 KiB
Go
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)
|
|
}
|
|
}
|