Restructure the monorepo into clear top-level directories: - backend/: Go module root (cmd, internal, pkg, configs, migrations, docs/swagger, scripts, tests, go.mod, Makefile, .air.toml) - deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd - docs/: project documentation + reports/ (moved from repo root) - AGENTS.md: new AI coding-agent guide at repo root Update all references to the new layout: - Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root) - docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile, env_file ../../.env, volume mounts ../../backend:/app - deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile - CI: working-directory: backend for go commands, file deploy/docker/Dockerfile, coverage path backend/coverage.out, health_check backend/scripts/ - backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../ - README: architecture tree, quickstart, config paths updated Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh, gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to preserve history. Build, vet, SQLite tests, and docker compose config verified.
172 lines
5.4 KiB
Go
172 lines
5.4 KiB
Go
package widget
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"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) {
|
|
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 {
|
|
c.JSON(http.StatusBadRequest, 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{
|
|
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 file upload for website_token=%s: %v", websiteToken, 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)
|
|
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)
|
|
} |