276 lines
9.4 KiB
Go
276 lines
9.4 KiB
Go
package v1
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
"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"
|
|
"github.com/gochat/gochat/pkg/response"
|
|
)
|
|
|
|
// CaptainDocumentHandler handles CaptainDocument REST API endpoints.
|
|
// Reference: Chatwoot enterprise/app/controllers/api/v1/captain/documents_controller.rb
|
|
type CaptainDocumentHandler struct {
|
|
svc *service.CaptainDocumentService
|
|
}
|
|
|
|
// NewCaptainDocumentHandler creates a new CaptainDocumentHandler.
|
|
func NewCaptainDocumentHandler(svc *service.CaptainDocumentService) *CaptainDocumentHandler {
|
|
return &CaptainDocumentHandler{svc: svc}
|
|
}
|
|
|
|
// Create creates a new captain document.
|
|
// POST /api/v1/accounts/:account_id/captain_assistants/:assistant_id/documents
|
|
// Supports both JSON (for URL/content input) and multipart/form-data (for PDF upload).
|
|
func (h *CaptainDocumentHandler) Create(c *gin.Context) {
|
|
accountID := parseAccountIDParam(c)
|
|
if accountID == 0 {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
|
|
var req service.CreateDocumentRequest
|
|
|
|
contentType := c.GetHeader("Content-Type")
|
|
if strings.Contains(contentType, "multipart/form-data") {
|
|
// Multipart form-data: PDF file upload or form fields
|
|
req.Name = c.PostForm("document[name]")
|
|
req.ExternalLink = c.PostForm("document[external_link]")
|
|
req.Content = c.PostForm("document[content]")
|
|
if assistantIDStr := c.PostForm("document[assistant_id]"); assistantIDStr != "" {
|
|
if id, err := strconv.ParseUint(assistantIDStr, 10, 64); err == nil {
|
|
req.AssistantID = uint(id)
|
|
}
|
|
}
|
|
|
|
// Check for PDF file
|
|
if fileHeader, err := c.FormFile("document[pdf_file]"); err == nil {
|
|
req.PdfFile = fileHeader
|
|
}
|
|
} else {
|
|
// JSON request
|
|
if err := bindNestedJSONPayload(c, "document", &req); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
}
|
|
|
|
assistantID := req.AssistantID
|
|
if assistantID == 0 {
|
|
assistantID, _ = parseUintParam(c, "assistant_id")
|
|
}
|
|
if assistantID == 0 {
|
|
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrValidation, "Missing Assistant")
|
|
return
|
|
}
|
|
|
|
doc, err := h.svc.Create(c.Request.Context(), assistantID, accountID, &req)
|
|
if err != nil {
|
|
applogger.L().Errorf("Create captain document: %v", err)
|
|
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, captainDocumentPayload(doc))
|
|
}
|
|
|
|
// Get retrieves a captain document by ID.
|
|
// GET /api/v1/accounts/:account_id/captain_documents/:id
|
|
func (h *CaptainDocumentHandler) Get(c *gin.Context) {
|
|
accountID := parseAccountIDParam(c)
|
|
if accountID == 0 {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
id, err := parseUintAnyParam(c, "document_id", "id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
|
|
return
|
|
}
|
|
|
|
doc, err := h.svc.GetByAccount(c.Request.Context(), accountID, id)
|
|
if err != nil {
|
|
applogger.L().Errorf("Get captain document: %v", err)
|
|
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, "document not found")
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, captainDocumentPayload(doc))
|
|
}
|
|
|
|
// Update updates an existing captain document.
|
|
// PUT /api/v1/accounts/:account_id/captain_documents/:id
|
|
func (h *CaptainDocumentHandler) Update(c *gin.Context) {
|
|
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
|
|
return
|
|
}
|
|
|
|
var req service.UpdateDocumentRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
|
|
doc, err := h.svc.Update(c.Request.Context(), uint(id), &req)
|
|
if err != nil {
|
|
applogger.L().Errorf("Update captain document: %v", err)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to update document")
|
|
return
|
|
}
|
|
|
|
response.OK(c, doc)
|
|
}
|
|
|
|
// Delete deletes a captain document.
|
|
// DELETE /api/v1/accounts/:account_id/captain_documents/:id
|
|
func (h *CaptainDocumentHandler) Delete(c *gin.Context) {
|
|
accountID := parseAccountIDParam(c)
|
|
if accountID == 0 {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
id, err := parseUintAnyParam(c, "document_id", "id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
|
|
return
|
|
}
|
|
|
|
if err := h.svc.DeleteByAccount(c.Request.Context(), accountID, id); err != nil {
|
|
applogger.L().Errorf("Delete captain document: %v", err)
|
|
response.AbortWithStatusError(c, captainAssistantErrorStatus(err), response.ErrInternal, "failed to delete document")
|
|
return
|
|
}
|
|
|
|
response.NoContent(c)
|
|
}
|
|
|
|
// List retrieves documents for an assistant.
|
|
// GET /api/v1/accounts/:account_id/captain_assistants/:assistant_id/documents
|
|
func (h *CaptainDocumentHandler) List(c *gin.Context) {
|
|
accountID := parseAccountIDParam(c)
|
|
if accountID == 0 {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
assistantID, _ := parseOptionalUintQueryParam(c, "assistant_id")
|
|
if assistantID == 0 {
|
|
assistantID, _ = parseUintParam(c, "assistant_id")
|
|
}
|
|
docs, count, currentPage, err := h.svc.ListByAccount(c.Request.Context(), accountID, service.ListDocumentsRequest{
|
|
AssistantID: assistantID,
|
|
Page: page,
|
|
PerPage: 25,
|
|
Filter: c.Query("filter"),
|
|
Source: c.Query("source"),
|
|
Sort: c.Query("sort"),
|
|
SearchKey: c.Query("search_key"),
|
|
})
|
|
if err != nil {
|
|
applogger.L().Errorf("List captain documents: %v", err)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to list documents")
|
|
return
|
|
}
|
|
|
|
payload := make([]gin.H, 0, len(docs))
|
|
for i := range docs {
|
|
payload = append(payload, captainDocumentPayload(&docs[i]))
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"payload": payload, "meta": gin.H{"total_count": count, "page": currentPage}})
|
|
}
|
|
|
|
// ProcessDocument triggers document content extraction and embedding generation.
|
|
// POST /api/v1/accounts/:account_id/captain_documents/:id/process
|
|
func (h *CaptainDocumentHandler) ProcessDocument(c *gin.Context) {
|
|
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
|
|
return
|
|
}
|
|
|
|
if err := h.svc.ProcessDocument(c.Request.Context(), uint(id)); err != nil {
|
|
applogger.L().Errorf("ProcessDocument: %v", err)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to process document")
|
|
return
|
|
}
|
|
|
|
response.OK(c, gin.H{"processed": true})
|
|
}
|
|
|
|
// SyncDocument triggers re-fetching content from the external URL.
|
|
// POST /api/v1/accounts/:account_id/captain_documents/:id/sync
|
|
func (h *CaptainDocumentHandler) SyncDocument(c *gin.Context) {
|
|
accountID := parseAccountIDParam(c)
|
|
if accountID == 0 {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
id, err := parseUintAnyParam(c, "document_id", "id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
|
|
return
|
|
}
|
|
|
|
if _, err := h.svc.RequestSyncDocumentByAccount(c.Request.Context(), accountID, id); err != nil {
|
|
applogger.L().Errorf("SyncDocument: %v", err)
|
|
response.AbortWithStatusError(c, captainAssistantErrorStatus(err), response.ErrInternal, "failed to sync document")
|
|
return
|
|
}
|
|
|
|
c.Status(http.StatusAccepted)
|
|
}
|
|
|
|
func captainDocumentPayload(doc *model.CaptainDocument) gin.H {
|
|
status := doc.Status
|
|
if status == "" {
|
|
status = model.DocumentStatusPending
|
|
}
|
|
syncStatus := doc.SyncStatus
|
|
if syncStatus == model.DocumentSyncStatusPending {
|
|
syncStatus = "syncing"
|
|
}
|
|
pdfDocument := doc.ContentType == "application/pdf" || doc.FileURL != ""
|
|
payload := gin.H{
|
|
"account_id": doc.AccountID,
|
|
"assistant": captainDocumentAssistantPayload(doc),
|
|
"content": doc.Content,
|
|
"content_type": doc.ContentType,
|
|
"created_at": doc.CreatedAt.Unix(),
|
|
"external_link": doc.ExternalLink,
|
|
"display_url": doc.ExternalLink,
|
|
"file_size": doc.FileSize,
|
|
"file_url": doc.FileURL,
|
|
"pdf_document": pdfDocument,
|
|
"id": doc.ID,
|
|
"name": doc.Name,
|
|
"status": status,
|
|
"sync_status": syncStatus,
|
|
"sync_in_progress": syncStatus == "syncing" || syncStatus == model.DocumentSyncStatusPending,
|
|
"last_synced_at": int64PointerValue(doc.LastSyncedAt),
|
|
"last_sync_attempted_at": int64PointerValue(doc.LastSyncAttemptedAt),
|
|
"last_sync_error_code": doc.LastSyncErrorCode,
|
|
"updated_at": doc.UpdatedAt.Unix(),
|
|
}
|
|
return payload
|
|
}
|
|
|
|
func captainDocumentAssistantPayload(doc *model.CaptainDocument) gin.H {
|
|
if doc.Assistant.ID == 0 {
|
|
return gin.H{"id": doc.AssistantID}
|
|
}
|
|
return captainAssistantPayload(&doc.Assistant)
|
|
}
|
|
|
|
func int64PointerValue(value *int64) any {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
return *value
|
|
}
|