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

343 lines
13 KiB
Go

package v1
import (
"encoding/json"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/service"
"github.com/gochat/gochat/pkg/pagination"
"github.com/gochat/gochat/pkg/response"
)
// IntegrationHookHandler handles IntegrationHook CRUD + ProcessEvent endpoints.
// Reference: Chatwoot Integrations::HooksController + AppsController
type IntegrationHookHandler struct {
svc *service.IntegrationHookService
}
// NewIntegrationHookHandler creates a new IntegrationHookHandler.
func NewIntegrationHookHandler(svc *service.IntegrationHookService) *IntegrationHookHandler {
return &IntegrationHookHandler{svc: svc}
}
// --- Integration Apps (catalog) ---
// ListApps retrieves all available integration apps.
// GET /api/v1/accounts/:account_id/integrations/apps
func (h *IntegrationHookHandler) ListApps(c *gin.Context) {
if !h.svc.Ready() {
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, "failed to list integration apps")
return
}
apps, err := h.svc.ListApps(c.Request.Context())
if err != nil {
handleServiceError(c, err)
return
}
accountID := getAccountID(c)
c.JSON(http.StatusOK, gin.H{"payload": h.serializeIntegrationApps(c, accountID, apps)})
}
// GetApp retrieves a single integration app by ID.
// GET /api/v1/accounts/:account_id/integrations/apps/:id
func (h *IntegrationHookHandler) GetApp(c *gin.Context) {
app, svcErr := h.svc.GetAppByID(c.Request.Context(), c.Param("id"))
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
c.JSON(http.StatusOK, h.serializeIntegrationApp(c, getAccountID(c), *app))
}
// --- Integration Hooks CRUD ---
// ListHooks retrieves all integration hooks for an account, paginated.
// GET /api/v1/accounts/:account_id/integrations/hooks
func (h *IntegrationHookHandler) ListHooks(c *gin.Context) {
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
page := pagination.Parse(c)
hooks, total, svcErr := h.svc.List(c.Request.Context(), accountID, page.Offset, page.PerPage)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
response.OKWithMeta(c, hooks, page.Page, page.PerPage, total)
}
// GetHook retrieves a single integration hook by ID.
// GET /api/v1/accounts/:account_id/integrations/hooks/:id
func (h *IntegrationHookHandler) GetHook(c *gin.Context) {
id, err := parseUintParam(c, "id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid hook id")
return
}
hook, svcErr := h.svc.GetScoped(c.Request.Context(), getAccountID(c), id)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
c.JSON(http.StatusOK, serializeIntegrationHook(*hook))
}
// CreateHook creates a new integration hook.
// POST /api/v1/accounts/:account_id/integrations/hooks
func (h *IntegrationHookHandler) CreateHook(c *gin.Context) {
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
var req service.CreateHookRequest
if err := bindJSONWrappedOrRaw(c, "hook", &req); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error())
return
}
hook, svcErr := h.svc.Create(c.Request.Context(), accountID, req)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
c.JSON(http.StatusOK, serializeIntegrationHook(*hook))
}
// UpdateHook updates an existing integration hook.
// PUT /api/v1/accounts/:account_id/integrations/hooks/:id
func (h *IntegrationHookHandler) UpdateHook(c *gin.Context) {
id, err := parseUintParam(c, "id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid hook id")
return
}
accountID := getAccountID(c)
var req service.UpdateHookRequest
if err := bindJSONWrappedOrRaw(c, "hook", &req); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error())
return
}
hook, svcErr := h.svc.UpdateScoped(c.Request.Context(), accountID, id, req)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
c.JSON(http.StatusOK, serializeIntegrationHook(*hook))
}
// DeleteHook deletes an integration hook.
// DELETE /api/v1/accounts/:account_id/integrations/hooks/:id
func (h *IntegrationHookHandler) DeleteHook(c *gin.Context) {
id, err := parseUintParam(c, "id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid hook id")
return
}
if svcErr := h.svc.DeleteScoped(c.Request.Context(), getAccountID(c), id); svcErr != nil {
handleServiceError(c, svcErr)
return
}
c.Status(http.StatusOK)
}
// ProcessHookEvent processes an incoming event for a hook.
// POST /api/v1/accounts/:account_id/integrations/hooks/:id/process_event
func (h *IntegrationHookHandler) ProcessHookEvent(c *gin.Context) {
id, err := parseUintParam(c, "id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid hook id")
return
}
var eventData map[string]interface{}
if err := c.ShouldBindJSON(&eventData); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error())
return
}
if svcErr := h.svc.ProcessEvent(c.Request.Context(), id, eventData); svcErr != nil {
if strings.Contains(strings.ToLower(svcErr.Error()), "not found") {
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, svcErr.Error())
return
}
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, svcErr.Error())
return
}
c.JSON(http.StatusOK, gin.H{"message": "event processed"})
}
func (h *IntegrationHookHandler) serializeIntegrationApps(c *gin.Context, accountID uint, apps []model.IntegrationApp) []gin.H {
payload := make([]gin.H, 0, len(apps))
for _, app := range apps {
payload = append(payload, h.serializeIntegrationApp(c, accountID, app))
}
return payload
}
func (h *IntegrationHookHandler) serializeIntegrationApp(c *gin.Context, accountID uint, app model.IntegrationApp) gin.H {
appID := integrationAppID(app)
hooks, _ := h.svc.ListHooksForApp(c.Request.Context(), accountID, appID)
serializedHooks := make([]gin.H, 0, len(hooks))
for _, hook := range hooks {
serializedHooks = append(serializedHooks, serializeIntegrationHook(hook))
}
return gin.H{
"id": appID,
"name": app.Name,
"description": app.Description,
"short_description": app.Description,
"enabled": len(serializedHooks) > 0,
"action": app.ActionURL,
"button": app.ActionURL,
"hook_type": integrationAppHookType(appID),
"allow_multiple_hooks": integrationAppAllowsMultipleHooks(appID),
"settings_form_schema": integrationAppSettingsFormSchema(appID),
"visible_properties": integrationAppVisibleProperties(appID),
"hooks": serializedHooks,
}
}
func serializeIntegrationHook(hook model.IntegrationHook) gin.H {
settings := gin.H{}
if len(hook.Settings) > 0 {
_ = json.Unmarshal(hook.Settings, &settings)
}
payload := gin.H{
"id": hook.ID,
"app_id": integrationHookAppID(hook),
"status": hook.Status != model.HookStatusInactive,
"account_id": hook.AccountID,
"hook_type": integrationHookKind(hook),
"settings": settings,
}
if hook.ReferenceID != "" {
payload["reference_id"] = hook.ReferenceID
}
if hook.InboxID != nil && *hook.InboxID != 0 {
inbox := gin.H{"id": *hook.InboxID}
if hook.Inbox != nil && hook.Inbox.ID != 0 {
inbox["name"] = hook.Inbox.Name
}
payload["inbox"] = inbox
}
return payload
}
func integrationAppID(app model.IntegrationApp) string {
if app.HookType != "" {
return string(app.HookType)
}
return strings.ToLower(strings.ReplaceAll(app.Name, " ", "_"))
}
func integrationHookAppID(hook model.IntegrationHook) string {
if hook.AppID != "" {
return hook.AppID
}
return string(hook.HookType)
}
func integrationAppHookType(appID string) string {
if appID == "dialogflow" {
return "inbox"
}
return "account"
}
func integrationAppAllowsMultipleHooks(appID string) bool {
return appID == "webhook" || appID == "dashboard_apps" || appID == "dialogflow"
}
func integrationAppSettingsFormSchema(appID string) []gin.H {
schemas := map[string][]gin.H{
"openai": {
{"label": "API Key", "type": "text", "name": "api_key", "validation": "required"},
{"label": "Show label suggestions", "type": "checkbox", "name": "label_suggestion", "validation": ""},
},
"dialogflow": {
{"label": "Dialogflow Project ID", "type": "text", "name": "project_id", "validation": "required", "validationName": "Project Id"},
{"label": "Dialogflow Project Key File", "type": "textarea", "name": "credentials", "validation": "required|JSON", "validationName": "Credentials", "validation-messages": gin.H{"JSON": "Invalid JSON", "required": "Credentials is required"}},
{"label": "Dialogflow Region", "type": "select", "name": "region", "default": "global", "options": []gin.H{{"label": "Global - Default", "value": "global"}, {"label": "AS-NE1 - Tokyo, Japan", "value": "asia-northeast1"}, {"label": "AU-SE1 - Sydney, Australia", "value": "australia-southeast1"}, {"label": "EU-W1 - St. Ghislain, Belgium", "value": "europe-west1"}, {"label": "EU-W2 - London, England", "value": "europe-west2"}}},
{"label": "Language Code", "type": "select", "name": "language_code", "default": "en-US", "help": "Language code for Dialogflow agent. Use \"auto\" to detect from contact language.", "options": []gin.H{{"label": "Auto-detect from contact", "value": "auto"}, {"label": "English (US)", "value": "en-US"}, {"label": "English (UK)", "value": "en-GB"}, {"label": "Spanish (Spain)", "value": "es-ES"}, {"label": "Spanish (Latin America)", "value": "es-419"}, {"label": "French", "value": "fr-FR"}, {"label": "German", "value": "de-DE"}, {"label": "Portuguese (Brazil)", "value": "pt-BR"}, {"label": "Portuguese (Portugal)", "value": "pt-PT"}, {"label": "Italian", "value": "it-IT"}, {"label": "Japanese", "value": "ja-JP"}, {"label": "Korean", "value": "ko-KR"}, {"label": "Chinese (Simplified)", "value": "zh-CN"}, {"label": "Chinese (Traditional)", "value": "zh-TW"}, {"label": "Hindi", "value": "hi-IN"}, {"label": "Arabic", "value": "ar"}, {"label": "Russian", "value": "ru-RU"}, {"label": "Dutch", "value": "nl-NL"}, {"label": "Polish", "value": "pl-PL"}, {"label": "Turkish", "value": "tr-TR"}, {"label": "Thai", "value": "th-TH"}, {"label": "Vietnamese", "value": "vi-VN"}, {"label": "Indonesian", "value": "id-ID"}}},
},
"google_translate": {
{"label": "Google Cloud Project ID", "type": "text", "name": "project_id", "validation": "required", "validationName": "Project Id"},
{"label": "Google Cloud Project Key File", "type": "textarea", "name": "credentials", "validation": "required|JSON", "validationName": "Credentials", "validation-messages": gin.H{"JSON": "Invalid JSON", "required": "Credentials is required"}},
},
"dyte": {
{"label": "Organization ID", "type": "text", "name": "organization_id", "validation": "required"},
{"label": "API Key", "type": "text", "name": "api_key", "validation": "required"},
},
"leadsquared": {
{"label": "Access Key", "type": "text", "name": "access_key", "validation": "required"},
{"label": "Secret Key", "type": "text", "name": "secret_key", "validation": "required"},
{"label": "Endpoint URL", "type": "text", "name": "endpoint_url", "validation": "required|url"},
{"label": "App URL", "type": "text", "name": "app_url", "validation": "required|url"},
},
}
if schema, ok := schemas[appID]; ok {
return schema
}
return []gin.H{}
}
func integrationAppVisibleProperties(appID string) []string {
properties := map[string][]string{
"openai": {"api_key", "label_suggestion"},
"dialogflow": {"project_id", "region", "language_code"},
"google_translate": {"project_id"},
"dyte": {"organization_id"},
"leadsquared": {"access_key", "endpoint_url", "app_url"},
}
if visibleProperties, ok := properties[appID]; ok {
return visibleProperties
}
return []string{}
}
func integrationHookKind(hook model.IntegrationHook) string {
if hook.InboxID != nil && *hook.InboxID != 0 {
return "inbox"
}
return "account"
}
// RegisterIntegrationHookRoutes registers integration hook routes on a router group.
func RegisterIntegrationHookRoutes(g *gin.RouterGroup, h *IntegrationHookHandler) {
// Integration apps catalog
apps := g.Group("/apps")
{
apps.GET("", h.ListApps)
apps.GET("/", h.ListApps)
apps.GET("/:id", h.GetApp)
}
// Integration hooks CRUD + process event
hooks := g.Group("/hooks")
{
hooks.GET("", h.ListHooks)
hooks.GET("/", h.ListHooks)
hooks.GET("/:id", h.GetHook)
hooks.POST("", h.CreateHook)
hooks.POST("/", h.CreateHook)
hooks.PUT("/:id", h.UpdateHook)
hooks.PATCH("/:id", h.UpdateHook)
hooks.DELETE("/:id", h.DeleteHook)
hooks.POST("/:id/process_event", h.ProcessHookEvent)
}
}