mirror of
https://github.com/warmbly/warmbly.git
synced 2026-08-19 08:01:16 +00:00
400 lines
9.6 KiB
Go
400 lines
9.6 KiB
Go
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"github.com/warmbly/warmbly/internal/api/middleware"
|
|
"github.com/warmbly/warmbly/internal/errx"
|
|
)
|
|
|
|
// GetWarmupAnalytics gets warmup statistics for the user
|
|
// GET /analytics/warmup
|
|
func (h *Handler) GetWarmupAnalytics(c *gin.Context) {
|
|
userIDStr := middleware.GetUserID(c)
|
|
userID, err := uuid.Parse(userIDStr)
|
|
if err != nil {
|
|
errx.Handle(c, errx.ErrAuth)
|
|
return
|
|
}
|
|
|
|
// Parse optional email_id filter
|
|
var emailAccountID *uuid.UUID
|
|
if emailIDStr := c.Query("email_id"); emailIDStr != "" {
|
|
if id, err := uuid.Parse(emailIDStr); err == nil {
|
|
emailAccountID = &id
|
|
}
|
|
}
|
|
|
|
// Parse date range (required)
|
|
fromStr := c.Query("from")
|
|
toStr := c.Query("to")
|
|
|
|
if fromStr == "" || toStr == "" {
|
|
errx.Handle(c, errx.New(errx.BadRequest, "from and to date parameters are required"))
|
|
return
|
|
}
|
|
|
|
from, err := time.Parse("2006-01-02", fromStr)
|
|
if err != nil {
|
|
errx.Handle(c, errx.New(errx.BadRequest, "Invalid from date format (expected YYYY-MM-DD)"))
|
|
return
|
|
}
|
|
|
|
to, err := time.Parse("2006-01-02", toStr)
|
|
if err != nil {
|
|
errx.Handle(c, errx.New(errx.BadRequest, "Invalid to date format (expected YYYY-MM-DD)"))
|
|
return
|
|
}
|
|
|
|
analytics, xerr := h.AnalyticsService.GetWarmupAnalytics(c.Request.Context(), userID, emailAccountID, from, to)
|
|
if xerr != nil {
|
|
errx.Handle(c, xerr)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, analytics)
|
|
}
|
|
|
|
// GetCampaignAnalytics gets analytics for a specific campaign
|
|
// GET /analytics/campaigns/:id
|
|
func (h *Handler) GetCampaignAnalytics(c *gin.Context) {
|
|
userIDStr := middleware.GetUserID(c)
|
|
userID, err := uuid.Parse(userIDStr)
|
|
if err != nil {
|
|
errx.Handle(c, errx.ErrAuth)
|
|
return
|
|
}
|
|
|
|
campaignIDStr := c.Param("id")
|
|
campaignID, err := uuid.Parse(campaignIDStr)
|
|
if err != nil {
|
|
errx.Handle(c, errx.ErrNotFound)
|
|
return
|
|
}
|
|
|
|
analytics, xerr := h.AnalyticsService.GetCampaignAnalytics(c.Request.Context(), userID, campaignID)
|
|
if xerr != nil {
|
|
errx.Handle(c, xerr)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, analytics)
|
|
}
|
|
|
|
// GetCampaignDailyStats gets daily statistics for a campaign
|
|
// GET /analytics/campaigns/:id/daily
|
|
func (h *Handler) GetCampaignDailyStats(c *gin.Context) {
|
|
userIDStr := middleware.GetUserID(c)
|
|
userID, err := uuid.Parse(userIDStr)
|
|
if err != nil {
|
|
errx.Handle(c, errx.ErrAuth)
|
|
return
|
|
}
|
|
|
|
campaignIDStr := c.Param("id")
|
|
campaignID, err := uuid.Parse(campaignIDStr)
|
|
if err != nil {
|
|
errx.Handle(c, errx.ErrNotFound)
|
|
return
|
|
}
|
|
|
|
// Parse date range
|
|
fromStr := c.Query("from")
|
|
toStr := c.Query("to")
|
|
|
|
if fromStr == "" || toStr == "" {
|
|
errx.Handle(c, errx.New(errx.BadRequest, "from and to date parameters are required"))
|
|
return
|
|
}
|
|
|
|
from, err := time.Parse("2006-01-02", fromStr)
|
|
if err != nil {
|
|
errx.Handle(c, errx.New(errx.BadRequest, "Invalid from date format (expected YYYY-MM-DD)"))
|
|
return
|
|
}
|
|
|
|
to, err := time.Parse("2006-01-02", toStr)
|
|
if err != nil {
|
|
errx.Handle(c, errx.New(errx.BadRequest, "Invalid to date format (expected YYYY-MM-DD)"))
|
|
return
|
|
}
|
|
|
|
stats, xerr := h.AnalyticsService.GetCampaignDailyStats(c.Request.Context(), userID, campaignID, from, to)
|
|
if xerr != nil {
|
|
errx.Handle(c, xerr)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"data": stats})
|
|
}
|
|
|
|
// GetAllAccountStatuses gets status of all email accounts
|
|
// GET /analytics/accounts
|
|
func (h *Handler) GetAllAccountStatuses(c *gin.Context) {
|
|
// Account lookups are org-scoped (emailRepo.Search filters on
|
|
// organization_id), so pass the selected org, not the user id.
|
|
orgID := middleware.GetOrganizationID(c)
|
|
if orgID == nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "no organization selected"))
|
|
return
|
|
}
|
|
|
|
statuses, xerr := h.AnalyticsService.GetAllAccountStatuses(c.Request.Context(), *orgID)
|
|
if xerr != nil {
|
|
errx.Handle(c, xerr)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"data": statuses})
|
|
}
|
|
|
|
// GetAccountStatus gets status of a specific email account
|
|
// GET /analytics/accounts/:id
|
|
func (h *Handler) GetAccountStatus(c *gin.Context) {
|
|
// Org-scoped like the list endpoint above.
|
|
orgID := middleware.GetOrganizationID(c)
|
|
if orgID == nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "no organization selected"))
|
|
return
|
|
}
|
|
|
|
accountIDStr := c.Param("id")
|
|
accountID, err := uuid.Parse(accountIDStr)
|
|
if err != nil {
|
|
errx.Handle(c, errx.ErrNotFound)
|
|
return
|
|
}
|
|
|
|
status, xerr := h.AnalyticsService.GetAccountStatus(c.Request.Context(), *orgID, accountID)
|
|
if xerr != nil {
|
|
errx.Handle(c, xerr)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, status)
|
|
}
|
|
|
|
// GetUsageOverview gets usage overview for the user
|
|
// GET /analytics/usage
|
|
func (h *Handler) GetUsageOverview(c *gin.Context) {
|
|
userIDStr := middleware.GetUserID(c)
|
|
userID, err := uuid.Parse(userIDStr)
|
|
if err != nil {
|
|
errx.Handle(c, errx.ErrAuth)
|
|
return
|
|
}
|
|
|
|
period := c.DefaultQuery("period", "day")
|
|
if period != "day" && period != "week" && period != "month" {
|
|
period = "day"
|
|
}
|
|
|
|
overview, xerr := h.AnalyticsService.GetUsageOverview(c.Request.Context(), userID, period)
|
|
if xerr != nil {
|
|
errx.Handle(c, xerr)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, overview)
|
|
}
|
|
|
|
// GetRealtimeInfo returns WebSocket connection info
|
|
// GET /realtime/info
|
|
func (h *Handler) GetRealtimeInfo(c *gin.Context) {
|
|
userIDStr := middleware.GetUserID(c)
|
|
userID, err := uuid.Parse(userIDStr)
|
|
if err != nil {
|
|
errx.Handle(c, errx.ErrAuth)
|
|
return
|
|
}
|
|
|
|
// Get WebSocket host from request or config
|
|
wsHost := c.Request.Header.Get("X-Forwarded-Host")
|
|
if wsHost == "" {
|
|
wsHost = c.Request.Host
|
|
}
|
|
wsHost = "wss://" + wsHost
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"websocket_url": wsHost + "/socket",
|
|
"topics": []string{
|
|
"user:" + userID.String(),
|
|
"campaign:*",
|
|
"account:*",
|
|
},
|
|
})
|
|
}
|
|
|
|
// GetDashboardAnalytics returns main dashboard analytics overview
|
|
// GET /analytics/dashboard?period=7d
|
|
func (h *Handler) GetDashboardAnalytics(c *gin.Context) {
|
|
orgID := middleware.GetOrganizationID(c)
|
|
if orgID == nil {
|
|
errx.Handle(c, errx.New(errx.BadRequest, "no organization selected"))
|
|
return
|
|
}
|
|
|
|
// Parse period (7d, 30d, 90d)
|
|
period := c.DefaultQuery("period", "7d")
|
|
if period != "7d" && period != "30d" && period != "90d" {
|
|
period = "7d"
|
|
}
|
|
|
|
analytics, xerr := h.AnalyticsService.GetDashboardAnalytics(c.Request.Context(), *orgID, period)
|
|
if xerr != nil {
|
|
errx.Handle(c, xerr)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, analytics)
|
|
}
|
|
|
|
// GetCampaignHourlyStats returns hourly statistics for a campaign on a specific date
|
|
// GET /analytics/campaigns/:id/hourly?date=2024-01-15
|
|
func (h *Handler) GetCampaignHourlyStats(c *gin.Context) {
|
|
userIDStr := middleware.GetUserID(c)
|
|
userID, err := uuid.Parse(userIDStr)
|
|
if err != nil {
|
|
errx.Handle(c, errx.ErrAuth)
|
|
return
|
|
}
|
|
|
|
campaignIDStr := c.Param("id")
|
|
campaignID, err := uuid.Parse(campaignIDStr)
|
|
if err != nil {
|
|
errx.Handle(c, errx.ErrNotFound)
|
|
return
|
|
}
|
|
|
|
// Parse date
|
|
dateStr := c.Query("date")
|
|
if dateStr == "" {
|
|
dateStr = time.Now().Format("2006-01-02")
|
|
}
|
|
|
|
date, err := time.Parse("2006-01-02", dateStr)
|
|
if err != nil {
|
|
errx.Handle(c, errx.New(errx.BadRequest, "Invalid date format (expected YYYY-MM-DD)"))
|
|
return
|
|
}
|
|
|
|
stats, xerr := h.AnalyticsService.GetCampaignHourlyStats(c.Request.Context(), userID, campaignID, date)
|
|
if xerr != nil {
|
|
errx.Handle(c, xerr)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"data": stats, "date": dateStr})
|
|
}
|
|
|
|
// CompareCampaigns returns comparison statistics for multiple campaigns
|
|
// GET /analytics/campaigns/compare?ids=uuid1,uuid2&from=2024-01-01&to=2024-01-31
|
|
func (h *Handler) CompareCampaigns(c *gin.Context) {
|
|
userIDStr := middleware.GetUserID(c)
|
|
userID, err := uuid.Parse(userIDStr)
|
|
if err != nil {
|
|
errx.Handle(c, errx.ErrAuth)
|
|
return
|
|
}
|
|
|
|
// Parse campaign IDs
|
|
idsStr := c.Query("ids")
|
|
if idsStr == "" {
|
|
errx.Handle(c, errx.New(errx.BadRequest, "ids parameter is required"))
|
|
return
|
|
}
|
|
|
|
var campaignIDs []uuid.UUID
|
|
for _, idStr := range splitAndTrim(idsStr) {
|
|
id, err := uuid.Parse(idStr)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
campaignIDs = append(campaignIDs, id)
|
|
}
|
|
|
|
if len(campaignIDs) == 0 {
|
|
errx.Handle(c, errx.New(errx.BadRequest, "at least one valid campaign ID is required"))
|
|
return
|
|
}
|
|
|
|
// Limit to 10 campaigns
|
|
if len(campaignIDs) > 10 {
|
|
campaignIDs = campaignIDs[:10]
|
|
}
|
|
|
|
// Parse date range
|
|
fromStr := c.Query("from")
|
|
toStr := c.Query("to")
|
|
|
|
if fromStr == "" || toStr == "" {
|
|
errx.Handle(c, errx.New(errx.BadRequest, "from and to date parameters are required"))
|
|
return
|
|
}
|
|
|
|
from, err := time.Parse("2006-01-02", fromStr)
|
|
if err != nil {
|
|
errx.Handle(c, errx.New(errx.BadRequest, "Invalid from date format (expected YYYY-MM-DD)"))
|
|
return
|
|
}
|
|
|
|
to, err := time.Parse("2006-01-02", toStr)
|
|
if err != nil {
|
|
errx.Handle(c, errx.New(errx.BadRequest, "Invalid to date format (expected YYYY-MM-DD)"))
|
|
return
|
|
}
|
|
|
|
comparison, xerr := h.AnalyticsService.CompareCampaigns(c.Request.Context(), userID, campaignIDs, from, to)
|
|
if xerr != nil {
|
|
errx.Handle(c, xerr)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, comparison)
|
|
}
|
|
|
|
// splitAndTrim splits a comma-separated string and trims whitespace
|
|
func splitAndTrim(s string) []string {
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
parts := make([]string, 0)
|
|
for _, part := range split(s, ',') {
|
|
trimmed := trim(part)
|
|
if trimmed != "" {
|
|
parts = append(parts, trimmed)
|
|
}
|
|
}
|
|
return parts
|
|
}
|
|
|
|
func split(s string, sep rune) []string {
|
|
var result []string
|
|
current := ""
|
|
for _, c := range s {
|
|
if c == sep {
|
|
result = append(result, current)
|
|
current = ""
|
|
} else {
|
|
current += string(c)
|
|
}
|
|
}
|
|
result = append(result, current)
|
|
return result
|
|
}
|
|
|
|
func trim(s string) string {
|
|
start := 0
|
|
end := len(s)
|
|
for start < end && (s[start] == ' ' || s[start] == '\t') {
|
|
start++
|
|
}
|
|
for end > start && (s[end-1] == ' ' || s[end-1] == '\t') {
|
|
end--
|
|
}
|
|
return s[start:end]
|
|
}
|