Files
gochat/internal/handler/api/v1/message_handler.go
T

307 lines
10 KiB
Go

package v1
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/gochat/gochat/internal/search"
"github.com/gochat/gochat/internal/service"
"github.com/gochat/gochat/pkg/pagination"
"github.com/gochat/gochat/pkg/response"
)
// MessageHandler handles message-related API endpoints.
// Reference: Chatwoot app/controllers/api/v1/messages_controller.rb
type MessageHandler struct {
svc *service.MessageService
}
// NewMessageHandler creates a new MessageHandler.
func NewMessageHandler(svc *service.MessageService) *MessageHandler {
return &MessageHandler{svc: svc}
}
// @Summary List messages in a conversation
// @Description Retrieves all messages for a conversation with pagination
// @Tags Messages
// @Accept json
// @Produce json
// @Param account_id path uint true "Account ID"
// @Param conversation_id path uint true "Conversation ID"
// @Param page query int false "Page number" default(1)
// @Param page_size query int false "Items per page" default(25)
// @Success 200 {object} []model.Message
// @Failure 400 {object} model.ErrorResponse
// @Failure 401 {object} model.ErrorResponse
// @Failure 404 {object} model.ErrorResponse
// @Failure 500 {object} model.ErrorResponse
// @Security ApiKeyAuth
// @Router /api/v1/accounts/{account_id}/conversations/{conversation_id}/messages [get]
// List retrieves all messages for a conversation with pagination.
// GET /api/v1/accounts/:account_id/conversations/:conversation_id/messages
// Reference: Chatwoot conversations#messages (index)
func (h *MessageHandler) List(c *gin.Context) {
conversationID, err := parseUintParam(c, "conversation_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid conversation_id")
return
}
p := pagination.Parse(c)
messages, total, svcErr := h.svc.ListByConversation(c.Request.Context(), conversationID, p.Offset, p.PerPage)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
response.OKWithMeta(c, toInterfaceSlice(messages), p.Page, p.PerPage, total)
}
// @Summary Create a message in a conversation
// @Description Creates a new message in an existing conversation
// @Tags Messages
// @Accept json
// @Produce json
// @Param account_id path uint true "Account ID"
// @Param conversation_id path uint true "Conversation ID"
// @Param body body service.CreateMessageRequest true "Message creation payload"
// @Success 201 {object} model.Message
// @Failure 400 {object} model.ErrorResponse
// @Failure 401 {object} model.ErrorResponse
// @Failure 404 {object} model.ErrorResponse
// @Failure 500 {object} model.ErrorResponse
// @Security ApiKeyAuth
// @Router /api/v1/accounts/{account_id}/conversations/{conversation_id}/messages [post]
// Create creates a new message in a conversation.
// POST /api/v1/accounts/:account_id/conversations/:conversation_id/messages
// Reference: Chatwoot conversations#messages (create)
func (h *MessageHandler) Create(c *gin.Context) {
accountID, err := parseUintParam(c, "account_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
userID := getUserID(c)
if userID == 0 {
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "user not authenticated")
return
}
var req service.CreateMessageRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
return
}
// If conversation_id is not in the request body, use the URL param
if req.ConversationID == 0 {
conversationID, urlErr := parseUintParam(c, "conversation_id")
if urlErr != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "conversation_id is required")
return
}
req.ConversationID = conversationID
}
message, svcErr := h.svc.Create(c.Request.Context(), accountID, userID, req)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
response.Created(c, message)
}
// @Summary Get a single message
// @Description Retrieves detailed information about a specific message
// @Tags Messages
// @Accept json
// @Produce json
// @Param account_id path uint true "Account ID"
// @Param conversation_id path uint true "Conversation ID"
// @Param id path uint true "Message ID"
// @Success 200 {object} model.Message
// @Failure 400 {object} model.ErrorResponse
// @Failure 401 {object} model.ErrorResponse
// @Failure 404 {object} model.ErrorResponse
// @Failure 500 {object} model.ErrorResponse
// @Security ApiKeyAuth
// @Router /api/v1/accounts/{account_id}/conversations/{conversation_id}/messages/{id} [get]
// Get retrieves a single message.
// GET /api/v1/accounts/:account_id/conversations/:conversation_id/messages/:id
// Reference: Chatwoot messages#show
func (h *MessageHandler) Get(c *gin.Context) {
accountID, err := parseUintParam(c, "account_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
messageID, err := parseUintParam(c, "message_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid message id")
return
}
message, svcErr := h.svc.GetByAccountAndID(c.Request.Context(), accountID, messageID)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
response.OK(c, message)
}
// Update updates a message's content.
// PATCH /api/v1/accounts/:account_id/conversations/:conversation_id/messages/:id
func (h *MessageHandler) Update(c *gin.Context) {
accountID, err := parseUintParam(c, "account_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
messageID, err := parseUintParam(c, "message_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid message id")
return
}
var req service.UpdateMessageRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
return
}
message, svcErr := h.svc.Update(c.Request.Context(), accountID, messageID, req)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
response.OK(c, message)
}
// @Summary Delete a message
// @Description Soft-deletes a message from a conversation
// @Tags Messages
// @Accept json
// @Produce json
// @Param account_id path uint true "Account ID"
// @Param conversation_id path uint true "Conversation ID"
// @Param id path uint true "Message ID"
// @Success 204 {object} object
// @Failure 400 {object} model.ErrorResponse
// @Failure 401 {object} model.ErrorResponse
// @Failure 404 {object} model.ErrorResponse
// @Failure 500 {object} model.ErrorResponse
// @Security ApiKeyAuth
// @Router /api/v1/accounts/{account_id}/conversations/{conversation_id}/messages/{id} [delete]
// Delete soft-deletes a message.
// DELETE /api/v1/accounts/:account_id/conversations/:conversation_id/messages/:id
func (h *MessageHandler) Delete(c *gin.Context) {
accountID, err := parseUintParam(c, "account_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
messageID, err := parseUintParam(c, "message_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid message id")
return
}
if svcErr := h.svc.Delete(c.Request.Context(), accountID, messageID); svcErr != nil {
handleServiceError(c, svcErr)
return
}
response.NoContent(c)
}
// Search searches messages by content within an account.
// GET /api/v1/accounts/:account_id/messages/search?q=...
// Reference: Chatwoot messages#search
func (h *MessageHandler) Search(c *gin.Context) {
accountID, err := parseUintParam(c, "account_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
q := c.Query("q")
if q == "" {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "search query 'q' is required")
return
}
p := pagination.Parse(c)
searchMode := search.ParseSearchMode(c.DefaultQuery("search_mode", ""))
messages, total, svcErr := h.svc.Search(c.Request.Context(), accountID, q, p.Offset, p.PerPage, searchMode)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
response.OKWithMeta(c, toInterfaceSlice(messages), p.Page, p.PerPage, total)
}
// Retry retries a failed message.
// POST /api/v1/accounts/:account_id/conversations/:conversation_id/messages/:id/retry
func (h *MessageHandler) Retry(c *gin.Context) {
accountID, err := parseUintParam(c, "account_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
messageID, err := parseUintParam(c, "message_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid message id")
return
}
message, svcErr := h.svc.Retry(c.Request.Context(), accountID, messageID)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
response.OK(c, message)
}
// Translate translates a message's content to a target language using LLM.
// POST /api/v1/accounts/:account_id/conversations/:conversation_id/messages/:id/translate
// Reference: Chatwoot messages#translate
func (h *MessageHandler) Translate(c *gin.Context) {
accountID, err := parseUintParam(c, "account_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
messageID, err := parseUintParam(c, "message_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid message id")
return
}
var req service.TranslateMessageRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
return
}
result, svcErr := h.svc.Translate(c.Request.Context(), accountID, messageID, req)
if svcErr != nil {
lower := strings.ToLower(svcErr.Error())
if strings.Contains(lower, "not found") {
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, svcErr.Error())
return
}
if strings.Contains(lower, "required") || strings.Contains(lower, "invalid") {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, svcErr.Error())
return
}
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, svcErr.Error())
return
}
response.OK(c, result)
}