package widget import ( "net/http" "strings" "github.com/gin-gonic/gin" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/service" applogger "github.com/gochat/gochat/pkg/logger" ) // --- Widget Theme Handlers (Public-facing, website_token path param) --- // M11: Extended theme configuration beyond widget_color // GetThemeConfig returns the custom theme configuration for a widget. // GET /widget/:website_token/theme_config // No widget_token required — theme is public config for the widget SDK to render. func (h *WidgetHandler) GetThemeConfig(c *gin.Context) { websiteToken := c.Param("website_token") if websiteToken == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "website_token is required"}) return } themeConfig, err := h.widgetService.GetThemeConfig(c.Request.Context(), websiteToken) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if themeConfig == nil { // No custom theme — return empty so SDK falls back to defaults c.JSON(http.StatusOK, gin.H{"theme": nil}) return } c.JSON(http.StatusOK, gin.H{"theme": themeConfig}) } // --- Widget Pre-Chat Form Handlers (Public-facing, website_token path param) --- // M11: Structured pre-chat form before starting conversation // GetPreChatForm returns the pre-chat form definition for a widget. // GET /widget/:website_token/pre_chat_form // No widget_token required — form definition is public config before visitor authenticates. func (h *WidgetHandler) GetPreChatForm(c *gin.Context) { websiteToken := c.Param("website_token") if websiteToken == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "website_token is required"}) return } form, err := h.widgetService.GetPreChatForm(c.Request.Context(), websiteToken) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if form == nil { // No pre-chat form — return empty so SDK skips form step c.JSON(http.StatusOK, gin.H{"pre_chat_form": nil}) return } c.JSON(http.StatusOK, gin.H{"pre_chat_form": form}) } // SubmitPreChatForm processes a visitor's pre-chat form submission. // POST /widget/:website_token/pre_chat_form // This creates/identifies the contact and returns a widget_token for subsequent requests. // Reference: Chatwoot widget SDK — pre-chat form submission flow func (h *WidgetHandler) SubmitPreChatForm(c *gin.Context) { websiteToken := c.Param("website_token") if websiteToken == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "website_token is required"}) return } var submission model.PreChatFormSubmission if err := c.ShouldBindJSON(&submission); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body", "details": err.Error()}) return } resp, err := h.widgetService.SubmitPreChatForm(c.Request.Context(), websiteToken, submission) if err != nil { status := http.StatusBadRequest if err.Error() == "pre-chat form is not enabled for this inbox" { status = http.StatusForbidden } c.JSON(status, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, resp) } // --- Widget File Upload Handlers (website_token path param) --- // M11: Staged file upload with attachment processing // StageFileUpload handles a file upload from the widget, staging it for later attachment. // POST /widget/:website_token/uploads // Accepts multipart/form-data with a "file" field. // Returns upload_uuid that can be referenced when sending a message with attachment. // Reference: Chatwoot widget SDK — file upload before sending message func (h *WidgetHandler) StageFileUpload(c *gin.Context) { c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, (16<<20)+(1<<20)) websiteToken := c.Param("website_token") if websiteToken == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "website_token is required"}) return } file, err := c.FormFile("file") if err != nil { status := http.StatusBadRequest if strings.Contains(strings.ToLower(err.Error()), "request body too large") { status = http.StatusRequestEntityTooLarge } c.JSON(status, gin.H{"error": "file field is required", "details": err.Error()}) return } fileReader, err := file.Open() if err != nil { c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to open uploaded file", "details": err.Error()}) return } defer fileReader.Close() req := service.WidgetUploadRequest{ WidgetToken: widgetTokenFromRequest(c), WebsiteToken: websiteToken, FileName: file.Filename, FileSize: file.Size, FileHeader: file, } resp, err := h.widgetService.StageFileUpload(c.Request.Context(), req, fileReader) if err != nil { applogger.L().Errorf("Failed to stage widget file upload: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, resp) } // GetFileUploadStatus checks the status of a staged file upload by UUID. // GET /widget/:website_token/uploads/:upload_uuid func (h *WidgetHandler) GetFileUploadStatus(c *gin.Context) { websiteToken := c.Param("website_token") if websiteToken == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "website_token is required"}) return } uploadUUID := c.Param("upload_uuid") if uploadUUID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "upload_uuid is required"}) return } resp, err := h.widgetService.GetFileUploadStatus(c.Request.Context(), websiteToken, uploadUUID, widgetTokenFromRequest(c)) if err != nil { applogger.L().Errorf("Failed to get upload status for uuid=%s: %v", uploadUUID, err) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if resp == nil { c.JSON(http.StatusNotFound, gin.H{"error": "upload not found"}) return } c.JSON(http.StatusOK, resp) }