package handlers import ( "context" "fmt" "io" "net/http" "net/url" "os" "regexp" "strconv" "strings" "time" "github.com/chromedp/chromedp" "github.com/gin-gonic/gin" "github.com/trackeep/backend/config" "github.com/trackeep/backend/models" "github.com/trackeep/backend/services" ) // GetBookmarks handles GET /api/v1/bookmarks func GetBookmarks(c *gin.Context) { // Check if demo mode is enabled if os.Getenv("VITE_DEMO_MODE") == "true" { // Return mock bookmarks for demo mode mockBookmarks := []models.Bookmark{ { ID: 1, Title: "React Documentation", URL: "https://react.dev", Description: "The official React documentation", UserID: 1, CreatedAt: time.Now(), UpdatedAt: time.Now(), }, { ID: 2, Title: "YouTube - Introduction to React Programming", URL: "https://www.youtube.com/watch?v=hTWKbfoikeg", Description: "Video from Programming Tutorials", UserID: 1, CreatedAt: time.Now(), UpdatedAt: time.Now(), }, { ID: 3, Title: "Docker Documentation", URL: "https://docs.docker.com", Description: "Official Docker documentation", UserID: 1, CreatedAt: time.Now(), UpdatedAt: time.Now(), }, } c.JSON(http.StatusOK, mockBookmarks) return } db := config.GetDB() var bookmarks []models.Bookmark // Get user ID from context (set by auth middleware) userID := c.GetUint("userID") if userID == 0 { c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"}) return } // Preload tags for the bookmarks if err := db.Where("user_id = ?", userID).Preload("Tags").Find(&bookmarks).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch bookmarks"}) return } c.JSON(http.StatusOK, bookmarks) } // CreateBookmark handles POST /api/v1/bookmarks func CreateBookmark(c *gin.Context) { db := config.GetDB() var bookmark models.Bookmark if err := c.ShouldBindJSON(&bookmark); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Set user ID from auth middleware userID := c.GetUint("userID") if userID == 0 { c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"}) return } bookmark.UserID = userID // Fetch website metadata if URL is provided if bookmark.URL != "" { // Use basic metadata fetching if metadata, err := services.GetCachedMetadata(bookmark.URL); err == nil { // Update bookmark with fetched metadata if bookmark.Title == "" && metadata.Title != "" { bookmark.Title = metadata.Title } if bookmark.Description == "" && metadata.Description != "" { bookmark.Description = metadata.Description } if metadata.Favicon != "" { bookmark.Favicon = metadata.Favicon } if metadata.Author != "" { bookmark.Author = metadata.Author } // Parse published date if available if metadata.PublishedAt != "" { if publishedAt, err := time.Parse(time.RFC3339, metadata.PublishedAt); err == nil { bookmark.PublishedAt = &publishedAt } } } } // Create bookmark if err := db.Create(&bookmark).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create bookmark"}) return } // Preload tags for response db.Preload("Tags").First(&bookmark, bookmark.ID) c.JSON(http.StatusCreated, bookmark) } // GetBookmark handles GET /api/v1/bookmarks/:id func GetBookmark(c *gin.Context) { db := config.GetDB() id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bookmark ID"}) return } var bookmark models.Bookmark userID := c.GetUint("userID") if userID == 0 { c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"}) return } // Find bookmark with tags if err := db.Where("id = ? AND user_id = ?", id, userID).Preload("Tags").First(&bookmark).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Bookmark not found"}) return } c.JSON(http.StatusOK, bookmark) } // UpdateBookmark handles PUT /api/v1/bookmarks/:id func UpdateBookmark(c *gin.Context) { db := config.GetDB() id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bookmark ID"}) return } var bookmark models.Bookmark userID := c.GetUint("userID") if userID == 0 { c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"}) return } // Find existing bookmark if err := db.Where("id = ? AND user_id = ?", id, userID).First(&bookmark).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Bookmark not found"}) return } // Update fields var updateData models.Bookmark if err := c.ShouldBindJSON(&updateData); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Update bookmark if err := db.Model(&bookmark).Updates(updateData).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update bookmark"}) return } // Get updated bookmark with tags db.Preload("Tags").First(&bookmark, bookmark.ID) c.JSON(http.StatusOK, bookmark) } // DeleteBookmark handles DELETE /api/v1/bookmarks/:id func DeleteBookmark(c *gin.Context) { db := config.GetDB() id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bookmark ID"}) return } var bookmark models.Bookmark userID := c.GetUint("userID") if userID == 0 { c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"}) return } // Find and delete bookmark if err := db.Where("id = ? AND user_id = ?", id, userID).First(&bookmark).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Bookmark not found"}) return } if err := db.Delete(&bookmark).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete bookmark"}) return } c.JSON(http.StatusOK, gin.H{"message": "Bookmark deleted successfully"}) } // RefreshBookmarkMetadata handles POST /api/v1/bookmarks/:id/refresh-metadata func RefreshBookmarkMetadata(c *gin.Context) { db := config.GetDB() id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bookmark ID"}) return } var bookmark models.Bookmark userID := c.GetUint("userID") if userID == 0 { c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"}) return } // Find existing bookmark if err := db.Where("id = ? AND user_id = ?", id, userID).First(&bookmark).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Bookmark not found"}) return } // Fetch fresh metadata if metadata, err := services.GetCachedMetadata(bookmark.URL); err == nil { // Update bookmark with basic metadata bookmark.Title = metadata.Title bookmark.Description = metadata.Description bookmark.Favicon = metadata.Favicon bookmark.Author = metadata.Author // Parse published date if available if metadata.PublishedAt != "" { if publishedAt, err := time.Parse(time.RFC3339, metadata.PublishedAt); err == nil { bookmark.PublishedAt = &publishedAt } } // Save updated bookmark if err := db.Save(&bookmark).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update bookmark"}) return } // Get updated bookmark with tags db.Preload("Tags").First(&bookmark, bookmark.ID) c.JSON(http.StatusOK, gin.H{ "message": "Metadata refreshed successfully", "bookmark": bookmark, }) } else { c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Failed to fetch metadata: %s", err.Error())}) } } // GetBookmarkMetadata handles POST /api/v1/bookmarks/metadata func GetBookmarkMetadata(c *gin.Context) { var request struct { URL string `json:"url" binding:"required"` } if err := c.ShouldBindJSON(&request); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Fetch metadata using basic service if metadata, err := services.GetCachedMetadata(request.URL); err == nil { // Return metadata from basic fetching response := gin.H{ "title": metadata.Title, "description": metadata.Description, "favicon": metadata.Favicon, "metadata": gin.H{ "siteName": metadata.SiteName, "description": metadata.Description, "image": metadata.Image, "author": metadata.Author, }, } c.JSON(http.StatusOK, response) } else { c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Failed to fetch metadata: %s", err.Error())}) } } // GetBookmarkContent handles POST /api/v1/bookmarks/content func GetBookmarkContent(c *gin.Context) { var request struct { URL string `json:"url" binding:"required"` } if err := c.ShouldBindJSON(&request); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Fetch full page content with screenshot content, err := fetchPageContentWithScreenshot(request.URL) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Failed to fetch content: %s", err.Error())}) return } // Return content as HTML c.Header("Content-Type", "text/html") c.String(http.StatusOK, content) } // fetchPageContentWithScreenshot fetches page content and generates a screenshot func fetchPageContentWithScreenshot(targetURL string) (string, error) { // Parse URL to ensure it's valid parsedURL, err := url.Parse(targetURL) if err != nil { return "", fmt.Errorf("invalid URL: %w", err) } // Create HTTP client with timeout for content fetching client := &http.Client{ Timeout: 15 * time.Second, } // Make request for basic content req, err := http.NewRequest("GET", targetURL, nil) if err != nil { return "", fmt.Errorf("failed to create request: %w", err) } // Set user agent to avoid being blocked req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8") resp, err := client.Do(req) if err != nil { return "", fmt.Errorf("failed to fetch URL: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status) } // Read response body body, err := io.ReadAll(resp.Body) if err != nil { return "", fmt.Errorf("failed to read response body: %w", err) } content := string(body) // Extract metadata for preview metadata, err := services.FetchWebsiteMetadata(targetURL) if err != nil { // Continue without metadata if it fails metadata = &services.WebsiteMetadata{ Title: parsedURL.Hostname(), } } // Try to capture screenshot var screenshotData []byte screenshotErr := captureScreenshot(targetURL, &screenshotData) // Generate preview HTML with screenshot if available previewHTML := generateEnhancedPreviewHTML(content, metadata, parsedURL, screenshotData, screenshotErr) return previewHTML, nil } // captureScreenshot captures a screenshot of the given URL using ChromeDP func captureScreenshot(targetURL string, screenshotData *[]byte) error { // Create a new Chrome context ctx, cancel := chromedp.NewContext(context.Background()) defer cancel() // Set a timeout for the entire operation ctx, cancel = context.WithTimeout(ctx, 30*time.Second) defer cancel() // Navigate to the URL and capture screenshot var buf []byte err := chromedp.Run(ctx, chromedp.Navigate(targetURL), chromedp.WaitReady("body"), // Wait for body to be ready chromedp.EmulateViewport(1200, 800), // Set viewport size chromedp.CaptureScreenshot(&buf), ) if err != nil { return fmt.Errorf("failed to capture screenshot: %w", err) } *screenshotData = buf return nil } // generateEnhancedPreviewHTML creates a clean preview with screenshot func generateEnhancedPreviewHTML(content string, metadata *services.WebsiteMetadata, parsedURL *url.URL, screenshotData []byte, screenshotErr error) string { // Extract main content title := metadata.Title if title == "" { title = parsedURL.Hostname() } description := metadata.Description if description == "" { // Try to extract a snippet from the content content = strings.ToLower(content) // Remove script and style tags re := regexp.MustCompile(`(?i)<(script|style)[^>]*>.*?`) content = re.ReplaceAllString(content, "") // Extract text content re = regexp.MustCompile(`<[^>]+>`) textContent := re.ReplaceAllString(content, " ") textContent = strings.TrimSpace(textContent) if len(textContent) > 200 { description = textContent[:200] + "..." } else { description = textContent } } favicon := metadata.Favicon if favicon == "" { favicon = fmt.Sprintf("https://www.google.com/s2/favicons?domain=%s&sz=128", parsedURL.Host) } // Convert screenshot to base64 if available var screenshotHTML string if screenshotErr == nil && len(screenshotData) > 0 { // In a real implementation, you'd encode to base64 and store/display it // For now, we'll add a placeholder screenshotHTML = `

Page Screenshot

Screenshot captured successfully (${len(screenshotData)} bytes)

(Screenshot display would be implemented here)

` } else { screenshotHTML = `

Page Screenshot

Could not capture screenshot: ` + screenshotErr.Error() + `

(Screenshot requires Chrome/Chromium to be installed)

` } // Generate enhanced preview HTML previewHTML := fmt.Sprintf(` Preview: %s
Site favicon

%s

%s
%s
Site favicon Site: %s

Description: %s

Author: %s

Visit Original Site
`, title, favicon, title[:1], // First letter for fallback title, parsedURL.String(), screenshotHTML, favicon, metadata.SiteName, description, metadata.Author, parsedURL.String(), ) return previewHTML }