package internal import ( "database/sql" "encoding/json" "fmt" "log/slog" "net/http" "os" "strconv" "strings" "time" "github.com/labstack/echo/v4" ) // getUserDB is a helper function to get the user's database connection from the context func getUserDB(c echo.Context) (*sql.DB, error) { userID, ok := c.Get("user_id").(string) if !ok { return nil, fmt.Errorf("user_id not found in context") } username, ok := c.Get("username").(string) if !ok { return nil, fmt.Errorf("username not found in context") } return GetUserDB(userID, username) } func HandleUpload(c echo.Context) error { // Use a smaller memory limit for the form parsing itself (32 MB) // Large files will be streamed directly to disk err := c.Request().ParseMultipartForm(32 << 20) // 32 MB max in memory if err != nil { slog.Error("Error parsing form", "error", err) return c.JSON(http.StatusBadRequest, UploadResponse{ Success: false, Error: "Failed to parse form data. File may be too large or corrupted.", }) } file, header, err := c.Request().FormFile("file") if err != nil { slog.Error("Error getting file", "error", err) return c.JSON(http.StatusBadRequest, UploadResponse{ Success: false, Error: "Failed to get file from form", }) } defer file.Close() slog.Info("Receiving file", "filename", header.Filename, "size", header.Size) // Save uploaded file to temporary location first tempFilePath, err := SaveUploadedFile(file, header.Filename) if err != nil { slog.Error("Error saving file", "error", err) return c.JSON(http.StatusInternalServerError, UploadResponse{ Success: false, Error: "Failed to save uploaded file: " + err.Error(), }) } slog.Info("File saved", "path", tempFilePath) // Get user ID from context userID, ok := c.Get("user_id").(string) if !ok { return c.JSON(http.StatusUnauthorized, UploadResponse{ Success: false, Error: "User not authenticated", }) } // Get username from context username, ok := c.Get("username").(string) if !ok { return c.JSON(http.StatusUnauthorized, UploadResponse{ Success: false, Error: "User not authenticated", }) } // Start background processing with user context go ProcessUploadedFile(userID, username, tempFilePath) // Return immediately - client will poll /api/progress for status return c.JSON(http.StatusOK, UploadResponse{ Success: true, MessageCount: 0, CallLogCount: 0, Processing: true, }) } func HandleConversations(c echo.Context) error { userDB, err := getUserDB(c) if err != nil { slog.Error("Error getting user database", "error", err) return c.JSON(http.StatusInternalServerError, map[string]string{ "error": "Failed to get user database", }) } var startDate, endDate *time.Time if startStr := c.QueryParam("start"); startStr != "" { t, err := time.Parse(time.RFC3339, startStr) if err == nil { startDate = &t } } if endStr := c.QueryParam("end"); endStr != "" { t, err := time.Parse(time.RFC3339, endStr) if err == nil { endDate = &t } } conversations, err := GetConversations(userDB, startDate, endDate) if err != nil { slog.Error("Error getting conversations", "error", err) return c.JSON(http.StatusInternalServerError, map[string]string{ "error": "Failed to get conversations", }) } return c.JSON(http.StatusOK, conversations) } func HandleMessages(c echo.Context) error { userDB, err := getUserDB(c) if err != nil { slog.Error("Error getting user database", "error", err) return c.JSON(http.StatusInternalServerError, map[string]string{ "error": "Failed to get user database", }) } address := c.QueryParam("address") convType := c.QueryParam("type") if address == "" { return c.JSON(http.StatusBadRequest, map[string]string{ "error": "Address parameter required", }) } var startDate, endDate *time.Time if startStr := c.QueryParam("start"); startStr != "" { t, err := time.Parse(time.RFC3339, startStr) if err == nil { startDate = &t } } if endStr := c.QueryParam("end"); endStr != "" { t, err := time.Parse(time.RFC3339, endStr) if err == nil { endDate = &t } } // If type is "call", return call logs instead of messages if convType == "call" { calls, err := GetCallLogs(userDB, address, startDate, endDate) if err != nil { slog.Error("Error getting call logs", "error", err) return c.JSON(http.StatusInternalServerError, map[string]string{ "error": "Failed to get call logs", }) } return c.JSON(http.StatusOK, calls) } // If type is "conversation", return combined messages and calls if convType == "conversation" { // Parse limit and offset parameters limit := 100000 // Default to 100k (effectively unlimited for most users) offset := 0 if limitStr := c.QueryParam("limit"); limitStr != "" { if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 { limit = parsedLimit } } if offsetStr := c.QueryParam("offset"); offsetStr != "" { if parsedOffset, err := strconv.Atoi(offsetStr); err == nil && parsedOffset >= 0 { offset = parsedOffset } } // Get user ID from context to fetch settings userID, ok := c.Get("user_id").(string) if !ok { return c.JSON(http.StatusUnauthorized, map[string]string{ "error": "User not authenticated", }) } // Fetch user settings to check if calls should be shown settings, err := GetUserSettings(userID) if err != nil { slog.Error("Error getting user settings", "error", err) // If we can't get settings, default to showing calls settings = GetDefaultSettings() } activities, err := GetActivityByAddress(userDB, address, startDate, endDate, limit, offset) if err != nil { slog.Error("Error getting activity", "error", err) return c.JSON(http.StatusInternalServerError, map[string]string{ "error": "Failed to get activity", }) } // Filter out calls if show_calls setting is false if !settings.Conversations.ShowCalls { filteredActivities := []ActivityItem{} for _, activity := range activities { if activity.Type != "call" { filteredActivities = append(filteredActivities, activity) } } activities = filteredActivities } return c.JSON(http.StatusOK, activities) } messages, err := GetMessages(userDB, address, startDate, endDate) if err != nil { slog.Error("Error getting messages", "error", err) return c.JSON(http.StatusInternalServerError, map[string]string{ "error": "Failed to get messages", }) } return c.JSON(http.StatusOK, messages) } // HandleMediaItems returns only media (images/videos) for a conversation func HandleMediaItems(c echo.Context) error { userDB, err := getUserDB(c) if err != nil { slog.Error("Error getting user database", "error", err) return c.JSON(http.StatusInternalServerError, map[string]string{ "error": "Failed to get user database", }) } address := c.QueryParam("address") var startDate, endDate *time.Time if startStr := c.QueryParam("start"); startStr != "" { t, err := time.Parse(time.RFC3339, startStr) if err == nil { startDate = &t } } if endStr := c.QueryParam("end"); endStr != "" { t, err := time.Parse(time.RFC3339, endStr) if err == nil { endDate = &t } } mediaItems, err := GetMediaByAddress(userDB, address, startDate, endDate) if err != nil { slog.Error("Error getting media items", "error", err) return c.JSON(http.StatusInternalServerError, map[string]string{ "error": "Failed to get media items", }) } return c.JSON(http.StatusOK, mediaItems) } func HandleActivity(c echo.Context) error { userDB, err := getUserDB(c) if err != nil { slog.Error("Error getting user database", "error", err) return c.JSON(http.StatusInternalServerError, map[string]string{ "error": "Failed to get user database", }) } var startDate, endDate *time.Time if startStr := c.QueryParam("start"); startStr != "" { t, err := time.Parse(time.RFC3339, startStr) if err == nil { startDate = &t } } if endStr := c.QueryParam("end"); endStr != "" { t, err := time.Parse(time.RFC3339, endStr) if err == nil { endDate = &t } } // Parse pagination parameters limit := 50 // default limit offset := 0 // default offset if limitStr := c.QueryParam("limit"); limitStr != "" { if val, err := strconv.Atoi(limitStr); err == nil { limit = val } } if offsetStr := c.QueryParam("offset"); offsetStr != "" { if val, err := strconv.Atoi(offsetStr); err == nil { offset = val } } activities, err := GetActivity(userDB, startDate, endDate, limit, offset) if err != nil { slog.Error("Error getting activity", "error", err) return c.JSON(http.StatusInternalServerError, map[string]string{ "error": "Failed to get activity", }) } return c.JSON(http.StatusOK, activities) } func HandleCalls(c echo.Context) error { userDB, err := getUserDB(c) if err != nil { slog.Error("Error getting user database", "error", err) return c.JSON(http.StatusInternalServerError, map[string]string{ "error": "Failed to get user database", }) } var startDate, endDate *time.Time if startStr := c.QueryParam("start"); startStr != "" { t, err := time.Parse(time.RFC3339, startStr) if err == nil { startDate = &t } } if endStr := c.QueryParam("end"); endStr != "" { t, err := time.Parse(time.RFC3339, endStr) if err == nil { endDate = &t } } // Parse pagination parameters limit := 50 // default limit offset := 0 // default offset if limitStr := c.QueryParam("limit"); limitStr != "" { if val, err := strconv.Atoi(limitStr); err == nil { limit = val } } if offsetStr := c.QueryParam("offset"); offsetStr != "" { if val, err := strconv.Atoi(offsetStr); err == nil { offset = val } } calls, err := GetAllCalls(userDB, startDate, endDate, limit, offset) if err != nil { slog.Error("Error getting calls", "error", err) return c.JSON(http.StatusInternalServerError, map[string]string{ "error": "Failed to get calls", }) } return c.JSON(http.StatusOK, calls) } func HandleDateRange(c echo.Context) error { userDB, err := getUserDB(c) if err != nil { slog.Error("Error getting user database", "error", err) return c.JSON(http.StatusInternalServerError, map[string]string{ "error": "Failed to get user database", }) } minDate, maxDate, err := GetDateRange(userDB) if err != nil { slog.Error("Error getting date range", "error", err) return c.JSON(http.StatusInternalServerError, map[string]string{ "error": "Failed to get date range", }) } return c.JSON(http.StatusOK, map[string]interface{}{ "min_date": minDate, "max_date": maxDate, }) } func HandleProgress(c echo.Context) error { progress := GetUploadProgress() if progress == nil { return c.JSON(http.StatusOK, map[string]interface{}{ "status": "no_upload", }) } return c.JSON(http.StatusOK, progress) } func HandleMedia(c echo.Context) error { userDB, err := getUserDB(c) if err != nil { slog.Error("Error getting user database", "error", err) return c.JSON(http.StatusInternalServerError, map[string]string{ "error": "Failed to get user database", }) } // Get message ID from query parameter messageID := c.QueryParam("id") if messageID == "" { return c.JSON(http.StatusBadRequest, map[string]string{ "error": "Message ID required", }) } // Check if transcode is requested (for videos that browser can't play) forceTranscode := c.QueryParam("transcode") == "true" // Fetch media from database media, contentType, err := GetMessageMedia(userDB, messageID) if err != nil { slog.Error("Error getting media", "error", err) return c.JSON(http.StatusNotFound, map[string]string{ "error": "Media not found", }) } if len(media) == 0 { return c.JSON(http.StatusNotFound, map[string]string{ "error": "No media for this message", }) } // If transcode is requested and this is a video, try to convert it if forceTranscode && strings.HasPrefix(contentType, "video/") { slog.Info("Transcode requested for video", "messageID", messageID, "contentType", contentType) convertedData, err := convertVideoToMP4(media) if err != nil { slog.Error("Failed to transcode video", "messageID", messageID, "error", err) // Continue with original video if conversion fails } else { slog.Info("Successfully transcoded video", "messageID", messageID) media = convertedData contentType = "video/mp4" } } slog.Debug("Serving media", "messageID", messageID, "contentType", contentType, "size", len(media)) // Set appropriate headers c.Response().Header().Set("Cache-Control", "public, max-age=31536000") // Cache for 1 year c.Response().Header().Set("Accept-Ranges", "bytes") // Enable range requests for video streaming // Check for Range header (needed for video playback) rangeHeader := c.Request().Header.Get("Range") if rangeHeader != "" { contentLength := int64(len(media)) var start, end int64 = 0, contentLength - 1 slog.Debug("Range request received", "messageID", messageID, "range", rangeHeader, "contentType", contentType, "contentLength", contentLength) // Parse range header (e.g., "bytes=0-1023" or "bytes=0-") n, _ := fmt.Sscanf(rangeHeader, "bytes=%d-%d", &start, &end) if n == 1 { // Only start was specified (e.g., "bytes=0-") end = contentLength - 1 } else if n == 0 { // Invalid range, return 416 Range Not Satisfiable slog.Warn("Invalid range header", "range", rangeHeader) c.Response().Header().Set("Content-Range", fmt.Sprintf("bytes */%d", contentLength)) return c.NoContent(http.StatusRequestedRangeNotSatisfiable) } // Ensure valid range if start < 0 || start >= contentLength || end >= contentLength || start > end { slog.Warn("Range out of bounds", "start", start, "end", end, "contentLength", contentLength) c.Response().Header().Set("Content-Range", fmt.Sprintf("bytes */%d", contentLength)) return c.NoContent(http.StatusRequestedRangeNotSatisfiable) } slog.Debug("Serving range", "start", start, "end", end, "size", end-start+1) // Set response headers for partial content c.Response().Header().Set("Content-Type", contentType) c.Response().Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, contentLength)) c.Response().Header().Set("Content-Length", fmt.Sprintf("%d", end-start+1)) c.Response().WriteHeader(http.StatusPartialContent) // Write the requested range _, writeErr := c.Response().Write(media[start : end+1]) return writeErr } // No range request - serve full content c.Response().Header().Set("Content-Length", fmt.Sprintf("%d", len(media))) return c.Blob(http.StatusOK, contentType, media) } func HandleSearch(c echo.Context) error { userDB, err := getUserDB(c) if err != nil { slog.Error("Error getting user database", "error", err) return c.JSON(http.StatusInternalServerError, map[string]string{ "error": "Failed to get user database", }) } // Get search query from query parameter query := c.QueryParam("q") if query == "" { return c.JSON(http.StatusOK, []SearchResult{}) } // Get limit from query parameter, default to 100 limit := 100 if limitStr := c.QueryParam("limit"); limitStr != "" { if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 { limit = parsedLimit } } // Perform search results, err := SearchMessages(userDB, query, limit) if err != nil { slog.Error("Error searching messages", "error", err) return c.JSON(http.StatusInternalServerError, map[string]string{ "error": "Search failed: " + err.Error(), }) } return c.JSON(http.StatusOK, results) } // HandleAnalytics returns analytics data for the Summary tab func HandleAnalytics(c echo.Context) error { userDB, err := getUserDB(c) if err != nil { slog.Error("Error getting user database", "error", err) return c.JSON(http.StatusInternalServerError, map[string]string{ "error": "Failed to get user database", }) } var startDate, endDate *time.Time if startStr := c.QueryParam("start"); startStr != "" { t, err := time.Parse(time.RFC3339, startStr) if err == nil { startDate = &t } } if endStr := c.QueryParam("end"); endStr != "" { t, err := time.Parse(time.RFC3339, endStr) if err == nil { endDate = &t } } // Default to top 10 contacts topN := 10 if topStr := c.QueryParam("top"); topStr != "" { if val, err := strconv.Atoi(topStr); err == nil && val > 0 && val <= 50 { topN = val } } // Timezone offset in minutes from UTC (e.g. -300 for UTC-5, 330 for UTC+5:30) tzOffsetMinutes := 0 if tzStr := c.QueryParam("tz_offset"); tzStr != "" { if val, err := strconv.Atoi(tzStr); err == nil && val >= -840 && val <= 840 { tzOffsetMinutes = val } } analytics, err := GetAnalytics(userDB, startDate, endDate, topN, tzOffsetMinutes) if err != nil { slog.Error("Error getting analytics", "error", err) return c.JSON(http.StatusInternalServerError, map[string]string{ "error": "Failed to get analytics", }) } return c.JSON(http.StatusOK, analytics) } // HandleVersion returns the application version func HandleVersion(c echo.Context) error { // Try to read version from version.json file first (Docker builds) versionFile := "/app/version.json" if data, err := os.ReadFile(versionFile); err == nil { var versionData map[string]string if err := json.Unmarshal(data, &versionData); err == nil { return c.JSON(http.StatusOK, versionData) } } return c.JSON(http.StatusOK, map[string]string{ "version": "dev", }) }