Files
gochat/backend/internal/handler/api/v1/webwidget_offline_handler.go
T
rogee aeddedf2a3 Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
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.
2026-07-07 14:44:12 +08:00

141 lines
4.8 KiB
Go

package v1
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/gochat/gochat/internal/service"
applogger "github.com/gochat/gochat/pkg/logger"
)
// WebWidgetOfflineHandler handles admin CRUD for widget offline messages.
// These endpoints require agent JWT authentication and account scope.
// Reference: Chatwoot admin view of pending offline messages from web widget visitors.
type WebWidgetOfflineHandler struct {
widgetSvc *service.WidgetService
inboxSvc *service.InboxService
}
// NewWebWidgetOfflineHandler creates a new admin widget offline message handler.
func NewWebWidgetOfflineHandler(widgetSvc *service.WidgetService, inboxSvc *service.InboxService) *WebWidgetOfflineHandler {
return &WebWidgetOfflineHandler{widgetSvc: widgetSvc, inboxSvc: inboxSvc}
}
// ListOfflineMessages retrieves paginated offline messages for an account.
// GET /api/v1/accounts/:account_id/web_widgets/offline_messages
func (h *WebWidgetOfflineHandler) ListOfflineMessages(c *gin.Context) {
accountIDStr := c.Param("id")
accountID, err := strconv.ParseUint(accountIDStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "25"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 25
}
msgs, total, err := h.widgetSvc.ListOfflineMessagesByAccount(c.Request.Context(), uint(accountID), page, pageSize)
if err != nil {
applogger.L().Errorf("Failed to list offline messages for account=%d: %v", accountID, err)
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"offline_messages": msgs,
"meta": gin.H{
"page": page,
"page_size": pageSize,
"total_count": total,
},
})
}
// ListOfflineMessagesByInbox retrieves offline messages for a specific inbox.
// GET /api/v1/accounts/:account_id/inboxes/:inbox_id/web_widget/offline_messages
func (h *WebWidgetOfflineHandler) ListOfflineMessagesByInbox(c *gin.Context) {
inboxIDStr := c.Param("inbox_id")
inboxID, err := strconv.ParseUint(inboxIDStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid inbox_id"})
return
}
accountIDStr := c.Param("id")
accountID, err := strconv.ParseUint(accountIDStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
return
}
// Verify inbox belongs to account
inbox, err := h.inboxSvc.GetByAccountAndID(c.Request.Context(), uint(accountID), uint(inboxID))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "inbox not found"})
return
}
if inbox.ChannelType != "web_widget" {
c.JSON(http.StatusBadRequest, gin.H{"error": "offline messages only supported for web_widget channel"})
return
}
msgs, err := h.widgetSvc.GetOfflineMessages(c.Request.Context(), uint(inboxID))
if err != nil {
applogger.L().Errorf("Failed to get offline messages for inbox=%d: %v", inboxID, err)
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"offline_messages": msgs})
}
// DismissOfflineMessage marks an offline message as dismissed by an agent.
// PUT /api/v1/accounts/:account_id/web_widgets/offline_messages/:offline_message_id/dismiss
func (h *WebWidgetOfflineHandler) DismissOfflineMessage(c *gin.Context) {
offlineMsgIDStr := c.Param("offline_message_id")
offlineMsgID, err := strconv.ParseUint(offlineMsgIDStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid offline_message_id"})
return
}
err = h.widgetSvc.DismissOfflineMessage(c.Request.Context(), uint(offlineMsgID))
if err != nil {
applogger.L().Errorf("Failed to dismiss offline message=%d: %v", offlineMsgID, err)
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "offline message dismissed"})
}
// ConvertOfflineMessage converts an offline message into a conversation.
// PUT /api/v1/accounts/:account_id/web_widgets/offline_messages/:offline_message_id/convert
func (h *WebWidgetOfflineHandler) ConvertOfflineMessage(c *gin.Context) {
offlineMsgIDStr := c.Param("offline_message_id")
offlineMsgID, err := strconv.ParseUint(offlineMsgIDStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid offline_message_id"})
return
}
conversation, message, err := h.widgetSvc.ConvertOfflineMessageToConversation(c.Request.Context(), uint(offlineMsgID))
if err != nil {
applogger.L().Errorf("Failed to convert offline message=%d: %v", offlineMsgID, err)
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"conversation": conversation,
"message": message,
})
}